Skip to main content

uqa_storage/key_value/inverted_index/
accelerators.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Skip positions and scorer-versioned bounds use the canonical occurrence view and mutation boundary.
8
9use super::{
10    keys, other_error, BTreeMap, DocId, KeyValueBatch, KeyValueInvertedIndex, OccurrenceRead,
11    StorageBackendResult, TokenTermKey,
12};
13use crate::block_max_index::{BlockMaxIndex, BlockMaxScorer, DEFAULT_BLOCK_SIZE};
14use crate::key_value::{
15    codec::{decode_u64_value, u64_value},
16    occurrence_format::BlockMaxValue,
17};
18
19impl KeyValueInvertedIndex {
20    pub fn flush_skip_pointers(&self) -> StorageBackendResult<()> {
21        self.mutate(|view, batch| view.rebuild_skips(batch))
22    }
23
24    pub fn skip_to(
25        &self,
26        field: &str,
27        term: &str,
28        target: DocId,
29    ) -> StorageBackendResult<(DocId, usize)> {
30        self.read(|view| view.skip_to(field, &TokenTermKey::from_text(term), target))
31    }
32
33    pub fn build_block_max_scores_key<S: BlockMaxScorer + ?Sized>(
34        &self,
35        field: &str,
36        term: &TokenTermKey,
37        scorer: &S,
38        fingerprint: &str,
39    ) -> StorageBackendResult<()> {
40        self.mutate(|view, batch| {
41            view.build_bounds(batch, field, Some(term), scorer, fingerprint, false)
42        })
43    }
44
45    pub fn build_all_block_max_scores<S: BlockMaxScorer + ?Sized>(
46        &self,
47        field: &str,
48        scorer: &S,
49    ) -> StorageBackendResult<()> {
50        self.mutate(|view, batch| view.build_bounds(batch, field, None, scorer, "", false))
51    }
52
53    pub(super) fn rebuild_block_max(
54        &self,
55        field: &str,
56        scorer: &dyn BlockMaxScorer,
57        fingerprint: &str,
58    ) -> StorageBackendResult<bool> {
59        if fingerprint.is_empty() {
60            return Err(other_error(
61                "persisted block-max scorer fingerprint must not be empty",
62            ));
63        }
64        self.mutate(|view, batch| {
65            view.build_bounds(batch, field, None, scorer, fingerprint, true)
66        })?;
67        Ok(true)
68    }
69
70    pub fn get_block_max_score(
71        &self,
72        field: &str,
73        term: &str,
74        block: usize,
75    ) -> StorageBackendResult<f64> {
76        self.read(|view| {
77            view.require_graph_format()?;
78            let block = u64::try_from(block).map_err(|_| other_error("block index exceeds u64"))?;
79            let key = keys::cluster_key(
80                view.table,
81                keys::BLOCK_MAX,
82                field,
83                &TokenTermKey::from_text(term),
84                block,
85            )?;
86            view.store
87                .get(&key)?
88                .map_or(Ok(0.0), |bytes| Ok(BlockMaxValue::decode(&bytes)?.score))
89        })
90    }
91
92    pub fn get_all_block_max_scores_key(
93        &self,
94        field: &str,
95        term: &TokenTermKey,
96    ) -> StorageBackendResult<Vec<f64>> {
97        self.read(|view| Ok(view.bounds(field, term, None)?.unwrap_or_default()))
98    }
99
100    pub fn get_versioned_block_max_scores_keys_bulk(
101        &self,
102        field: &str,
103        terms: &[TokenTermKey],
104        fingerprint: &str,
105    ) -> StorageBackendResult<Vec<Option<Vec<f64>>>> {
106        self.read(|view| {
107            terms
108                .iter()
109                .map(|term| view.bounds(field, term, Some(fingerprint)))
110                .collect()
111        })
112    }
113
114    pub fn load_block_max_into(&self, target: &mut BlockMaxIndex) -> StorageBackendResult<()> {
115        self.read(|view| {
116            view.require_graph_format()?;
117            let prefix = keys::kind_prefix(view.table, keys::BLOCK_MAX)?;
118            let mut by_term = BTreeMap::<(String, TokenTermKey), Vec<f64>>::new();
119            view.store.visit_prefix(&prefix, &mut |key, bytes| {
120                view.store.control().check()?;
121                let (field, term, ordinal) = keys::read_cluster(key, keys::BLOCK_MAX)?;
122                let scores = by_term.entry((field, term)).or_default();
123                if ordinal != scores.len() as u64 {
124                    return Err(other_error("non-contiguous occurrence block-max bounds"));
125                }
126                scores.push(BlockMaxValue::decode(bytes)?.score);
127                Ok(())
128            })?;
129            for ((field, term), scores) in by_term {
130                target.set_block_maxes_key(view.table, &field, &term, scores)?;
131            }
132            Ok(())
133        })
134    }
135}
136
137impl OccurrenceRead<'_> {
138    pub(super) fn invalidate_accelerators(
139        &self,
140        batch: &mut dyn KeyValueBatch,
141    ) -> StorageBackendResult<()> {
142        for kind in [keys::SKIP, keys::BLOCK_MAX] {
143            batch.invalidate_occurrence_prefix(&keys::kind_prefix(self.table, kind)?)?;
144        }
145        Ok(())
146    }
147
148    fn fence_accelerators(&self, batch: &mut dyn KeyValueBatch) -> StorageBackendResult<()> {
149        // Canonical writes also write this format record. A build or invalidation based on an older source cannot publish after the competing change, including caches absent from its original view.
150        batch.put(
151            &keys::kind_prefix(self.table, keys::FORMAT)?,
152            keys::FORMAT_NAME,
153        )
154    }
155
156    fn rebuild_skips(&self, batch: &mut dyn KeyValueBatch) -> StorageBackendResult<()> {
157        self.require_graph_format()?;
158        batch.delete_prefix(&keys::kind_prefix(self.table, keys::SKIP)?)?;
159        let fields = self.field_names()?;
160        for field in &fields {
161            for term in self.vocabulary_keys(field)? {
162                let mut cursor = self.posting_cursor_key(field, &term)?;
163                while let Some(entry) = cursor.current() {
164                    self.store.control().check()?;
165                    let offset = cursor.ordinal();
166                    if offset % DEFAULT_BLOCK_SIZE as u64 == 0 {
167                        batch.put(
168                            &keys::cluster_key(self.table, keys::SKIP, field, &term, entry.doc_id)?,
169                            &u64_value(offset),
170                        )?;
171                    }
172                    cursor.advance()?;
173                }
174            }
175        }
176        if !fields.is_empty() {
177            self.fence_accelerators(batch)?;
178        }
179        Ok(())
180    }
181
182    fn skip_to(
183        &self,
184        field: &str,
185        term: &TokenTermKey,
186        target: DocId,
187    ) -> StorageBackendResult<(DocId, usize)> {
188        self.require_graph_format()?;
189        let prefix = keys::term_prefix(self.table, keys::SKIP, field, term)?;
190        let mut found = (0, 0);
191        self.store.visit_prefix(&prefix, &mut |key, bytes| {
192            self.store.control().check()?;
193            let (_, _, doc_id) = keys::read_cluster(key, keys::SKIP)?;
194            let offset = usize::try_from(decode_u64_value(bytes)?)
195                .map_err(|_| other_error("skip offset exceeds usize"))?;
196            if doc_id <= target {
197                found = (doc_id, offset);
198            }
199            Ok(())
200        })?;
201        Ok(found)
202    }
203
204    fn build_bounds<S: BlockMaxScorer + ?Sized>(
205        &self,
206        batch: &mut dyn KeyValueBatch,
207        field: &str,
208        term: Option<&TokenTermKey>,
209        scorer: &S,
210        fingerprint: &str,
211        replace: bool,
212    ) -> StorageBackendResult<()> {
213        self.require_graph_format()?;
214        if replace {
215            batch.delete_prefix(&keys::field_prefix(self.table, keys::BLOCK_MAX, field)?)?;
216        }
217        if self.field_doc_count(field)? == 0 {
218            return Ok(());
219        }
220        if let Some(term) = term {
221            self.build_term_bounds(batch, field, term, scorer, fingerprint)?;
222        } else {
223            for term in self.vocabulary_keys(field)? {
224                self.build_term_bounds(batch, field, &term, scorer, fingerprint)?;
225            }
226        }
227        self.fence_accelerators(batch)
228    }
229
230    fn build_term_bounds<S: BlockMaxScorer + ?Sized>(
231        &self,
232        batch: &mut dyn KeyValueBatch,
233        field: &str,
234        term: &TokenTermKey,
235        scorer: &S,
236        fingerprint: &str,
237    ) -> StorageBackendResult<()> {
238        batch.delete_prefix(&keys::term_prefix(
239            self.table,
240            keys::BLOCK_MAX,
241            field,
242            term,
243        )?)?;
244        let mut cursor = self.posting_cursor_key(field, term)?;
245        let mut count = 0usize;
246        let mut block = 0u64;
247        let mut maximum = 0.0_f64;
248        while let Some(entry) = cursor.current() {
249            self.store.control().check()?;
250            let score = scorer.score(entry.term_freq, entry.doc_length, cursor.doc_freq());
251            crate::block_max_index::validate_score(score)?;
252            maximum = maximum.max(score);
253            count += 1;
254            if count == DEFAULT_BLOCK_SIZE {
255                self.put_bound(batch, field, term, block, maximum, fingerprint)?;
256                block = block
257                    .checked_add(1)
258                    .ok_or_else(|| other_error("block index overflow"))?;
259                count = 0;
260                maximum = 0.0;
261            }
262            cursor.advance()?;
263        }
264        if count != 0 {
265            self.put_bound(batch, field, term, block, maximum, fingerprint)?;
266        }
267        Ok(())
268    }
269
270    fn put_bound(
271        &self,
272        batch: &mut dyn KeyValueBatch,
273        field: &str,
274        term: &TokenTermKey,
275        block: u64,
276        score: f64,
277        fingerprint: &str,
278    ) -> StorageBackendResult<()> {
279        let value = BlockMaxValue { score, fingerprint }.encode(self.store.control())?;
280        batch.put(
281            &keys::cluster_key(self.table, keys::BLOCK_MAX, field, term, block)?,
282            &value,
283        )
284    }
285
286    fn bounds(
287        &self,
288        field: &str,
289        term: &TokenTermKey,
290        fingerprint: Option<&str>,
291    ) -> StorageBackendResult<Option<Vec<f64>>> {
292        self.require_graph_format()?;
293        let prefix = keys::term_prefix(self.table, keys::BLOCK_MAX, field, term)?;
294        let mut scores = Vec::new();
295        let mut matches = true;
296        self.store.visit_prefix(&prefix, &mut |key, bytes| {
297            self.store.control().check()?;
298            let (_, _, block) = keys::read_cluster(key, keys::BLOCK_MAX)?;
299            if block != scores.len() as u64 {
300                return Err(other_error("non-contiguous occurrence block-max bounds"));
301            }
302            let bound = BlockMaxValue::decode(bytes)?;
303            matches &= fingerprint.is_none_or(|fingerprint| bound.fingerprint == fingerprint);
304            scores.push(bound.score);
305            Ok(())
306        })?;
307        Ok((matches && !scores.is_empty()).then_some(scores))
308    }
309}