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