Skip to main content

uqa_storage/inverted_index/
memory.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Memory provider mutations, projections, and exact analyzer bindings.
8
9use super::{
10    checked_sum_u64, counter_error, usize_to_u64, Analyzer, AnalyzerBindings, AnalyzerPhase, Arc,
11    BTreeMap, BTreeSet, DocId, FieldName, IndexStats, IndexedFieldMetadata, InvertedIndex,
12    MaterializedPostingCursor, MemoryInvertedIndex, PostingCursor, PostingEntry, PostingList,
13    PostingScore, StorageBackendError, StorageBackendResult, TokenOccurrence, TokenTermKey,
14};
15
16impl InvertedIndex for MemoryInvertedIndex {
17    fn field_stats_scalar_budgeted(
18        &self,
19        field: &str,
20        control: &crate::read_control::StorageReadControl,
21    ) -> StorageBackendResult<IndexStats> {
22        self.check_retained_read()?;
23        control.check()?;
24        let stats = self.field_stats_scalar(field)?;
25        self.check_retained_read()?;
26        control.check()?;
27        Ok(stats)
28    }
29
30    fn posting_read_cursor_key_budgeted<'a>(
31        &'a self,
32        field: &'a str,
33        term: &'a TokenTermKey,
34        control: &crate::read_control::StorageReadControl,
35    ) -> StorageBackendResult<crate::clustered_postings::BudgetedPostingReadCursor<'a>> {
36        let cursor =
37            super::read_cursor::MemoryPostingReadCursor::with_control(self, field, term, control)?;
38        crate::clustered_postings::BudgetedPostingReadCursor::new(cursor, control)
39    }
40
41    fn get_occurrences_budgeted(
42        &self,
43        doc_id: DocId,
44        field: &str,
45        term: &TokenTermKey,
46        control: &crate::read_control::StorageReadControl,
47    ) -> StorageBackendResult<uqa_core::memory::Budgeted<Vec<TokenOccurrence>>> {
48        self.check_retained_read()?;
49        let postings = super::read_cursor::controlled_postings(self, field, term, control)?;
50        let mut output = uqa_core::memory::BudgetedVec::new(control.memory());
51        if let Some(posting) = postings.and_then(|postings| postings.get(&doc_id)) {
52            output.reserve(posting.occurrences.len())?;
53            for occurrence in &posting.occurrences {
54                self.check_retained_read()?;
55                control.check()?;
56                output.push(*occurrence)?;
57            }
58        }
59        self.check_retained_read()?;
60        control.check()?;
61        let (values, memory) = output.into_parts();
62        Ok(uqa_core::memory::Budgeted::new(values, memory))
63    }
64
65    fn posting_read_cursor_key<'a>(
66        &'a self,
67        field: &'a str,
68        term: &TokenTermKey,
69    ) -> StorageBackendResult<Box<dyn crate::clustered_postings::PostingReadCursor + 'a>> {
70        Ok(Box::new(super::read_cursor::MemoryPostingReadCursor::new(
71            self, field, term,
72        )?))
73    }
74
75    fn analyzer(&self) -> &Analyzer {
76        self.bindings.default_configuration()
77    }
78
79    fn default_analyzer_binding(
80        &self,
81    ) -> StorageBackendResult<crate::inverted_index::AnalyzerDefault> {
82        self.check_retained_read()?;
83        Ok(self.bindings.default_binding())
84    }
85
86    fn add_document(
87        &mut self,
88        doc_id: DocId,
89        fields: BTreeMap<FieldName, String>,
90    ) -> StorageBackendResult<()> {
91        // Resolve and analyze every field before touching postings. A deferred default can fail even when another field already has a valid revision.
92        let staged = self.stage_document(doc_id, fields)?;
93        let plan = self.state.plan_replacement(doc_id, &staged.fields)?;
94        Arc::make_mut(&mut self.state).apply_replacement(doc_id, staged, plan)
95    }
96
97    fn remove_document(&mut self, doc_id: DocId) -> StorageBackendResult<()> {
98        let Some(super::MemoryDocument {
99            terms: keys,
100            fields: lengths,
101        }) = self.state.document(doc_id)?.cloned()
102        else {
103            return Ok(());
104        };
105        let next_doc_count = self
106            .state
107            .doc_count
108            .checked_sub(1)
109            .ok_or_else(|| counter_error("document count"))?;
110        for key in &keys {
111            if !self
112                .state
113                .index
114                .get(key)
115                .is_some_and(|postings| postings.contains_key(&doc_id))
116            {
117                return Err(StorageBackendError::Other(format!(
118                    "inverted-index document {doc_id} references a missing posting"
119                )));
120            }
121        }
122        let mut next_field_counters = BTreeMap::new();
123        for (field, metadata) in &lengths {
124            let total = self
125                .state
126                .field_counters
127                .get(field)
128                .map_or(0, |counters| counters.total)
129                .checked_sub(metadata.length)
130                .ok_or_else(|| counter_error("total field length"))?;
131            let field_docs = self
132                .state
133                .field_counters
134                .get(field)
135                .map_or(0, |counters| counters.docs)
136                .checked_sub(1)
137                .ok_or_else(|| counter_error("field document count"))?;
138            next_field_counters.insert(field.clone(), (total, field_docs));
139        }
140
141        let state = Arc::make_mut(&mut self.state);
142        for key in keys {
143            state.remove_posting(doc_id, &key)?;
144        }
145        state.remove_document_metadata(doc_id);
146        for (field, (total, field_docs)) in next_field_counters {
147            super::footprint::set_counter(
148                &mut state.field_counters,
149                field,
150                (field_docs != 0).then_some(super::MemoryFieldCounters {
151                    total,
152                    docs: field_docs,
153                }),
154                &mut state.retention,
155            );
156        }
157        state.doc_count = next_doc_count;
158        Ok(())
159    }
160
161    fn try_rebuild_documents(
162        &mut self,
163        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
164    ) -> StorageBackendResult<()> {
165        self.rebuild_documents_inner(documents, None)
166    }
167
168    fn try_rebuild_documents_cancellable(
169        &mut self,
170        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
171        cancellation: &uqa_core::CancellationToken,
172    ) -> StorageBackendResult<()> {
173        self.rebuild_documents_inner(documents, Some(cancellation))
174    }
175
176    fn try_add_documents(
177        &mut self,
178        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
179    ) -> StorageBackendResult<()> {
180        self.add_document_batch(documents)
181    }
182
183    fn try_add_documents_observed(
184        &mut self,
185        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
186        visit: &mut super::InvertedIndexChangeVisitor<'_>,
187    ) -> StorageBackendResult<()> {
188        self.add_document_batch_observed(documents, Some(visit))
189    }
190
191    fn clear(&mut self) -> StorageBackendResult<()> {
192        // Clearing a shared snapshot must not copy the state it is discarding.
193        if let Some(state) = Arc::get_mut(&mut self.state) {
194            *state = super::MemoryIndexState::default();
195        } else {
196            self.state = Arc::default();
197        }
198        Ok(())
199    }
200
201    fn get_posting_list(&self, field: &str, term: &str) -> StorageBackendResult<PostingList> {
202        self.check_retained_read()?;
203        self.get_posting_list_key(field, &TokenTermKey::from_text(term))
204    }
205
206    fn get_posting_list_key(
207        &self,
208        field: &str,
209        term: &TokenTermKey,
210    ) -> StorageBackendResult<PostingList> {
211        self.check_retained_read()?;
212        let entries = self
213            .state
214            .index
215            .get(&(field.to_owned(), term.clone()))
216            .into_iter()
217            .flat_map(uqa_core::memory::OwnedMap::values)
218            .map(super::MemoryPosting::projection)
219            .collect();
220        Ok(PostingList::from_sorted_unchecked(entries))
221    }
222
223    fn posting_cursor(
224        &self,
225        field: &str,
226        term: &str,
227    ) -> StorageBackendResult<Box<dyn PostingCursor>> {
228        self.check_retained_read()?;
229        self.posting_cursor_key(field, &TokenTermKey::from_text(term))
230    }
231
232    fn posting_cursor_key(
233        &self,
234        field: &str,
235        term: &TokenTermKey,
236    ) -> StorageBackendResult<Box<dyn PostingCursor>> {
237        self.check_retained_read()?;
238        let entries = self
239            .state
240            .index
241            .get(&(field.to_owned(), term.clone()))
242            .into_iter()
243            .flat_map(uqa_core::memory::OwnedMap::values)
244            .map(|posting| {
245                Ok(PostingScore {
246                    doc_id: posting.doc_id,
247                    term_freq: usize_to_u64(posting.occurrences.len(), "term frequency")?,
248                    doc_length: self.get_doc_length(posting.doc_id, field)?,
249                })
250            })
251            .collect::<StorageBackendResult<Vec<_>>>()?;
252        Ok(Box::new(MaterializedPostingCursor::new(entries)?))
253    }
254
255    fn get_occurrence_postings(
256        &self,
257        field: &str,
258        term: &TokenTermKey,
259    ) -> StorageBackendResult<Vec<crate::clustered_postings::OccurrencePosting>> {
260        self.check_retained_read()?;
261        self.state
262            .index
263            .get(&(field.to_owned(), term.clone()))
264            .into_iter()
265            .flat_map(uqa_core::memory::OwnedMap::values)
266            .map(|posting| {
267                Ok(crate::clustered_postings::OccurrencePosting {
268                    doc_id: posting.doc_id,
269                    doc_length: self.get_doc_length(posting.doc_id, field)?,
270                    occurrences: posting.occurrences.clone(),
271                })
272            })
273            .collect()
274    }
275
276    fn get_occurrences(
277        &self,
278        doc_id: DocId,
279        field: &str,
280        term: &TokenTermKey,
281    ) -> StorageBackendResult<Vec<TokenOccurrence>> {
282        self.check_retained_read()?;
283        Ok(self
284            .state
285            .index
286            .get(&(field.to_owned(), term.clone()))
287            .and_then(|postings| postings.get(&doc_id))
288            .map_or_else(Vec::new, |posting| posting.occurrences.clone()))
289    }
290
291    fn indexed_field_metadata(
292        &self,
293        doc_id: DocId,
294        field: &str,
295    ) -> StorageBackendResult<Option<IndexedFieldMetadata>> {
296        self.check_retained_read()?;
297        Ok(self
298            .state
299            .documents
300            .get(&doc_id)
301            .and_then(|document| document.fields.get(field))
302            .copied())
303    }
304
305    fn for_each_posting(
306        &self,
307        field: &str,
308        term: &str,
309        visit: &mut dyn FnMut(&PostingEntry),
310    ) -> StorageBackendResult<()> {
311        self.check_retained_read()?;
312        if let Some(postings) = self
313            .state
314            .index
315            .get(&(field.to_owned(), TokenTermKey::from_text(term)))
316        {
317            for posting in postings.values() {
318                visit(&posting.projection());
319            }
320        }
321        Ok(())
322    }
323
324    fn for_each_term_freq(
325        &self,
326        field: &str,
327        term: &str,
328        visit: &mut dyn FnMut(DocId, u64),
329    ) -> StorageBackendResult<()> {
330        self.check_retained_read()?;
331        if let Some(postings) = self
332            .state
333            .index
334            .get(&(field.to_owned(), TokenTermKey::from_text(term)))
335        {
336            for posting in postings.values() {
337                visit(
338                    posting.doc_id,
339                    usize_to_u64(posting.occurrences.len(), "term frequency")?,
340                );
341            }
342        }
343        Ok(())
344    }
345
346    fn doc_freq(&self, field: &str, term: &str) -> StorageBackendResult<u64> {
347        self.check_retained_read()?;
348        self.doc_freq_key(field, &TokenTermKey::from_text(term))
349    }
350
351    fn doc_freq_key(&self, field: &str, term: &TokenTermKey) -> StorageBackendResult<u64> {
352        self.check_retained_read()?;
353        self.state
354            .index
355            .get(&(field.to_owned(), term.clone()))
356            .map_or(Ok(0), |postings| {
357                usize_to_u64(postings.len(), "document frequency")
358            })
359    }
360
361    fn get_doc_length(&self, doc_id: DocId, field: &str) -> StorageBackendResult<u64> {
362        self.check_retained_read()?;
363        Ok(self
364            .state
365            .documents
366            .get(&doc_id)
367            .and_then(|document| document.fields.get(field))
368            .map_or(0, |metadata| metadata.length))
369    }
370
371    fn get_term_freq(&self, doc_id: DocId, field: &str, term: &str) -> StorageBackendResult<u64> {
372        self.check_retained_read()?;
373        self.get_term_freq_key(doc_id, field, &TokenTermKey::from_text(term))
374    }
375
376    fn get_term_freq_key(
377        &self,
378        doc_id: DocId,
379        field: &str,
380        term: &TokenTermKey,
381    ) -> StorageBackendResult<u64> {
382        self.check_retained_read()?;
383        self.state
384            .index
385            .get(&(field.to_owned(), term.clone()))
386            .and_then(|postings| postings.get(&doc_id))
387            .map_or(Ok(0), |posting| {
388                usize_to_u64(posting.occurrences.len(), "term frequency")
389            })
390    }
391
392    fn doc_count(&self) -> StorageBackendResult<u64> {
393        self.check_retained_read()?;
394        Ok(self.state.doc_count)
395    }
396
397    fn total_field_length(&self, field: &str) -> StorageBackendResult<u64> {
398        self.check_retained_read()?;
399        Ok(self
400            .state
401            .field_counters
402            .get(field)
403            .map_or(0, |counters| counters.total))
404    }
405
406    fn vocabulary_terms(&self, field: &str) -> StorageBackendResult<Vec<String>> {
407        self.check_retained_read()?;
408        self.vocabulary_keys(field)?
409            .into_iter()
410            .map(|key| Ok(key.to_term().into_string()?))
411            .collect()
412    }
413
414    fn vocabulary_keys(&self, field: &str) -> StorageBackendResult<Vec<TokenTermKey>> {
415        self.check_retained_read()?;
416        Ok(self
417            .state
418            .index
419            .keys()
420            .filter(|(indexed_field, _)| indexed_field == field)
421            .map(|(_, term)| term.clone())
422            .collect())
423    }
424
425    fn stats(&self) -> StorageBackendResult<IndexStats> {
426        self.check_retained_read()?;
427        let mut s = IndexStats::default();
428        s.total_docs = self.state.doc_count;
429        if self.state.doc_count > 0 {
430            let total = checked_sum_u64(
431                self.state
432                    .field_counters
433                    .values()
434                    .map(|counters| counters.total),
435                "total document length",
436            )?;
437            s.avg_doc_length = total as f64 / self.state.doc_count as f64;
438        }
439        for ((field, term), inner) in &self.state.index {
440            let term = term.to_term();
441            let frequency = usize_to_u64(inner.len(), "document frequency")?;
442            if let Some(text) = term.as_str() {
443                s.set_doc_freq(field.clone(), text, frequency);
444            } else {
445                s.set_doc_freq_utf16(field.clone(), term.into_utf16(), frequency);
446            }
447        }
448        Ok(s)
449    }
450
451    fn posting_count(&self, field: Option<&str>) -> StorageBackendResult<u64> {
452        self.check_retained_read()?;
453        checked_sum_u64(
454            self.state
455                .index
456                .iter()
457                .filter(|((f, _), _)| field.is_none_or(|target| f == target))
458                .map(|(_, postings)| usize_to_u64(postings.len(), "posting count"))
459                .collect::<StorageBackendResult<Vec<_>>>()?,
460            "posting count",
461        )
462    }
463
464    fn doc_length_count(&self, field: Option<&str>) -> StorageBackendResult<u64> {
465        self.check_retained_read()?;
466        Ok(match field {
467            Some(target) => self
468                .state
469                .field_counters
470                .get(target)
471                .map_or(0, |counters| counters.docs),
472            None => checked_sum_u64(
473                self.state
474                    .field_counters
475                    .values()
476                    .map(|counters| counters.docs),
477                "document-length row count",
478            )?,
479        })
480    }
481
482    fn term_count(&self, field: Option<&str>) -> StorageBackendResult<u64> {
483        self.check_retained_read()?;
484        usize_to_u64(
485            self.state
486                .index
487                .keys()
488                .filter(|(f, _)| field.is_none_or(|target| f == target))
489                .map(|(_, term)| term)
490                .collect::<BTreeSet<_>>()
491                .len(),
492            "term count",
493        )
494    }
495
496    fn snapshot(&self) -> StorageBackendResult<Arc<dyn InvertedIndex>> {
497        if let Some(control) = &self.read_control {
498            return self.controlled_snapshot(control);
499        }
500        Ok(Arc::new(self.shared_snapshot()))
501    }
502
503    fn snapshot_with_control(
504        &self,
505        control: &crate::read_control::StorageReadControl,
506    ) -> StorageBackendResult<Arc<dyn InvertedIndex>> {
507        self.controlled_snapshot(control)
508    }
509
510    fn writable_snapshot(&self) -> StorageBackendResult<Box<dyn InvertedIndex>> {
511        if self.read_control.is_some() {
512            return Err(StorageBackendError::Other(
513                "cannot write a retained inverted-index snapshot".into(),
514            ));
515        }
516        Ok(Box::new(self.shared_snapshot()))
517    }
518
519    fn field_names(&self) -> StorageBackendResult<Vec<FieldName>> {
520        self.check_retained_read()?;
521        Ok(self.state.field_counters.keys().cloned().collect())
522    }
523
524    fn set_field_analyzer(
525        &mut self,
526        field: &str,
527        analyzer: Analyzer,
528        phase: AnalyzerPhase,
529    ) -> Result<(), String> {
530        let mut candidate = self.bindings.clone();
531        candidate
532            .bind(field, &analyzer, phase)
533            .map_err(|error| error.to_string())?;
534        self.validate_index_revision_change(field, &candidate)?;
535        self.bindings = candidate;
536        Ok(())
537    }
538
539    fn remove_field_analyzers(&mut self, field: &str) -> Result<(), String> {
540        let mut candidate = self.bindings.clone();
541        candidate.remove(field);
542        self.validate_index_revision_change(field, &candidate)?;
543        self.bindings = candidate;
544        Ok(())
545    }
546
547    fn get_field_analyzer(&self, field: &str) -> Analyzer {
548        self.bindings.index_configuration(field).clone()
549    }
550
551    fn get_search_analyzer(&self, field: &str) -> Analyzer {
552        self.bindings.search_configuration(field).clone()
553    }
554
555    fn index_analyzer_revision(
556        &self,
557        field: &str,
558    ) -> StorageBackendResult<Arc<uqa_analysis::CompiledAnalyzer>> {
559        self.check_retained_read()?;
560        Ok(self.bindings.index_revision(field)?)
561    }
562
563    fn search_analyzer_revision(
564        &self,
565        field: &str,
566    ) -> StorageBackendResult<Arc<uqa_analysis::CompiledAnalyzer>> {
567        self.check_retained_read()?;
568        Ok(self.bindings.search_revision(field)?)
569    }
570
571    fn set_field_analyzer_revision(
572        &mut self,
573        field: &str,
574        revision: Arc<uqa_analysis::CompiledAnalyzer>,
575        phase: AnalyzerPhase,
576    ) -> Result<(), String> {
577        let mut candidate = self.bindings.clone();
578        candidate
579            .bind_revision(field, revision, phase)
580            .map_err(|error| error.to_string())?;
581        self.validate_index_revision_change(field, &candidate)?;
582        self.bindings = candidate;
583        Ok(())
584    }
585
586    fn set_field_analyzer_revisions(
587        &mut self,
588        field: &str,
589        index: Arc<uqa_analysis::CompiledAnalyzer>,
590        search: Arc<uqa_analysis::CompiledAnalyzer>,
591    ) -> Result<(), String> {
592        let mut candidate = self.bindings.clone();
593        candidate
594            .bind_revisions(field, index, search)
595            .map_err(|error| error.to_string())?;
596        self.validate_index_revision_change(field, &candidate)?;
597        self.bindings = candidate;
598        Ok(())
599    }
600
601    fn rebuild_with_analyzer_revision(
602        &mut self,
603        field: &str,
604        revision: Arc<uqa_analysis::CompiledAnalyzer>,
605        phase: AnalyzerPhase,
606        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
607    ) -> StorageBackendResult<()> {
608        let mut replacement = Self::with_bindings(self.bindings.clone());
609        replacement
610            .set_field_analyzer_revision(field, revision, phase)
611            .map_err(crate::StorageBackendError::Other)?;
612        replacement.try_rebuild_documents(documents)?;
613        *self = replacement;
614        Ok(())
615    }
616
617    fn rebuild_with_analyzer_revision_cancellable(
618        &mut self,
619        field: &str,
620        revision: Arc<uqa_analysis::CompiledAnalyzer>,
621        phase: AnalyzerPhase,
622        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
623        cancellation: &uqa_core::CancellationToken,
624    ) -> StorageBackendResult<()> {
625        cancellation.check()?;
626        let mut replacement = Self::with_bindings(self.bindings.clone());
627        replacement
628            .set_field_analyzer_revision(field, revision, phase)
629            .map_err(crate::StorageBackendError::Other)?;
630        replacement.rebuild_documents_inner(documents, Some(cancellation))?;
631        *self = replacement;
632        Ok(())
633    }
634}
635
636impl MemoryInvertedIndex {
637    fn rebuild_documents_inner(
638        &mut self,
639        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
640        cancellation: Option<&uqa_core::CancellationToken>,
641    ) -> StorageBackendResult<()> {
642        let mut replacement = Self::with_bindings(self.bindings.clone());
643        for (doc_id, fields) in documents {
644            if let Some(cancellation) = cancellation {
645                cancellation.check()?;
646            }
647            if !fields.is_empty() {
648                let staged = replacement.stage_document_inner(doc_id, fields, cancellation)?;
649                let plan = replacement.state.plan_replacement(doc_id, &staged.fields)?;
650                Arc::make_mut(&mut replacement.state).apply_replacement(doc_id, staged, plan)?;
651            }
652        }
653        if let Some(cancellation) = cancellation {
654            cancellation.check()?;
655        }
656        *self = replacement;
657        Ok(())
658    }
659
660    fn validate_index_revision_change(
661        &self,
662        field: &str,
663        candidate: &AnalyzerBindings,
664    ) -> Result<(), String> {
665        if self
666            .state
667            .field_counters
668            .get(field)
669            .is_some_and(|counters| counters.docs > 0)
670        {
671            let current = self
672                .bindings
673                .index_revision(field)
674                .map_err(|error| error.to_string())?;
675            let proposed = candidate
676                .index_revision(field)
677                .map_err(|error| error.to_string())?;
678            if current.descriptor().fingerprint() != proposed.descriptor().fingerprint() {
679                return Err(format!("field `{field}` has indexed documents; changing its index analyzer requires an atomic source rebuild"));
680            }
681        }
682        Ok(())
683    }
684}