Skip to main content

diskann_disk/search/provider/
disk_provider.rs

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