1use std::{
7 collections::HashMap,
8 num::NonZeroUsize,
9 sync::{
10 atomic::{AtomicU64, AtomicUsize},
11 Arc,
12 },
13 time::Instant,
14};
15
16use crate::data_model::GraphDataType;
17use diskann::{
18 error::IntoANNResult,
19 graph::{
20 self,
21 ext::labeled::{self, QueryLabelProvider},
22 glue::{self, DefaultPostProcessor, SearchPostProcess, SearchStrategy},
23 search::{AdaptiveL, InlineFilterSearch, Knn},
24 search_output_buffer, DiskANNIndex,
25 },
26 neighbor::{Neighbor, NeighborPriorityQueue},
27 provider::{DataProvider, DefaultContext, HasId, NoopGuard},
28 utils::{IntoUsize, VectorRepr},
29 ANNError, ANNResult,
30};
31use diskann_providers::storage::StorageReadProvider;
32use diskann_providers::{
33 model::{
34 compute_pq_distance,
35 graph::provider::{determinant_diversity, DeterminantDiversityParams},
36 },
37 storage::{get_compressed_pq_file, get_disk_index_file, get_pq_pivot_file, LoadWith},
38};
39use diskann_utils::{
40 object_pool::{ObjectPool, PoolOption, TryAsPooled},
41 views::Matrix,
42};
43
44use crate::search::pq::{quantizer_preprocess, PQData, PQScratch};
45use diskann_vector::{distance::Metric, DistanceFunction};
46use tokio::runtime::Runtime;
47use tracing::debug;
48
49use crate::{
50 data_model::{CachingStrategy, GraphHeader},
51 search::{
52 provider::{
53 aligned_file_reader::AlignedFileReaderFactory,
54 disk_vertex_provider_factory::DiskVertexProviderFactory,
55 },
56 search_mode::SearchMode,
57 traits::{VertexProvider, VertexProviderFactory},
58 },
59 storage::{api::AsyncDiskLoadContext, disk_index_reader::DiskIndexReader},
60 utils::QueryStatistics,
61};
62
63pub struct DiskProvider<Data>
73where
74 Data: GraphDataType<VectorIdType = u32>,
75{
76 graph_header: GraphHeader,
78
79 distance_comparer: <Data::VectorDataType as VectorRepr>::Distance,
81
82 pq_data: Arc<PQData>,
84
85 num_points: usize,
87
88 metric: Metric,
90
91 search_io_limit: usize,
93}
94
95impl<Data> DataProvider for DiskProvider<Data>
96where
97 Data: GraphDataType<VectorIdType = u32>,
98{
99 type Context = DefaultContext;
100
101 type InternalId = u32;
102
103 type ExternalId = u32;
104
105 type Guard = NoopGuard<u32>;
106
107 type Error = ANNError;
108
109 fn to_internal_id(
111 &self,
112 _context: &DefaultContext,
113 gid: &Self::ExternalId,
114 ) -> Result<Self::InternalId, Self::Error> {
115 Ok(*gid)
116 }
117
118 fn to_external_id(
120 &self,
121 _context: &DefaultContext,
122 id: Self::InternalId,
123 ) -> Result<Self::ExternalId, Self::Error> {
124 Ok(id)
125 }
126}
127
128impl<Data> LoadWith<AsyncDiskLoadContext> for DiskProvider<Data>
129where
130 Data: GraphDataType<VectorIdType = u32>,
131{
132 type Error = ANNError;
133
134 async fn load_with<P>(provider: &P, ctx: &AsyncDiskLoadContext) -> ANNResult<Self>
135 where
136 P: StorageReadProvider,
137 {
138 debug!(
139 "DiskProvider::load_with() called with file: {:?}",
140 get_disk_index_file(ctx.quant_load_context.metadata.prefix())
141 );
142
143 let graph_header = {
144 let caching_strategy = if ctx.num_nodes_to_cache > 0 {
145 CachingStrategy::StaticCacheWithBfsNodes(ctx.num_nodes_to_cache)
146 } else {
147 CachingStrategy::None
148 };
149
150 let vertex_provider_factory =
151 DiskVertexProviderFactory::<Data, AlignedFileReaderFactory>::from_disk_index_path(
152 get_disk_index_file(ctx.quant_load_context.metadata.prefix()),
153 caching_strategy,
154 )?;
155 VertexProviderFactory::get_header(&vertex_provider_factory)?
156 };
157
158 let metric = ctx.quant_load_context.metric;
159 let num_points = ctx.num_points;
160
161 let index_path_prefix = ctx.quant_load_context.metadata.prefix();
162 let index_reader = DiskIndexReader::new(
163 get_pq_pivot_file(index_path_prefix),
164 get_compressed_pq_file(index_path_prefix),
165 provider,
166 )?;
167
168 Self::new(
169 &index_reader,
170 graph_header,
171 metric,
172 num_points,
173 ctx.search_io_limit,
174 )
175 }
176}
177
178impl<Data> DiskProvider<Data>
179where
180 Data: GraphDataType<VectorIdType = u32>,
181{
182 fn new(
183 disk_index_reader: &DiskIndexReader,
184 graph_header: GraphHeader,
185 metric: Metric,
186 num_points: usize,
187 search_io_limit: usize,
188 ) -> ANNResult<Self> {
189 let distance_comparer =
190 Data::VectorDataType::distance(metric, Some(graph_header.metadata().dims));
191
192 let pq_data = disk_index_reader.get_pq_data();
193
194 Ok(Self {
195 graph_header,
196 distance_comparer,
197 pq_data,
198 num_points,
199 metric,
200 search_io_limit,
201 })
202 }
203}
204
205type PostprocessFilter<'a> = &'a (dyn Fn(&u32) -> bool + Send + Sync);
219
220#[derive(Clone, Copy)]
226pub enum PostprocessStrategy<'a> {
227 AcceptAll,
230 Apply(PostprocessFilter<'a>),
232}
233
234pub struct DiskSearchStrategy<'a, Data, ProviderFactory>
235where
236 Data: GraphDataType<VectorIdType = u32>,
237 ProviderFactory: VertexProviderFactory<Data>,
238{
239 io_tracker: &'a IOTracker,
241 postprocess_filter: PostprocessStrategy<'a>,
245
246 vertex_provider_factory: &'a ProviderFactory,
248
249 scratch_pool: &'a Arc<ObjectPool<DiskSearchScratch<Data, ProviderFactory::VertexProviderType>>>,
251}
252
253struct IOTracker {
256 io_time_us: AtomicU64,
257 preprocess_time_us: AtomicU64,
258 io_count: AtomicUsize,
259}
260
261impl Default for IOTracker {
262 fn default() -> Self {
263 Self {
264 io_time_us: AtomicU64::new(0),
265 preprocess_time_us: AtomicU64::new(0),
266 io_count: AtomicUsize::new(0),
267 }
268 }
269}
270
271impl IOTracker {
272 fn add_time(category: &AtomicU64, time: u64) {
273 category.fetch_add(time, std::sync::atomic::Ordering::Relaxed);
274 }
275
276 fn time(category: &AtomicU64) -> u64 {
277 category.load(std::sync::atomic::Ordering::Relaxed)
278 }
279
280 fn add_io_count(&self, count: usize) {
281 self.io_count
282 .fetch_add(count, std::sync::atomic::Ordering::Relaxed);
283 }
284
285 fn io_count(&self) -> usize {
286 self.io_count.load(std::sync::atomic::Ordering::Relaxed)
287 }
288}
289
290#[derive(Clone, Copy)]
291pub struct RerankAndFilter<'a> {
292 filter: PostprocessStrategy<'a>,
293}
294
295#[derive(Clone, Copy)]
296pub struct DeterminantDiversityAndFilter<'a> {
297 filter: PostprocessStrategy<'a>,
298 params: DeterminantDiversityParams,
299}
300
301#[derive(Clone, Copy)]
305pub enum DiskSearchPostProcessor<'a> {
306 RerankAndFilter(RerankAndFilter<'a>),
307 DeterminantDiversity(DeterminantDiversityAndFilter<'a>),
308}
309
310impl<'a> RerankAndFilter<'a> {
311 pub fn new(filter: PostprocessStrategy<'a>) -> Self {
312 Self { filter }
313 }
314}
315
316impl<'a> DeterminantDiversityAndFilter<'a> {
317 pub fn new(filter: PostprocessStrategy<'a>, params: DeterminantDiversityParams) -> Self {
318 Self { filter, params }
319 }
320}
321
322impl<Data, VP>
323 SearchPostProcess<
324 DiskAccessor<'_, Data, VP>,
325 &[Data::VectorDataType],
326 (
327 <DiskProvider<Data> as DataProvider>::InternalId,
328 Data::AssociatedDataType,
329 ),
330 > for RerankAndFilter<'_>
331where
332 Data: GraphDataType<VectorIdType = u32>,
333 VP: VertexProvider<Data>,
334{
335 type Error = ANNError;
336 async fn post_process<I, B>(
337 &self,
338 accessor: &mut DiskAccessor<'_, Data, VP>,
339 query: &[Data::VectorDataType],
340 candidates: I,
341 output: &mut B,
342 ) -> Result<usize, Self::Error>
343 where
344 I: Iterator<Item = Neighbor<u32>> + Send,
345 B: search_output_buffer::SearchOutputBuffer<(u32, Data::AssociatedDataType)>
346 + Send
347 + ?Sized,
348 {
349 let provider = accessor.provider;
350
351 let mut uncached_ids = Vec::new();
352 let mut reranked = {
353 let mut process = |n: u32| {
354 if let Some(entry) = accessor.scratch.distance_cache.get(&n) {
355 Some(Ok::<((u32, _), f32), ANNError>(((n, entry.1), entry.0)))
356 } else {
357 uncached_ids.push(n);
358 None
359 }
360 };
361 match self.filter {
362 PostprocessStrategy::AcceptAll => candidates
363 .map(|n| n.id)
364 .filter_map(&mut process)
365 .collect::<Result<Vec<_>, _>>()?,
366 PostprocessStrategy::Apply(f) => candidates
367 .map(|n| n.id)
368 .filter(|id| f(id))
369 .filter_map(&mut process)
370 .collect::<Result<Vec<_>, _>>()?,
371 }
372 };
373 if !uncached_ids.is_empty() {
374 ensure_vertex_loaded(&mut accessor.scratch.vertex_provider, &uncached_ids)?;
375 for n in &uncached_ids {
376 let v = accessor.scratch.vertex_provider.get_vector(n)?;
377 let d = provider.distance_comparer.evaluate_similarity(query, v);
378 let a = accessor.scratch.vertex_provider.get_associated_data(n)?;
379 reranked.push(((*n, *a), d));
380 }
381 }
382
383 reranked
385 .sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
386 Ok(output.extend(reranked))
388 }
389}
390
391impl<Data, VP>
392 SearchPostProcess<
393 DiskAccessor<'_, Data, VP>,
394 &[Data::VectorDataType],
395 (
396 <DiskProvider<Data> as DataProvider>::InternalId,
397 Data::AssociatedDataType,
398 ),
399 > for DeterminantDiversityAndFilter<'_>
400where
401 Data: GraphDataType<VectorIdType = u32>,
402 VP: VertexProvider<Data>,
403{
404 type Error = ANNError;
405 async fn post_process<I, B>(
406 &self,
407 accessor: &mut DiskAccessor<'_, Data, VP>,
408 query: &[Data::VectorDataType],
409 candidates: I,
410 output: &mut B,
411 ) -> Result<usize, Self::Error>
412 where
413 I: Iterator<Item = Neighbor<u32>> + Send,
414 B: search_output_buffer::SearchOutputBuffer<(u32, Data::AssociatedDataType)>
415 + Send
416 + ?Sized,
417 {
418 let provider = accessor.provider;
419 let query_f32 = Data::VectorDataType::as_f32(query).map_err(Into::into)?;
420
421 let candidate_ids: Vec<u32> = match self.filter {
422 PostprocessStrategy::AcceptAll => candidates.map(|candidate| candidate.id).collect(),
423 PostprocessStrategy::Apply(f) => candidates
424 .map(|candidate| candidate.id)
425 .filter(|id| f(id))
426 .collect(),
427 };
428
429 if candidate_ids.is_empty() {
430 return Ok(0);
431 }
432
433 ensure_vertex_loaded(&mut accessor.scratch.vertex_provider, &candidate_ids)?;
434
435 let mut candidate_vectors = Matrix::new(0.0f32, candidate_ids.len(), query_f32.len());
436 let mut candidate_distances = Vec::with_capacity(candidate_ids.len());
437 let mut associated_data = Vec::with_capacity(candidate_ids.len());
438
439 for (row_idx, id) in candidate_ids.iter().enumerate() {
440 let vector = accessor.scratch.vertex_provider.get_vector(id)?;
441 let distance = provider
442 .distance_comparer
443 .evaluate_similarity(query, vector);
444 let vector_f32 = Data::VectorDataType::as_f32(vector).map_err(Into::into)?;
445 let data = accessor.scratch.vertex_provider.get_associated_data(id)?;
446
447 candidate_vectors
448 .row_mut(row_idx)
449 .copy_from_slice(&vector_f32);
450 candidate_distances.push(distance);
451 associated_data.push(*data);
452 }
453
454 let reranked = determinant_diversity(
455 candidate_vectors.as_mut_view(),
456 &candidate_distances,
457 &query_f32,
458 usize::MAX,
459 &self.params,
460 )?;
461
462 Ok(output.extend(reranked.into_iter().map(|idx| {
463 let id = candidate_ids[idx];
464 let distance = candidate_distances[idx];
465 ((id, associated_data[idx]), distance)
466 })))
467 }
468}
469
470impl<Data, VP>
471 SearchPostProcess<
472 DiskAccessor<'_, Data, VP>,
473 &[Data::VectorDataType],
474 (
475 <DiskProvider<Data> as DataProvider>::InternalId,
476 Data::AssociatedDataType,
477 ),
478 > for DiskSearchPostProcessor<'_>
479where
480 Data: GraphDataType<VectorIdType = u32>,
481 VP: VertexProvider<Data>,
482{
483 type Error = ANNError;
484 async fn post_process<I, B>(
485 &self,
486 accessor: &mut DiskAccessor<'_, Data, VP>,
487 query: &[Data::VectorDataType],
488 candidates: I,
489 output: &mut B,
490 ) -> Result<usize, Self::Error>
491 where
492 I: Iterator<Item = Neighbor<u32>> + Send,
493 B: search_output_buffer::SearchOutputBuffer<(u32, Data::AssociatedDataType)>
494 + Send
495 + ?Sized,
496 {
497 match self {
498 DiskSearchPostProcessor::RerankAndFilter(pp) => {
499 pp.post_process(accessor, query, candidates, output).await
500 }
501 DiskSearchPostProcessor::DeterminantDiversity(pp) => {
502 pp.post_process(accessor, query, candidates, output).await
503 }
504 }
505 }
506}
507
508impl<'this, Data, ProviderFactory>
509 SearchStrategy<'this, DiskProvider<Data>, &'this [Data::VectorDataType]>
510 for DiskSearchStrategy<'this, Data, ProviderFactory>
511where
512 Data: GraphDataType<VectorIdType = u32>,
513 ProviderFactory: VertexProviderFactory<Data>,
514{
515 type SearchAccessor = DiskAccessor<'this, Data, ProviderFactory::VertexProviderType>;
516 type SearchAccessorError = ANNError;
517
518 fn search_accessor(
519 &'this self,
520 provider: &'this DiskProvider<Data>,
521 _context: &DefaultContext,
522 query: &'this [Data::VectorDataType],
523 ) -> Result<Self::SearchAccessor, Self::SearchAccessorError> {
524 DiskAccessor::new(
525 provider,
526 self.io_tracker,
527 query,
528 self.vertex_provider_factory,
529 self.scratch_pool,
530 )
531 }
532}
533
534impl<'this, Data, ProviderFactory>
535 DefaultPostProcessor<
536 'this,
537 DiskProvider<Data>,
538 &'this [Data::VectorDataType],
539 (
540 <DiskProvider<Data> as DataProvider>::InternalId,
541 Data::AssociatedDataType,
542 ),
543 > for DiskSearchStrategy<'this, Data, ProviderFactory>
544where
545 Data: GraphDataType<VectorIdType = u32>,
546 ProviderFactory: VertexProviderFactory<Data>,
547{
548 type Processor = RerankAndFilter<'this>;
549
550 fn default_post_processor(&'this self) -> Self::Processor {
551 RerankAndFilter::new(self.postprocess_filter)
552 }
553}
554
555struct DiskSearchScratch<Data, VP>
558where
559 Data: GraphDataType<VectorIdType = u32>,
560 VP: VertexProvider<Data>,
561{
562 distance_cache: HashMap<u32, (f32, Data::AssociatedDataType)>,
563 pq_scratch: PQScratch,
564 vertex_provider: VP,
565}
566
567#[derive(Clone)]
568struct DiskSearchScratchArgs<'a, ProviderFactory> {
569 graph_degree: usize,
570 pq_dim: usize,
571 num_pq_chunks: usize,
572 num_pq_centers: usize,
573 vertex_factory: &'a ProviderFactory,
574 graph_header: &'a GraphHeader,
575}
576
577impl<Data, ProviderFactory> TryAsPooled<&DiskSearchScratchArgs<'_, ProviderFactory>>
578 for DiskSearchScratch<Data, ProviderFactory::VertexProviderType>
579where
580 Data: GraphDataType<VectorIdType = u32>,
581 ProviderFactory: VertexProviderFactory<Data>,
582{
583 type Error = ANNError;
584
585 fn try_create(args: &DiskSearchScratchArgs<ProviderFactory>) -> Result<Self, Self::Error> {
586 let pq_scratch = PQScratch::new(
587 args.graph_degree,
588 args.pq_dim,
589 args.num_pq_chunks,
590 args.num_pq_centers,
591 )?;
592
593 const DEFAULT_BEAM_WIDTH: usize = 0; let vertex_provider = args
595 .vertex_factory
596 .create_vertex_provider(DEFAULT_BEAM_WIDTH, args.graph_header)?;
597
598 Ok(Self {
599 distance_cache: HashMap::new(),
600 pq_scratch,
601 vertex_provider,
602 })
603 }
604
605 fn try_modify(
606 &mut self,
607 _args: &DiskSearchScratchArgs<ProviderFactory>,
608 ) -> Result<(), Self::Error> {
609 self.distance_cache.clear();
610 self.vertex_provider.clear();
611 Ok(())
612 }
613}
614
615pub struct DiskAccessor<'a, Data, VP>
616where
617 Data: GraphDataType<VectorIdType = u32>,
618 VP: VertexProvider<Data>,
619{
620 provider: &'a DiskProvider<Data>,
621 io_tracker: &'a IOTracker,
622 scratch: PoolOption<DiskSearchScratch<Data, VP>>,
623 query: &'a [Data::VectorDataType],
624}
625
626impl<Data, VP> DiskAccessor<'_, Data, VP>
627where
628 Data: GraphDataType<VectorIdType = u32>,
629 VP: VertexProvider<Data>,
630{
631 fn pq_distances<F>(&mut self, ids: &[u32], mut f: F) -> ANNResult<()>
634 where
635 F: FnMut(f32, u32),
636 {
637 let pq_scratch = &mut self.scratch.pq_scratch;
638 compute_pq_distance(
639 ids,
640 self.provider.pq_data.get_num_chunks(),
641 &pq_scratch.aligned_pqtable_dist_scratch,
642 self.provider.pq_data.pq_compressed_data().as_slice(),
643 &mut pq_scratch.aligned_pq_coord_scratch,
644 &mut pq_scratch.aligned_dist_scratch,
645 )?;
646
647 for (i, id) in ids.iter().enumerate() {
648 let distance = self.scratch.pq_scratch.aligned_dist_scratch[i];
649 f(distance, *id);
650 }
651
652 Ok(())
653 }
654}
655
656impl<Data, VP> HasId for DiskAccessor<'_, Data, VP>
657where
658 Data: GraphDataType<VectorIdType = u32>,
659 VP: VertexProvider<Data>,
660{
661 type Id = u32;
662}
663
664impl<Data, VP> glue::SearchAccessor for DiskAccessor<'_, Data, VP>
665where
666 Data: GraphDataType<VectorIdType = u32>,
667 VP: VertexProvider<Data>,
668{
669 async fn starting_points(&self) -> ANNResult<Vec<u32>> {
670 let start_vertex_id = self.provider.graph_header.metadata().medoid as u32;
671 Ok(vec![start_vertex_id])
672 }
673
674 async fn start_point_distances<F>(&mut self, mut f: F) -> ANNResult<()>
675 where
676 F: FnMut(Self::Id, f32) + Send,
677 {
678 let start_vertex_id = self.provider.graph_header.metadata().medoid as u32;
679 self.pq_distances(&[start_vertex_id], |dist, id| f(id, dist))
680 }
681
682 fn expand_beam<Itr, P, F>(
683 &mut self,
684 ids: Itr,
685 mut pred: P,
686 mut f: F,
687 ) -> impl std::future::Future<Output = ANNResult<()>> + Send
688 where
689 Itr: Iterator<Item = Self::Id> + Send,
690 P: glue::HybridPredicate<Self::Id> + Send + Sync,
691 F: FnMut(Self::Id, f32) + Send,
692 {
693 let result = (|| {
694 let io_limit = self.provider.search_io_limit - self.io_tracker.io_count();
695 let load_ids: Box<[_]> = ids.take(io_limit).collect();
696
697 self.ensure_loaded(&load_ids)?;
698 let mut ids = Vec::new();
699 for i in load_ids {
700 ids.clear();
701 ids.extend(
702 self.scratch
703 .vertex_provider
704 .get_adjacency_list(&i)?
705 .iter()
706 .copied()
707 .filter(|id| pred.eval_mut(id)),
708 );
709
710 self.pq_distances(&ids, &mut |dist, id| f(id, dist))?;
711 }
712
713 Ok(())
714 })();
715
716 std::future::ready(result)
717 }
718
719 fn terminate_early(&mut self) -> bool {
720 self.io_tracker.io_count() > self.provider.search_io_limit
721 }
722}
723
724impl<'a, Data, VP> DiskAccessor<'a, Data, VP>
725where
726 Data: GraphDataType<VectorIdType = u32>,
727 VP: VertexProvider<Data>,
728{
729 fn new<VPF>(
730 provider: &'a DiskProvider<Data>,
731 io_tracker: &'a IOTracker,
732 query: &'a [Data::VectorDataType],
733 vertex_provider_factory: &'a VPF,
734 scratch_pool: &'a Arc<ObjectPool<DiskSearchScratch<Data, VP>>>,
735 ) -> ANNResult<Self>
736 where
737 VPF: VertexProviderFactory<Data, VertexProviderType = VP>,
738 {
739 let mut scratch = PoolOption::try_pooled(
740 scratch_pool,
741 &DiskSearchScratchArgs {
742 graph_degree: provider.graph_header.max_degree::<Data::VectorDataType>()?,
743 pq_dim: provider.pq_data.get_dim(),
744 num_pq_chunks: provider.pq_data.get_num_chunks(),
745 num_pq_centers: provider.pq_data.get_num_centers(),
746 vertex_factory: vertex_provider_factory,
747 graph_header: &provider.graph_header,
748 },
749 )?;
750
751 let f32_query = Data::VectorDataType::as_f32(query).into_ann_result()?;
753 scratch.pq_scratch.set(&f32_query)?;
754 let start_vertex_id = provider.graph_header.metadata().medoid as u32;
755
756 let timer = Instant::now();
757 quantizer_preprocess(
758 &mut scratch.pq_scratch,
759 &provider.pq_data,
760 provider.metric,
761 &[start_vertex_id],
762 )?;
763 IOTracker::add_time(
764 &io_tracker.preprocess_time_us,
765 timer.elapsed().as_micros() as u64,
766 );
767
768 Ok(Self {
769 provider,
770 io_tracker,
771 scratch,
772 query,
773 })
774 }
775
776 fn ensure_loaded(&mut self, ids: &[u32]) -> Result<(), ANNError> {
777 if ids.is_empty() {
778 return Ok(());
779 }
780 let scratch = &mut self.scratch;
781 let timer = Instant::now();
782 ensure_vertex_loaded(&mut scratch.vertex_provider, ids)?;
783 IOTracker::add_time(
784 &self.io_tracker.io_time_us,
785 timer.elapsed().as_micros() as u64,
786 );
787 self.io_tracker.add_io_count(ids.len());
788 for id in ids {
789 let distance = self
790 .provider
791 .distance_comparer
792 .evaluate_similarity(self.query, scratch.vertex_provider.get_vector(id)?);
793 let associated_data = *scratch.vertex_provider.get_associated_data(id)?;
794 scratch
795 .distance_cache
796 .insert(*id, (distance, associated_data));
797 }
798 Ok(())
799 }
800}
801
802pub struct DiskIndexSearcher<
806 Data,
807 ProviderFactory = DiskVertexProviderFactory<Data, AlignedFileReaderFactory>,
808> where
809 Data: GraphDataType<VectorIdType = u32>,
810 ProviderFactory: VertexProviderFactory<Data>,
811{
812 index: DiskANNIndex<DiskProvider<Data>>,
813 runtime: Runtime,
814
815 vertex_provider_factory: ProviderFactory,
817
818 scratch_pool: Arc<ObjectPool<DiskSearchScratch<Data, ProviderFactory::VertexProviderType>>>,
820}
821
822#[derive(Debug)]
823pub struct SearchResultStats {
824 pub cmps: u32,
825 pub result_count: u32,
826 pub query_statistics: QueryStatistics,
827}
828
829pub struct SearchResult<AssociatedData> {
834 pub results: Vec<SearchResultItem<AssociatedData>>,
836 pub stats: SearchResultStats,
837}
838
839pub struct SearchResultItem<AssociatedData> {
844 pub vertex_id: u32,
846 pub data: AssociatedData,
849 pub distance: f32,
851}
852
853impl<Data, ProviderFactory> DiskIndexSearcher<Data, ProviderFactory>
854where
855 Data: GraphDataType<VectorIdType = u32>,
856 ProviderFactory: VertexProviderFactory<Data>,
857{
858 pub fn new(
868 num_threads: usize,
869 search_io_limit: usize,
870 disk_index_reader: &DiskIndexReader,
871 vertex_provider_factory: ProviderFactory,
872 metric: Metric,
873 runtime: Option<Runtime>,
874 ) -> ANNResult<Self> {
875 let runtime = match runtime {
876 Some(rt) => rt,
877 None => tokio::runtime::Builder::new_current_thread().build()?,
878 };
879
880 let graph_header = vertex_provider_factory.get_header()?;
881 let metadata = graph_header.metadata();
882 let max_degree = graph_header.max_degree::<Data::VectorDataType>()? as u32;
883
884 let config = graph::config::Builder::new(
885 max_degree.into_usize(),
886 graph::config::MaxDegree::default_slack(),
887 1, metric.into(),
889 )
890 .build()?;
891
892 debug!("Creating DiskIndexSearcher with index_config: {:?}", config);
893
894 let graph_header = vertex_provider_factory.get_header()?;
895 let pq_data = disk_index_reader.get_pq_data();
896 let scratch_pool_args = DiskSearchScratchArgs {
897 graph_degree: graph_header.max_degree::<Data::VectorDataType>()?,
898 pq_dim: pq_data.get_dim(),
899 num_pq_chunks: pq_data.get_num_chunks(),
900 num_pq_centers: pq_data.get_num_centers(),
901 vertex_factory: &vertex_provider_factory,
902 graph_header: &graph_header,
903 };
904 let scratch_pool = Arc::new(ObjectPool::try_new(&scratch_pool_args, 0, None)?);
905
906 let disk_provider = DiskProvider::new(
907 disk_index_reader,
908 graph_header,
909 metric,
910 metadata.num_pts.into_usize(),
911 search_io_limit,
912 )?;
913
914 let index = DiskANNIndex::new(config, disk_provider, NonZeroUsize::new(num_threads));
915 Ok(Self {
916 index,
917 runtime,
918 vertex_provider_factory,
919 scratch_pool,
920 })
921 }
922
923 fn search_strategy<'a>(
925 &'a self,
926 io_tracker: &'a IOTracker,
927 postprocess_filter: PostprocessStrategy<'a>,
928 ) -> DiskSearchStrategy<'a, Data, ProviderFactory> {
929 DiskSearchStrategy {
930 io_tracker,
931 postprocess_filter,
932 vertex_provider_factory: &self.vertex_provider_factory,
933 scratch_pool: &self.scratch_pool,
934 }
935 }
936
937 async fn flat_search<OB>(
946 &self,
947 strategy: &DiskSearchStrategy<'_, Data, ProviderFactory>,
948 query: &[Data::VectorDataType],
949 vector_filter: Option<&(dyn Fn(&u32) -> bool + Send + Sync)>,
950 neighbors_before_reranking: usize,
951 output: &mut OB,
952 ) -> ANNResult<graph::index::SearchStats>
953 where
954 OB: search_output_buffer::SearchOutputBuffer<(u32, Data::AssociatedDataType)> + Send,
955 {
956 let provider = self.index.provider();
957 let mut accessor = strategy
958 .search_accessor(provider, &DefaultContext, query)
959 .into_ann_result()?;
960
961 let batch_size = accessor.scratch.pq_scratch.max_vectors();
964
965 if batch_size == 0 {
969 return Err(ANNError::message(
970 diskann::ANNErrorKind::IndexError,
971 "pq scratch must support at least one vector",
972 ));
973 }
974
975 let mut id_buffer = Vec::with_capacity(batch_size);
976
977 let mut best = NeighborPriorityQueue::new(neighbors_before_reranking);
978 let mut cmps = 0u32;
979
980 let mut iter =
983 (0..provider.num_points as u32).filter(|id| vector_filter.is_none_or(|f| f(id)));
984 loop {
985 id_buffer.clear();
986 id_buffer.extend(iter.by_ref().take(batch_size));
987
988 if id_buffer.is_empty() {
989 break;
990 }
991
992 accessor.pq_distances(&id_buffer, |dist, id| best.insert(Neighbor::new(id, dist)))?;
993 cmps += id_buffer.len() as u32;
994 }
995
996 let result_count = strategy
997 .default_post_processor()
998 .post_process(&mut accessor, query, best.iter(), output)
999 .await
1000 .into_ann_result()?;
1001
1002 Ok(graph::index::SearchStats {
1003 cmps,
1004 hops: 0,
1005 result_count: result_count as u32,
1006 range_search_second_round: false,
1007 })
1008 }
1009
1010 async fn filter_search<'a, OB>(
1027 &self,
1028 strategy: DiskSearchStrategy<'a, Data, ProviderFactory>,
1029 query: &[Data::VectorDataType],
1030 knn: Knn,
1031 label_provider: &(dyn QueryLabelProvider<u32> + 'a),
1032 adaptive_l: Option<AdaptiveL>,
1033 output: &mut OB,
1034 ) -> ANNResult<graph::index::SearchStats>
1035 where
1036 OB: search_output_buffer::SearchOutputBuffer<(u32, Data::AssociatedDataType)> + Send,
1037 {
1038 let filtered_strategy = labeled::Filtered::new(strategy, label_provider);
1039 let search = InlineFilterSearch::new(knn, adaptive_l);
1040 self.index
1041 .search(search, &filtered_strategy, &DefaultContext, query, output)
1042 .await
1043 }
1044
1045 pub fn search(
1048 &self,
1049 query: &[Data::VectorDataType],
1050 return_list_size: u32,
1051 search_list_size: u32,
1052 beam_width: Option<usize>,
1053 mode: SearchMode<'_>,
1054 ) -> ANNResult<SearchResult<Data::AssociatedDataType>> {
1055 let mut query_stats = QueryStatistics::default();
1056 let mut indices = vec![0u32; return_list_size as usize];
1057 let mut distances = vec![0f32; return_list_size as usize];
1058 let mut associated_data =
1059 vec![Data::AssociatedDataType::default(); return_list_size as usize];
1060
1061 let stats = self.search_internal(
1062 query,
1063 return_list_size as usize,
1064 search_list_size,
1065 beam_width,
1066 &mut query_stats,
1067 &mut indices,
1068 &mut distances,
1069 &mut associated_data,
1070 &mode,
1071 )?;
1072
1073 let mut search_result = SearchResult {
1074 results: Vec::with_capacity(return_list_size as usize),
1075 stats,
1076 };
1077
1078 for ((vertex_id, distance), associated_data) in indices
1079 .into_iter()
1080 .zip(distances.into_iter())
1081 .zip(associated_data.into_iter())
1082 {
1083 search_result.results.push(SearchResultItem {
1084 vertex_id,
1085 distance,
1086 data: associated_data,
1087 });
1088 }
1089
1090 Ok(search_result)
1091 }
1092
1093 #[allow(clippy::too_many_arguments)]
1096 pub(crate) fn search_internal(
1097 &self,
1098 query: &[Data::VectorDataType],
1099 k_value: usize,
1100 search_list_size: u32,
1101 beam_width: Option<usize>,
1102 query_stats: &mut QueryStatistics,
1103 indices: &mut [u32],
1104 distances: &mut [f32],
1105 associated_data: &mut [Data::AssociatedDataType],
1106 mode: &SearchMode<'_>,
1107 ) -> ANNResult<SearchResultStats> {
1108 let mut result_output_buffer = search_output_buffer::IdDistanceAssociatedData::new(
1109 &mut indices[..k_value],
1110 &mut distances[..k_value],
1111 &mut associated_data[..k_value],
1112 );
1113
1114 let timer = Instant::now();
1115 let k = k_value;
1116 let l = search_list_size as usize;
1117
1118 let io_tracker = IOTracker::default();
1119
1120 let stats = match mode {
1130 SearchMode::FlatScan { filter } => {
1131 let strategy = self.search_strategy(&io_tracker, PostprocessStrategy::AcceptAll);
1132 self.runtime.block_on(self.flat_search(
1133 &strategy,
1134 query,
1135 filter.as_deref(),
1136 l,
1137 &mut result_output_buffer,
1138 ))?
1139 }
1140 SearchMode::Graph { filter } => {
1141 let strategy = self.search_strategy(
1142 &io_tracker,
1143 filter
1144 .as_deref()
1145 .map_or(PostprocessStrategy::AcceptAll, PostprocessStrategy::Apply),
1146 );
1147 let knn_search = Knn::new(k, l, beam_width)?;
1148 self.runtime.block_on(self.index.search(
1149 knn_search,
1150 &strategy,
1151 &DefaultContext,
1152 query,
1153 &mut result_output_buffer,
1154 ))?
1155 }
1156 SearchMode::InlineFilter { filter, adaptive_l } => {
1157 let strategy = self.search_strategy(&io_tracker, PostprocessStrategy::AcceptAll);
1161 let knn_search = Knn::new(k, l, beam_width)?;
1162 self.runtime.block_on(self.filter_search(
1163 strategy,
1164 query,
1165 knn_search,
1166 filter.as_ref(),
1167 adaptive_l.clone(),
1168 &mut result_output_buffer,
1169 ))?
1170 }
1171 SearchMode::DiverseGraph { filter, params } => {
1172 let postprocess_config = filter
1176 .as_deref()
1177 .map_or(PostprocessStrategy::AcceptAll, PostprocessStrategy::Apply);
1178 let strategy = self.search_strategy(&io_tracker, postprocess_config);
1179 let knn_search = Knn::new(k, l, beam_width)?;
1180 let processor = DiskSearchPostProcessor::DeterminantDiversity(
1181 DeterminantDiversityAndFilter::new(postprocess_config, *params),
1182 );
1183 self.runtime.block_on(self.index.search_with(
1184 knn_search,
1185 &strategy,
1186 processor,
1187 &DefaultContext,
1188 query,
1189 &mut result_output_buffer,
1190 ))?
1191 }
1192 };
1193 query_stats.total_comparisons = stats.cmps;
1194 query_stats.search_hops = stats.hops;
1195
1196 query_stats.total_execution_time_us = timer.elapsed().as_micros();
1197 query_stats.io_time_us = IOTracker::time(&io_tracker.io_time_us) as u128;
1198 query_stats.total_io_operations = io_tracker.io_count() as u32;
1199 query_stats.total_vertices_loaded = io_tracker.io_count() as u32;
1200 query_stats.query_pq_preprocess_time_us =
1201 IOTracker::time(&io_tracker.preprocess_time_us) as u128;
1202 query_stats.cpu_time_us = query_stats.total_execution_time_us
1203 - query_stats.io_time_us
1204 - query_stats.query_pq_preprocess_time_us;
1205 Ok(SearchResultStats {
1206 cmps: query_stats.total_comparisons,
1207 result_count: stats.result_count,
1208 query_statistics: query_stats.clone(),
1209 })
1210 }
1211}
1212
1213fn ensure_vertex_loaded<Data: GraphDataType, V: VertexProvider<Data>>(
1219 vertex_provider: &mut V,
1220 ids: &[Data::VectorIdType],
1221) -> ANNResult<()> {
1222 vertex_provider.load_vertices(ids)?;
1223 for (idx, id) in ids.iter().enumerate() {
1224 vertex_provider.process_loaded_node(id, idx)?;
1225 }
1226 Ok(())
1227}
1228
1229#[cfg(test)]
1230mod disk_provider_tests {
1231 use crate::test_utils::{GraphDataF32VectorU32Data, GraphDataF32VectorUnitData};
1232 use diskann::{
1233 graph::{
1234 search::{record::VisitedSearchRecord, Knn},
1235 KnnSearchError,
1236 },
1237 utils::IntoUsize,
1238 ANNErrorKind,
1239 };
1240 use diskann_providers::storage::{
1241 DynWriteProvider, StorageReadProvider, VirtualStorageProvider,
1242 };
1243 use diskann_providers::utils::{create_thread_pool, PQPathNames, ParallelIteratorInPool};
1244 use diskann_utils::{io::read_bin, test_data_root};
1245 use diskann_vector::distance::Metric;
1246 use rayon::prelude::IndexedParallelIterator;
1247 use rstest::rstest;
1248 use vfs::OverlayFS;
1249
1250 use super::*;
1251 use crate::{
1252 build::builder::core::disk_index_builder_tests::{IndexBuildFixture, TestParams},
1253 search::provider::aligned_file_reader::VirtualAlignedReaderFactory,
1254 utils::QueryStatistics,
1255 };
1256
1257 const TEST_INDEX_PREFIX_128DIM: &str =
1258 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search";
1259 const TEST_INDEX_128DIM: &str =
1260 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_disk.index";
1261 const TEST_PQ_PIVOT_128DIM: &str =
1262 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_pq_pivots.bin";
1263 const TEST_PQ_COMPRESSED_128DIM: &str =
1264 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_pq_compressed.bin";
1265 const TEST_TRUTH_RESULT_10PTS_128DIM: &str =
1266 "/disk_index_search/disk_index_10pts_idx_uint32_truth_search_res.bin";
1267 const TEST_QUERY_10PTS_128DIM: &str = "/disk_index_search/disk_index_sample_query_10pts.fbin";
1268
1269 const TEST_INDEX_PREFIX_100DIM: &str = "/disk_index_search/256pts_100dim_f32_truth_Index";
1270 const TEST_INDEX_100DIM: &str = "/disk_index_search/256pts_100dim_f32_truth_Index_disk.index";
1271 const TEST_PQ_PIVOT_100DIM: &str =
1272 "/disk_index_search/256pts_100dim_f32_truth_Index_pq_pivots.bin";
1273 const TEST_PQ_COMPRESSED_100DIM: &str =
1274 "/disk_index_search/256pts_100dim_f32_truth_Index_pq_compressed.bin";
1275 const TEST_TRUTH_RESULT_10PTS_100DIM: &str =
1276 "/disk_index_search/256pts_100dim_f32_truth_query_result.bin";
1277 const TEST_QUERY_10PTS_100DIM: &str = "/disk_index_search/10pts_100dim_f32_base_query.bin";
1278 const TEST_DATA_FILE: &str = "/disk_index_search/disk_index_siftsmall_learn_256pts_data.fbin";
1279 const TEST_INDEX: &str =
1280 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_disk.index";
1281 const TEST_INDEX_PREFIX: &str =
1282 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search";
1283 const TEST_PQ_PIVOT: &str =
1284 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_pq_pivots.bin";
1285 const TEST_PQ_COMPRESSED: &str =
1286 "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_truth_search_pq_compressed.bin";
1287
1288 #[test]
1289 fn test_disk_search_k10_l20_single_or_multi_thread_100dim() {
1290 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1291
1292 let search_engine = create_disk_index_searcher(
1293 CreateDiskIndexSearcherParams {
1294 max_thread_num: 5,
1295 pq_pivot_file_path: TEST_PQ_PIVOT_100DIM,
1296 pq_compressed_file_path: TEST_PQ_COMPRESSED_100DIM,
1297 index_path: TEST_INDEX_100DIM,
1298 index_path_prefix: TEST_INDEX_PREFIX_100DIM,
1299 ..Default::default()
1300 },
1301 &storage_provider,
1302 );
1303 test_disk_search(TestDiskSearchParams {
1305 storage_provider: storage_provider.as_ref(),
1306 index_search_engine: &search_engine,
1307 thread_num: 1,
1308 query_file_path: TEST_QUERY_10PTS_100DIM,
1309 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_100DIM,
1310 k: 10,
1311 l: 20,
1312 });
1313 test_disk_search(TestDiskSearchParams {
1315 storage_provider: storage_provider.as_ref(),
1316 index_search_engine: &search_engine,
1317 thread_num: 5,
1318 query_file_path: TEST_QUERY_10PTS_100DIM,
1319 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_100DIM,
1320 k: 10,
1321 l: 20,
1322 });
1323 }
1324
1325 #[rstest]
1326 #[case(CachingStrategy::None)]
1327 #[case(CachingStrategy::StaticCacheWithBfsNodes(32))]
1328 fn test_disk_search_k10_l20_single_or_multi_thread_128dim(
1329 #[case] caching_strategy: CachingStrategy,
1330 ) {
1331 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1332
1333 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
1334 CreateDiskIndexSearcherParams {
1335 max_thread_num: 5,
1336 pq_pivot_file_path: TEST_PQ_PIVOT_128DIM,
1337 pq_compressed_file_path: TEST_PQ_COMPRESSED_128DIM,
1338 index_path: TEST_INDEX_128DIM,
1339 index_path_prefix: TEST_INDEX_PREFIX_128DIM,
1340 caching_strategy,
1341 ..Default::default()
1342 },
1343 &storage_provider,
1344 );
1345 test_disk_search(TestDiskSearchParams {
1347 storage_provider: storage_provider.as_ref(),
1348 index_search_engine: &search_engine,
1349 thread_num: 1,
1350 query_file_path: TEST_QUERY_10PTS_128DIM,
1351 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_128DIM,
1352 k: 10,
1353 l: 20,
1354 });
1355 test_disk_search(TestDiskSearchParams {
1357 storage_provider: storage_provider.as_ref(),
1358 index_search_engine: &search_engine,
1359 thread_num: 5,
1360 query_file_path: TEST_QUERY_10PTS_128DIM,
1361 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_128DIM,
1362 k: 10,
1363 l: 20,
1364 });
1365 }
1366
1367 fn get_truth_associated_data<StorageReader: StorageReadProvider>(
1368 storage_provider: &StorageReader,
1369 ) -> Vec<u32> {
1370 const ASSOCIATED_DATA_FILE: &str = "/sift/siftsmall_learn_256pts_u32_associated_data.fbin";
1371
1372 let data =
1373 read_bin::<u32>(&mut storage_provider.open_reader(ASSOCIATED_DATA_FILE).unwrap())
1374 .unwrap();
1375 data.into_inner().into_vec()
1376 }
1377
1378 #[test]
1379 fn test_disk_search_with_associated_data_k10_l20_single_or_multi_thread_128dim() {
1380 let storage_provider = VirtualStorageProvider::new_overlay(test_data_root());
1381 let index_path_prefix = "/disk_index_search/disk_index_sift_learn_R4_L50_A1.2_test_disk_index_search_associated_data";
1382 let params = TestParams {
1383 data_path: TEST_DATA_FILE.to_string(),
1384 index_path_prefix: index_path_prefix.to_string(),
1385 associated_data_path: Some(
1386 "/sift/siftsmall_learn_256pts_u32_associated_data.fbin".to_string(),
1387 ),
1388 ..TestParams::default()
1389 };
1390 let fixture = IndexBuildFixture::new(storage_provider, params).unwrap();
1391 fixture.build::<GraphDataF32VectorU32Data>().unwrap();
1393 {
1394 let search_engine = create_disk_index_searcher::<GraphDataF32VectorU32Data>(
1395 CreateDiskIndexSearcherParams {
1396 max_thread_num: 5,
1397 pq_pivot_file_path: format!("{}_pq_pivots.bin", index_path_prefix).as_str(),
1398 pq_compressed_file_path: format!("{}_pq_compressed.bin", index_path_prefix)
1399 .as_str(),
1400 index_path: format!("{}_disk.index", index_path_prefix).as_str(), index_path_prefix,
1402 ..Default::default()
1403 },
1404 &fixture.storage_provider,
1405 );
1406
1407 test_disk_search_with_associated(
1409 TestDiskSearchAssociateParams {
1410 storage_provider: fixture.storage_provider.as_ref(),
1411 index_search_engine: &search_engine,
1412 thread_num: 1,
1413 query_file_path: TEST_QUERY_10PTS_128DIM,
1414 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_128DIM,
1415 k: 10,
1416 l: 20,
1417 },
1418 None,
1419 );
1420
1421 test_disk_search_with_associated(
1423 TestDiskSearchAssociateParams {
1424 storage_provider: fixture.storage_provider.as_ref(),
1425 index_search_engine: &search_engine,
1426 thread_num: 5,
1427 query_file_path: TEST_QUERY_10PTS_128DIM,
1428 truth_result_file_path: TEST_TRUTH_RESULT_10PTS_128DIM,
1429 k: 10,
1430 l: 20,
1431 },
1432 None,
1433 );
1434 }
1435
1436 fixture
1437 .storage_provider
1438 .delete(&format!("{}_disk.index", index_path_prefix))
1439 .expect("Failed to delete file");
1440 fixture
1441 .storage_provider
1442 .delete(&format!("{}_pq_pivots.bin", index_path_prefix))
1443 .expect("Failed to delete file");
1444 fixture
1445 .storage_provider
1446 .delete(&format!("{}_pq_compressed.bin", index_path_prefix))
1447 .expect("Failed to delete file");
1448 }
1449
1450 struct CreateDiskIndexSearcherParams<'a> {
1451 max_thread_num: usize,
1452 pq_pivot_file_path: &'a str,
1453 pq_compressed_file_path: &'a str,
1454 index_path: &'a str,
1455 index_path_prefix: &'a str,
1456 io_limit: usize,
1457 caching_strategy: CachingStrategy,
1458 }
1459
1460 impl Default for CreateDiskIndexSearcherParams<'_> {
1461 fn default() -> Self {
1462 Self {
1463 max_thread_num: 1,
1464 pq_pivot_file_path: "",
1465 pq_compressed_file_path: "",
1466 index_path: "",
1467 index_path_prefix: "",
1468 io_limit: usize::MAX,
1469 caching_strategy: CachingStrategy::None,
1470 }
1471 }
1472 }
1473
1474 fn create_disk_index_searcher<Data>(
1475 params: CreateDiskIndexSearcherParams,
1476 storage_provider: &Arc<VirtualStorageProvider<OverlayFS>>,
1477 ) -> DiskIndexSearcher<
1478 Data,
1479 DiskVertexProviderFactory<Data, VirtualAlignedReaderFactory<OverlayFS>>,
1480 >
1481 where
1482 Data: GraphDataType<VectorIdType = u32>,
1483 {
1484 assert!(params.io_limit > 0);
1485
1486 let runtime = tokio::runtime::Builder::new_multi_thread()
1487 .worker_threads(params.max_thread_num)
1488 .build()
1489 .unwrap();
1490
1491 let disk_index_reader = DiskIndexReader::new(
1492 params.pq_pivot_file_path.to_string(),
1493 params.pq_compressed_file_path.to_string(),
1494 storage_provider.as_ref(),
1495 )
1496 .unwrap();
1497
1498 let aligned_reader_factory = VirtualAlignedReaderFactory::new(
1499 get_disk_index_file(params.index_path_prefix),
1500 Arc::clone(storage_provider),
1501 );
1502 let vertex_provider_factory = DiskVertexProviderFactory::<Data, _>::new(
1503 aligned_reader_factory,
1504 params.caching_strategy,
1505 )
1506 .unwrap();
1507
1508 DiskIndexSearcher::<Data, DiskVertexProviderFactory<Data, _>>::new(
1509 params.max_thread_num,
1510 params.io_limit,
1511 &disk_index_reader,
1512 vertex_provider_factory,
1513 Metric::L2,
1514 Some(runtime),
1515 )
1516 .unwrap()
1517 }
1518
1519 fn load_query_result<StorageReader: StorageReadProvider>(
1520 storage_provider: &StorageReader,
1521 query_result_path: &str,
1522 ) -> Vec<u32> {
1523 let result =
1524 read_bin::<u32>(&mut storage_provider.open_reader(query_result_path).unwrap()).unwrap();
1525 result.into_inner().into_vec()
1526 }
1527
1528 struct TestDiskSearchParams<'a, StorageType> {
1529 storage_provider: &'a StorageType,
1530 index_search_engine: &'a DiskIndexSearcher<
1531 GraphDataF32VectorUnitData,
1532 DiskVertexProviderFactory<
1533 GraphDataF32VectorUnitData,
1534 VirtualAlignedReaderFactory<OverlayFS>,
1535 >,
1536 >,
1537 thread_num: u64,
1538 query_file_path: &'a str,
1539 truth_result_file_path: &'a str,
1540 k: usize,
1541 l: usize,
1542 }
1543
1544 struct TestDiskSearchAssociateParams<'a, StorageType> {
1545 storage_provider: &'a StorageType,
1546 index_search_engine: &'a DiskIndexSearcher<
1547 GraphDataF32VectorU32Data,
1548 DiskVertexProviderFactory<
1549 GraphDataF32VectorU32Data,
1550 VirtualAlignedReaderFactory<OverlayFS>,
1551 >,
1552 >,
1553 thread_num: u64,
1554 query_file_path: &'a str,
1555 truth_result_file_path: &'a str,
1556 k: usize,
1557 l: usize,
1558 }
1559
1560 fn test_disk_search<StorageType: StorageReadProvider>(
1561 params: TestDiskSearchParams<StorageType>,
1562 ) {
1563 let queries = read_bin::<f32>(
1564 &mut params
1565 .storage_provider
1566 .open_reader(params.query_file_path)
1567 .unwrap(),
1568 )
1569 .unwrap();
1570 let truth_result =
1571 load_query_result(params.storage_provider, params.truth_result_file_path);
1572
1573 let pool = create_thread_pool(params.thread_num.into_usize()).unwrap();
1574 queries
1575 .par_row_iter()
1576 .enumerate()
1577 .for_each_in_pool(pool.as_ref(), |(i, query)| {
1578 let mut query_stats = QueryStatistics::default();
1579 let mut indices = vec![0u32; 10];
1580 let mut distances = vec![0f32; 10];
1581 let mut associated_data = vec![(); 10];
1582
1583 let result = params.index_search_engine.search_internal(
1584 query,
1585 params.k,
1586 params.l as u32,
1587 None, &mut query_stats,
1589 &mut indices,
1590 &mut distances,
1591 &mut associated_data,
1592 &SearchMode::graph(),
1593 );
1594
1595 let truth_slice = &truth_result[i * params.k..(i + 1) * params.k];
1597
1598 assert!(result.is_ok(), "Expected search to succeed");
1599
1600 let result_unwrapped = result.unwrap();
1601 assert!(
1602 result_unwrapped.query_statistics.total_io_operations > 0,
1603 "Expected IO operations to be greater than 0"
1604 );
1605 assert!(
1606 result_unwrapped.query_statistics.total_vertices_loaded > 0,
1607 "Expected vertices loaded to be greater than 0"
1608 );
1609
1610 assert_eq!(
1612 indices, truth_slice,
1613 "Results DO NOT match with the truth result for query {}",
1614 i
1615 );
1616 });
1617 }
1618
1619 fn test_disk_search_with_associated<StorageType: StorageReadProvider>(
1620 params: TestDiskSearchAssociateParams<StorageType>,
1621 beam_width: Option<usize>,
1622 ) {
1623 let queries = read_bin::<f32>(
1624 &mut params
1625 .storage_provider
1626 .open_reader(params.query_file_path)
1627 .unwrap(),
1628 )
1629 .unwrap();
1630 let truth_result =
1631 load_query_result(params.storage_provider, params.truth_result_file_path);
1632 let pool = create_thread_pool(params.thread_num.into_usize()).unwrap();
1633 queries
1634 .par_row_iter()
1635 .enumerate()
1636 .for_each_in_pool(pool.as_ref(), |(i, query)| {
1637 let result = params
1638 .index_search_engine
1639 .search(
1640 query,
1641 params.k as u32,
1642 params.l as u32,
1643 beam_width,
1644 SearchMode::graph(),
1645 )
1646 .unwrap();
1647 let indices: Vec<u32> = result.results.iter().map(|item| item.vertex_id).collect();
1648 let associated_data: Vec<u32> =
1649 result.results.iter().map(|item| item.data).collect();
1650 let truth_data = get_truth_associated_data(params.storage_provider);
1651 let associated_data_truth: Vec<u32> = indices
1652 .iter()
1653 .map(|&vid| truth_data[vid as usize])
1654 .collect();
1655 assert_eq!(
1656 associated_data, associated_data_truth,
1657 "Associated data DO NOT match with the truth result for query {}, associated_data from search: {:?}, associated_data from truth result: {:?}",
1658 i,associated_data, associated_data_truth
1659 );
1660 let truth_slice = &truth_result[i * params.k..(i + 1) * params.k];
1661 assert_eq!(
1662 indices, truth_slice,
1663 "Results DO NOT match with the truth result for query {}",
1664 i
1665 );
1666 });
1667 }
1668
1669 #[test]
1670 fn test_disk_search_invalid_input() {
1671 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1672 let ctx = &DefaultContext;
1673
1674 let params = CreateDiskIndexSearcherParams {
1675 max_thread_num: 5,
1676 pq_pivot_file_path: TEST_PQ_PIVOT_128DIM,
1677 pq_compressed_file_path: TEST_PQ_COMPRESSED_128DIM,
1678 index_path: TEST_INDEX_128DIM,
1679 index_path_prefix: TEST_INDEX_PREFIX_128DIM,
1680 ..Default::default()
1681 };
1682
1683 let paths = PQPathNames::for_disk_index(TEST_INDEX_PREFIX_128DIM);
1684 assert_eq!(
1685 paths.pivots, params.pq_pivot_file_path,
1686 "pq_pivot_file_path is not correct"
1687 );
1688 assert_eq!(
1689 paths.compressed_data, params.pq_compressed_file_path,
1690 "pq_compressed_file_path is not correct"
1691 );
1692 assert_eq!(
1693 params.index_path,
1694 format!("{}_disk.index", params.index_path_prefix),
1695 "index_path is not correct"
1696 );
1697
1698 let res = Knn::new_default(20, 10);
1700 assert!(res.is_err());
1701 assert_eq!(
1702 <KnnSearchError as std::convert::Into<ANNError>>::into(res.unwrap_err()).kind(),
1703 ANNErrorKind::IndexError
1704 );
1705 let res = Knn::new(10, 10, Some(0));
1707 assert!(res.is_err());
1708
1709 let search_engine =
1710 create_disk_index_searcher::<GraphDataF32VectorU32Data>(params, &storage_provider);
1711
1712 assert_eq!(
1714 search_engine
1715 .index
1716 .data_provider
1717 .to_external_id(ctx, 0)
1718 .unwrap(),
1719 0
1720 );
1721 assert_eq!(
1722 search_engine
1723 .index
1724 .data_provider
1725 .to_internal_id(ctx, &0)
1726 .unwrap(),
1727 0
1728 );
1729
1730 let provider_max_degree = search_engine
1731 .index
1732 .data_provider
1733 .graph_header
1734 .max_degree::<<GraphDataF32VectorU32Data as GraphDataType>::VectorDataType>()
1735 .unwrap();
1736 let index_max_degree = search_engine.index.config.pruned_degree().get();
1737 assert_eq!(provider_max_degree, index_max_degree);
1738
1739 let query = vec![0f32; 128];
1740 let mut query_stats = QueryStatistics::default();
1741 let mut indices = vec![0u32; 10];
1742 let mut distances = vec![0f32; 10];
1743 let mut associated_data = vec![0u32; 10];
1744
1745 let result = search_engine.search_internal(
1747 &query,
1748 10,
1749 10 - 1,
1750 None,
1751 &mut query_stats,
1752 &mut indices,
1753 &mut distances,
1754 &mut associated_data,
1755 &SearchMode::graph(),
1756 );
1757
1758 assert!(result.is_err());
1759 assert_eq!(result.unwrap_err().kind(), ANNErrorKind::IndexError);
1760 }
1761
1762 #[test]
1763 fn test_disk_search_beam_search() {
1764 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1765
1766 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
1767 CreateDiskIndexSearcherParams {
1768 max_thread_num: 1,
1769 pq_pivot_file_path: TEST_PQ_PIVOT,
1770 pq_compressed_file_path: TEST_PQ_COMPRESSED,
1771 index_path: TEST_INDEX,
1772 index_path_prefix: TEST_INDEX_PREFIX,
1773 ..Default::default()
1774 },
1775 &storage_provider,
1776 );
1777
1778 let query_vector: [f32; 128] = [1f32; 128];
1779 let mut indices = vec![0u32; 10];
1780 let mut distances = vec![0f32; 10];
1781 let mut associated_data = vec![(); 10];
1782
1783 let mut result_output_buffer = search_output_buffer::IdDistanceAssociatedData::new(
1784 &mut indices,
1785 &mut distances,
1786 &mut associated_data,
1787 );
1788 let io_tracker = IOTracker::default();
1789 let strategy = search_engine.search_strategy(&io_tracker, PostprocessStrategy::AcceptAll);
1790 let mut search_record = VisitedSearchRecord::new(0);
1791 let search_params = Knn::new(10, 10, Some(4)).unwrap();
1792 let recorded_search =
1793 diskann::graph::search::RecordedKnn::new(search_params, &mut search_record);
1794 search_engine
1795 .runtime
1796 .block_on(search_engine.index.search(
1797 recorded_search,
1798 &strategy,
1799 &DefaultContext,
1800 query_vector.as_slice(),
1801 &mut result_output_buffer,
1802 ))
1803 .unwrap();
1804
1805 let ids = search_record
1806 .visited
1807 .iter()
1808 .map(|n| n.id)
1809 .collect::<Vec<_>>();
1810
1811 const EXPECTED_NODES: [u32; 18] = [
1812 72, 118, 108, 86, 84, 152, 170, 82, 114, 87, 207, 176, 79, 153, 67, 165, 141, 180,
1813 ]; assert_eq!(ids, &EXPECTED_NODES);
1816
1817 let return_list_size = 10;
1818 let search_list_size = 10;
1819 let result = search_engine.search(
1820 &query_vector,
1821 return_list_size,
1822 search_list_size,
1823 Some(4),
1824 SearchMode::graph(),
1825 );
1826 assert!(result.is_ok(), "Expected search to succeed");
1827 let search_result = result.unwrap();
1828 assert_eq!(
1829 search_result.results.len() as u32,
1830 return_list_size,
1831 "Expected result count to match"
1832 );
1833 assert_eq!(
1834 indices,
1835 vec![152, 72, 170, 118, 87, 165, 79, 141, 108, 86],
1836 "Expected indices to match"
1837 );
1838 }
1839
1840 #[test]
1841 fn test_disk_search_determinant_diversity() {
1842 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1843 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
1844 CreateDiskIndexSearcherParams {
1845 max_thread_num: 1,
1846 pq_pivot_file_path: TEST_PQ_PIVOT,
1847 pq_compressed_file_path: TEST_PQ_COMPRESSED,
1848 index_path: TEST_INDEX,
1849 index_path_prefix: TEST_INDEX_PREFIX,
1850 ..Default::default()
1851 },
1852 &storage_provider,
1853 );
1854
1855 let query_vector: [f32; 128] = [1f32; 128];
1856 let return_list_size = 10u32;
1857 let search_list_size = 20u32;
1858
1859 let baseline = search_engine
1862 .search(
1863 &query_vector,
1864 search_list_size,
1865 search_list_size,
1866 Some(4),
1867 SearchMode::graph(),
1868 )
1869 .unwrap();
1870 let baseline_ids: std::collections::HashSet<u32> =
1871 baseline.results.iter().map(|r| r.vertex_id).collect();
1872 let baseline_top1 = baseline
1873 .results
1874 .first()
1875 .expect("baseline returned no results");
1876
1877 let params = DeterminantDiversityParams::new(2.0, 0.01).unwrap();
1879 let result = search_engine
1880 .search(
1881 &query_vector,
1882 return_list_size,
1883 search_list_size,
1884 Some(4),
1885 SearchMode::diverse_graph(params),
1886 )
1887 .unwrap();
1888 let det_div_ids: Vec<u32> = result.results.iter().map(|r| r.vertex_id).collect();
1889
1890 assert_eq!(
1891 det_div_ids.len(),
1892 return_list_size as usize,
1893 "det-div should return k results when the candidate pool is large enough"
1894 );
1895 for id in &det_div_ids {
1896 assert!(
1897 baseline_ids.contains(id),
1898 "det-div selected id {} that is not in the search candidate pool",
1899 id
1900 );
1901 }
1902
1903 let mut unique = std::collections::HashSet::new();
1904 for id in &det_div_ids {
1905 assert!(unique.insert(*id), "det-div produced duplicate id {}", id);
1906 }
1907
1908 assert_eq!(
1911 result.results[0].vertex_id, baseline_top1.vertex_id,
1912 "det-div top-1 should be the nearest neighbor (highest similarity)"
1913 );
1914
1915 let pure_params = DeterminantDiversityParams::new(2.0, 0.0).unwrap();
1917 let pure_result = search_engine
1918 .search(
1919 &query_vector,
1920 return_list_size,
1921 search_list_size,
1922 Some(4),
1923 SearchMode::diverse_graph(pure_params),
1924 )
1925 .unwrap();
1926 let pure_ids: Vec<u32> = pure_result.results.iter().map(|r| r.vertex_id).collect();
1927 for id in &pure_ids {
1928 assert!(
1929 baseline_ids.contains(id),
1930 "det-div(eta=0) selected id {} that is not in the search candidate pool",
1931 id
1932 );
1933 }
1934
1935 let excluded = baseline_top1.vertex_id;
1938 let filtered = search_engine
1939 .search(
1940 &query_vector,
1941 return_list_size,
1942 search_list_size,
1943 Some(4),
1944 SearchMode::diverse_graph_filtered(move |id: &u32| *id != excluded, params),
1945 )
1946 .unwrap();
1947 let filtered_ids: Vec<u32> = filtered.results.iter().map(|r| r.vertex_id).collect();
1948 assert!(
1949 !filtered_ids.contains(&excluded),
1950 "det-div results must respect the vector filter"
1951 );
1952 }
1953
1954 #[cfg(feature = "experimental_diversity_search")]
1955 #[test]
1956 fn test_disk_search_diversity_search() {
1957 use diskann::graph::DiverseSearchParams;
1958 use diskann::neighbor::AttributeValueProvider;
1959 use std::collections::HashMap;
1960
1961 #[derive(Debug, Clone)]
1963 struct TestAttributeProvider {
1964 attributes: HashMap<u32, u32>,
1965 }
1966 impl TestAttributeProvider {
1967 fn new() -> Self {
1968 Self {
1969 attributes: HashMap::new(),
1970 }
1971 }
1972 fn insert(&mut self, id: u32, attribute: u32) {
1973 self.attributes.insert(id, attribute);
1974 }
1975 }
1976 impl diskann::provider::HasId for TestAttributeProvider {
1977 type Id = u32;
1978 }
1979
1980 impl AttributeValueProvider for TestAttributeProvider {
1981 type Value = u32;
1982
1983 fn get(&self, id: Self::Id) -> Option<Self::Value> {
1984 self.attributes.get(&id).copied()
1985 }
1986 }
1987
1988 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
1989
1990 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
1991 CreateDiskIndexSearcherParams {
1992 max_thread_num: 1,
1993 pq_pivot_file_path: TEST_PQ_PIVOT,
1994 pq_compressed_file_path: TEST_PQ_COMPRESSED,
1995 index_path: TEST_INDEX,
1996 index_path_prefix: TEST_INDEX_PREFIX,
1997 ..Default::default()
1998 },
1999 &storage_provider,
2000 );
2001
2002 let query_vector: [f32; 128] = [1f32; 128];
2003
2004 let mut attribute_provider = TestAttributeProvider::new();
2006 let num_vectors = 256; for i in 0..num_vectors {
2008 let label = (i % 15) + 1;
2010 attribute_provider.insert(i, label);
2011 }
2012 let attribute_provider = std::sync::Arc::new(attribute_provider);
2014
2015 let mut indices = vec![0u32; 10];
2016 let mut distances = vec![0f32; 10];
2017 let mut associated_data = vec![(); 10];
2018
2019 let mut result_output_buffer = search_output_buffer::IdDistanceAssociatedData::new(
2020 &mut indices,
2021 &mut distances,
2022 &mut associated_data,
2023 );
2024 let io_tracker = IOTracker::default();
2025 let strategy = search_engine.search_strategy(&io_tracker, PostprocessStrategy::AcceptAll);
2026
2027 let diverse_params = DiverseSearchParams::new(
2029 0, 3, attribute_provider.clone(),
2032 );
2033
2034 let search_params = Knn::new(10, 20, None).unwrap();
2035
2036 let diverse_search = diskann::graph::search::Diverse::new(search_params, diverse_params);
2037 let stats = search_engine
2038 .runtime
2039 .block_on(search_engine.index.search(
2040 diverse_search,
2041 &strategy,
2042 &DefaultContext,
2043 query_vector.as_slice(),
2044 &mut result_output_buffer,
2045 ))
2046 .unwrap();
2047
2048 assert!(
2050 stats.result_count > 0,
2051 "Expected to get some results during diversity search"
2052 );
2053
2054 let return_list_size = 10;
2055 let search_list_size = 20;
2056 let diverse_results_k = 1;
2057 let diverse_params = DiverseSearchParams::new(
2058 0, diverse_results_k,
2060 attribute_provider.clone(),
2061 );
2062
2063 let mut indices2 = vec![0u32; return_list_size as usize];
2065 let mut distances2 = vec![0f32; return_list_size as usize];
2066 let mut associated_data2 = vec![(); return_list_size as usize];
2067 let mut result_output_buffer2 = search_output_buffer::IdDistanceAssociatedData::new(
2068 &mut indices2,
2069 &mut distances2,
2070 &mut associated_data2,
2071 );
2072 let io_tracker2 = IOTracker::default();
2073 let strategy2 = search_engine.search_strategy(&io_tracker2, PostprocessStrategy::AcceptAll);
2074 let search_params2 =
2075 Knn::new(return_list_size as usize, search_list_size as usize, None).unwrap();
2076
2077 let diverse_search2 = diskann::graph::search::Diverse::new(search_params2, diverse_params);
2078 let stats = search_engine
2079 .runtime
2080 .block_on(search_engine.index.search(
2081 diverse_search2,
2082 &strategy2,
2083 &DefaultContext,
2084 query_vector.as_slice(),
2085 &mut result_output_buffer2,
2086 ))
2087 .unwrap();
2088
2089 assert!(
2091 stats.result_count > 0,
2092 "Expected diversity search to return results"
2093 );
2094 assert!(
2095 stats.result_count <= return_list_size,
2096 "Expected result count to be <= {}",
2097 return_list_size
2098 );
2099
2100 assert!(
2102 stats.result_count > 0,
2103 "Expected to get some search results"
2104 );
2105
2106 println!("\n=== Diversity Search Results ===");
2108 println!("Query: [1f32; 128]");
2109 println!("diverse_results_k: {}", diverse_results_k);
2110 println!("Total results: {}\n", stats.result_count);
2111 println!("{:<10} {:<15} {:<10}", "Vertex ID", "Distance", "Label");
2112 println!("{}", "-".repeat(35));
2113 for i in 0..stats.result_count as usize {
2114 let attribute_value = attribute_provider.get(indices2[i]).unwrap_or(0);
2115 println!(
2116 "{:<10} {:<15.2} {:<10}",
2117 indices2[i], distances2[i], attribute_value
2118 );
2119 }
2120
2121 for i in 0..(stats.result_count as usize).saturating_sub(1) {
2123 assert!(distances2[i] >= 0.0, "Expected non-negative distance");
2124 assert!(
2125 distances2[i] <= distances2[i + 1],
2126 "Expected distances to be sorted in ascending order"
2127 );
2128 }
2129
2130 let mut attribute_counts = HashMap::new();
2132 for item in indices2.iter().take(stats.result_count as usize) {
2133 if let Some(attribute_value) = attribute_provider.get(*item) {
2134 *attribute_counts.entry(attribute_value).or_insert(0) += 1;
2135 }
2136 }
2137
2138 println!("\n=== Attribute Distribution ===");
2140 let mut sorted_attrs: Vec<_> = attribute_counts.iter().collect();
2141 sorted_attrs.sort_by_key(|(k, _)| *k);
2142 for (attribute_value, count) in &sorted_attrs {
2143 println!(
2144 "Label {}: {} occurrences (max allowed: {})",
2145 attribute_value, count, diverse_results_k
2146 );
2147 }
2148 println!("Total unique labels: {}", attribute_counts.len());
2149 println!("================================\n");
2150
2151 for (attribute_value, count) in &attribute_counts {
2153 println!(
2154 "Assert: Label {} has {} occurrences (max: {})",
2155 attribute_value, count, diverse_results_k
2156 );
2157 assert!(
2158 *count <= diverse_results_k,
2159 "Attribute value {} appears {} times, which exceeds diverse_results_k of {}",
2160 attribute_value,
2161 count,
2162 diverse_results_k
2163 );
2164 }
2165
2166 println!(
2169 "Assert: Found {} unique labels (expected at least 2)",
2170 attribute_counts.len()
2171 );
2172 assert!(
2173 attribute_counts.len() >= 2,
2174 "Expected at least 2 different attribute values for diversity, got {}",
2175 attribute_counts.len()
2176 );
2177 }
2178
2179 #[rstest]
2180 #[case(
2182 |_id: &u32| true,
2183 false,
2184 10,
2185 vec![152, 118, 72, 170, 87, 141, 79, 207, 124, 86],
2186 vec![256101.7, 256675.3, 256709.69, 256712.5, 256760.08, 256958.5, 257006.1, 257025.7, 257105.67, 257107.67],
2187 )]
2188 #[case(
2191 |id: &u32| *id == 0 || *id == 1,
2192 false,
2193 0,
2194 vec![0; 10],
2195 vec![0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
2196 )]
2197 #[case(
2200 |id: &u32| *id == 0 || *id == 1,
2201 true,
2202 2,
2203 vec![1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
2204 vec![257247.28, 258179.28, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
2205 )]
2206 #[case(
2209 |id: &u32| *id == 72 || *id == 87 || *id == 170,
2210 false,
2211 3,
2212 vec![72, 170, 87, 0, 0, 0, 0, 0, 0, 0],
2213 vec![256709.69, 256712.5, 256760.08, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
2214 )]
2215 #[case(
2218 |id: &u32| *id == 72 || *id == 87 || *id == 170,
2219 true,
2220 3,
2221 vec![72, 170, 87, 0, 0, 0, 0, 0, 0, 0],
2222 vec![256709.69, 256712.5, 256760.08, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
2223 )]
2224 fn test_search_with_vector_filter(
2225 #[case] vector_filter: fn(&u32) -> bool,
2226 #[case] is_flat_search: bool,
2227 #[case] expected_result_count: u32,
2228 #[case] expected_indices: Vec<u32>,
2229 #[case] expected_distances: Vec<f32>,
2230 ) {
2231 let check_distances = |got: &[f32], expected: &[f32]| -> bool {
2236 const ABS_TOLERANCE: f32 = 0.02;
2237 assert_eq!(got.len(), expected.len());
2238 for (i, (g, e)) in std::iter::zip(got.iter(), expected.iter()).enumerate() {
2239 if (g - e).abs() > ABS_TOLERANCE {
2240 panic!(
2241 "distances differ at position {} by more than {}\n\n\
2242 got: {:?}\nexpected: {:?}",
2243 i, ABS_TOLERANCE, got, expected,
2244 );
2245 }
2246 }
2247 true
2248 };
2249
2250 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
2251
2252 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
2253 CreateDiskIndexSearcherParams {
2254 max_thread_num: 5,
2255 pq_pivot_file_path: TEST_PQ_PIVOT_128DIM,
2256 pq_compressed_file_path: TEST_PQ_COMPRESSED_128DIM,
2257 index_path: TEST_INDEX_128DIM,
2258 index_path_prefix: TEST_INDEX_PREFIX_128DIM,
2259 ..Default::default()
2260 },
2261 &storage_provider,
2262 );
2263 let query = vec![0.1f32; 128];
2264 let mut query_stats = QueryStatistics::default();
2265 let mut indices = vec![0u32; 10];
2266 let mut distances = vec![0f32; 10];
2267 let mut associated_data = vec![(); 10];
2268
2269 let make_mode = || -> SearchMode<'static> {
2272 if is_flat_search {
2273 SearchMode::flat_filtered(vector_filter)
2274 } else {
2275 SearchMode::graph_filtered(vector_filter)
2276 }
2277 };
2278
2279 let result = search_engine.search_internal(
2280 &query,
2281 10,
2282 10,
2283 None, &mut query_stats,
2285 &mut indices,
2286 &mut distances,
2287 &mut associated_data,
2288 &make_mode(),
2289 );
2290
2291 assert!(result.is_ok(), "Expected search to succeed");
2292 assert_eq!(
2293 result.unwrap().result_count,
2294 expected_result_count,
2295 "Expected result count to match"
2296 );
2297 assert_eq!(indices, expected_indices, "Expected indices to match");
2298 assert!(
2299 check_distances(&distances, &expected_distances),
2300 "Expected distances to match"
2301 );
2302
2303 let result_with_filter = search_engine.search(
2304 &query,
2305 10,
2306 10,
2307 None, make_mode(),
2309 );
2310
2311 assert!(result_with_filter.is_ok(), "Expected search to succeed");
2312 let result_with_filter_unwrapped = result_with_filter.unwrap();
2313 assert_eq!(
2314 result_with_filter_unwrapped.stats.result_count, expected_result_count,
2315 "Expected result count to match"
2316 );
2317 let actual_indices = result_with_filter_unwrapped
2318 .results
2319 .iter()
2320 .map(|x| x.vertex_id)
2321 .collect::<Vec<_>>();
2322 assert_eq!(
2323 actual_indices, expected_indices,
2324 "Expected indices to match"
2325 );
2326 let actual_distances = result_with_filter_unwrapped
2327 .results
2328 .iter()
2329 .map(|x| x.distance)
2330 .collect::<Vec<_>>();
2331 assert!(
2332 check_distances(&actual_distances, &expected_distances),
2333 "Expected distances to match"
2334 );
2335 }
2336
2337 #[test]
2354 fn test_adaptive_l_with_no_filter_matches_plain_knn() {
2355 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
2356 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
2357 CreateDiskIndexSearcherParams {
2358 max_thread_num: 1,
2359 pq_pivot_file_path: TEST_PQ_PIVOT_128DIM,
2360 pq_compressed_file_path: TEST_PQ_COMPRESSED_128DIM,
2361 index_path: TEST_INDEX_128DIM,
2362 index_path_prefix: TEST_INDEX_PREFIX_128DIM,
2363 ..Default::default()
2364 },
2365 &storage_provider,
2366 );
2367 let query = vec![0.1f32; 128];
2368
2369 let plain = search_engine
2370 .search(&query, 10, 10, None, SearchMode::graph())
2371 .expect("plain Knn must succeed");
2372
2373 let inline_no_filter = search_engine
2374 .search(
2375 &query,
2376 10,
2377 10,
2378 None,
2379 SearchMode::inline_filter(
2380 |_| true,
2381 Some(AdaptiveL::new(5, 16.0).expect("valid AdaptiveL")),
2382 ),
2383 )
2384 .expect("inline filter with accept-all predicate must succeed");
2385
2386 let plain_ids: Vec<u32> = plain.results.iter().map(|r| r.vertex_id).collect();
2387 let inline_ids: Vec<u32> = inline_no_filter
2388 .results
2389 .iter()
2390 .map(|r| r.vertex_id)
2391 .collect();
2392
2393 assert_eq!(
2394 plain.stats.result_count, inline_no_filter.stats.result_count,
2395 "no-filter inline path must return same result count as plain Knn"
2396 );
2397 assert_eq!(
2398 plain_ids, inline_ids,
2399 "no-filter inline path must return the same top-k IDs as plain Knn"
2400 );
2401 }
2402
2403 #[test]
2404 fn test_adaptive_l_with_selective_predicate_returns_only_matches() {
2405 let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
2406 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
2407 CreateDiskIndexSearcherParams {
2408 max_thread_num: 1,
2409 pq_pivot_file_path: TEST_PQ_PIVOT_128DIM,
2410 pq_compressed_file_path: TEST_PQ_COMPRESSED_128DIM,
2411 index_path: TEST_INDEX_128DIM,
2412 index_path_prefix: TEST_INDEX_PREFIX_128DIM,
2413 ..Default::default()
2414 },
2415 &storage_provider,
2416 );
2417 let query = vec![0.1f32; 128];
2418 let predicate = |id: &u32| *id == 72 || *id == 87 || *id == 170;
2421
2422 let result = search_engine
2423 .search(
2424 &query,
2425 10,
2426 10,
2427 None,
2428 SearchMode::inline_filter(
2429 predicate,
2430 Some(AdaptiveL::new(5, 16.0).expect("valid AdaptiveL")),
2431 ),
2432 )
2433 .expect("inline filter search with AdaptiveL must succeed");
2434
2435 let count = result.stats.result_count as usize;
2439 let ids: Vec<u32> = result
2440 .results
2441 .iter()
2442 .take(count)
2443 .map(|r| r.vertex_id)
2444 .collect();
2445 for id in &ids {
2446 assert!(
2447 predicate(id),
2448 "AdaptiveL result must only contain predicate-matching IDs; got {id} in {ids:?}"
2449 );
2450 }
2451 assert!(
2452 !ids.is_empty(),
2453 "AdaptiveL on a fixture with reachable matches must return at least one match"
2454 );
2455 }
2456
2457 #[test]
2458 fn test_beam_search_respects_io_limit() {
2459 let io_limit = 11; let storage_provider = Arc::new(VirtualStorageProvider::new_overlay(test_data_root()));
2461
2462 let search_engine = create_disk_index_searcher::<GraphDataF32VectorUnitData>(
2463 CreateDiskIndexSearcherParams {
2464 max_thread_num: 1,
2465 pq_pivot_file_path: TEST_PQ_PIVOT,
2466 pq_compressed_file_path: TEST_PQ_COMPRESSED,
2467 index_path: TEST_INDEX,
2468 index_path_prefix: TEST_INDEX_PREFIX,
2469 io_limit,
2470 ..Default::default()
2471 },
2472 &storage_provider,
2473 );
2474 let query_vector: [f32; 128] = [1f32; 128];
2475
2476 let mut indices = vec![0u32; 10];
2477 let mut distances = vec![0f32; 10];
2478 let mut associated_data = vec![(); 10];
2479
2480 let mut result_output_buffer = search_output_buffer::IdDistanceAssociatedData::new(
2481 &mut indices,
2482 &mut distances,
2483 &mut associated_data,
2484 );
2485
2486 let io_tracker = IOTracker::default();
2487 let strategy = search_engine.search_strategy(&io_tracker, PostprocessStrategy::AcceptAll);
2488
2489 let mut search_record = VisitedSearchRecord::new(0);
2490 let search_params = Knn::new(10, 10, Some(4)).unwrap();
2491 let recorded_search =
2492 diskann::graph::search::RecordedKnn::new(search_params, &mut search_record);
2493 search_engine
2494 .runtime
2495 .block_on(search_engine.index.search(
2496 recorded_search,
2497 &strategy,
2498 &DefaultContext,
2499 query_vector.as_slice(),
2500 &mut result_output_buffer,
2501 ))
2502 .unwrap();
2503 let visited_ids = search_record
2504 .visited
2505 .iter()
2506 .map(|n| n.id)
2507 .collect::<Vec<_>>();
2508
2509 let query_stats = strategy.io_tracker;
2510 assert!(
2512 query_stats.io_count() <= io_limit,
2513 "Expected IO operations to be <= {}, but got {}",
2514 io_limit,
2515 query_stats.io_count()
2516 );
2517
2518 const EXPECTED_NODES: [u32; 17] = [
2519 72, 118, 108, 86, 84, 152, 170, 82, 114, 87, 207, 176, 79, 153, 67, 165, 141,
2520 ]; let mut matching_count = 0;
2524 for expected_node in EXPECTED_NODES.iter() {
2525 if visited_ids.contains(expected_node) {
2526 matching_count += 1;
2527 }
2528 }
2529
2530 let recall = (matching_count as f32 / EXPECTED_NODES.len() as f32) * 100.0;
2532
2533 assert!(recall >= 60.0, "Match percentage is below 60%: {}", recall);
2536 }
2537}