1use std::collections::{BTreeMap, BTreeSet};
14use std::sync::Arc;
15
16use uqa_analysis::Analyzer;
17use uqa_core::memory::OwnedMap;
18use uqa_core::{DocId, FieldName, IndexStats, Payload, PostingEntry, PostingList, TokenOccurrence};
19
20use crate::TokenTermKey;
21
22use crate::backend::{StorageBackendError, StorageBackendResult};
23use crate::block_max_index::BlockMaxScorer;
24use crate::clustered_postings::{MaterializedPostingCursor, PostingCursor, PostingScore};
25
26mod analysis;
27pub(crate) use analysis::analyze_index_field_with_scratch;
28mod batch;
29mod bindings;
30mod changes;
31mod contract;
32pub mod defaults;
33mod footprint;
34mod memory;
35mod metadata;
36mod read_cursor;
37mod retained;
38mod snapshot;
39
40#[cfg(test)]
41mod tests;
42
43pub use analysis::{
44 analyze_index_field, analyze_index_field_budgeted, analyze_index_field_cancellable,
45 analyze_query_graph, analyze_query_graph_budgeted, analyze_query_terms,
46 analyze_query_terms_budgeted, AnalyzedField, IndexedFieldMetadata, RetainedAnalyzedField,
47};
48pub use bindings::{AnalyzerBindings, AnalyzerDefault, RetainedAnalyzerBindings};
49pub use changes::{visit_field_replacement, InvertedIndexChange, InvertedIndexChangeVisitor};
50pub use contract::{AnalyzerPhase, InvertedIndex};
51pub use metadata::IndexedFieldRevision;
52pub use retained::RetainedInvertedIndexBuilder;
53
54pub fn validate_linear_analyzer(analyzer: &Analyzer) -> StorageBackendResult<()> {
56 if analyzer.uses_korean_stages() {
57 return Err(StorageBackendError::Other("Korean analyzers require immutable analyzer revisions and lossless token-graph storage".into()));
58 }
59 if analyzer.uses_japanese_stages() {
60 return Err(StorageBackendError::Other("Japanese analyzers require immutable analyzer revisions and lossless token-graph storage".into()));
61 }
62 Ok(())
63}
64
65pub fn validate_linear_revision(
67 revision: &uqa_analysis::CompiledAnalyzer,
68) -> StorageBackendResult<()> {
69 validate_linear_analyzer(&revision.descriptor().configuration()?)?;
70 if revision.descriptor().length_policy() != uqa_analysis::TokenLengthPolicy::EmittedTokens {
71 return Err(StorageBackendError::Other(
72 "overlap-discounted analyzer revisions require occurrence storage".into(),
73 ));
74 }
75 Ok(())
76}
77
78fn counter_error(context: &str) -> StorageBackendError {
79 StorageBackendError::Other(format!("inverted-index {context} overflow or corruption"))
80}
81
82fn usize_to_u64(value: usize, context: &str) -> StorageBackendResult<u64> {
83 u64::try_from(value).map_err(|_| counter_error(context))
84}
85
86fn checked_sum_u64(
87 values: impl IntoIterator<Item = u64>,
88 context: &str,
89) -> StorageBackendResult<u64> {
90 values.into_iter().try_fold(0_u64, |total, value| {
91 total
92 .checked_add(value)
93 .ok_or_else(|| counter_error(context))
94 })
95}
96
97#[derive(Debug)]
98pub struct MemoryInvertedIndex {
99 bindings: AnalyzerBindings,
100 state: Arc<MemoryIndexState>,
102 state_memory: Option<Arc<uqa_core::memory::MemoryReservation>>,
104 read_control: Option<crate::read_control::StorageReadControl>,
105}
106
107impl Clone for MemoryInvertedIndex {
109 fn clone(&self) -> Self {
110 Self {
111 bindings: self.bindings.clone(),
112 state: Arc::new((*self.state).clone()),
113 state_memory: None,
114 read_control: None,
115 }
116 }
117}
118
119#[derive(Debug, Default)]
120struct MemoryIndexState {
121 index: OwnedMap<PostingKey, OwnedMap<DocId, MemoryPosting>>,
123 documents: OwnedMap<DocId, MemoryDocument>,
125 field_counters: OwnedMap<FieldName, MemoryFieldCounters>,
127 doc_count: u64,
128 retention: footprint::RetainedPayload,
129}
130
131type PostingKey = (FieldName, TokenTermKey);
132
133#[derive(Debug, Clone, PartialEq, Eq)]
134struct MemoryDocument {
135 fields: OwnedMap<FieldName, IndexedFieldMetadata>,
137 terms: Vec<PostingKey>,
139}
140
141#[derive(Debug, Clone)]
142struct MemoryPosting {
143 doc_id: DocId,
144 occurrences: Vec<TokenOccurrence>,
145}
146
147impl MemoryPosting {
148 fn new(doc_id: DocId, occurrences: Vec<TokenOccurrence>) -> Self {
149 debug_assert!(occurrences
150 .windows(2)
151 .all(|pair| pair[0].position <= pair[1].position));
152 Self {
153 doc_id,
154 occurrences,
155 }
156 }
157
158 fn projection(&self) -> PostingEntry {
159 let mut positions: Vec<_> = self
161 .occurrences
162 .iter()
163 .map(|occurrence| occurrence.position)
164 .collect();
165 positions.dedup();
166 PostingEntry::new(
167 self.doc_id,
168 Payload {
169 positions,
170 score: 0.0,
171 fields: BTreeMap::new(),
172 },
173 )
174 }
175}
176
177struct StagedMemoryDocument {
178 fields: OwnedMap<FieldName, IndexedFieldMetadata>,
179 terms: Vec<PostingKey>,
180 postings: Vec<(PostingKey, MemoryPosting)>,
181}
182
183struct MemoryReplacementPlan {
184 old_terms: Vec<PostingKey>,
185 next_doc_count: u64,
186 field_counters: OwnedMap<FieldName, MemoryFieldCounters>,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190struct MemoryFieldCounters {
191 total: u64,
192 docs: u64,
193}
194
195impl MemoryInvertedIndex {
196 pub fn new(analyzer: Analyzer) -> Self {
197 Self::with_bindings(AnalyzerBindings::new(analyzer))
198 }
199
200 fn with_bindings(bindings: AnalyzerBindings) -> Self {
201 Self {
202 bindings,
203 state: Arc::default(),
204 state_memory: None,
205 read_control: None,
206 }
207 }
208
209 fn shared_snapshot(&self) -> Self {
210 Self {
211 bindings: self.bindings.clone(),
212 state: Arc::clone(&self.state),
213 state_memory: self.state_memory.clone(),
214 read_control: self.read_control.clone(),
215 }
216 }
217
218 fn check_retained_read(&self) -> StorageBackendResult<()> {
219 self.read_control
220 .as_ref()
221 .map_or(Ok(()), crate::read_control::StorageReadControl::check)
222 }
223
224 fn stage_document(
225 &self,
226 doc_id: DocId,
227 fields: BTreeMap<FieldName, String>,
228 ) -> StorageBackendResult<StagedMemoryDocument> {
229 self.stage_document_inner(doc_id, fields, None)
230 }
231
232 fn stage_document_inner(
233 &self,
234 doc_id: DocId,
235 fields: BTreeMap<FieldName, String>,
236 cancellation: Option<&uqa_core::CancellationToken>,
237 ) -> StorageBackendResult<StagedMemoryDocument> {
238 let mut metadata = OwnedMap::new();
239 let mut terms = Vec::new();
240 let mut postings = Vec::new();
241 for (field, text) in fields {
242 if let Some(cancellation) = cancellation {
243 cancellation.check()?;
244 }
245 let revision = self.bindings.index_revision(&field)?;
246 let analyzed = match cancellation {
247 Some(cancellation) => {
248 analyze_index_field_cancellable(&revision, &text, cancellation)?
249 }
250 None => analyze_index_field(&revision, &text)?,
251 };
252 metadata.insert(
253 field.clone(),
254 IndexedFieldMetadata::new(&revision, &analyzed),
255 );
256 terms.reserve(analyzed.terms.len());
257 for (term, occurrences) in analyzed.terms {
258 if let Some(cancellation) = cancellation {
259 cancellation.check()?;
260 }
261 let key = (field.clone(), term);
262 terms.push(key.clone());
263 postings.push((key, MemoryPosting::new(doc_id, occurrences)));
264 }
265 }
266 Ok(StagedMemoryDocument {
267 fields: metadata,
268 terms,
269 postings,
270 })
271 }
272}
273
274impl MemoryIndexState {
275 fn document(&self, doc_id: DocId) -> StorageBackendResult<Option<&MemoryDocument>> {
276 let document = self.documents.get(&doc_id);
277 if document.is_some_and(|document| document.fields.is_empty()) {
278 return Err(StorageBackendError::Other(format!(
279 "inverted-index document {doc_id} has inconsistent reverse-index state"
280 )));
281 }
282 Ok(document)
283 }
284
285 fn plan_replacement(
286 &self,
287 doc_id: DocId,
288 new_fields: &OwnedMap<FieldName, IndexedFieldMetadata>,
289 ) -> StorageBackendResult<MemoryReplacementPlan> {
290 self.plan_replacement_with_names(doc_id, new_fields, |field| Ok(field.clone()))
291 }
292
293 fn plan_replacement_with_names(
294 &self,
295 doc_id: DocId,
296 new_fields: &OwnedMap<FieldName, IndexedFieldMetadata>,
297 mut copy_name: impl FnMut(&FieldName) -> StorageBackendResult<FieldName>,
298 ) -> StorageBackendResult<MemoryReplacementPlan> {
299 let previous = self.document(doc_id)?;
300 let old_terms = previous
301 .map(|document| document.terms.clone())
302 .unwrap_or_default();
303 let empty_fields = OwnedMap::new();
304 let old_fields = previous.map_or(&empty_fields, |document| &document.fields);
305 let next_doc_count = self
306 .doc_count
307 .checked_sub(u64::from(previous.is_some()))
308 .ok_or_else(|| counter_error("document count"))?
309 .checked_add(u64::from(!new_fields.is_empty()))
310 .ok_or_else(|| counter_error("document count"))?;
311 for key in &old_terms {
312 if !self
313 .index
314 .get(key)
315 .is_some_and(|postings| postings.contains_key(&doc_id))
316 {
317 return Err(StorageBackendError::Other(format!(
318 "inverted-index document {doc_id} references a missing posting"
319 )));
320 }
321 }
322
323 let mut old_keys = old_fields.keys().peekable();
325 let mut new_keys = new_fields.keys().peekable();
326 let affected_fields = std::iter::from_fn(|| match (old_keys.peek(), new_keys.peek()) {
327 (Some(old), Some(new)) => match old.cmp(new) {
328 std::cmp::Ordering::Less => old_keys.next(),
329 std::cmp::Ordering::Greater => new_keys.next(),
330 std::cmp::Ordering::Equal => {
331 old_keys.next();
332 new_keys.next()
333 }
334 },
335 (Some(_), None) => old_keys.next(),
336 (None, Some(_)) => new_keys.next(),
337 (None, None) => None,
338 });
339 let mut field_counters = OwnedMap::new();
340 for field in affected_fields {
341 let old_length = old_fields.get(field).map_or(0, |metadata| metadata.length);
342 let new_length = new_fields.get(field).map_or(0, |metadata| metadata.length);
343 let total = self
344 .field_counters
345 .get(field)
346 .map_or(0, |counters| counters.total)
347 .checked_sub(old_length)
348 .ok_or_else(|| counter_error("total field length"))?
349 .checked_add(new_length)
350 .ok_or_else(|| counter_error("total field length"))?;
351 let field_docs = self
352 .field_counters
353 .get(field)
354 .map_or(0, |counters| counters.docs)
355 .checked_sub(u64::from(old_fields.contains_key(field)))
356 .ok_or_else(|| counter_error("field document count"))?
357 .checked_add(u64::from(new_fields.contains_key(field)))
358 .ok_or_else(|| counter_error("field document count"))?;
359 field_counters.insert(
360 copy_name(field)?,
361 MemoryFieldCounters {
362 total,
363 docs: field_docs,
364 },
365 );
366 }
367 Ok(MemoryReplacementPlan {
368 old_terms,
369 next_doc_count,
370 field_counters,
371 })
372 }
373
374 fn apply_replacement(
375 &mut self,
376 doc_id: DocId,
377 staged: StagedMemoryDocument,
378 plan: MemoryReplacementPlan,
379 ) -> StorageBackendResult<()> {
380 for key in plan.old_terms {
381 self.remove_posting(doc_id, &key)?;
382 }
383 self.remove_document_metadata(doc_id);
384 for (field, counters) in plan.field_counters {
385 footprint::set_counter(
386 &mut self.field_counters,
387 field,
388 (counters.docs != 0).then_some(counters),
389 &mut self.retention,
390 );
391 }
392 for (key, entry) in staged.postings {
393 self.insert_posting(doc_id, key, entry);
394 }
395 self.doc_count = plan.next_doc_count;
396 if !staged.fields.is_empty() {
397 self.insert_document_metadata(doc_id, staged.fields, staged.terms);
398 }
399 Ok(())
400 }
401}