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 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 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 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}