uqa_storage/diskann_index/
scoring.rs1use super::{format::DiskANNVectorVersion, DiskANNCanonicalRead};
10use crate::{
11 mvcc::VersionError,
12 read_control::StorageReadControl,
13 vector_index::{
14 cosine_similarity_controlled,
15 query::{postings_from_unique_scores, VectorQueryBuffer},
16 validate_threshold, validate_vector_values_controlled,
17 },
18 StorageBackendResult,
19};
20use uqa_core::{DocId, PostingList};
21
22pub(super) mod selection;
23mod statistics;
24pub use statistics::DiskANNScoringStats;
25pub(crate) use statistics::ScoringStatsExt;
26#[cfg(test)]
27mod tests;
28
29#[derive(Debug, Clone, Copy)]
31pub struct DiskANNDocumentScore {
32 document: DocId,
33 version: DiskANNVectorVersion,
34 vectors: u64,
35 score: f32,
36}
37
38impl DiskANNDocumentScore {
39 pub fn document(&self) -> DocId {
41 self.document
42 }
43 pub fn version(&self) -> DiskANNVectorVersion {
45 self.version
46 }
47 pub fn vector_count(&self) -> u64 {
49 self.vectors
50 }
51 pub fn raw_cosine(&self) -> f32 {
53 self.score
54 }
55}
56
57pub struct DiskANNCanonicalScorer<'a> {
59 source: &'a dyn DiskANNCanonicalRead,
60 query: &'a [f32],
61 control: &'a StorageReadControl,
62}
63
64impl<'a> DiskANNCanonicalScorer<'a> {
65 pub fn new(
66 source: &'a dyn DiskANNCanonicalRead,
67 query: &'a [f32],
68 control: &'a StorageReadControl,
69 ) -> StorageBackendResult<Self> {
70 source.check_control(control)?;
71 if source.dimensions() == 0 {
72 return Err(invalid("canonical scoring requires positive dimensions"));
73 }
74 validate_vector_values_controlled(source.dimensions(), query, Some(control))?;
75 source.check_control(control)?;
76 Ok(Self {
77 source,
78 query,
79 control,
80 })
81 }
82
83 pub fn score_document(
85 &self,
86 document: DocId,
87 ) -> StorageBackendResult<Option<DiskANNDocumentScore>> {
88 self.document(document).map(|(_, score)| score)
89 }
90
91 pub fn score_candidate(
93 &self,
94 document: DocId,
95 ordinal: u32,
96 version: DiskANNVectorVersion,
97 ) -> StorageBackendResult<Option<DiskANNDocumentScore>> {
98 self.source.check_control(self.control)?;
99 if self.source.origin(document, self.control)? != Some(version) {
100 self.source.check_control(self.control)?;
101 return Ok(None);
102 }
103 let (actual, score) = self.document(document)?;
104 if actual != Some(version) {
105 return Err(invalid("canonical origin changed on a retained source"));
106 }
107 let score = score
108 .filter(|score| u64::from(ordinal) < score.vectors)
109 .ok_or_else(|| invalid("candidate ordinal is outside its canonical tensor"))?;
110 Ok(Some(score))
111 }
112
113 pub fn visit_scores(
115 &self,
116 visit: &mut dyn FnMut(DiskANNDocumentScore) -> StorageBackendResult<()>,
117 ) -> StorageBackendResult<()> {
118 self.source.check_control(self.control)?;
119 let mut after = None;
120 while let Some(document) = self.source.next_document_after(after, self.control)? {
121 if after.is_some_and(|previous| document <= previous) {
122 return Err(invalid("canonical scoring cursor did not advance"));
123 }
124 let (version, score) = self.document(document)?;
125 if version.is_none() {
126 return Err(invalid("enumerated canonical document has no origin"));
127 }
128 if let Some(score) = score {
129 visit(score)?;
130 }
131 after = Some(document);
132 }
133 self.source.check_control(self.control)
134 }
135
136 pub fn search_exact_knn(&self, k: usize) -> StorageBackendResult<PostingList> {
138 self.search_exact_knn_with_stats(k)
139 .map(|(postings, _)| postings)
140 }
141
142 pub fn search_exact_knn_with_stats(
144 &self,
145 k: usize,
146 ) -> StorageBackendResult<(PostingList, DiskANNScoringStats)> {
147 self.source.check_control(self.control)?;
148 let mut stats = DiskANNScoringStats::default();
149 if k == 0 {
150 return Ok((PostingList::new(), stats));
151 }
152 let mut selected = selection::TopK::new(k, self.control);
153 self.visit_scores(&mut |score| {
154 stats.record(score.vector_count())?;
155 selected.offer(score)
156 })?;
157 let result = selected.finish(self.control)?;
158 self.source.check_control(self.control)?;
159 Ok((result, stats))
160 }
161
162 pub fn search_threshold(&self, threshold: f32) -> StorageBackendResult<PostingList> {
164 self.search_threshold_with_stats(threshold)
165 .map(|(postings, _)| postings)
166 }
167
168 pub fn search_threshold_with_stats(
170 &self,
171 threshold: f32,
172 ) -> StorageBackendResult<(PostingList, DiskANNScoringStats)> {
173 self.source.check_control(self.control)?;
174 validate_threshold(threshold)?;
175 let mut scores = VectorQueryBuffer::new(Some(self.control));
176 let mut stats = DiskANNScoringStats::default();
177 self.visit_scores(&mut |score| {
178 stats.record(score.vector_count())?;
179 if score.score >= threshold {
180 scores.push((score.document, score.score))?;
181 }
182 Ok(())
183 })?;
184 let result = postings_from_unique_scores(scores, None, Some(self.control))?;
185 self.source.check_control(self.control)?;
186 Ok((result, stats))
187 }
188
189 fn document(
190 &self,
191 document: DocId,
192 ) -> StorageBackendResult<(Option<DiskANNVectorVersion>, Option<DiskANNDocumentScore>)> {
193 self.source.check_control(self.control)?;
194 let mut version = None;
195 let mut best: Option<f32> = None;
196 let mut count = 0_u64;
197 let mut failure = None;
198 let returned =
199 self.source
200 .visit_document(document, self.control, &mut |ordinal, current, raw| {
201 if failure.is_none() {
202 let result = (|| {
203 self.source.check_control(self.control)?;
204 if u64::from(ordinal) != count
205 || version.is_some_and(|version| version != current)
206 {
207 return Err(invalid(
208 "canonical tensor identity changed while scoring",
209 ));
210 }
211 validate_vector_values_controlled(
212 self.source.dimensions(),
213 raw,
214 Some(self.control),
215 )?;
216 let score = cosine_similarity_controlled(self.query, raw, || {
217 self.source.check_control(self.control)
218 })?;
219 if best.is_none_or(|best| score.total_cmp(&best).is_gt()) {
220 best = Some(score);
221 }
222 version = Some(current);
223 count += 1;
224 Ok(())
225 })();
226 if let Err(error) = result {
227 failure = Some(error);
228 }
229 }
230 if failure.is_some() {
231 Err(invalid("canonical score consumer rejected an ordinal"))
232 } else {
233 Ok(())
234 }
235 });
236 if let Some(error) = failure {
237 return Err(error);
238 }
239 let returned = returned?;
240 self.source.check_control(self.control)?;
241 if count != 0 && returned != version {
242 return Err(invalid("canonical tensor returned a different origin"));
243 }
244 let score = best.map(|score| DiskANNDocumentScore {
245 document,
246 version: version.expect("a scored ordinal has an origin"),
247 vectors: count,
248 score,
249 });
250 Ok((returned, score))
251 }
252}
253
254fn invalid(message: &'static str) -> crate::StorageBackendError {
255 VersionError::InvalidEncoding(message).into_storage_error()
256}