1use std::{
7 collections::HashMap,
8 future::Future,
9 num::NonZeroUsize,
10 ops::Range,
11 sync::{
12 atomic::{AtomicU64, AtomicUsize},
13 Arc,
14 },
15 time::Instant,
16};
17
18use crate::data_model::GraphDataType;
19use diskann::{
20 graph::{
21 self,
22 glue::{
23 self, DefaultPostProcessor, ExpandBeam, IdIterator, SearchExt, SearchPostProcess,
24 SearchStrategy,
25 },
26 search::Knn,
27 search_output_buffer, AdjacencyList, DiskANNIndex,
28 },
29 neighbor::Neighbor,
30 provider::{
31 Accessor, BuildQueryComputer, DataProvider, DefaultContext, DelegateNeighbor, HasId,
32 NeighborAccessor, NoopGuard,
33 },
34 utils::{IntoUsize, VectorRepr},
35 ANNError, ANNResult,
36};
37use diskann_providers::storage::StorageReadProvider;
38use diskann_providers::{
39 model::{compute_pq_distance, compute_pq_distance_for_pq_coordinates},
40 storage::{get_compressed_pq_file, get_disk_index_file, get_pq_pivot_file, LoadWith},
41};
42use diskann_utils::object_pool::{ObjectPool, PoolOption, TryAsPooled};
43
44use crate::search::pq::{quantizer_preprocess, PQData, PQScratch};
45use diskann_vector::{distance::Metric, DistanceFunction, PreprocessedDistanceFunction};
46use futures_util::future;
47use tokio::runtime::Runtime;
48use tracing::debug;
49
50use crate::{
51 data_model::{CachingStrategy, GraphHeader},
52 filter_parameter::{default_vector_filter, VectorFilter},
53 search::{
54 provider::disk_vertex_provider_factory::DiskVertexProviderFactory,
55 traits::{VertexProvider, VertexProviderFactory},
56 },
57 storage::{api::AsyncDiskLoadContext, disk_index_reader::DiskIndexReader},
58 utils::AlignedFileReaderFactory,
59 utils::QueryStatistics,
60};
61
62pub struct DiskProvider<Data>
72where
73 Data: GraphDataType<VectorIdType = u32>,
74{
75 graph_header: GraphHeader,
77
78 distance_comparer: <Data::VectorDataType as VectorRepr>::Distance,
80
81 pq_data: Arc<PQData>,
83
84 num_points: usize,
86
87 metric: Metric,
89
90 search_io_limit: usize,
92}
93
94impl<Data> DataProvider for DiskProvider<Data>
95where
96 Data: GraphDataType<VectorIdType = u32>,
97{
98 type Context = DefaultContext;
99
100 type InternalId = u32;
101
102 type ExternalId = u32;
103
104 type Guard = NoopGuard<u32>;
105
106 type Error = ANNError;
107
108 fn to_internal_id(
110 &self,
111 _context: &DefaultContext,
112 gid: &Self::ExternalId,
113 ) -> Result<Self::InternalId, Self::Error> {
114 Ok(*gid)
115 }
116
117 fn to_external_id(
119 &self,
120 _context: &DefaultContext,
121 id: Self::InternalId,
122 ) -> Result<Self::ExternalId, Self::Error> {
123 Ok(id)
124 }
125}
126
127impl<Data> LoadWith<AsyncDiskLoadContext> for DiskProvider<Data>
128where
129 Data: GraphDataType<VectorIdType = u32>,
130{
131 type Error = ANNError;
132
133 async fn load_with<P>(provider: &P, ctx: &AsyncDiskLoadContext) -> ANNResult<Self>
134 where
135 P: StorageReadProvider,
136 {
137 debug!(
138 "DiskProvider::load_with() called with file: {:?}",
139 get_disk_index_file(ctx.quant_load_context.metadata.prefix())
140 );
141
142 let graph_header = {
143 let aligned_reader_factory = AlignedFileReaderFactory::new(get_disk_index_file(
144 ctx.quant_load_context.metadata.prefix(),
145 ));
146
147 let caching_strategy = if ctx.num_nodes_to_cache > 0 {
148 CachingStrategy::StaticCacheWithBfsNodes(ctx.num_nodes_to_cache)
149 } else {
150 CachingStrategy::None
151 };
152
153 let vertex_provider_factory = DiskVertexProviderFactory::<Data, _>::new(
154 aligned_reader_factory,
155 caching_strategy,
156 )?;
157 VertexProviderFactory::get_header(&vertex_provider_factory)?
158 };
159
160 let metric = ctx.quant_load_context.metric;
161 let num_points = ctx.num_points;
162
163 let index_path_prefix = ctx.quant_load_context.metadata.prefix();
164 let index_reader = DiskIndexReader::<<Data as GraphDataType>::VectorDataType>::new(
165 get_pq_pivot_file(index_path_prefix),
166 get_compressed_pq_file(index_path_prefix),
167 provider,
168 )?;
169
170 Self::new(
171 &index_reader,
172 graph_header,
173 metric,
174 num_points,
175 ctx.search_io_limit,
176 )
177 }
178}
179
180impl<Data> DiskProvider<Data>
181where
182 Data: GraphDataType<VectorIdType = u32>,
183{
184 fn new(
185 disk_index_reader: &DiskIndexReader<Data::VectorDataType>,
186 graph_header: GraphHeader,
187 metric: Metric,
188 num_points: usize,
189 search_io_limit: usize,
190 ) -> ANNResult<Self> {
191 let distance_comparer =
192 Data::VectorDataType::distance(metric, Some(graph_header.metadata().dims));
193
194 let pq_data = disk_index_reader.get_pq_data();
195
196 Ok(Self {
197 graph_header,
198 distance_comparer,
199 pq_data,
200 num_points,
201 metric,
202 search_io_limit,
203 })
204 }
205}
206
207pub struct DiskSearchStrategy<'a, Data, ProviderFactory>
218where
219 Data: GraphDataType<VectorIdType = u32>,
220 ProviderFactory: VertexProviderFactory<Data>,
221{
222 io_tracker: IOTracker,
224 vector_filter: &'a (dyn Fn(&u32) -> bool + Send + Sync), query: &'a [Data::VectorDataType],
226
227 vertex_provider_factory: &'a ProviderFactory,
229
230 scratch_pool: &'a Arc<ObjectPool<DiskSearchScratch<Data, ProviderFactory::VertexProviderType>>>,
232}
233
234struct IOTracker {
237 io_time_us: AtomicU64,
238 preprocess_time_us: AtomicU64,
239 io_count: AtomicUsize,
240}
241
242impl Default for IOTracker {
243 fn default() -> Self {
244 Self {
245 io_time_us: AtomicU64::new(0),
246 preprocess_time_us: AtomicU64::new(0),
247 io_count: AtomicUsize::new(0),
248 }
249 }
250}
251
252impl IOTracker {
253 fn add_time(category: &AtomicU64, time: u64) {
254 category.fetch_add(time, std::sync::atomic::Ordering::Relaxed);
255 }
256
257 fn time(category: &AtomicU64) -> u64 {
258 category.load(std::sync::atomic::Ordering::Relaxed)
259 }
260
261 fn add_io_count(&self, count: usize) {
262 self.io_count
263 .fetch_add(count, std::sync::atomic::Ordering::Relaxed);
264 }
265
266 fn io_count(&self) -> usize {
267 self.io_count.load(std::sync::atomic::Ordering::Relaxed)
268 }
269}
270
271#[derive(Clone, Copy)]
272pub struct RerankAndFilter<'a> {
273 filter: &'a (dyn Fn(&u32) -> bool + Send + Sync),
274}
275
276impl<'a> RerankAndFilter<'a> {
277 fn new(filter: &'a (dyn Fn(&u32) -> bool + Send + Sync)) -> Self {
278 Self { filter }
279 }
280}
281
282impl<Data, VP>
283 SearchPostProcess<
284 DiskAccessor<'_, Data, VP>,
285 &[Data::VectorDataType],
286 (
287 <DiskProvider<Data> as DataProvider>::InternalId,
288 Data::AssociatedDataType,
289 ),
290 > for RerankAndFilter<'_>
291where
292 Data: GraphDataType<VectorIdType = u32>,
293 VP: VertexProvider<Data>,
294{
295 type Error = ANNError;
296 async fn post_process<I, B>(
297 &self,
298 accessor: &mut DiskAccessor<'_, Data, VP>,
299 query: &[Data::VectorDataType],
300 _computer: &DiskQueryComputer,
301 candidates: I,
302 output: &mut B,
303 ) -> Result<usize, Self::Error>
304 where
305 I: Iterator<Item = Neighbor<u32>> + Send,
306 B: search_output_buffer::SearchOutputBuffer<(u32, Data::AssociatedDataType)>
307 + Send
308 + ?Sized,
309 {
310 let provider = accessor.provider;
311
312 let mut uncached_ids = Vec::new();
313 let mut reranked = candidates
314 .map(|n| n.id)
315 .filter(|id| (self.filter)(id))
316 .filter_map(|n| {
317 if let Some(entry) = accessor.scratch.distance_cache.get(&n) {
318 Some(Ok::<((u32, _), f32), ANNError>(((n, entry.1), entry.0)))
319 } else {
320 uncached_ids.push(n);
321 None
322 }
323 })
324 .collect::<Result<Vec<_>, _>>()?;
325 if !uncached_ids.is_empty() {
326 ensure_vertex_loaded(&mut accessor.scratch.vertex_provider, &uncached_ids)?;
327 for n in &uncached_ids {
328 let v = accessor.scratch.vertex_provider.get_vector(n)?;
329 let d = provider.distance_comparer.evaluate_similarity(query, v);
330 let a = accessor.scratch.vertex_provider.get_associated_data(n)?;
331 reranked.push(((*n, *a), d));
332 }
333 }
334
335 reranked
337 .sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
338 Ok(output.extend(reranked))
340 }
341}
342
343impl<'this, Data, ProviderFactory> SearchStrategy<DiskProvider<Data>, &[Data::VectorDataType]>
344 for DiskSearchStrategy<'this, Data, ProviderFactory>
345where
346 Data: GraphDataType<VectorIdType = u32>,
347 ProviderFactory: VertexProviderFactory<Data>,
348{
349 type QueryComputer = DiskQueryComputer;
350 type SearchAccessor<'a> = DiskAccessor<'a, Data, ProviderFactory::VertexProviderType>;
351 type SearchAccessorError = ANNError;
352
353 fn search_accessor<'a>(
354 &'a self,
355 provider: &'a DiskProvider<Data>,
356 _context: &DefaultContext,
357 ) -> Result<Self::SearchAccessor<'a>, Self::SearchAccessorError> {
358 DiskAccessor::new(
359 provider,
360 &self.io_tracker,
361 self.query,
362 self.vertex_provider_factory,
363 self.scratch_pool,
364 )
365 }
366}
367
368impl<'this, Data, ProviderFactory>
369 DefaultPostProcessor<
370 DiskProvider<Data>,
371 &[Data::VectorDataType],
372 (
373 <DiskProvider<Data> as DataProvider>::InternalId,
374 Data::AssociatedDataType,
375 ),
376 > for DiskSearchStrategy<'this, Data, ProviderFactory>
377where
378 Data: GraphDataType<VectorIdType = u32>,
379 ProviderFactory: VertexProviderFactory<Data>,
380{
381 type Processor = RerankAndFilter<'this>;
382
383 fn default_post_processor(&self) -> Self::Processor {
384 RerankAndFilter::new(self.vector_filter)
385 }
386}
387
388pub struct DiskQueryComputer {
390 num_pq_chunks: usize,
391 query_centroid_l2_distance: Vec<f32>,
392}
393
394impl PreprocessedDistanceFunction<&[u8], f32> for DiskQueryComputer {
395 fn evaluate_similarity(&self, changing: &[u8]) -> f32 {
396 let mut dist = 0.0f32;
397 #[allow(clippy::expect_used)]
398 compute_pq_distance_for_pq_coordinates(
399 changing,
400 self.num_pq_chunks,
401 &self.query_centroid_l2_distance,
402 std::slice::from_mut(&mut dist),
403 )
404 .expect("PQ distance compute for PQ coordinates is expected to succeed");
405 dist
406 }
407}
408
409impl<Data, VP> BuildQueryComputer<&[Data::VectorDataType]> for DiskAccessor<'_, Data, VP>
410where
411 Data: GraphDataType<VectorIdType = u32>,
412 VP: VertexProvider<Data>,
413{
414 type QueryComputerError = ANNError;
415 type QueryComputer = DiskQueryComputer;
416
417 fn build_query_computer(
418 &self,
419 _from: &[Data::VectorDataType],
420 ) -> Result<Self::QueryComputer, Self::QueryComputerError> {
421 Ok(DiskQueryComputer {
422 num_pq_chunks: self.provider.pq_data.get_num_chunks(),
423 query_centroid_l2_distance: self
424 .scratch
425 .pq_scratch
426 .aligned_pqtable_dist_scratch
427 .to_vec(),
428 })
429 }
430
431 async fn distances_unordered<Itr, F>(
432 &mut self,
433 vec_id_itr: Itr,
434 _computer: &Self::QueryComputer,
435 f: F,
436 ) -> Result<(), Self::GetError>
437 where
438 F: Send + FnMut(f32, Self::Id),
439 Itr: Iterator<Item = Self::Id>,
440 {
441 self.pq_distances(&vec_id_itr.collect::<Box<[_]>>(), f)
442 }
443}
444
445impl<Data, VP> ExpandBeam<&[Data::VectorDataType]> for DiskAccessor<'_, Data, VP>
446where
447 Data: GraphDataType<VectorIdType = u32>,
448 VP: VertexProvider<Data>,
449{
450 fn expand_beam<Itr, P, F>(
451 &mut self,
452 ids: Itr,
453 _computer: &Self::QueryComputer,
454 mut pred: P,
455 mut f: F,
456 ) -> impl std::future::Future<Output = Result<(), Self::GetError>> + Send
457 where
458 Itr: Iterator<Item = Self::Id> + Send,
459 P: glue::HybridPredicate<Self::Id> + Send + Sync,
460 F: FnMut(f32, Self::Id) + Send,
461 {
462 let result = (|| {
463 let io_limit = self.provider.search_io_limit - self.io_tracker.io_count();
464 let load_ids: Box<[_]> = ids.take(io_limit).collect();
465
466 self.ensure_loaded(&load_ids)?;
467 let mut ids = Vec::new();
468 for i in load_ids {
469 ids.clear();
470 ids.extend(
471 self.scratch
472 .vertex_provider
473 .get_adjacency_list(&i)?
474 .iter()
475 .copied()
476 .filter(|id| pred.eval_mut(id)),
477 );
478
479 self.pq_distances(&ids, &mut f)?;
480 }
481
482 Ok(())
483 })();
484
485 std::future::ready(result)
486 }
487}
488
489struct DiskSearchScratch<Data, VP>
492where
493 Data: GraphDataType<VectorIdType = u32>,
494 VP: VertexProvider<Data>,
495{
496 distance_cache: HashMap<u32, (f32, Data::AssociatedDataType)>,
497 pq_scratch: PQScratch,
498 vertex_provider: VP,
499}
500
501#[derive(Clone)]
502struct DiskSearchScratchArgs<'a, ProviderFactory> {
503 graph_degree: usize,
504 dim: usize,
505 num_pq_chunks: usize,
506 num_pq_centers: usize,
507 vertex_factory: &'a ProviderFactory,
508 graph_header: &'a GraphHeader,
509}
510
511impl<Data, ProviderFactory> TryAsPooled<&DiskSearchScratchArgs<'_, ProviderFactory>>
512 for DiskSearchScratch<Data, ProviderFactory::VertexProviderType>
513where
514 Data: GraphDataType<VectorIdType = u32>,
515 ProviderFactory: VertexProviderFactory<Data>,
516{
517 type Error = ANNError;
518
519 fn try_create(args: &DiskSearchScratchArgs<ProviderFactory>) -> Result<Self, Self::Error> {
520 let pq_scratch = PQScratch::new(
521 args.graph_degree,
522 args.dim,
523 args.num_pq_chunks,
524 args.num_pq_centers,
525 )?;
526
527 const DEFAULT_BEAM_WIDTH: usize = 0; let vertex_provider = args
529 .vertex_factory
530 .create_vertex_provider(DEFAULT_BEAM_WIDTH, args.graph_header)?;
531
532 Ok(Self {
533 distance_cache: HashMap::new(),
534 pq_scratch,
535 vertex_provider,
536 })
537 }
538
539 fn try_modify(
540 &mut self,
541 _args: &DiskSearchScratchArgs<ProviderFactory>,
542 ) -> Result<(), Self::Error> {
543 self.distance_cache.clear();
544 self.vertex_provider.clear();
545 Ok(())
546 }
547}
548
549pub struct DiskAccessor<'a, Data, VP>
550where
551 Data: GraphDataType<VectorIdType = u32>,
552 VP: VertexProvider<Data>,
553{
554 provider: &'a DiskProvider<Data>,
555 io_tracker: &'a IOTracker,
556 scratch: PoolOption<DiskSearchScratch<Data, VP>>,
557 query: &'a [Data::VectorDataType],
558}
559
560impl<Data, VP> DiskAccessor<'_, Data, VP>
561where
562 Data: GraphDataType<VectorIdType = u32>,
563 VP: VertexProvider<Data>,
564{
565 fn pq_distances<F>(&mut self, ids: &[u32], mut f: F) -> ANNResult<()>
568 where
569 F: FnMut(f32, u32),
570 {
571 let pq_scratch = &mut self.scratch.pq_scratch;
572 compute_pq_distance(
573 ids,
574 self.provider.pq_data.get_num_chunks(),
575 &pq_scratch.aligned_pqtable_dist_scratch,
576 self.provider.pq_data.pq_compressed_data().as_slice(),
577 &mut pq_scratch.aligned_pq_coord_scratch,
578 &mut pq_scratch.aligned_dist_scratch,
579 )?;
580
581 for (i, id) in ids.iter().enumerate() {
582 let distance = self.scratch.pq_scratch.aligned_dist_scratch[i];
583 f(distance, *id);
584 }
585
586 Ok(())
587 }
588}
589
590impl<Data, VP> SearchExt for DiskAccessor<'_, Data, VP>
591where
592 Data: GraphDataType<VectorIdType = u32>,
593 VP: VertexProvider<Data>,
594{
595 async fn starting_points(&self) -> ANNResult<Vec<u32>> {
596 let start_vertex_id = self.provider.graph_header.metadata().medoid as u32;
597 Ok(vec![start_vertex_id])
598 }
599
600 fn terminate_early(&mut self) -> bool {
601 self.io_tracker.io_count() > self.provider.search_io_limit
602 }
603}
604
605impl<'a, Data, VP> DiskAccessor<'a, Data, VP>
606where
607 Data: GraphDataType<VectorIdType = u32>,
608 VP: VertexProvider<Data>,
609{
610 fn new<VPF>(
611 provider: &'a DiskProvider<Data>,
612 io_tracker: &'a IOTracker,
613 query: &'a [Data::VectorDataType],
614 vertex_provider_factory: &'a VPF,
615 scratch_pool: &'a Arc<ObjectPool<DiskSearchScratch<Data, VP>>>,
616 ) -> ANNResult<Self>
617 where
618 VPF: VertexProviderFactory<Data, VertexProviderType = VP>,
619 {
620 let mut scratch = PoolOption::try_pooled(
621 scratch_pool,
622 &DiskSearchScratchArgs {
623 graph_degree: provider.graph_header.max_degree::<Data::VectorDataType>()?,
624 dim: provider.graph_header.metadata().dims,
625 num_pq_chunks: provider.pq_data.get_num_chunks(),
626 num_pq_centers: provider.pq_data.get_num_centers(),
627 vertex_factory: vertex_provider_factory,
628 graph_header: &provider.graph_header,
629 },
630 )?;
631
632 scratch
633 .pq_scratch
634 .set(provider.graph_header.metadata().dims, query)?;
635 let start_vertex_id = provider.graph_header.metadata().medoid as u32;
636
637 let timer = Instant::now();
638 quantizer_preprocess(
639 &mut scratch.pq_scratch,
640 &provider.pq_data,
641 provider.metric,
642 &[start_vertex_id],
643 )?;
644 IOTracker::add_time(
645 &io_tracker.preprocess_time_us,
646 timer.elapsed().as_micros() as u64,
647 );
648
649 Ok(Self {
650 provider,
651 io_tracker,
652 scratch,
653 query,
654 })
655 }
656
657 fn ensure_loaded(&mut self, ids: &[u32]) -> Result<(), ANNError> {
658 if ids.is_empty() {
659 return Ok(());
660 }
661 let scratch = &mut self.scratch;
662 let timer = Instant::now();
663 ensure_vertex_loaded(&mut scratch.vertex_provider, ids)?;
664 IOTracker::add_time(
665 &self.io_tracker.io_time_us,
666 timer.elapsed().as_micros() as u64,
667 );
668 self.io_tracker.add_io_count(ids.len());
669 for id in ids {
670 let distance = self
671 .provider
672 .distance_comparer
673 .evaluate_similarity(self.query, scratch.vertex_provider.get_vector(id)?);
674 let associated_data = *scratch.vertex_provider.get_associated_data(id)?;
675 scratch
676 .distance_cache
677 .insert(*id, (distance, associated_data));
678 }
679 Ok(())
680 }
681}
682
683impl<Data, VP> HasId for DiskAccessor<'_, Data, VP>
684where
685 Data: GraphDataType<VectorIdType = u32>,
686 VP: VertexProvider<Data>,
687{
688 type Id = u32;
689}
690
691impl<Data, VP> Accessor for DiskAccessor<'_, Data, VP>
692where
693 Data: GraphDataType<VectorIdType = u32>,
694 VP: VertexProvider<Data>,
695{
696 type Element<'a>
699 = &'a [u8]
700 where
701 Self: 'a;
702
703 type ElementRef<'a> = &'a [u8];
705
706 type GetError = ANNError;
708
709 fn get_element(
710 &mut self,
711 id: Self::Id,
712 ) -> impl Future<Output = Result<Self::Element<'_>, Self::GetError>> + Send {
713 std::future::ready(self.provider.pq_data.get_compressed_vector(id as usize))
714 }
715}
716
717impl<Data, VP> IdIterator<Range<u32>> for DiskAccessor<'_, Data, VP>
718where
719 Data: GraphDataType<VectorIdType = u32>,
720 VP: VertexProvider<Data>,
721{
722 async fn id_iterator(&mut self) -> Result<Range<u32>, ANNError> {
723 Ok(0..self.provider.num_points as u32)
724 }
725}
726
727impl<'a, 'b, Data, VP> DelegateNeighbor<'a> for DiskAccessor<'b, Data, VP>
728where
729 Data: GraphDataType<VectorIdType = u32>,
730 VP: VertexProvider<Data>,
731{
732 type Delegate = AsNeighborAccessor<'a, 'b, Data, VP>;
733 fn delegate_neighbor(&'a mut self) -> Self::Delegate {
734 AsNeighborAccessor(self)
735 }
736}
737
738pub struct AsNeighborAccessor<'a, 'b, Data, VP>(&'a mut DiskAccessor<'b, Data, VP>)
744where
745 Data: GraphDataType<VectorIdType = u32>,
746 VP: VertexProvider<Data>;
747
748impl<Data, VP> HasId for AsNeighborAccessor<'_, '_, Data, VP>
749where
750 Data: GraphDataType<VectorIdType = u32>,
751 VP: VertexProvider<Data>,
752{
753 type Id = u32;
754}
755
756impl<Data, VP> NeighborAccessor for AsNeighborAccessor<'_, '_, Data, VP>
757where
758 Data: GraphDataType<VectorIdType = u32>,
759 VP: VertexProvider<Data>,
760{
761 fn get_neighbors(
762 self,
763 id: Self::Id,
764 neighbors: &mut AdjacencyList<Self::Id>,
765 ) -> impl Future<Output = ANNResult<Self>> + Send {
766 if self.0.io_tracker.io_count() > self.0.provider.search_io_limit {
767 return future::ok(self); }
769
770 if let Err(e) = ensure_vertex_loaded(&mut self.0.scratch.vertex_provider, &[id]) {
771 return future::err(e);
772 }
773 let list = match self.0.scratch.vertex_provider.get_adjacency_list(&id) {
774 Ok(list) => list,
775 Err(e) => return future::err(e),
776 };
777 neighbors.overwrite_trusted(list);
778 future::ok(self)
779 }
780}
781
782pub struct DiskIndexSearcher<
786 Data,
787 ProviderFactory = DiskVertexProviderFactory<Data, AlignedFileReaderFactory>,
788> where
789 Data: GraphDataType<VectorIdType = u32>,
790 ProviderFactory: VertexProviderFactory<Data>,
791{
792 index: DiskANNIndex<DiskProvider<Data>>,
793 runtime: Runtime,
794
795 vertex_provider_factory: ProviderFactory,
797
798 scratch_pool: Arc<ObjectPool<DiskSearchScratch<Data, ProviderFactory::VertexProviderType>>>,
800}
801
802#[derive(Debug)]
803pub struct SearchResultStats {
804 pub cmps: u32,
805 pub result_count: u32,
806 pub query_statistics: QueryStatistics,
807}
808
809pub struct SearchResult<AssociatedData> {
814 pub results: Vec<SearchResultItem<AssociatedData>>,
816 pub stats: SearchResultStats,
817}
818
819pub struct SearchResultItem<AssociatedData> {
824 pub vertex_id: u32,
826 pub data: AssociatedData,
829 pub distance: f32,
831}
832
833impl<Data, ProviderFactory> DiskIndexSearcher<Data, ProviderFactory>
834where
835 Data: GraphDataType<VectorIdType = u32>,
836 ProviderFactory: VertexProviderFactory<Data>,
837{
838 pub fn new(
848 num_threads: usize,
849 search_io_limit: usize,
850 disk_index_reader: &DiskIndexReader<Data::VectorDataType>,
851 vertex_provider_factory: ProviderFactory,
852 metric: Metric,
853 runtime: Option<Runtime>,
854 ) -> ANNResult<Self> {
855 let runtime = match runtime {
856 Some(rt) => rt,
857 None => tokio::runtime::Builder::new_current_thread().build()?,
858 };
859
860 let graph_header = vertex_provider_factory.get_header()?;
861 let metadata = graph_header.metadata();
862 let max_degree = graph_header.max_degree::<Data::VectorDataType>()? as u32;
863
864 let config = graph::config::Builder::new(
865 max_degree.into_usize(),
866 graph::config::MaxDegree::default_slack(),
867 1, metric.into(),
869 )
870 .build()?;
871
872 debug!("Creating DiskIndexSearcher with index_config: {:?}", config);
873
874 let graph_header = vertex_provider_factory.get_header()?;
875 let pq_data = disk_index_reader.get_pq_data();
876 let scratch_pool_args = DiskSearchScratchArgs {
877 graph_degree: graph_header.max_degree::<Data::VectorDataType>()?,
878 dim: graph_header.metadata().dims,
879 num_pq_chunks: pq_data.get_num_chunks(),
880 num_pq_centers: pq_data.get_num_centers(),
881 vertex_factory: &vertex_provider_factory,
882 graph_header: &graph_header,
883 };
884 let scratch_pool = Arc::new(ObjectPool::try_new(&scratch_pool_args, 0, None)?);
885
886 let disk_provider = DiskProvider::new(
887 disk_index_reader,
888 graph_header,
889 metric,
890 metadata.num_pts.into_usize(),
891 search_io_limit,
892 )?;
893
894 let index = DiskANNIndex::new(config, disk_provider, NonZeroUsize::new(num_threads));
895 Ok(Self {
896 index,
897 runtime,
898 vertex_provider_factory,
899 scratch_pool,
900 })
901 }
902
903 fn search_strategy<'a>(
905 &'a self,
906 query: &'a [Data::VectorDataType],
907 vector_filter: &'a (dyn Fn(&Data::VectorIdType) -> bool + Send + Sync),
908 ) -> DiskSearchStrategy<'a, Data, ProviderFactory> {
909 DiskSearchStrategy {
910 io_tracker: IOTracker::default(),
911 vector_filter,
912 query,
913 vertex_provider_factory: &self.vertex_provider_factory,
914 scratch_pool: &self.scratch_pool,
915 }
916 }
917
918 pub fn search(
921 &self,
922 query: &[Data::VectorDataType],
923 return_list_size: u32,
924 search_list_size: u32,
925 beam_width: Option<usize>,
926 vector_filter: Option<VectorFilter<Data>>,
927 is_flat_search: bool,
928 ) -> ANNResult<SearchResult<Data::AssociatedDataType>> {
929 let mut query_stats = QueryStatistics::default();
930 let mut indices = vec![0u32; return_list_size as usize];
931 let mut distances = vec![0f32; return_list_size as usize];
932 let mut associated_data =
933 vec![Data::AssociatedDataType::default(); return_list_size as usize];
934
935 let stats = self.search_internal(
936 query,
937 return_list_size as usize,
938 search_list_size,
939 beam_width,
940 &mut query_stats,
941 &mut indices,
942 &mut distances,
943 &mut associated_data,
944 &vector_filter.unwrap_or(default_vector_filter::<Data>()),
945 is_flat_search,
946 )?;
947
948 let mut search_result = SearchResult {
949 results: Vec::with_capacity(return_list_size as usize),
950 stats,
951 };
952
953 for ((vertex_id, distance), associated_data) in indices
954 .into_iter()
955 .zip(distances.into_iter())
956 .zip(associated_data.into_iter())
957 {
958 search_result.results.push(SearchResultItem {
959 vertex_id,
960 distance,
961 data: associated_data,
962 });
963 }
964
965 Ok(search_result)
966 }
967
968 #[allow(clippy::too_many_arguments)]
971 pub(crate) fn search_internal(
972 &self,
973 query: &[Data::VectorDataType],
974 k_value: usize,
975 search_list_size: u32,
976 beam_width: Option<usize>,
977 query_stats: &mut QueryStatistics,
978 indices: &mut [u32],
979 distances: &mut [f32],
980 associated_data: &mut [Data::AssociatedDataType],
981 vector_filter: &(dyn Fn(&Data::VectorIdType) -> bool + Send + Sync),
982 is_flat_search: bool,
983 ) -> ANNResult<SearchResultStats> {
984 let mut result_output_buffer = search_output_buffer::IdDistanceAssociatedData::new(
985 &mut indices[..k_value],
986 &mut distances[..k_value],
987 &mut associated_data[..k_value],
988 );
989
990 let strategy = self.search_strategy(query, vector_filter);
991 let timer = Instant::now();
992 let k = k_value;
993 let l = search_list_size as usize;
994 let stats = if is_flat_search {
995 self.runtime.block_on(self.index.flat_search(
996 &strategy,
997 &DefaultContext,
998 strategy.query,
999 vector_filter,
1000 &Knn::new(k, l, beam_width)?,
1001 &mut result_output_buffer,
1002 ))?
1003 } else {
1004 let knn_search = Knn::new(k, l, beam_width)?;
1005 self.runtime.block_on(self.index.search(
1006 knn_search,
1007 &strategy,
1008 &DefaultContext,
1009 strategy.query,
1010 &mut result_output_buffer,
1011 ))?
1012 };
1013 query_stats.total_comparisons = stats.cmps;
1014 query_stats.search_hops = stats.hops;
1015
1016 query_stats.total_execution_time_us = timer.elapsed().as_micros();
1017 query_stats.io_time_us = IOTracker::time(&strategy.io_tracker.io_time_us) as u128;
1018 query_stats.total_io_operations = strategy.io_tracker.io_count() as u32;
1019 query_stats.total_vertices_loaded = strategy.io_tracker.io_count() as u32;
1020 query_stats.query_pq_preprocess_time_us =
1021 IOTracker::time(&strategy.io_tracker.preprocess_time_us) as u128;
1022 query_stats.cpu_time_us = query_stats.total_execution_time_us
1023 - query_stats.io_time_us
1024 - query_stats.query_pq_preprocess_time_us;
1025 Ok(SearchResultStats {
1026 cmps: query_stats.total_comparisons,
1027 result_count: stats.result_count,
1028 query_statistics: query_stats.clone(),
1029 })
1030 }
1031}
1032
1033fn ensure_vertex_loaded<Data: GraphDataType, V: VertexProvider<Data>>(
1039 vertex_provider: &mut V,
1040 ids: &[Data::VectorIdType],
1041) -> ANNResult<()> {
1042 vertex_provider.load_vertices(ids)?;
1043 for (idx, id) in ids.iter().enumerate() {
1044 vertex_provider.process_loaded_node(id, idx)?;
1045 }
1046 Ok(())
1047}
1048
1049#[cfg(test)]
1050mod disk_provider_tests {
1051 use crate::test_utils::{GraphDataF32VectorU32Data, GraphDataF32VectorUnitData};
1052 use diskann::{
1053 graph::{
1054 search::{record::VisitedSearchRecord, Knn},
1055 KnnSearchError,
1056 },
1057 utils::IntoUsize,
1058 ANNErrorKind,
1059 };
1060 use diskann_providers::storage::{
1061 DynWriteProvider, StorageReadProvider, VirtualStorageProvider,
1062 };
1063 use diskann_providers::utils::{create_thread_pool, PQPathNames, ParallelIteratorInPool};
1064 use diskann_utils::{io::read_bin, test_data_root};
1065 use diskann_vector::distance::Metric;
1066 use rayon::prelude::IndexedParallelIterator;
1067 use rstest::rstest;
1068 use vfs::OverlayFS;
1069
1070 use super::*;
1071 use crate::{
1072 build::builder::core::disk_index_builder_tests::{IndexBuildFixture, TestParams},
1073 utils::{QueryStatistics, VirtualAlignedReaderFactory},
1074 };
1075
1076 const TEST_INDEX_PREFIX_128DIM: &str =
1077 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search";
1078 const TEST_INDEX_128DIM: &str =
1079 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_disk.index";
1080 const TEST_PQ_PIVOT_128DIM: &str =
1081 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_pq_pivots.bin";
1082 const TEST_PQ_COMPRESSED_128DIM: &str =
1083 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_pq_compressed.bin";
1084 const TEST_TRUTH_RESULT_10PTS_128DIM: &str =
1085 "/disk_index_search/disk_index_10pts_idx_uint32_truth_search_res.bin";
1086 const TEST_QUERY_10PTS_128DIM: &str = "/disk_index_search/disk_index_sample_query_10pts.fbin";
1087
1088 const TEST_INDEX_PREFIX_100DIM: &str = "/disk_index_search/256pts_100dim_f32_truth_Index";
1089 const TEST_INDEX_100DIM: &str = "/disk_index_search/256pts_100dim_f32_truth_Index_disk.index";
1090 const TEST_PQ_PIVOT_100DIM: &str =
1091 "/disk_index_search/256pts_100dim_f32_truth_Index_pq_pivots.bin";
1092 const TEST_PQ_COMPRESSED_100DIM: &str =
1093 "/disk_index_search/256pts_100dim_f32_truth_Index_pq_compressed.bin";
1094 const TEST_TRUTH_RESULT_10PTS_100DIM: &str =
1095 "/disk_index_search/256pts_100dim_f32_truth_query_result.bin";
1096 const TEST_QUERY_10PTS_100DIM: &str = "/disk_index_search/10pts_100dim_f32_base_query.bin";
1097 const TEST_DATA_FILE: &str = "/disk_index_search/disk_index_siftsmall_learn_256pts_data.fbin";
1098 const TEST_INDEX: &str =
1099 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_disk.index";
1100 const TEST_INDEX_PREFIX: &str =
1101 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search";
1102 const TEST_PQ_PIVOT: &str =
1103 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_pq_pivots.bin";
1104 const TEST_PQ_COMPRESSED: &str =
1105 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_pq_compressed.bin";
1106
1107 #[test]
1108 fn test_disk_search_k10_l20_single_or_multi_thread_100dim() {
1109 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1110
1111 let search_engine = create_disk_index_searcher(
1112 CreateDiskIndexSearcherParams {
1113 max_thread_num: 5,
1114 pq_pivot_file_path: TEST_PQ_PIVOT_100DIM,
1115 pq_compressed_file_path: TEST_PQ_COMPRESSED_100DIM,
1116 index_path: TEST_INDEX_100DIM,
1117 index_path_prefix: TEST_INDEX_PREFIX_100DIM,
1118 ..Default::default()
1119 },
1120 &storage_provider,
1121 );
1122 test_disk_search(TestDiskSearchParams {
1124 storage_provider: storage_provider.as_ref(),
1125 index_search_engine: &search_engine,
1126 thread_num: 1,
1127 query_file_path: TEST_QUERY_10PTS_100DIM,
1128 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_100DIM,
1129 k: 10,
1130 l: 20,
1131 });
1132 test_disk_search(TestDiskSearchParams {
1134 storage_provider: storage_provider.as_ref(),
1135 index_search_engine: &search_engine,
1136 thread_num: 5,
1137 query_file_path: TEST_QUERY_10PTS_100DIM,
1138 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_100DIM,
1139 k: 10,
1140 l: 20,
1141 });
1142 }
1143
1144 #[test]
1145 fn test_disk_search_k10_l20_single_or_multi_thread_128dim() {
1146 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1147
1148 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
1149 CreateDiskIndexSearcherParams {
1150 max_thread_num: 5,
1151 pq_pivot_file_path: TEST_PQ_PIVOT_128DIM,
1152 pq_compressed_file_path: TEST_PQ_COMPRESSED_128DIM,
1153 index_path: TEST_INDEX_128DIM,
1154 index_path_prefix: TEST_INDEX_PREFIX_128DIM,
1155 ..Default::default()
1156 },
1157 &storage_provider,
1158 );
1159 test_disk_search(TestDiskSearchParams {
1161 storage_provider: storage_provider.as_ref(),
1162 index_search_engine: &search_engine,
1163 thread_num: 1,
1164 query_file_path: TEST_QUERY_10PTS_128DIM,
1165 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_128DIM,
1166 k: 10,
1167 l: 20,
1168 });
1169 test_disk_search(TestDiskSearchParams {
1171 storage_provider: storage_provider.as_ref(),
1172 index_search_engine: &search_engine,
1173 thread_num: 5,
1174 query_file_path: TEST_QUERY_10PTS_128DIM,
1175 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_128DIM,
1176 k: 10,
1177 l: 20,
1178 });
1179 }
1180
1181 fn get_truth_associated_data<StorageReader: StorageReadProvider>(
1182 storage_provider: &StorageReader,
1183 ) -> Vec<u32> {
1184 const ASSOCIATED_DATA_FILE: &str = "/sift/siftsmall_learn_256pts_u32_associated_data.fbin";
1185
1186 let data =
1187 read_bin::<u32>(&mut storage_provider.open_reader(ASSOCIATED_DATA_FILE).unwrap())
1188 .unwrap();
1189 data.into_inner().into_vec()
1190 }
1191
1192 #[test]
1193 fn test_disk_search_with_associated_data_k10_l20_single_or_multi_thread_128dim() {
1194 let storage_provider = VirtualStorageProvider::new_overlay(test_data_root());
1195 let index_path_prefix = "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_test_disk_index_search_associated_data";
1196 let params = TestParams {
1197 data_path: TEST_DATA_FILE.to_string(),
1198 index_path_prefix: index_path_prefix.to_string(),
1199 associated_data_path: Some(
1200 "/sift/siftsmall_learn_256pts_u32_associated_data.fbin".to_string(),
1201 ),
1202 ..TestParams::default()
1203 };
1204 let fixture = IndexBuildFixture::new(storage_provider, params).unwrap();
1205 fixture.build::<GraphDataF32VectorU32Data>().unwrap();
1207 {
1208 let search_engine = create_disk_index_searcher::<GraphDataF32VectorU32Data>(
1209 CreateDiskIndexSearcherParams {
1210 max_thread_num: 5,
1211 pq_pivot_file_path: format!("{}_pq_pivots.bin", index_path_prefix).as_str(),
1212 pq_compressed_file_path: format!("{}_pq_compressed.bin", index_path_prefix)
1213 .as_str(),
1214 index_path: format!("{}_disk.index", index_path_prefix).as_str(), index_path_prefix,
1216 ..Default::default()
1217 },
1218 &fixture.storage_provider,
1219 );
1220
1221 test_disk_search_with_associated(
1223 TestDiskSearchAssociateParams {
1224 storage_provider: fixture.storage_provider.as_ref(),
1225 index_search_engine: &search_engine,
1226 thread_num: 1,
1227 query_file_path: TEST_QUERY_10PTS_128DIM,
1228 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_128DIM,
1229 k: 10,
1230 l: 20,
1231 },
1232 None,
1233 );
1234
1235 test_disk_search_with_associated(
1237 TestDiskSearchAssociateParams {
1238 storage_provider: fixture.storage_provider.as_ref(),
1239 index_search_engine: &search_engine,
1240 thread_num: 5,
1241 query_file_path: TEST_QUERY_10PTS_128DIM,
1242 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_128DIM,
1243 k: 10,
1244 l: 20,
1245 },
1246 None,
1247 );
1248 }
1249
1250 fixture
1251 .storage_provider
1252 .delete(&format!("{}_disk.index", index_path_prefix))
1253 .expect("Failed to delete file");
1254 fixture
1255 .storage_provider
1256 .delete(&format!("{}_pq_pivots.bin", index_path_prefix))
1257 .expect("Failed to delete file");
1258 fixture
1259 .storage_provider
1260 .delete(&format!("{}_pq_compressed.bin", index_path_prefix))
1261 .expect("Failed to delete file");
1262 }
1263
1264 struct CreateDiskIndexSearcherParams<'a> {
1265 max_thread_num: usize,
1266 pq_pivot_file_path: &'a str,
1267 pq_compressed_file_path: &'a str,
1268 index_path: &'a str,
1269 index_path_prefix: &'a str,
1270 io_limit: usize,
1271 }
1272
1273 impl Default for CreateDiskIndexSearcherParams<'_> {
1274 fn default() -> Self {
1275 Self {
1276 max_thread_num: 1,
1277 pq_pivot_file_path: "",
1278 pq_compressed_file_path: "",
1279 index_path: "",
1280 index_path_prefix: "",
1281 io_limit: usize::MAX,
1282 }
1283 }
1284 }
1285
1286 fn create_disk_index_searcher<Data>(
1287 params: CreateDiskIndexSearcherParams,
1288 storage_provider: &Arc<VirtualStorageProvider<OverlayFS>>,
1289 ) -> DiskIndexSearcher<
1290 Data,
1291 DiskVertexProviderFactory<Data, VirtualAlignedReaderFactory<OverlayFS>>,
1292 >
1293 where
1294 Data: GraphDataType<VectorIdType = u32>,
1295 {
1296 assert!(params.io_limit > 0);
1297
1298 let runtime = tokio::runtime::Builder::new_multi_thread()
1299 .worker_threads(params.max_thread_num)
1300 .build()
1301 .unwrap();
1302
1303 let disk_index_reader = DiskIndexReader::<Data::VectorDataType>::new(
1304 params.pq_pivot_file_path.to_string(),
1305 params.pq_compressed_file_path.to_string(),
1306 storage_provider.as_ref(),
1307 )
1308 .unwrap();
1309
1310 let aligned_reader_factory = VirtualAlignedReaderFactory::new(
1311 get_disk_index_file(params.index_path_prefix),
1312 Arc::clone(storage_provider),
1313 );
1314 let caching_strategy = CachingStrategy::None;
1315 let vertex_provider_factory =
1316 DiskVertexProviderFactory::<Data, _>::new(aligned_reader_factory, caching_strategy)
1317 .unwrap();
1318
1319 DiskIndexSearcher::<Data, DiskVertexProviderFactory<Data, _>>::new(
1320 params.max_thread_num,
1321 params.io_limit,
1322 &disk_index_reader,
1323 vertex_provider_factory,
1324 Metric::L2,
1325 Some(runtime),
1326 )
1327 .unwrap()
1328 }
1329
1330 fn load_query_result<StorageReader: StorageReadProvider>(
1331 storage_provider: &StorageReader,
1332 query_result_path: &str,
1333 ) -> Vec<u32> {
1334 let result =
1335 read_bin::<u32>(&mut storage_provider.open_reader(query_result_path).unwrap()).unwrap();
1336 result.into_inner().into_vec()
1337 }
1338
1339 struct TestDiskSearchParams<'a, StorageType> {
1340 storage_provider: &'a StorageType,
1341 index_search_engine: &'a DiskIndexSearcher<
1342 GraphDataF32VectorUnitData,
1343 DiskVertexProviderFactory<
1344 GraphDataF32VectorUnitData,
1345 VirtualAlignedReaderFactory<OverlayFS>,
1346 >,
1347 >,
1348 thread_num: u64,
1349 query_file_path: &'a str,
1350 truth_result_file_path: &'a str,
1351 k: usize,
1352 l: usize,
1353 }
1354
1355 struct TestDiskSearchAssociateParams<'a, StorageType> {
1356 storage_provider: &'a StorageType,
1357 index_search_engine: &'a DiskIndexSearcher<
1358 GraphDataF32VectorU32Data,
1359 DiskVertexProviderFactory<
1360 GraphDataF32VectorU32Data,
1361 VirtualAlignedReaderFactory<OverlayFS>,
1362 >,
1363 >,
1364 thread_num: u64,
1365 query_file_path: &'a str,
1366 truth_result_file_path: &'a str,
1367 k: usize,
1368 l: usize,
1369 }
1370
1371 fn test_disk_search<StorageType: StorageReadProvider>(
1372 params: TestDiskSearchParams<StorageType>,
1373 ) {
1374 let queries = read_bin::<f32>(
1375 &mut params
1376 .storage_provider
1377 .open_reader(params.query_file_path)
1378 .unwrap(),
1379 )
1380 .unwrap();
1381 let truth_result =
1382 load_query_result(params.storage_provider, params.truth_result_file_path);
1383
1384 let pool = create_thread_pool(params.thread_num.into_usize()).unwrap();
1385 queries
1386 .par_row_iter()
1387 .enumerate()
1388 .for_each_in_pool(pool.as_ref(), |(i, query)| {
1389 let mut query_stats = QueryStatistics::default();
1390 let mut indices = vec![0u32; 10];
1391 let mut distances = vec![0f32; 10];
1392 let mut associated_data = vec![(); 10];
1393
1394 let result = params.index_search_engine.search_internal(
1395 query,
1396 params.k,
1397 params.l as u32,
1398 None, &mut query_stats,
1400 &mut indices,
1401 &mut distances,
1402 &mut associated_data,
1403 &(|_| true),
1404 false,
1405 );
1406
1407 let truth_slice = &truth_result[i * params.k..(i + 1) * params.k];
1409
1410 assert!(result.is_ok(), "Expected search to succeed");
1411
1412 let result_unwrapped = result.unwrap();
1413 assert!(
1414 result_unwrapped.query_statistics.total_io_operations > 0,
1415 "Expected IO operations to be greater than 0"
1416 );
1417 assert!(
1418 result_unwrapped.query_statistics.total_vertices_loaded > 0,
1419 "Expected vertices loaded to be greater than 0"
1420 );
1421
1422 assert_eq!(
1424 indices, truth_slice,
1425 "Results DO NOT match with the truth result for query {}",
1426 i
1427 );
1428 });
1429 }
1430
1431 fn test_disk_search_with_associated<StorageType: StorageReadProvider>(
1432 params: TestDiskSearchAssociateParams<StorageType>,
1433 beam_width: Option<usize>,
1434 ) {
1435 let queries = read_bin::<f32>(
1436 &mut params
1437 .storage_provider
1438 .open_reader(params.query_file_path)
1439 .unwrap(),
1440 )
1441 .unwrap();
1442 let truth_result =
1443 load_query_result(params.storage_provider, params.truth_result_file_path);
1444 let pool = create_thread_pool(params.thread_num.into_usize()).unwrap();
1445 queries
1446 .par_row_iter()
1447 .enumerate()
1448 .for_each_in_pool(pool.as_ref(), |(i, query)| {
1449 let result = params
1450 .index_search_engine
1451 .search(query, params.k as u32, params.l as u32, beam_width, None, false)
1452 .unwrap();
1453 let indices: Vec<u32> = result.results.iter().map(|item| item.vertex_id).collect();
1454 let associated_data: Vec<u32> =
1455 result.results.iter().map(|item| item.data).collect();
1456 let truth_data = get_truth_associated_data(params.storage_provider);
1457 let associated_data_truth: Vec<u32> = indices
1458 .iter()
1459 .map(|&vid| truth_data[vid as usize])
1460 .collect();
1461 assert_eq!(
1462 associated_data, associated_data_truth,
1463 "Associated data DO NOT match with the truth result for query {}, associated_data from search: {:?}, associated_data from truth result: {:?}",
1464 i,associated_data, associated_data_truth
1465 );
1466 let truth_slice = &truth_result[i * params.k..(i + 1) * params.k];
1467 assert_eq!(
1468 indices, truth_slice,
1469 "Results DO NOT match with the truth result for query {}",
1470 i
1471 );
1472 });
1473 }
1474
1475 #[test]
1476 fn test_disk_search_invalid_input() {
1477 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1478 let ctx = &DefaultContext;
1479
1480 let params = CreateDiskIndexSearcherParams {
1481 max_thread_num: 5,
1482 pq_pivot_file_path: TEST_PQ_PIVOT_128DIM,
1483 pq_compressed_file_path: TEST_PQ_COMPRESSED_128DIM,
1484 index_path: TEST_INDEX_128DIM,
1485 index_path_prefix: TEST_INDEX_PREFIX_128DIM,
1486 ..Default::default()
1487 };
1488
1489 let paths = PQPathNames::for_disk_index(TEST_INDEX_PREFIX_128DIM);
1490 assert_eq!(
1491 paths.pivots, params.pq_pivot_file_path,
1492 "pq_pivot_file_path is not correct"
1493 );
1494 assert_eq!(
1495 paths.compressed_data, params.pq_compressed_file_path,
1496 "pq_compressed_file_path is not correct"
1497 );
1498 assert_eq!(
1499 params.index_path,
1500 format!("{}_disk.index", params.index_path_prefix),
1501 "index_path is not correct"
1502 );
1503
1504 let res = Knn::new_default(20, 10);
1506 assert!(res.is_err());
1507 assert_eq!(
1508 <KnnSearchError as std::convert::Into<ANNError>>::into(res.unwrap_err()).kind(),
1509 ANNErrorKind::IndexError
1510 );
1511 let res = Knn::new(10, 10, Some(0));
1513 assert!(res.is_err());
1514
1515 let search_engine =
1516 create_disk_index_searcher::<GraphDataF32VectorU32Data>(params, &storage_provider);
1517
1518 assert_eq!(
1520 search_engine
1521 .index
1522 .data_provider
1523 .to_external_id(ctx, 0)
1524 .unwrap(),
1525 0
1526 );
1527 assert_eq!(
1528 search_engine
1529 .index
1530 .data_provider
1531 .to_internal_id(ctx, &0)
1532 .unwrap(),
1533 0
1534 );
1535
1536 let provider_max_degree = search_engine
1537 .index
1538 .data_provider
1539 .graph_header
1540 .max_degree::<<GraphDataF32VectorU32Data as GraphDataType>::VectorDataType>()
1541 .unwrap();
1542 let index_max_degree = search_engine.index.config.pruned_degree().get();
1543 assert_eq!(provider_max_degree, index_max_degree);
1544
1545 let query = vec![0f32; 128];
1546 let mut query_stats = QueryStatistics::default();
1547 let mut indices = vec![0u32; 10];
1548 let mut distances = vec![0f32; 10];
1549 let mut associated_data = vec![0u32; 10];
1550
1551 let result = search_engine.search_internal(
1553 &query,
1554 10,
1555 10 - 1,
1556 None,
1557 &mut query_stats,
1558 &mut indices,
1559 &mut distances,
1560 &mut associated_data,
1561 &|_| true,
1562 false,
1563 );
1564
1565 assert!(result.is_err());
1566 assert_eq!(result.unwrap_err().kind(), ANNErrorKind::IndexError);
1567 }
1568
1569 #[test]
1570 fn test_disk_search_beam_search() {
1571 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1572
1573 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
1574 CreateDiskIndexSearcherParams {
1575 max_thread_num: 1,
1576 pq_pivot_file_path: TEST_PQ_PIVOT,
1577 pq_compressed_file_path: TEST_PQ_COMPRESSED,
1578 index_path: TEST_INDEX,
1579 index_path_prefix: TEST_INDEX_PREFIX,
1580 ..Default::default()
1581 },
1582 &storage_provider,
1583 );
1584
1585 let query_vector: [f32; 128] = [1f32; 128];
1586 let mut indices = vec![0u32; 10];
1587 let mut distances = vec![0f32; 10];
1588 let mut associated_data = vec![(); 10];
1589
1590 let mut result_output_buffer = search_output_buffer::IdDistanceAssociatedData::new(
1591 &mut indices,
1592 &mut distances,
1593 &mut associated_data,
1594 );
1595 let strategy = search_engine.search_strategy(&query_vector, &|_| true);
1596 let mut search_record = VisitedSearchRecord::new(0);
1597 let search_params = Knn::new(10, 10, Some(4)).unwrap();
1598 let recorded_search =
1599 diskann::graph::search::RecordedKnn::new(search_params, &mut search_record);
1600 search_engine
1601 .runtime
1602 .block_on(search_engine.index.search(
1603 recorded_search,
1604 &strategy,
1605 &DefaultContext,
1606 query_vector.as_slice(),
1607 &mut result_output_buffer,
1608 ))
1609 .unwrap();
1610
1611 let ids = search_record
1612 .visited
1613 .iter()
1614 .map(|n| n.id)
1615 .collect::<Vec<_>>();
1616
1617 const EXPECTED_NODES: [u32; 18] = [
1618 72, 118, 108, 86, 84, 152, 170, 82, 114, 87, 207, 176, 79, 153, 67, 165, 141, 180,
1619 ]; assert_eq!(ids, &EXPECTED_NODES);
1622
1623 let return_list_size = 10;
1624 let search_list_size = 10;
1625 let result = search_engine.search(
1626 &query_vector,
1627 return_list_size,
1628 search_list_size,
1629 Some(4),
1630 None,
1631 false,
1632 );
1633 assert!(result.is_ok(), "Expected search to succeed");
1634 let search_result = result.unwrap();
1635 assert_eq!(
1636 search_result.results.len() as u32,
1637 return_list_size,
1638 "Expected result count to match"
1639 );
1640 assert_eq!(
1641 indices,
1642 vec![152, 72, 170, 118, 87, 165, 79, 141, 108, 86],
1643 "Expected indices to match"
1644 );
1645 }
1646
1647 #[cfg(feature = "experimental_diversity_search")]
1648 #[test]
1649 fn test_disk_search_diversity_search() {
1650 use diskann::graph::DiverseSearchParams;
1651 use diskann::neighbor::AttributeValueProvider;
1652 use std::collections::HashMap;
1653
1654 #[derive(Debug, Clone)]
1656 struct TestAttributeProvider {
1657 attributes: HashMap<u32, u32>,
1658 }
1659 impl TestAttributeProvider {
1660 fn new() -> Self {
1661 Self {
1662 attributes: HashMap::new(),
1663 }
1664 }
1665 fn insert(&mut self, id: u32, attribute: u32) {
1666 self.attributes.insert(id, attribute);
1667 }
1668 }
1669 impl diskann::provider::HasId for TestAttributeProvider {
1670 type Id = u32;
1671 }
1672
1673 impl AttributeValueProvider for TestAttributeProvider {
1674 type Value = u32;
1675
1676 fn get(&self, id: Self::Id) -> Option<Self::Value> {
1677 self.attributes.get(&id).copied()
1678 }
1679 }
1680
1681 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1682
1683 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
1684 CreateDiskIndexSearcherParams {
1685 max_thread_num: 1,
1686 pq_pivot_file_path: TEST_PQ_PIVOT,
1687 pq_compressed_file_path: TEST_PQ_COMPRESSED,
1688 index_path: TEST_INDEX,
1689 index_path_prefix: TEST_INDEX_PREFIX,
1690 ..Default::default()
1691 },
1692 &storage_provider,
1693 );
1694
1695 let query_vector: [f32; 128] = [1f32; 128];
1696
1697 let mut attribute_provider = TestAttributeProvider::new();
1699 let num_vectors = 256; for i in 0..num_vectors {
1701 let label = (i % 15) + 1;
1703 attribute_provider.insert(i, label);
1704 }
1705 let attribute_provider = std::sync::Arc::new(attribute_provider);
1707
1708 let mut indices = vec![0u32; 10];
1709 let mut distances = vec![0f32; 10];
1710 let mut associated_data = vec![(); 10];
1711
1712 let mut result_output_buffer = search_output_buffer::IdDistanceAssociatedData::new(
1713 &mut indices,
1714 &mut distances,
1715 &mut associated_data,
1716 );
1717 let strategy = search_engine.search_strategy(&query_vector, &|_| true);
1718
1719 let diverse_params = DiverseSearchParams::new(
1721 0, 3, attribute_provider.clone(),
1724 );
1725
1726 let search_params = Knn::new(10, 20, None).unwrap();
1727
1728 let diverse_search = diskann::graph::search::Diverse::new(search_params, diverse_params);
1729 let stats = search_engine
1730 .runtime
1731 .block_on(search_engine.index.search(
1732 diverse_search,
1733 &strategy,
1734 &DefaultContext,
1735 query_vector.as_slice(),
1736 &mut result_output_buffer,
1737 ))
1738 .unwrap();
1739
1740 assert!(
1742 stats.result_count > 0,
1743 "Expected to get some results during diversity search"
1744 );
1745
1746 let return_list_size = 10;
1747 let search_list_size = 20;
1748 let diverse_results_k = 1;
1749 let diverse_params = DiverseSearchParams::new(
1750 0, diverse_results_k,
1752 attribute_provider.clone(),
1753 );
1754
1755 let mut indices2 = vec![0u32; return_list_size as usize];
1757 let mut distances2 = vec![0f32; return_list_size as usize];
1758 let mut associated_data2 = vec![(); return_list_size as usize];
1759 let mut result_output_buffer2 = search_output_buffer::IdDistanceAssociatedData::new(
1760 &mut indices2,
1761 &mut distances2,
1762 &mut associated_data2,
1763 );
1764 let strategy2 = search_engine.search_strategy(&query_vector, &|_| true);
1765 let search_params2 =
1766 Knn::new(return_list_size as usize, search_list_size as usize, None).unwrap();
1767
1768 let diverse_search2 = diskann::graph::search::Diverse::new(search_params2, diverse_params);
1769 let stats = search_engine
1770 .runtime
1771 .block_on(search_engine.index.search(
1772 diverse_search2,
1773 &strategy2,
1774 &DefaultContext,
1775 query_vector.as_slice(),
1776 &mut result_output_buffer2,
1777 ))
1778 .unwrap();
1779
1780 assert!(
1782 stats.result_count > 0,
1783 "Expected diversity search to return results"
1784 );
1785 assert!(
1786 stats.result_count <= return_list_size,
1787 "Expected result count to be <= {}",
1788 return_list_size
1789 );
1790
1791 assert!(
1793 stats.result_count > 0,
1794 "Expected to get some search results"
1795 );
1796
1797 println!("\n=== Diversity Search Results ===");
1799 println!("Query: [1f32; 128]");
1800 println!("diverse_results_k: {}", diverse_results_k);
1801 println!("Total results: {}\n", stats.result_count);
1802 println!("{:<10} {:<15} {:<10}", "Vertex ID", "Distance", "Label");
1803 println!("{}", "-".repeat(35));
1804 for i in 0..stats.result_count as usize {
1805 let attribute_value = attribute_provider.get(indices2[i]).unwrap_or(0);
1806 println!(
1807 "{:<10} {:<15.2} {:<10}",
1808 indices2[i], distances2[i], attribute_value
1809 );
1810 }
1811
1812 for i in 0..(stats.result_count as usize).saturating_sub(1) {
1814 assert!(distances2[i] >= 0.0, "Expected non-negative distance");
1815 assert!(
1816 distances2[i] <= distances2[i + 1],
1817 "Expected distances to be sorted in ascending order"
1818 );
1819 }
1820
1821 let mut attribute_counts = HashMap::new();
1823 for item in indices2.iter().take(stats.result_count as usize) {
1824 if let Some(attribute_value) = attribute_provider.get(*item) {
1825 *attribute_counts.entry(attribute_value).or_insert(0) += 1;
1826 }
1827 }
1828
1829 println!("\n=== Attribute Distribution ===");
1831 let mut sorted_attrs: Vec<_> = attribute_counts.iter().collect();
1832 sorted_attrs.sort_by_key(|(k, _)| *k);
1833 for (attribute_value, count) in &sorted_attrs {
1834 println!(
1835 "Label {}: {} occurrences (max allowed: {})",
1836 attribute_value, count, diverse_results_k
1837 );
1838 }
1839 println!("Total unique labels: {}", attribute_counts.len());
1840 println!("================================\n");
1841
1842 for (attribute_value, count) in &attribute_counts {
1844 println!(
1845 "Assert: Label {} has {} occurrences (max: {})",
1846 attribute_value, count, diverse_results_k
1847 );
1848 assert!(
1849 *count <= diverse_results_k,
1850 "Attribute value {} appears {} times, which exceeds diverse_results_k of {}",
1851 attribute_value,
1852 count,
1853 diverse_results_k
1854 );
1855 }
1856
1857 println!(
1860 "Assert: Found {} unique labels (expected at least 2)",
1861 attribute_counts.len()
1862 );
1863 assert!(
1864 attribute_counts.len() >= 2,
1865 "Expected at least 2 different attribute values for diversity, got {}",
1866 attribute_counts.len()
1867 );
1868 }
1869
1870 #[rstest]
1871 #[case(
1873 |_id: &u32| true,
1874 false,
1875 10,
1876 vec![152, 118, 72, 170, 87, 141, 79, 207, 124, 86],
1877 vec![256101.7, 256675.3, 256709.69, 256712.5, 256760.08, 256958.5, 257006.1, 257025.7, 257105.67, 257107.67],
1878 )]
1879 #[case(
1882 |id: &u32| *id == 0 || *id == 1,
1883 false,
1884 0,
1885 vec![0; 10],
1886 vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
1887 )]
1888 #[case(
1891 |id: &u32| *id == 0 || *id == 1,
1892 true,
1893 2,
1894 vec![1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
1895 vec![257247.28, 258179.28, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
1896 )]
1897 #[case(
1900 |id: &u32| *id == 72 || *id == 87 || *id == 170,
1901 false,
1902 3,
1903 vec![72, 170, 87, 0, 0, 0, 0, 0, 0, 0],
1904 vec![256709.69, 256712.5, 256760.08, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
1905 )]
1906 #[case(
1909 |id: &u32| *id == 72 || *id == 87 || *id == 170,
1910 true,
1911 3,
1912 vec![72, 170, 87, 0, 0, 0, 0, 0, 0, 0],
1913 vec![256709.69, 256712.5, 256760.08, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
1914 )]
1915 fn test_search_with_vector_filter(
1916 #[case] vector_filter: fn(&u32) -> bool,
1917 #[case] is_flat_search: bool,
1918 #[case] expected_result_count: u32,
1919 #[case] expected_indices: Vec<u32>,
1920 #[case] expected_distances: Vec<f32>,
1921 ) {
1922 let check_distances = |got: &[f32], expected: &[f32]| -> bool {
1927 const ABS_TOLERANCE: f32 = 0.02;
1928 assert_eq!(got.len(), expected.len());
1929 for (i, (g, e)) in std::iter::zip(got.iter(), expected.iter()).enumerate() {
1930 if (g - e).abs() > ABS_TOLERANCE {
1931 panic!(
1932 "distances differ at position {} by more than {}\n\n\
1933 got: {:?}\nexpected: {:?}",
1934 i, ABS_TOLERANCE, got, expected,
1935 );
1936 }
1937 }
1938 true
1939 };
1940
1941 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1942
1943 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
1944 CreateDiskIndexSearcherParams {
1945 max_thread_num: 5,
1946 pq_pivot_file_path: TEST_PQ_PIVOT_128DIM,
1947 pq_compressed_file_path: TEST_PQ_COMPRESSED_128DIM,
1948 index_path: TEST_INDEX_128DIM,
1949 index_path_prefix: TEST_INDEX_PREFIX_128DIM,
1950 ..Default::default()
1951 },
1952 &storage_provider,
1953 );
1954 let query = vec![0.1f32; 128];
1955 let mut query_stats = QueryStatistics::default();
1956 let mut indices = vec![0u32; 10];
1957 let mut distances = vec![0f32; 10];
1958 let mut associated_data = vec![(); 10];
1959
1960 let result = search_engine.search_internal(
1961 &query,
1962 10,
1963 10,
1964 None, &mut query_stats,
1966 &mut indices,
1967 &mut distances,
1968 &mut associated_data,
1969 &vector_filter,
1970 is_flat_search,
1971 );
1972
1973 assert!(result.is_ok(), "Expected search to succeed");
1974 assert_eq!(
1975 result.unwrap().result_count,
1976 expected_result_count,
1977 "Expected result count to match"
1978 );
1979 assert_eq!(indices, expected_indices, "Expected indices to match");
1980 assert!(
1981 check_distances(&distances, &expected_distances),
1982 "Expected distances to match"
1983 );
1984
1985 let result_with_filter = search_engine.search(
1986 &query,
1987 10,
1988 10,
1989 None, Some(Box::new(vector_filter)),
1991 is_flat_search,
1992 );
1993
1994 assert!(result_with_filter.is_ok(), "Expected search to succeed");
1995 let result_with_filter_unwrapped = result_with_filter.unwrap();
1996 assert_eq!(
1997 result_with_filter_unwrapped.stats.result_count, expected_result_count,
1998 "Expected result count to match"
1999 );
2000 let actual_indices = result_with_filter_unwrapped
2001 .results
2002 .iter()
2003 .map(|x| x.vertex_id)
2004 .collect::<Vec<_>>();
2005 assert_eq!(
2006 actual_indices, expected_indices,
2007 "Expected indices to match"
2008 );
2009 let actual_distances = result_with_filter_unwrapped
2010 .results
2011 .iter()
2012 .map(|x| x.distance)
2013 .collect::<Vec<_>>();
2014 assert!(
2015 check_distances(&actual_distances, &expected_distances),
2016 "Expected distances to match"
2017 );
2018 }
2019
2020 #[test]
2021 fn test_beam_search_respects_io_limit() {
2022 let io_limit = 11; let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
2024
2025 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
2026 CreateDiskIndexSearcherParams {
2027 max_thread_num: 1,
2028 pq_pivot_file_path: TEST_PQ_PIVOT,
2029 pq_compressed_file_path: TEST_PQ_COMPRESSED,
2030 index_path: TEST_INDEX,
2031 index_path_prefix: TEST_INDEX_PREFIX,
2032 io_limit,
2033 },
2034 &storage_provider,
2035 );
2036 let query_vector: [f32; 128] = [1f32; 128];
2037
2038 let mut indices = vec![0u32; 10];
2039 let mut distances = vec![0f32; 10];
2040 let mut associated_data = vec![(); 10];
2041
2042 let mut result_output_buffer = search_output_buffer::IdDistanceAssociatedData::new(
2043 &mut indices,
2044 &mut distances,
2045 &mut associated_data,
2046 );
2047
2048 let strategy = search_engine.search_strategy(&query_vector, &|_| true);
2049
2050 let mut search_record = VisitedSearchRecord::new(0);
2051 let search_params = Knn::new(10, 10, Some(4)).unwrap();
2052 let recorded_search =
2053 diskann::graph::search::RecordedKnn::new(search_params, &mut search_record);
2054 search_engine
2055 .runtime
2056 .block_on(search_engine.index.search(
2057 recorded_search,
2058 &strategy,
2059 &DefaultContext,
2060 query_vector.as_slice(),
2061 &mut result_output_buffer,
2062 ))
2063 .unwrap();
2064 let visited_ids = search_record
2065 .visited
2066 .iter()
2067 .map(|n| n.id)
2068 .collect::<Vec<_>>();
2069
2070 let query_stats = strategy.io_tracker;
2071 assert!(
2073 query_stats.io_count() <= io_limit,
2074 "Expected IO operations to be <= {}, but got {}",
2075 io_limit,
2076 query_stats.io_count()
2077 );
2078
2079 const EXPECTED_NODES: [u32; 17] = [
2080 72, 118, 108, 86, 84, 152, 170, 82, 114, 87, 207, 176, 79, 153, 67, 165, 141,
2081 ]; let mut matching_count = 0;
2085 for expected_node in EXPECTED_NODES.iter() {
2086 if visited_ids.contains(expected_node) {
2087 matching_count += 1;
2088 }
2089 }
2090
2091 let recall = (matching_count as f32 / EXPECTED_NODES.len() as f32) * 100.0;
2093
2094 assert!(recall >= 60.0, "Match percentage is below 60%: {}", recall);
2097 }
2098}