diskann_benchmark_core/search/graph/
multihop.rs1use std::sync::Arc;
7
8use diskann::{
9 ANNResult,
10 graph::{self, glue},
11 provider,
12};
13use diskann_utils::{future::AsyncFriendly, views::Matrix};
14
15use crate::search::{self, Search, graph::Strategy};
16
17#[derive(Debug)]
28pub struct MultiHop<DP, T, S>
29where
30 DP: provider::DataProvider,
31{
32 index: Arc<graph::DiskANNIndex<DP>>,
33 queries: Arc<Matrix<T>>,
34 strategy: Strategy<S>,
35 labels: Arc<[Arc<dyn graph::index::QueryLabelProvider<DP::InternalId>>]>,
36}
37
38impl<DP, T, S> MultiHop<DP, T, S>
39where
40 DP: provider::DataProvider,
41{
42 pub fn new(
62 index: Arc<graph::DiskANNIndex<DP>>,
63 queries: Arc<Matrix<T>>,
64 strategy: Strategy<S>,
65 labels: Arc<[Arc<dyn graph::index::QueryLabelProvider<DP::InternalId>>]>,
66 ) -> anyhow::Result<Arc<Self>> {
67 strategy.length_compatible(queries.nrows())?;
68
69 if labels.len() != queries.nrows() {
70 Err(anyhow::anyhow!(
71 "Number of label providers ({}) must be equal to the number of queries ({})",
72 labels.len(),
73 queries.nrows()
74 ))
75 } else {
76 Ok(Arc::new(Self {
77 index,
78 queries,
79 strategy,
80 labels,
81 }))
82 }
83 }
84}
85
86impl<DP, T, S> Search for MultiHop<DP, T, S>
87where
88 DP: provider::DataProvider<Context: Default, ExternalId: search::Id>,
89 S: for<'a> glue::DefaultSearchStrategy<'a, DP, &'a [T], DP::ExternalId> + Clone + AsyncFriendly,
90 T: AsyncFriendly + Clone,
91{
92 type Id = DP::ExternalId;
93 type Parameters = graph::search::Knn;
94 type Output = super::knn::Metrics;
95
96 fn num_queries(&self) -> usize {
97 self.queries.nrows()
98 }
99
100 fn id_count(&self, parameters: &Self::Parameters) -> search::IdCount {
101 search::IdCount::Fixed(parameters.k_value())
102 }
103
104 async fn search<O>(
105 &self,
106 parameters: &Self::Parameters,
107 buffer: &mut O,
108 index: usize,
109 ) -> ANNResult<Self::Output>
110 where
111 O: graph::SearchOutputBuffer<DP::ExternalId> + Send,
112 {
113 let context = DP::Context::default();
114 let multihop_search =
115 graph::search::MultihopFilterSearch::new(*parameters, &*self.labels[index]);
116 let stats = self
117 .index
118 .search(
119 multihop_search,
120 self.strategy.get(index)?,
121 &context,
122 self.queries.row(index),
123 buffer,
124 )
125 .await?;
126
127 Ok(super::knn::Metrics {
128 comparisons: stats.cmps,
129 hops: stats.hops,
130 })
131 }
132}
133
134#[cfg(test)]
139mod tests {
140 use std::num::NonZeroUsize;
141
142 use super::*;
143
144 use crate::recall::GroundTruthMode;
145 use diskann::graph::{index::QueryLabelProvider, test::provider};
146
147 #[derive(Debug)]
149 struct NoOdds;
150
151 impl graph::index::QueryLabelProvider<u32> for NoOdds {
152 fn is_match(&self, id: u32) -> bool {
153 id.is_multiple_of(2)
154 }
155 }
156
157 #[test]
158 fn test_multihop() {
159 let nearest_neighbors = 5;
160
161 let index = search::graph::test_grid_provider();
162
163 let mut queries = Matrix::new(0.0f32, 5, index.provider().dim());
164 queries.row_mut(0).copy_from_slice(&[0.0, 0.0, 0.0, 0.0]);
165 queries.row_mut(1).copy_from_slice(&[4.0, 0.0, 0.0, 0.0]);
166 queries.row_mut(2).copy_from_slice(&[0.0, 4.0, 0.0, 0.0]);
167 queries.row_mut(3).copy_from_slice(&[0.0, 0.0, 4.0, 0.0]);
168 queries.row_mut(4).copy_from_slice(&[0.0, 0.0, 0.0, 4.0]);
169
170 let queries = Arc::new(queries);
171
172 let multihop = MultiHop::new(
173 index,
174 queries.clone(),
175 Strategy::broadcast(provider::Strategy::new()),
176 (0..queries.nrows())
177 .map(|_| -> Arc<dyn QueryLabelProvider<_>> { Arc::new(NoOdds {}) })
178 .collect(),
179 )
180 .unwrap();
181
182 let rt = crate::tokio::runtime(2).unwrap();
184 let results = search::search(
185 multihop.clone(),
186 graph::search::Knn::new(nearest_neighbors, 10, None).unwrap(),
187 NonZeroUsize::new(2).unwrap(),
188 &rt,
189 )
190 .unwrap();
191
192 assert_eq!(results.len(), queries.nrows());
193 let rows = results.ids().as_rows();
194 assert_eq!(*rows.row(0).first().unwrap(), 0);
195
196 for r in 0..rows.nrows() {
198 assert_eq!(rows.row(r).len(), nearest_neighbors);
199 for &id in rows.row(r) {
200 assert_eq!(id % 2, 0, "Found odd ID {} in row {}", id, r);
201 }
202 }
203
204 const TWO: NonZeroUsize = NonZeroUsize::new(2).unwrap();
205 let setup = search::Setup {
206 threads: TWO,
207 tasks: TWO,
208 reps: TWO,
209 };
210
211 let parameters = [
213 search::Run::new(
214 graph::search::Knn::new(nearest_neighbors, 10, None).unwrap(),
215 setup.clone(),
216 ),
217 search::Run::new(
218 graph::search::Knn::new(nearest_neighbors, 15, None).unwrap(),
219 setup.clone(),
220 ),
221 ];
222
223 let recall_k = nearest_neighbors;
224 let recall_n = nearest_neighbors;
225
226 let all = search::search_all(
227 multihop,
228 parameters,
229 search::graph::knn::Aggregator::new(
230 rows,
231 recall_k,
232 recall_n,
233 GroundTruthMode::Flexible,
234 ),
235 )
236 .unwrap();
237
238 assert_eq!(all.len(), 2);
239 for summary in all {
240 assert_eq!(summary.setup, setup);
241 assert_eq!(summary.end_to_end_latencies.len(), TWO.get());
242 assert_eq!(summary.mean_latencies.len(), TWO.get());
243 assert_eq!(summary.p90_latencies.len(), TWO.get());
244 assert_eq!(summary.p99_latencies.len(), TWO.get());
245
246 assert_ne!(summary.mean_cmps, 0.0);
247 assert_ne!(summary.mean_hops, 0.0);
248
249 let recall = summary.recall;
250 assert_eq!(recall.recall_k, recall_k);
251 assert_eq!(recall.recall_n, recall_n);
252 assert_eq!(recall.num_queries, queries.nrows());
253 assert_eq!(recall.average, 1.0, "we used a search as the groundtruth");
254 }
255 }
256
257 #[test]
258 fn test_multihop_error() {
259 let index = search::graph::test_grid_provider();
260 let queries = Arc::new(Matrix::new(0.0f32, 2, index.provider().dim()));
261
262 let labels: Arc<[_]> = (0..queries.nrows() + 1)
263 .map(|_| -> Arc<dyn QueryLabelProvider<_>> { Arc::new(NoOdds {}) })
264 .collect();
265
266 let strategy = provider::Strategy::new();
267
268 let err = MultiHop::new(
270 index.clone(),
271 queries.clone(),
272 Strategy::collection([strategy.clone()]),
273 labels.clone(),
274 )
275 .unwrap_err();
276 let msg = err.to_string();
277 assert!(
278 msg.contains("1 strategy was provided when 2 were expected"),
279 "failed with {msg}"
280 );
281
282 let err = MultiHop::new(
284 index,
285 queries.clone(),
286 Strategy::broadcast(strategy.clone()),
287 labels.clone(),
288 )
289 .unwrap_err();
290 let msg = err.to_string();
291 assert!(
292 msg.contains(
293 "Number of label providers (3) must be equal to the number of queries (2)"
294 ),
295 "failed with {msg}"
296 );
297 }
298}