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