/rust/registry/src/index.crates.io-1949cf8c6b5b557f/diskann-0.54.0/src/flat/index.rs
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) Microsoft Corporation. |
3 | | * Licensed under the MIT license. |
4 | | */ |
5 | | |
6 | | //! [`FlatIndex`] — the index wrapper for a [`DataProvider`](crate::provider::DataProvider) |
7 | | //! over which we do flat search. |
8 | | use std::num::NonZeroUsize; |
9 | | |
10 | | use diskann_utils::future::SendFuture; |
11 | | |
12 | | use crate::{ |
13 | | ANNResult, |
14 | | error::{ErrorExt, IntoANNResult}, |
15 | | flat::{DistancesUnordered, SearchStrategy}, |
16 | | graph::SearchOutputBuffer, |
17 | | neighbor::{Neighbor, NeighborPriorityQueue, NeighborPriorityQueueIdType}, |
18 | | provider::DataProvider, |
19 | | }; |
20 | | |
21 | | /// Statistics collected during a flat search. |
22 | | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] |
23 | | pub struct SearchStats { |
24 | | /// The total number of distance computations performed during the scan. |
25 | | pub cmps: u32, |
26 | | |
27 | | /// The total number of results written to the output buffer. |
28 | | pub result_count: u32, |
29 | | } |
30 | | |
31 | | /// A thin wrapper around a [`DataProvider`] used for flat search. |
32 | | #[derive(Debug)] |
33 | | pub struct FlatIndex<P: DataProvider> { |
34 | | /// The backing provider. |
35 | | provider: P, |
36 | | } |
37 | | |
38 | | impl<P: DataProvider> FlatIndex<P> { |
39 | | /// Construct a new [`FlatIndex`] around `provider`. |
40 | 0 | pub fn new(provider: P) -> Self { |
41 | 0 | Self { provider } |
42 | 0 | } |
43 | | |
44 | | /// Borrow the underlying provider. |
45 | 0 | pub fn provider(&self) -> &P { |
46 | 0 | &self.provider |
47 | 0 | } |
48 | | |
49 | | /// Brute-force k-nearest-neighbor flat search. |
50 | | /// |
51 | | /// Streams every element produced by the strategy's visitor through the query |
52 | | /// computer, keeps the best `k` candidates in a [`NeighborPriorityQueue`], and |
53 | | /// writes the `(id, distance)` survivors into `output` in best-first order. |
54 | 0 | pub fn knn_search<S, T, OB>( |
55 | 0 | &self, |
56 | 0 | k: NonZeroUsize, |
57 | 0 | strategy: &S, |
58 | 0 | context: &P::Context, |
59 | 0 | query: T, |
60 | 0 | output: &mut OB, |
61 | 0 | ) -> impl SendFuture<ANNResult<SearchStats>> |
62 | 0 | where |
63 | 0 | S: SearchStrategy<P, T>, |
64 | 0 | S::Id: NeighborPriorityQueueIdType, |
65 | 0 | T: Send + Sync, |
66 | 0 | OB: SearchOutputBuffer<S::Id> + Send + ?Sized, |
67 | | { |
68 | 0 | async move { |
69 | 0 | let mut visitor = strategy |
70 | 0 | .create_visitor(&self.provider, context) |
71 | 0 | .into_ann_result()?; |
72 | | |
73 | 0 | let computer = strategy.build_query_computer(query).into_ann_result()?; |
74 | | |
75 | 0 | let k = k.get(); |
76 | 0 | let mut queue = NeighborPriorityQueue::new(k); |
77 | 0 | let mut cmps: u32 = 0; |
78 | | |
79 | 0 | visitor |
80 | 0 | .distances_unordered(&computer, |id, dist| { |
81 | 0 | cmps += 1; |
82 | 0 | queue.insert(Neighbor::new(id, dist)); |
83 | 0 | }) |
84 | 0 | .await |
85 | 0 | .escalate("flat scan must complete to produce correct k-NN results")?; |
86 | | |
87 | 0 | let result_count = |
88 | 0 | output.extend(queue.iter().take(k).map(|n| (n.id, n.distance))) as u32; |
89 | | |
90 | 0 | Ok(SearchStats { cmps, result_count }) |
91 | 0 | } |
92 | 0 | } |
93 | | } |
94 | | |
95 | | /////////// |
96 | | // Tests // |
97 | | /////////// |
98 | | |
99 | | #[cfg(test)] |
100 | | mod tests { |
101 | | use crate::flat::{ |
102 | | FlatIndex, |
103 | | test::{ |
104 | | harness::KnnOracleRun, |
105 | | provider::{self as flat_provider}, |
106 | | }, |
107 | | }; |
108 | | use crate::graph::test::synthetic::Grid; |
109 | | |
110 | | fn fixture(grid: Grid, size: usize) -> (FlatIndex<flat_provider::Provider>, usize) { |
111 | | let provider = flat_provider::Provider::grid(grid, size).unwrap(); |
112 | | let len = provider.len(); |
113 | | (FlatIndex::new(provider), len) |
114 | | } |
115 | | |
116 | | /// `knn_search` returns a `Send` future, and a shared `&FlatIndex` can serve |
117 | | /// many concurrent searches on a multi-threaded runtime, each producing the |
118 | | /// correct top-k independently. |
119 | | #[tokio::test(flavor = "multi_thread", worker_threads = 4)] |
120 | | async fn multithreaded_knn_search() { |
121 | | use std::sync::Arc; |
122 | | |
123 | | let (index, len) = fixture(Grid::Two, 4); |
124 | | let index = Arc::new(index); |
125 | | |
126 | | // Mix of corner, axis-aligned, and off-grid queries; k spans 1..=len. |
127 | | let cases: &[(&[f32], usize)] = &[ |
128 | | (&[-1.0, -1.0], 1), |
129 | | (&[1.0, 1.0], len), |
130 | | (&[-1.0, 1.0], len / 2), |
131 | | (&[1.0, -1.0], len - 1), |
132 | | (&[0.0, 0.0], 3), |
133 | | (&[3.0, 3.0], len), |
134 | | (&[-2.0, 0.5], 2), |
135 | | (&[0.5, -0.5], len), |
136 | | ]; |
137 | | |
138 | | let mut set = tokio::task::JoinSet::new(); |
139 | | for (query, k) in cases { |
140 | | let index = Arc::clone(&index); |
141 | | let query: Vec<f32> = query.to_vec(); |
142 | | let k = *k; |
143 | | set.spawn(async move { |
144 | | let outcome = KnnOracleRun::run( |
145 | | &index, |
146 | | &flat_provider::Strategy::new(index.provider().dim()), |
147 | | &query, |
148 | | k, |
149 | | ) |
150 | | .await |
151 | | .expect("knn_search failed"); |
152 | | (query, k, outcome) |
153 | | }); |
154 | | } |
155 | | |
156 | | while let Some(joined) = set.join_next().await { |
157 | | let (query, k, outcome) = joined.expect("task panicked"); |
158 | | assert_eq!( |
159 | | outcome.top_k, outcome.ground_truth, |
160 | | "query = {query:?}, k = {k}: top-k must match brute force", |
161 | | ); |
162 | | assert_eq!(outcome.stats.cmps as usize, len); |
163 | | assert_eq!(outcome.stats.result_count as usize, k.min(len)); |
164 | | } |
165 | | } |
166 | | |
167 | | //////////// |
168 | | // Errors // |
169 | | //////////// |
170 | | |
171 | | /// A transient error from the visitor's scan must escalate up through `knn_search`. |
172 | | #[test] |
173 | | fn transient_scan_error() { |
174 | | // The flat scan touches every id, so any transient id is guaranteed to be hit. |
175 | | for transient_ids in [&[0u32][..], &[3][..], &[1, 2, 5][..]] { |
176 | | let strategy = |
177 | | flat_provider::Strategy::with_transient(2, transient_ids.iter().copied()); |
178 | | let (index, _) = fixture(Grid::Two, 3); |
179 | | let err = KnnOracleRun::run_sync(&index, &strategy, &[1.0, 0.0], 4) |
180 | | .expect_err("transient error during full scan must escalate"); |
181 | | |
182 | | let msg = format!("{err}"); |
183 | | assert!( |
184 | | transient_ids |
185 | | .iter() |
186 | | .any(|id| msg.contains(&format!("id {id}"))), |
187 | | "transients = {transient_ids:?}: expected error to name one of the \ |
188 | | transient ids, got: {msg}", |
189 | | ); |
190 | | } |
191 | | } |
192 | | |
193 | | /// Run `knn_search` via the harness, assert it fails, and check the error |
194 | | /// message contains `expected_msg`. |
195 | | fn assert_search_error(strategy: &flat_provider::Strategy, query: &[f32], expected_msg: &str) { |
196 | | let (index, _) = fixture(Grid::Two, 3); |
197 | | let err = KnnOracleRun::run_sync(&index, strategy, query, 4) |
198 | | .expect_err("expected knn_search to fail"); |
199 | | |
200 | | let msg = format!("{err}"); |
201 | | assert!( |
202 | | msg.contains(expected_msg), |
203 | | "expected error containing {expected_msg:?}, got: {msg}", |
204 | | ); |
205 | | } |
206 | | |
207 | | #[test] |
208 | | fn strategy_constructor_errors() { |
209 | | // Strategy/provider expect dim=2, query has dim=3. |
210 | | assert_search_error( |
211 | | &flat_provider::Strategy::new(2), |
212 | | &[0.0, 0.0, 0.0], |
213 | | "dimension mismatch", |
214 | | ); |
215 | | |
216 | | // Strategy expects dim=5, provider has dim=2. |
217 | | assert_search_error( |
218 | | &flat_provider::Strategy::new(5), |
219 | | &[0.0, 0.0], |
220 | | "dimension mismatch", |
221 | | ); |
222 | | } |
223 | | } |