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