1use std::sync::Arc;
10
11use serde::{Deserialize, Serialize};
12use serde_json::value::RawValue;
13use uqa_analysis::{AnalyzerResources, CompiledAnalyzer};
14
15use crate::{AnalyzerPhase, InvertedIndex, StorageBackendError, StorageBackendResult};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum AnalyzerBindingOwner {
20 Field,
22 Gin,
24}
25
26#[derive(Debug, Clone)]
27pub struct BoundAnalyzerRevision {
28 pub name: Option<String>,
29 pub compiled: Arc<CompiledAnalyzer>,
30}
31
32#[derive(Debug, Clone)]
33pub struct FieldAnalyzerBinding {
34 pub index: BoundAnalyzerRevision,
35 pub search: BoundAnalyzerRevision,
36 pub owner: AnalyzerBindingOwner,
37 pub last_phase: AnalyzerPhase,
38}
39
40#[derive(Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42struct StoredSide {
43 name: Option<String>,
44 descriptor: Box<RawValue>,
45}
46
47#[derive(Serialize, Deserialize)]
48#[serde(deny_unknown_fields)]
49struct StoredBinding {
50 format: String,
51 version: u32,
52 owner: AnalyzerBindingOwner,
53 last_phase: AnalyzerPhase,
54 index: StoredSide,
55 search: StoredSide,
56}
57
58impl FieldAnalyzerBinding {
59 pub fn unassigned(index: Arc<CompiledAnalyzer>, search: Arc<CompiledAnalyzer>) -> Self {
60 Self {
61 index: BoundAnalyzerRevision {
62 name: None,
63 compiled: index,
64 },
65 search: BoundAnalyzerRevision {
66 name: None,
67 compiled: search,
68 },
69 owner: AnalyzerBindingOwner::Field,
70 last_phase: AnalyzerPhase::Both,
71 }
72 }
73
74 pub fn assigned(
75 &self,
76 name: &str,
77 compiled: Arc<CompiledAnalyzer>,
78 phase: AnalyzerPhase,
79 owner: AnalyzerBindingOwner,
80 ) -> Self {
81 let mut result = self.clone();
82 let revision = BoundAnalyzerRevision {
83 name: Some(name.to_owned()),
84 compiled,
85 };
86 match phase {
87 AnalyzerPhase::Index => result.index = revision,
88 AnalyzerPhase::Search => result.search = revision,
89 AnalyzerPhase::Both => {
90 result.index = revision.clone();
91 result.search = revision;
92 }
93 }
94 result.owner = owner;
95 result.last_phase = phase;
96 result
97 }
98
99 pub fn uses_name(&self, name: &str) -> bool {
100 self.index.name.as_deref() == Some(name) || self.search.name.as_deref() == Some(name)
101 }
102
103 pub fn last_assignment(&self) -> Option<(String, String)> {
105 let side = match self.last_phase {
106 AnalyzerPhase::Index | AnalyzerPhase::Both => &self.index,
107 AnalyzerPhase::Search => &self.search,
108 };
109 side.name
110 .clone()
111 .map(|name| (name, self.phase_name().to_owned()))
112 }
113
114 pub fn phase_name(&self) -> &'static str {
115 match self.last_phase {
116 AnalyzerPhase::Index => "index",
117 AnalyzerPhase::Search => "search",
118 AnalyzerPhase::Both => "both",
119 }
120 }
121
122 pub fn install(&self, field: &str, index: &mut dyn InvertedIndex) -> StorageBackendResult<()> {
124 self.validate()?;
125 index
126 .set_field_analyzer_revisions(
127 field,
128 self.index.compiled.clone(),
129 self.search.compiled.clone(),
130 )
131 .map_err(StorageBackendError::Other)
132 }
133
134 pub fn to_json(&self) -> StorageBackendResult<String> {
135 self.validate()?;
136 let side = |revision: &BoundAnalyzerRevision| -> StorageBackendResult<StoredSide> {
137 Ok(StoredSide {
138 name: revision.name.clone(),
139 descriptor: RawValue::from_string(
140 revision.compiled.descriptor().canonical_json().to_owned(),
141 )?,
142 })
143 };
144 Ok(serde_json::to_string(&StoredBinding {
145 format: "uqa-field-analyzer-binding".into(),
146 version: 1,
147 owner: self.owner,
148 last_phase: self.last_phase,
149 index: side(&self.index)?,
150 search: side(&self.search)?,
151 })?)
152 }
153
154 pub fn from_json(json: &str, resources: &AnalyzerResources) -> StorageBackendResult<Self> {
155 let limit = resources.limits().max_descriptor_bytes.saturating_mul(3);
157 if json.len() > limit {
158 return Err(StorageBackendError::Other(
159 "analyzer binding exceeds descriptor limits".into(),
160 ));
161 }
162 let saved: StoredBinding = serde_json::from_str(json)?;
163 if saved.format != "uqa-field-analyzer-binding" || saved.version != 1 {
164 return Err(StorageBackendError::Other(
165 "unsupported analyzer binding format".into(),
166 ));
167 }
168 let side = |saved: StoredSide| -> StorageBackendResult<BoundAnalyzerRevision> {
169 Ok(BoundAnalyzerRevision {
170 name: saved.name,
171 compiled: resources.restore_json(saved.descriptor.get())?,
172 })
173 };
174 let restored = Self {
175 owner: saved.owner,
176 last_phase: saved.last_phase,
177 index: side(saved.index)?,
178 search: side(saved.search)?,
179 };
180 restored.validate()?;
181 Ok(restored)
182 }
183
184 fn validate(&self) -> StorageBackendResult<()> {
185 for side in [&self.index, &self.search] {
186 if side
187 .name
188 .as_deref()
189 .is_some_and(|name| name.is_empty() || name.trim() != name)
190 {
191 return Err(StorageBackendError::Other(
192 "analyzer binding has an invalid name".into(),
193 ));
194 }
195 }
196 let invalid_last_assignment = match self.last_phase {
197 AnalyzerPhase::Index => self.index.name.is_none(),
198 AnalyzerPhase::Search => self.search.name.is_none(),
199 AnalyzerPhase::Both => {
200 self.index.name != self.search.name
201 || (self.index.name.is_some()
202 && self.index.compiled.descriptor().fingerprint()
203 != self.search.compiled.descriptor().fingerprint())
204 }
205 };
206 if invalid_last_assignment {
207 return Err(StorageBackendError::Other(
208 "analyzer binding disagrees with its last assignment".into(),
209 ));
210 }
211 if self.owner == AnalyzerBindingOwner::Gin
212 && (self.index.name.is_none()
213 || self.index.name != self.search.name
214 || self.index.compiled.descriptor().fingerprint()
215 != self.search.compiled.descriptor().fingerprint()
216 || self.last_phase != AnalyzerPhase::Both)
217 {
218 return Err(StorageBackendError::Other(
219 "GIN analyzer ownership requires one named revision on both sides".into(),
220 ));
221 }
222 Ok(())
223 }
224}