Skip to main content

uqa_storage_sqlite/inverted_index/
trait_impl.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `InvertedIndex` trait implementation and read/statistics surface.
8
9use super::queries::project_postings;
10use super::{
11    clustered_result, decode_index_u64, encode_index_u64, invalidate_posting_accelerators, params,
12    params_from_iter, quote_ident, Analyzer, AnalyzerPhase, Arc, BTreeMap, BlockMaxScorer, DocId,
13    FieldName, IndexStats, InvertedIndex, OptionalExtension, PostingCursor, PostingList,
14    SQLiteError, SQLiteInvertedIndex, SqlValue, StorageBackendResult,
15};
16use super::{IndexedFieldMetadata, TokenTermKey};
17use uqa_storage::clustered_postings::{cluster_id, decode_all_scores, OccurrencePosting};
18
19impl InvertedIndex for SQLiteInvertedIndex {
20    fn visit_score_clusters(
21        &self,
22        field: &str,
23        term: &TokenTermKey,
24        after: Option<u64>,
25        limit: usize,
26        control: &uqa_storage::read_control::StorageReadControl,
27        visit: &mut uqa_storage::clustered_postings::ScoreClusterVisitor<'_>,
28    ) -> StorageBackendResult<()> {
29        Ok(self.visit_clusters_budgeted(field, term, after, limit, control, visit)?)
30    }
31
32    fn get_occurrences_budgeted(
33        &self,
34        doc_id: DocId,
35        field: &str,
36        term: &TokenTermKey,
37        control: &uqa_storage::read_control::StorageReadControl,
38    ) -> StorageBackendResult<uqa_core::memory::Budgeted<Vec<uqa_core::TokenOccurrence>>> {
39        Ok(self.occurrences_budgeted(doc_id, field, term, control)?)
40    }
41
42    fn field_stats_scalar_budgeted(
43        &self,
44        field: &str,
45        control: &uqa_storage::read_control::StorageReadControl,
46    ) -> StorageBackendResult<IndexStats> {
47        Ok(self.scalar_stats_budgeted(field, control)?)
48    }
49
50    fn analyzer(&self) -> &Analyzer {
51        self.bindings.default_configuration()
52    }
53
54    fn add_document(
55        &mut self,
56        doc_id: DocId,
57        fields: BTreeMap<FieldName, String>,
58    ) -> StorageBackendResult<()> {
59        Ok(self.add_document_inner(doc_id, fields)?)
60    }
61
62    fn try_add_documents(
63        &mut self,
64        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
65    ) -> StorageBackendResult<()> {
66        Ok(self.add_documents_inner(documents)?)
67    }
68
69    fn remove_document(&mut self, doc_id: DocId) -> StorageBackendResult<()> {
70        Ok(self.remove_document_inner(doc_id)?)
71    }
72
73    fn clear(&mut self) -> StorageBackendResult<()> {
74        self.conn.with_mut(|conn| {
75            let tx = conn.savepoint()?;
76            invalidate_posting_accelerators(&tx, &self.table)?;
77            self.clear_index_on(&tx)?;
78            tx.commit()?;
79            Ok(())
80        })?;
81        Ok(())
82    }
83
84    fn source_rebuild_required(&self) -> StorageBackendResult<bool> {
85        Ok(self.conn.with(|conn| self.needs_source_rebuild_on(conn))?)
86    }
87
88    fn try_rebuild_documents(
89        &mut self,
90        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
91    ) -> StorageBackendResult<()> {
92        Ok(self.rebuild_documents_inner(documents)?)
93    }
94
95    fn try_rebuild_documents_cancellable(
96        &mut self,
97        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
98        cancellation: &uqa_core::CancellationToken,
99    ) -> StorageBackendResult<()> {
100        Ok(self.rebuild_documents_with_cancellation(documents, Some(cancellation))?)
101    }
102
103    fn get_posting_list(&self, field: &str, term: &str) -> StorageBackendResult<PostingList> {
104        self.get_posting_list_key(field, &TokenTermKey::from_text(term))
105    }
106
107    fn get_posting_list_key(
108        &self,
109        field: &str,
110        term: &TokenTermKey,
111    ) -> StorageBackendResult<PostingList> {
112        Ok(project_postings(self.get_occurrence_postings(field, term)?))
113    }
114
115    fn get_occurrence_postings(
116        &self,
117        field: &str,
118        term: &TokenTermKey,
119    ) -> StorageBackendResult<Vec<OccurrencePosting>> {
120        Ok(self
121            .occurrence_postings_bulk(field, std::slice::from_ref(term))?
122            .pop()
123            .unwrap_or_default())
124    }
125
126    fn get_occurrences(
127        &self,
128        doc_id: DocId,
129        field: &str,
130        term: &TokenTermKey,
131    ) -> StorageBackendResult<Vec<uqa_core::TokenOccurrence>> {
132        Ok(self.conn.with(|conn| {
133            self.require_graph_format_on(conn)?;
134            let entries = super::load_cluster(conn, &self.table, field, term, cluster_id(doc_id))?;
135            let Some(posting) = entries.into_iter().find(|entry| entry.doc_id == doc_id) else {
136                return Ok(Vec::new());
137            };
138            self.validate_posting_metadata_on(conn, field, &posting)?;
139            Ok(posting.occurrences)
140        })?)
141    }
142
143    fn indexed_field_metadata(
144        &self,
145        doc_id: DocId,
146        field: &str,
147    ) -> StorageBackendResult<Option<IndexedFieldMetadata>> {
148        let doc_id = encode_index_u64("document", doc_id)?;
149        Ok(self.conn.with(|conn| {
150            self.require_graph_format_on(conn)?;
151            self.read_field_metadata_on(conn, doc_id, field)
152        })?)
153    }
154
155    fn get_posting_lists_bulk(
156        &self,
157        field: &str,
158        terms: &[String],
159    ) -> StorageBackendResult<Vec<PostingList>> {
160        let keys = terms
161            .iter()
162            .map(|term| TokenTermKey::from_text(term))
163            .collect::<Vec<_>>();
164        Ok(self
165            .occurrence_postings_bulk(field, &keys)?
166            .into_iter()
167            .map(project_postings)
168            .collect())
169    }
170
171    fn posting_cursor(
172        &self,
173        field: &str,
174        term: &str,
175    ) -> StorageBackendResult<Box<dyn PostingCursor>> {
176        self.posting_cursor_key(field, &TokenTermKey::from_text(term))
177    }
178
179    fn posting_cursor_key(
180        &self,
181        field: &str,
182        term: &TokenTermKey,
183    ) -> StorageBackendResult<Box<dyn PostingCursor>> {
184        self.cursor_for_term(field, term)
185    }
186
187    fn posting_cursors_bulk(
188        &self,
189        field: &str,
190        terms: &[String],
191    ) -> StorageBackendResult<Vec<Box<dyn PostingCursor>>> {
192        let keys = terms
193            .iter()
194            .map(|term| TokenTermKey::from_text(term))
195            .collect::<Vec<_>>();
196        self.cursors_for_terms(field, &keys)
197    }
198
199    fn posting_cursors_keys_bulk(
200        &self,
201        field: &str,
202        terms: &[TokenTermKey],
203    ) -> StorageBackendResult<Vec<Box<dyn PostingCursor>>> {
204        self.cursors_for_terms(field, terms)
205    }
206
207    fn get_posting_lists_keys_bulk(
208        &self,
209        field: &str,
210        terms: &[TokenTermKey],
211    ) -> StorageBackendResult<Vec<PostingList>> {
212        Ok(self
213            .occurrence_postings_bulk(field, terms)?
214            .into_iter()
215            .map(project_postings)
216            .collect())
217    }
218
219    fn persisted_block_max_scores_keys_bulk(
220        &self,
221        field: &str,
222        terms: &[TokenTermKey],
223        scorer_fingerprint: &str,
224    ) -> StorageBackendResult<Vec<Option<Vec<f64>>>> {
225        if scorer_fingerprint.is_empty() {
226            return Ok(vec![None; terms.len()]);
227        }
228        self.get_versioned_block_max_scores_keys_bulk(field, terms, scorer_fingerprint)
229    }
230
231    fn rebuild_persisted_block_max(
232        &mut self,
233        field: &str,
234        scorer: &dyn BlockMaxScorer,
235        scorer_fingerprint: &str,
236    ) -> StorageBackendResult<bool> {
237        if scorer_fingerprint.is_empty() {
238            return Err(SQLiteError::StorageBackend(
239                "persisted block-max scorer fingerprint must not be empty".into(),
240            )
241            .into());
242        }
243        let terms = self.vocabulary_keys(field)?;
244        self.ensure_aux_tables(field)?;
245        let table = self.blockmax_table_name(field);
246        self.conn.with_mut(|conn| {
247            conn.execute(&format!("DELETE FROM {}", quote_ident(&table)), [])?;
248            Ok(())
249        })?;
250        for term in terms {
251            self.build_block_max_scores_key(field, &term, scorer, scorer_fingerprint)?;
252        }
253        Ok(true)
254    }
255
256    fn persisted_block_max_scores(
257        &self,
258        field: &str,
259        term: &str,
260        scorer_fingerprint: &str,
261    ) -> StorageBackendResult<Option<Vec<f64>>> {
262        if scorer_fingerprint.is_empty() {
263            return Ok(None);
264        }
265        self.get_versioned_block_max_scores(field, term, scorer_fingerprint)
266    }
267
268    fn persisted_block_max_scores_bulk(
269        &self,
270        field: &str,
271        terms: &[String],
272        scorer_fingerprint: &str,
273    ) -> StorageBackendResult<Vec<Option<Vec<f64>>>> {
274        if scorer_fingerprint.is_empty() {
275            return Ok(vec![None; terms.len()]);
276        }
277        self.get_versioned_block_max_scores_bulk(field, terms, scorer_fingerprint)
278    }
279
280    fn for_each_term_freq(
281        &self,
282        field: &str,
283        term: &str,
284        visit: &mut dyn FnMut(DocId, u64),
285    ) -> StorageBackendResult<()> {
286        let mut cursor = self.posting_cursor(field, term)?;
287        while let Some(entry) = cursor.current() {
288            visit(entry.doc_id, entry.term_freq);
289            cursor.advance()?;
290        }
291        Ok(())
292    }
293
294    fn doc_freq(&self, field: &str, term: &str) -> StorageBackendResult<u64> {
295        self.doc_freq_key(field, &TokenTermKey::from_text(term))
296    }
297
298    fn doc_freq_key(&self, field: &str, term: &TokenTermKey) -> StorageBackendResult<u64> {
299        Ok(self.posting_cursor_key(field, term)?.doc_freq())
300    }
301
302    fn get_doc_length(&self, doc_id: DocId, field: &str) -> StorageBackendResult<u64> {
303        Ok(self
304            .get_doc_lengths_bulk(&[doc_id], field)?
305            .get(&doc_id)
306            .copied()
307            .unwrap_or(0))
308    }
309
310    fn get_doc_lengths_bulk(
311        &self,
312        doc_ids: &[DocId],
313        field: &str,
314    ) -> StorageBackendResult<BTreeMap<DocId, u64>> {
315        Ok(self.conn.with(|conn| {
316            self.require_graph_format_on(conn)?;
317            let mut out = BTreeMap::new();
318            for chunk in doc_ids.chunks(900) {
319                let ids = (3..chunk.len()+3).map(|parameter| format!("?{parameter}")).collect::<Vec<_>>().join(", ");
320                let sql = format!("WITH lengths AS (SELECT doc_id, length FROM _occurrence_lengths WHERE table_name = ?1 AND field = ?2 AND doc_id IN ({ids})), documents AS (SELECT doc_id, metadata_blob FROM _occurrence_documents WHERE table_name = ?1 AND field = ?2 AND doc_id IN ({ids})) SELECT COALESCE(lengths.doc_id, documents.doc_id), length, metadata_blob FROM lengths FULL OUTER JOIN documents USING(doc_id)");
321                let mut values = vec![SqlValue::Text(self.table.clone()), SqlValue::Text(field.into())];
322                for doc_id in chunk {
323                    values.push(SqlValue::Integer(encode_index_u64("document", *doc_id)?));
324                }
325                let mut statement = conn.prepare(&sql)?;
326                let rows = statement.query_map(params_from_iter(values), |row| Ok((row.get::<_, i64>(0)?, row.get::<_, Option<i64>>(1)?, row.get::<_, Option<Vec<u8>>>(2)?)))?;
327                for row in rows {
328                    let (doc_id, length, metadata) = row?;
329                    let doc_id = decode_index_u64("document id", doc_id)?;
330                    let length = length.map(|length| decode_index_u64("document length", length)).transpose()?;
331                    let metadata = metadata.map(|bytes| clustered_result(IndexedFieldMetadata::from_bytes(&bytes))).transpose()?;
332                    let (Some(length), Some(metadata)) = (length, metadata) else {
333                        return Err(SQLiteError::StorageBackend("indexed field length and source metadata disagree".into()));
334                    };
335                    let stats = self.stored_field_stats_on(conn, field)?.ok_or_else(|| SQLiteError::StorageBackend("indexed field revision is missing".into()))?;
336                    if metadata.length != length || metadata.revision() != stats.revision {
337                        return Err(SQLiteError::StorageBackend("indexed field length or revision disagrees with source metadata".into()));
338                    }
339                    out.insert(doc_id, length);
340                }
341            }
342            Ok(out)
343        })?)
344    }
345
346    fn get_scoring_inputs_bulk(
347        &self,
348        doc_ids: &[DocId],
349        field: &str,
350        terms: &[String],
351    ) -> StorageBackendResult<Vec<(u64, Vec<u64>)>> {
352        let keys = terms
353            .iter()
354            .map(|term| TokenTermKey::from_text(term))
355            .collect::<Vec<_>>();
356        self.get_scoring_inputs_keys_bulk(doc_ids, field, &keys)
357    }
358
359    fn get_scoring_inputs_keys_bulk(
360        &self,
361        doc_ids: &[DocId],
362        field: &str,
363        terms: &[TokenTermKey],
364    ) -> StorageBackendResult<Vec<(u64, Vec<u64>)>> {
365        if doc_ids.is_empty() {
366            return Ok(Vec::new());
367        }
368
369        let doc_lengths = self.get_doc_lengths_bulk(doc_ids, field)?;
370        let mut inputs: Vec<(u64, Vec<u64>)> = doc_ids
371            .iter()
372            .map(|doc_id| {
373                (
374                    doc_lengths.get(doc_id).copied().unwrap_or(0),
375                    vec![0; terms.len()],
376                )
377            })
378            .collect();
379        if terms.is_empty() {
380            return Ok(inputs);
381        }
382
383        let mut output_positions = BTreeMap::<DocId, Vec<usize>>::new();
384        for (position, doc_id) in doc_ids.iter().copied().enumerate() {
385            output_positions.entry(doc_id).or_default().push(position);
386        }
387        for (term_index, mut cursor) in self
388            .posting_cursors_keys_bulk(field, terms)?
389            .into_iter()
390            .enumerate()
391        {
392            while let Some(entry) = cursor.current() {
393                if let Some(positions) = output_positions.get(&entry.doc_id) {
394                    for position in positions {
395                        inputs[*position].1[term_index] = entry.term_freq;
396                        inputs[*position].0 = entry.doc_length;
397                    }
398                }
399                cursor.advance()?;
400            }
401        }
402        Ok(inputs)
403    }
404
405    fn get_term_freq(&self, doc_id: DocId, field: &str, term: &str) -> StorageBackendResult<u64> {
406        self.get_term_freq_key(doc_id, field, &TokenTermKey::from_text(term))
407    }
408
409    fn get_term_freq_key(
410        &self,
411        doc_id: DocId,
412        field: &str,
413        term: &TokenTermKey,
414    ) -> StorageBackendResult<u64> {
415        let cluster = encode_index_u64("posting cluster", cluster_id(doc_id))?;
416        Ok(self.conn.with(|conn| {
417            self.require_graph_format_on(conn)?;
418            let row: Option<(i64, Vec<u8>)> = conn.query_row("SELECT posting_count, score_blob FROM _occurrence_clusters WHERE table_name = ?1 AND field = ?2 AND term = ?3 AND cluster_id = ?4", params![self.table, field, term.as_bytes(), cluster], |row| Ok((row.get(0)?, row.get(1)?))).optional()?;
419            match row {
420                Some((count, bytes)) => {
421                    // Validate the version and redundant row count before decoding only the requested cluster.
422                    super::posting_cursor_from_rows(vec![(cluster, count, bytes.clone())])?;
423                    let scores = clustered_result(decode_all_scores(cluster_id(doc_id), &bytes))?;
424                    Ok(scores.binary_search_by_key(&doc_id, |entry| entry.doc_id).ok().map_or(0, |position| scores[position].term_freq))
425                }
426                None => Ok(0),
427            }
428        })?)
429    }
430
431    fn doc_count(&self) -> StorageBackendResult<u64> {
432        Ok(self.conn.with(|c| {
433            self.require_graph_format_on(c)?;
434            let n: i64 = c.query_row(
435                "SELECT COUNT(DISTINCT doc_id) FROM _occurrence_lengths
436                     WHERE table_name = ?1",
437                params![self.table],
438                |r| r.get(0),
439            )?;
440            decode_index_u64("document count", n)
441        })?)
442    }
443
444    fn total_field_length(&self, field: &str) -> StorageBackendResult<u64> {
445        Ok(self.conn.with(|conn| {
446            self.require_graph_format_on(conn)?;
447            Ok(self
448                .stored_field_stats_on(conn, field)?
449                .map_or(0, |stats| stats.total_length))
450        })?)
451    }
452
453    fn vocabulary_terms(&self, field: &str) -> StorageBackendResult<Vec<String>> {
454        self.terms_for_field(field)
455    }
456
457    fn vocabulary_keys(&self, field: &str) -> StorageBackendResult<Vec<TokenTermKey>> {
458        Ok(self.conn.with(|conn| {
459            self.require_graph_format_on(conn)?;
460            let mut statement = conn.prepare("SELECT DISTINCT term FROM _occurrence_clusters WHERE table_name = ?1 AND field = ?2 ORDER BY term")?;
461            let rows = statement.query_map(params![self.table, field], |row| row.get::<_, Vec<u8>>(0))?;
462            rows.map(|row| clustered_result(TokenTermKey::from_bytes(row?))).collect()
463        })?)
464    }
465
466    fn field_doc_count(&self, field: &str) -> StorageBackendResult<u64> {
467        Ok(self.conn.with(|conn| {
468            self.require_graph_format_on(conn)?;
469            Ok(self
470                .stored_field_stats_on(conn, field)?
471                .map_or(0, |stats| stats.doc_count))
472        })?)
473    }
474
475    fn stats(&self) -> StorageBackendResult<IndexStats> {
476        let doc_count = self.doc_count()?;
477        let mut s = IndexStats::default();
478        s.total_docs = doc_count;
479        if doc_count > 0 {
480            let total: u64 = self.conn.with(|c| {
481                self.require_graph_format_on(c)?;
482                let n: i64 = c.query_row(
483                    "SELECT COALESCE(SUM(total_length), 0) FROM _occurrence_fields
484                         WHERE table_name = ?1",
485                    params![self.table],
486                    |r| r.get(0),
487                )?;
488                decode_index_u64("total indexed length", n)
489            })?;
490            s.avg_doc_length = total as f64 / doc_count as f64;
491        }
492        let pairs = self
493            .conn
494            .with(|conn| self.term_frequencies_on(conn, None))?;
495        for ((field, term), df) in pairs {
496            let term = term.to_term();
497            if let Some(text) = term.as_str() {
498                s.set_doc_freq(field, text, df);
499            } else {
500                s.set_doc_freq_utf16(field, term.into_utf16(), df);
501            }
502        }
503        Ok(s)
504    }
505
506    fn posting_count(&self, field: Option<&str>) -> StorageBackendResult<u64> {
507        Ok(self.conn.with(|conn| {
508            self.term_frequencies_on(conn, field)?
509                .into_values()
510                .try_fold(0_u64, |total, count| {
511                    total
512                        .checked_add(count)
513                        .ok_or_else(|| SQLiteError::StorageBackend("posting count overflow".into()))
514                })
515        })?)
516    }
517
518    fn doc_length_count(&self, field: Option<&str>) -> StorageBackendResult<u64> {
519        Ok(self.conn.with(|c| {
520            self.require_graph_format_on(c)?;
521            let n: i64 = if let Some(field) = field {
522                c.query_row(
523                    "SELECT COUNT(*) FROM _occurrence_lengths
524                         WHERE table_name = ?1 AND field = ?2",
525                    params![self.table, field],
526                    |r| r.get(0),
527                )?
528            } else {
529                c.query_row(
530                    "SELECT COUNT(*) FROM _occurrence_lengths WHERE table_name = ?1",
531                    params![self.table],
532                    |r| r.get(0),
533                )?
534            };
535            decode_index_u64("document length count", n)
536        })?)
537    }
538
539    fn term_count(&self, field: Option<&str>) -> StorageBackendResult<u64> {
540        Ok(self.conn.with(|conn| {
541            let terms = self
542                .term_frequencies_on(conn, field)?
543                .into_keys()
544                .map(|(_, term)| term)
545                .collect::<std::collections::BTreeSet<_>>();
546            Ok(terms.len() as u64)
547        })?)
548    }
549
550    fn snapshot(&self) -> StorageBackendResult<Arc<dyn InvertedIndex>> {
551        Ok(Arc::new(self.clone()))
552    }
553
554    fn field_names(&self) -> StorageBackendResult<Vec<FieldName>> {
555        Ok(self.conn.with(|c| {
556            self.require_graph_format_on(c)?;
557            let mut stmt =
558                c.prepare("SELECT DISTINCT field FROM _occurrence_lengths WHERE table_name = ?1")?;
559            let rows = stmt.query_map([&self.table], |row| row.get::<_, String>(0))?;
560            let mut fields = Vec::new();
561            for row in rows {
562                fields.push(row?);
563            }
564            Ok(fields)
565        })?)
566    }
567
568    fn set_field_analyzer(
569        &mut self,
570        field: &str,
571        analyzer: Analyzer,
572        phase: AnalyzerPhase,
573    ) -> Result<(), String> {
574        let mut candidate = self.bindings.clone();
575        candidate
576            .bind(field, &analyzer, phase)
577            .map_err(|error| error.to_string())?;
578        self.validate_index_revision_change(field, &candidate)
579            .map_err(|error| error.to_string())?;
580        self.bindings = candidate;
581        Ok(())
582    }
583
584    fn remove_field_analyzers(&mut self, field: &str) -> Result<(), String> {
585        let mut candidate = self.bindings.clone();
586        candidate.remove(field);
587        self.validate_index_revision_change(field, &candidate)
588            .map_err(|error| error.to_string())?;
589        self.bindings = candidate;
590        Ok(())
591    }
592
593    fn get_field_analyzer(&self, field: &str) -> Analyzer {
594        self.bindings.index_configuration(field).clone()
595    }
596    fn get_search_analyzer(&self, field: &str) -> Analyzer {
597        self.bindings.search_configuration(field).clone()
598    }
599    fn index_analyzer_revision(
600        &self,
601        field: &str,
602    ) -> StorageBackendResult<Arc<uqa_analysis::CompiledAnalyzer>> {
603        Ok(self.bindings.index_revision(field)?)
604    }
605    fn search_analyzer_revision(
606        &self,
607        field: &str,
608    ) -> StorageBackendResult<Arc<uqa_analysis::CompiledAnalyzer>> {
609        Ok(self.bindings.search_revision(field)?)
610    }
611
612    fn set_field_analyzer_revision(
613        &mut self,
614        field: &str,
615        revision: Arc<uqa_analysis::CompiledAnalyzer>,
616        phase: AnalyzerPhase,
617    ) -> Result<(), String> {
618        let mut candidate = self.bindings.clone();
619        candidate
620            .bind_revision(field, revision, phase)
621            .map_err(|error| error.to_string())?;
622        self.validate_index_revision_change(field, &candidate)
623            .map_err(|error| error.to_string())?;
624        self.bindings = candidate;
625        Ok(())
626    }
627
628    fn set_field_analyzer_revisions(
629        &mut self,
630        field: &str,
631        index: Arc<uqa_analysis::CompiledAnalyzer>,
632        search: Arc<uqa_analysis::CompiledAnalyzer>,
633    ) -> Result<(), String> {
634        let mut candidate = self.bindings.clone();
635        candidate
636            .bind_revisions(field, index, search)
637            .map_err(|error| error.to_string())?;
638        self.validate_index_revision_change(field, &candidate)
639            .map_err(|error| error.to_string())?;
640        self.bindings = candidate;
641        Ok(())
642    }
643
644    fn rebuild_with_analyzer_revision(
645        &mut self,
646        field: &str,
647        revision: Arc<uqa_analysis::CompiledAnalyzer>,
648        phase: AnalyzerPhase,
649        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
650    ) -> StorageBackendResult<()> {
651        let mut replacement = self.clone();
652        replacement.bindings.bind_revision(field, revision, phase)?;
653        replacement.rebuild_documents_inner(documents)?;
654        *self = replacement;
655        Ok(())
656    }
657
658    fn rebuild_with_analyzer_revision_cancellable(
659        &mut self,
660        field: &str,
661        revision: Arc<uqa_analysis::CompiledAnalyzer>,
662        phase: AnalyzerPhase,
663        documents: Vec<(DocId, BTreeMap<FieldName, String>)>,
664        cancellation: &uqa_core::CancellationToken,
665    ) -> StorageBackendResult<()> {
666        cancellation.check()?;
667        let mut replacement = self.clone();
668        replacement.bindings.bind_revision(field, revision, phase)?;
669        replacement.rebuild_documents_with_cancellation(documents, Some(cancellation))?;
670        *self = replacement;
671        Ok(())
672    }
673}