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