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<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 = graph::search::MultihopSearch::new(*parameters, &*self.labels[index]);
115 let stats = self
116 .index
117 .search(
118 multihop_search,
119 self.strategy.get(index)?,
120 &context,
121 self.queries.row(index),
122 buffer,
123 )
124 .await?;
125
126 Ok(super::knn::Metrics {
127 comparisons: stats.cmps,
128 hops: stats.hops,
129 })
130 }
131}
132
133#[cfg(test)]
138mod tests {
139 use std::num::NonZeroUsize;
140
141 use super::*;
142
143 use crate::recall::GroundTruthMode;
144 use diskann::graph::{index::QueryLabelProvider, test::provider};
145
146 #[derive(Debug)]
148 struct NoOdds;
149
150 impl graph::index::QueryLabelProvider<u32> for NoOdds {
151 fn is_match(&self, id: u32) -> bool {
152 id.is_multiple_of(2)
153 }
154 }
155
156 #[test]
157 fn test_multihop() {
158 let nearest_neighbors = 5;
159
160 let index = search::graph::test_grid_provider();
161
162 let mut queries = Matrix::new(0.0f32, 5, index.provider().dim());
163 queries.row_mut(0).copy_from_slice(&[0.0, 0.0, 0.0, 0.0]);
164 queries.row_mut(1).copy_from_slice(&[4.0, 0.0, 0.0, 0.0]);
165 queries.row_mut(2).copy_from_slice(&[0.0, 4.0, 0.0, 0.0]);
166 queries.row_mut(3).copy_from_slice(&[0.0, 0.0, 4.0, 0.0]);
167 queries.row_mut(4).copy_from_slice(&[0.0, 0.0, 0.0, 4.0]);
168
169 let queries = Arc::new(queries);
170
171 let multihop = MultiHop::new(
172 index,
173 queries.clone(),
174 Strategy::broadcast(provider::Strategy::new()),
175 (0..queries.nrows())
176 .map(|_| -> Arc<dyn QueryLabelProvider<_>> { Arc::new(NoOdds {}) })
177 .collect(),
178 )
179 .unwrap();
180
181 let rt = crate::tokio::runtime(2).unwrap();
183 let results = search::search(
184 multihop.clone(),
185 graph::search::Knn::new(nearest_neighbors, 10, None).unwrap(),
186 NonZeroUsize::new(2).unwrap(),
187 &rt,
188 )
189 .unwrap();
190
191 assert_eq!(results.len(), queries.nrows());
192 let rows = results.ids().as_rows();
193 assert_eq!(*rows.row(0).first().unwrap(), 0);
194
195 for r in 0..rows.nrows() {
197 assert_eq!(rows.row(r).len(), nearest_neighbors);
198 for &id in rows.row(r) {
199 assert_eq!(id % 2, 0, "Found odd ID {} in row {}", id, r);
200 }
201 }
202
203 const TWO: NonZeroUsize = NonZeroUsize::new(2).unwrap();
204 let setup = search::Setup {
205 threads: TWO,
206 tasks: TWO,
207 reps: TWO,
208 };
209
210 let parameters = [
212 search::Run::new(
213 graph::search::Knn::new(nearest_neighbors, 10, None).unwrap(),
214 setup.clone(),
215 ),
216 search::Run::new(
217 graph::search::Knn::new(nearest_neighbors, 15, None).unwrap(),
218 setup.clone(),
219 ),
220 ];
221
222 let recall_k = nearest_neighbors;
223 let recall_n = nearest_neighbors;
224
225 let all = search::search_all(
226 multihop,
227 parameters,
228 search::graph::knn::Aggregator::new(
229 rows,
230 recall_k,
231 recall_n,
232 GroundTruthMode::Flexible,
233 ),
234 )
235 .unwrap();
236
237 assert_eq!(all.len(), 2);
238 for summary in all {
239 assert_eq!(summary.setup, setup);
240 assert_eq!(summary.end_to_end_latencies.len(), TWO.get());
241 assert_eq!(summary.mean_latencies.len(), TWO.get());
242 assert_eq!(summary.p90_latencies.len(), TWO.get());
243 assert_eq!(summary.p99_latencies.len(), TWO.get());
244
245 assert_ne!(summary.mean_cmps, 0.0);
246 assert_ne!(summary.mean_hops, 0.0);
247
248 let recall = summary.recall;
249 assert_eq!(recall.recall_k, recall_k);
250 assert_eq!(recall.recall_n, recall_n);
251 assert_eq!(recall.num_queries, queries.nrows());
252 assert_eq!(recall.average, 1.0, "we used a search as the groundtruth");
253 }
254 }
255
256 #[test]
257 fn test_multihop_error() {
258 let index = search::graph::test_grid_provider();
259 let queries = Arc::new(Matrix::new(0.0f32, 2, index.provider().dim()));
260
261 let labels: Arc<[_]> = (0..queries.nrows() + 1)
262 .map(|_| -> Arc<dyn QueryLabelProvider<_>> { Arc::new(NoOdds {}) })
263 .collect();
264
265 let strategy = provider::Strategy::new();
266
267 let err = MultiHop::new(
269 index.clone(),
270 queries.clone(),
271 Strategy::collection([strategy.clone()]),
272 labels.clone(),
273 )
274 .unwrap_err();
275 let msg = err.to_string();
276 assert!(
277 msg.contains("1 strategy was provided when 2 were expected"),
278 "failed with {msg}"
279 );
280
281 let err = MultiHop::new(
283 index,
284 queries.clone(),
285 Strategy::broadcast(strategy.clone()),
286 labels.clone(),
287 )
288 .unwrap_err();
289 let msg = err.to_string();
290 assert!(
291 msg.contains(
292 "Number of label providers (3) must be equal to the number of queries (2)"
293 ),
294 "failed with {msg}"
295 );
296 }
297}