1use 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 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 source: &mut dyn super::TextIndexSource,
164 ) -> StorageBackendResult<()> {
165 self.rebuild_documents_inner(source, None)
166 }
167
168 fn try_rebuild_documents_cancellable(
169 &mut self,
170 source: &mut dyn super::TextIndexSource,
171 cancellation: &uqa_core::CancellationToken,
172 ) -> StorageBackendResult<()> {
173 self.rebuild_documents_inner(source, 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 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 source: &mut dyn super::TextIndexSource,
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(source)?;
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 source: &mut dyn super::TextIndexSource,
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(source, Some(cancellation))?;
631 *self = replacement;
632 Ok(())
633 }
634}
635
636impl MemoryInvertedIndex {
637 fn rebuild_documents_inner(
638 &mut self,
639 source: &mut dyn super::TextIndexSource,
640 cancellation: Option<&uqa_core::CancellationToken>,
641 ) -> StorageBackendResult<()> {
642 let mut replacement = Self::with_bindings(self.bindings.clone());
643 while let Some((doc_id, fields)) = source.next_document()? {
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}