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