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