uqa_storage/diskann_index/
query.rs1use std::sync::Arc;
10use uqa_core::PostingList;
11
12use super::{
13 pages::{DiskANNOriginReader, DiskANNPageSource, DiskANNReadLimits, DiskANNReader},
14 search::{DiskANNTraversal, DiskANNTraversalStats},
15 DiskANNCanonicalCounts, DiskANNCanonicalRead, DiskANNCanonicalScorer,
16 DiskANNCanonicalVectorVisitor, DiskANNQueryRead, ExactVectorReason, NavigationInput,
17};
18use crate::{
19 read_control::StorageReadControl, vector_index::DiskANNIndexParams, StorageBackendResult,
20};
21
22mod candidates;
23mod matching;
24mod retained;
25mod selection;
26mod statistics;
27mod values;
28pub use retained::RetainedDiskANNIndex;
29pub use selection::{DiskANNReadChanges, DiskANNReadSnapshot};
30pub use statistics::DiskANNQueryWork;
31#[cfg(test)]
32mod tests;
33
34pub struct DiskANNQueryResult {
36 pub postings: PostingList,
37 pub exact_reason: Option<ExactVectorReason>,
38 pub traversal: DiskANNTraversalStats,
39 pub work: DiskANNQueryWork,
40 pub populations: Option<DiskANNCanonicalCounts>,
42}
43
44pub struct DiskANNQuery<'a> {
46 canonical: &'a dyn DiskANNQueryRead,
47 reader: DiskANNReader,
48 origins: DiskANNOriginReader,
49 control: StorageReadControl,
50}
51
52impl<'a> DiskANNQuery<'a> {
53 pub fn open(
55 canonical: &'a dyn DiskANNQueryRead,
56 source: Arc<dyn DiskANNPageSource>,
57 parameters: DiskANNIndexParams,
58 limits: DiskANNReadLimits,
59 control: &StorageReadControl,
60 ) -> StorageBackendResult<Self> {
61 canonical.check_control(control)?;
62 let reader = DiskANNReader::open(
63 source.clone(),
64 canonical.dimensions(),
65 parameters,
66 limits,
67 control,
68 )?;
69 let origins = DiskANNOriginReader::open(source, limits.max_record_bytes, control)?;
70 if reader.manifest() != origins.manifest() {
71 return Err(invalid(
72 "physical manifest changed while preparing the query",
73 ));
74 }
75 canonical.check_control(control)?;
76 Ok(Self {
77 canonical,
78 reader,
79 origins,
80 control: control.clone(),
81 })
82 }
83
84 pub fn search_knn(
86 &self,
87 query: &[f32],
88 k: usize,
89 control: &StorageReadControl,
90 ) -> StorageBackendResult<DiskANNQueryResult> {
91 self.check(control)?;
92 let scorer = DiskANNCanonicalScorer::new(self, query, control)?;
93 if k == 0 {
94 self.check(control)?;
95 return Ok(result(
96 PostingList::new(),
97 None,
98 DiskANNTraversalStats::default(),
99 DiskANNQueryWork::default(),
100 ));
101 }
102 let navigation =
103 match NavigationInput::from_raw(self.canonical.dimensions(), query, control)? {
104 NavigationInput::Exact(reason) => {
105 let (postings, exact) =
106 if let Some(source) = self.canonical.unversioned_vectors() {
107 values::exact(&**source, query, k, control)?
108 } else {
109 scorer.search_exact_knn_with_stats(k)?
110 };
111 self.check(control)?;
112 return Ok(result(
113 postings,
114 Some(reason),
115 DiskANNTraversalStats::default(),
116 DiskANNQueryWork {
117 exact,
118 ..DiskANNQueryWork::default()
119 },
120 ));
121 }
122 NavigationInput::Navigable(navigation) => navigation,
123 };
124 let mut traversal = DiskANNTraversal::new(self.reader.clone(), &navigation, control)?;
125 drop(navigation);
126 let mut candidates = candidates::Candidates::new(self, scorer, k, control);
127 loop {
128 self.check(control)?;
129 let nodes = traversal.next_beam()?;
130 if nodes.is_empty() {
131 break;
132 }
133 candidates.nodes(&nodes)?;
134 }
135 candidates.side()?;
136 candidates.changes()?;
137 let mut raw_vectors = None;
138 if let Some(source) = self.canonical.unversioned_vectors() {
139 let mut current = 0_u64;
140 values::visit(&**source, control, &mut |document| {
141 self.check(control)?;
142 let vectors =
143 if let Some(origin) = self.canonical.document_origin(document, control)? {
144 origin.count()
145 } else {
146 let (score, vectors) = values::score(&**source, document, query, control)?;
147 if let Some(score) = score {
148 candidates.offer_unversioned(document, score, vectors)?;
149 }
150 vectors
151 };
152 current = current
153 .checked_add(vectors)
154 .ok_or_else(|| invalid("raw canonical population overflow"))?;
155 Ok(())
156 })?;
157 raw_vectors = Some(current);
158 }
159 while candidates.len() < k {
160 self.check(control)?;
161 let nodes = traversal.complete_next_beam()?;
162 if nodes.is_empty() {
163 break;
164 }
165 candidates.nodes(&nodes)?;
166 }
167 let mut work = candidates.work();
168 work.pages = traversal.page_stats();
169 let postings = candidates.finish()?;
170 self.check(control)?;
171 let mut result = result(postings, None, traversal.stats(), work);
172 if let Some(current) = raw_vectors {
173 let changed = work
174 .changed
175 .vectors
176 .checked_add(work.unversioned.vectors)
177 .ok_or_else(|| invalid("raw uncovered population overflow"))?;
178 result.populations = Some(DiskANNCanonicalCounts::new(current, changed)?);
179 }
180 Ok(result)
181 }
182
183 pub fn search_threshold(
185 &self,
186 query: &[f32],
187 threshold: f32,
188 control: &StorageReadControl,
189 ) -> StorageBackendResult<PostingList> {
190 self.search_threshold_with_stats(query, threshold, control)
191 .map(|result| result.postings)
192 }
193
194 pub fn search_threshold_with_stats(
196 &self,
197 query: &[f32],
198 threshold: f32,
199 control: &StorageReadControl,
200 ) -> StorageBackendResult<DiskANNQueryResult> {
201 self.check(control)?;
202 let scorer = DiskANNCanonicalScorer::new(self, query, control)?;
203 let (postings, exact) = if let Some(source) = self.canonical.unversioned_vectors() {
204 values::threshold(&**source, query, threshold, control)?
205 } else {
206 scorer.search_threshold_with_stats(threshold)?
207 };
208 self.check(control)?;
209 Ok(result(
210 postings,
211 None,
212 DiskANNTraversalStats::default(),
213 DiskANNQueryWork {
214 exact,
215 ..DiskANNQueryWork::default()
216 },
217 ))
218 }
219
220 fn check(&self, control: &StorageReadControl) -> StorageBackendResult<()> {
221 self.control.check()?;
222 self.canonical.check_control(control)
223 }
224}
225
226impl DiskANNCanonicalRead for DiskANNQuery<'_> {
227 fn check_control(&self, control: &StorageReadControl) -> StorageBackendResult<()> {
228 self.check(control)
229 }
230 fn dimensions(&self) -> u32 {
231 self.canonical.dimensions()
232 }
233 fn next_document_after(
234 &self,
235 after: Option<uqa_core::DocId>,
236 control: &StorageReadControl,
237 ) -> StorageBackendResult<Option<uqa_core::DocId>> {
238 self.check(control)?;
239 let result = self.canonical.next_document_after(after, control)?;
240 self.check(control)?;
241 Ok(result)
242 }
243 fn origin(
244 &self,
245 document: uqa_core::DocId,
246 control: &StorageReadControl,
247 ) -> StorageBackendResult<Option<super::format::DiskANNVectorVersion>> {
248 self.check(control)?;
249 let result = self.canonical.origin(document, control)?;
250 self.check(control)?;
251 Ok(result)
252 }
253 fn visit_document(
254 &self,
255 document: uqa_core::DocId,
256 control: &StorageReadControl,
257 visit: &mut DiskANNCanonicalVectorVisitor<'_>,
258 ) -> StorageBackendResult<Option<super::format::DiskANNVectorVersion>> {
259 self.check(control)?;
260 let result =
261 self.canonical
262 .visit_document(document, control, &mut |ordinal, version, raw| {
263 self.check(control)?;
264 visit(ordinal, version, raw)
265 })?;
266 self.check(control)?;
267 Ok(result)
268 }
269}
270
271fn result(
272 postings: PostingList,
273 exact_reason: Option<ExactVectorReason>,
274 traversal: DiskANNTraversalStats,
275 work: DiskANNQueryWork,
276) -> DiskANNQueryResult {
277 DiskANNQueryResult {
278 postings,
279 exact_reason,
280 traversal,
281 work,
282 populations: None,
283 }
284}
285
286fn invalid(message: &'static str) -> crate::StorageBackendError {
287 crate::mvcc::VersionError::InvalidEncoding(message).into_storage_error()
288}