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