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    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
63///////////////////
64// Disk Provider //
65///////////////////
66
67/// The DiskProvider is a data provider that loads data from disk using the disk readers
68/// The data format for disk is different from that of the in-memory providers.
69/// The disk format stores both the vectors and the adjacency list next to each other for
70/// better locality for quicker access.
71/// Please refer to the RFC documentation at [`docs\rfcs\cy2025\disk_provider_for_async_index.md`] for design details.
72pub struct DiskProvider<Data>
73where
74    Data: GraphDataType<VectorIdType = u32>,
75{
76    /// Holds the graph header information that contains metadata about disk-index file.
77    graph_header: GraphHeader,
78
79    // Full precision distance comparer used in post_process to reorder results.
80    distance_comparer: <Data::VectorDataType as VectorRepr>::Distance,
81
82    /// The PQ data used for quantization.
83    pq_data: Arc<PQData>,
84
85    /// The number of points in the graph.
86    num_points: usize,
87
88    /// Metric used for distance computation.
89    metric: Metric,
90
91    /// The number of IO operations that can be done in parallel.
92    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    /// Translate an external id to its corresponding internal id.
110    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    /// Translate an internal id its corresponding external id.
119    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
208/// The search strategy for the disk provider. This is used to create the search accessor
209/// for use in search in quant space and post_process function to reorder with full precision vectors.
210///
211/// # Why vertex_provider_factory and scratch_pool are here instead of DiskProvider
212///
213/// The DataProvider trait requires 'static bounds for multi-threaded async contexts,
214/// but vertex_provider_factory may have non-'static lifetime bounds (e.g., borrowing
215/// from local data structures). Moving these components to the search strategy allows
216/// DiskProvider to satisfy 'static constraints while enabling flexible per-search
217/// resource management.
218pub struct DiskSearchStrategy<'a, Data, ProviderFactory>
219where
220    Data: GraphDataType<VectorIdType = u32>,
221    ProviderFactory: VertexProviderFactory<Data>,
222{
223    // This needs to be Arc instead of Rc because DiskSearchStrategy has "Send" trait bound, though this is not expected to be shared across threads.
224    io_tracker: IOTracker,
225    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.
226    query: &'a [Data::VectorDataType],
227
228    /// The vertex provider factory is used to create the vertex provider for each search instance.
229    vertex_provider_factory: &'a ProviderFactory,
230
231    /// Scratch pool for disk search operations that need allocations.
232    scratch_pool: &'a Arc<ObjectPool<DiskSearchScratch<Data, ProviderFactory::VertexProviderType>>>,
233}
234
235// Struct to track IO. This is used by single thread, but needs to be Atomic as the Strategy has "Send" trait bound.
236// There should be minimal to no overhead compared to using a raw reference.
237struct 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        // Sort the full precision distances.
337        reranked
338            .sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
339        // Store the reranked results.
340        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
389/// The query computer for the disk provider. This is used to compute the distance between the query vector and the PQ coordinates.
390pub 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
490// Scratch space for disk search operations that need allocations.
491// These allocations are amortized across searches using the scratch pool.
492struct 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; // Setting as 0 to avoid preallocation of memory.
529        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    // Compute the PQ distance between each ID in `ids` and the distance table stored in
567    // `self`, invoking the callback with the results of each computation in order.
568    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        // Decode caller's native vector representation into `f32`; downstream PQ kernels operate purely on `&[f32]`.
634        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    /// This accessor returns raw slices. There *is* a chance of racing when the fast
698    /// providers are used. We just have to live with it.
699    type Element<'a>
700        = &'a [u8]
701    where
702        Self: 'a;
703
704    /// `ElementRef` can have arbitrary lifetimes.
705    type ElementRef<'a> = &'a [u8];
706
707    /// Choose to panic on an out-of-bounds access rather than propagate an error.
708    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
739/// A light-weight wrapper around `&mut DiskAccessor` used to tailor the semantics of
740/// [`NeighborAccessor`].
741///
742/// This implementation ensures that the vector data for adjacency lists is also retrieved
743/// and cached to enhance reranking.
744pub 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); // Returning empty results in `neighbors` out param if IO limit is reached.
769        }
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
783/// [`DiskIndexSearcher`] is a helper class to make it easy to construct index
784/// and do repeated search operations. It is a wrapper around the index.
785/// This is useful for drivers such as search_disk_index.exe in tools.
786pub 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    /// The vertex provider factory is used to create the vertex provider for each search instance.
797    vertex_provider_factory: ProviderFactory,
798
799    /// Scratch pool for disk search operations that need allocations.
800    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
810/// `SearchResult` is a struct representing the result of a search operation.
811///
812/// It contains a list of vector results and a statistics object
813///
814pub struct SearchResult<AssociatedData> {
815    /// A list of nearest neighbors resulting from the search.
816    pub results: Vec<SearchResultItem<AssociatedData>>,
817    pub stats: SearchResultStats,
818}
819
820/// `VectorResult` is a struct representing a nearest neighbor resulting from a search.
821///
822/// It contains the vertex id, associated data, and the distance to the query vector.
823///
824pub struct SearchResultItem<AssociatedData> {
825    /// The vertex id of the nearest neighbor.
826    pub vertex_id: u32,
827    /// The associated data of the nearest neighbor as a fixed size byte array.
828    /// The length is determined when the index is created.
829    pub data: AssociatedData,
830    /// The distance between the nearest neighbor and the query vector.
831    pub distance: f32,
832}
833
834impl<Data, ProviderFactory> DiskIndexSearcher<Data, ProviderFactory>
835where
836    Data: GraphDataType<VectorIdType = u32>,
837    ProviderFactory: VertexProviderFactory<Data>,
838{
839    /// Create a new asynchronous disk searcher instance.
840    ///
841    /// # Arguments
842    /// * `num_threads` - The maximum number of threads to use.
843    /// * `search_io_limit` - I/O operation limit.
844    /// * `disk_index_reader` - The disk index reader.
845    /// * `vertex_provider_factory` - The vertex provider factory.
846    /// * `metric` - Distance metric used for vector similarity calculations.
847    /// * `runtime` - Tokio runtime handle for executing async operations.
848    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, // build-search-list-size
869            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    /// Helper method to create a DiskSearchStrategy with common parameters
905    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    /// Perform a search on the disk index.
920    /// return the list of nearest neighbors and associated data.
921    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    /// Perform a raw search on the disk index.
970    /// This is a lower-level API that allows more control over the search parameters and output buffers.
971    #[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
1034/// Helper function to ensure vertices are loaded and processed.
1035///
1036/// This is a convenience function that combines `load_vertices` and `process_loaded_node`
1037/// for each vertex ID. It first loads all the vertices in batch, then processes each
1038/// loaded node.
1039fn 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 single thread.
1124        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 multi thread.
1134        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 single thread.
1161        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 multi thread.
1171        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        // Build the index with the associated data
1207        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(), //TEST_INDEX_128DIM,
1216                    index_path_prefix,
1217                    ..Default::default()
1218                },
1219                &fixture.storage_provider,
1220            );
1221
1222            // Test single thread.
1223            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 multi thread.
1237            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, // beam_width
1400                    &mut query_stats,
1401                    &mut indices,
1402                    &mut distances,
1403                    &mut associated_data,
1404                    &(|_| true),
1405                    false,
1406                );
1407
1408                // Calculate the range of the truth_result for this query
1409                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                // Compare res with truth_slice using assert_eq!
1424                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        // Test error case: l < k
1506        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        // Test error case: beam_width = 0
1513        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        // minor validation tests to improve code coverage
1520        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        // Set L: {} to a value of at least K:
1553        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        ]; //Expected nodes for query = [1f32; 128] with beam_width=4
1621
1622        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        // Simple test attribute provider
1656        #[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        // Create attribute provider with random labels (1 to 3) for all vectors
1699        let mut attribute_provider = TestAttributeProvider::new();
1700        let num_vectors = 256; // Number of vectors in the test dataset
1701        for i in 0..num_vectors {
1702            // Assign labels 1-3 based on modulo to ensure distribution
1703            let label = (i % 15) + 1;
1704            attribute_provider.insert(i, label);
1705        }
1706        // Wrap in Arc once to avoid cloning the HashMap later
1707        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        // Create diverse search parameters with attribute provider
1721        let diverse_params = DiverseSearchParams::new(
1722            0, // diverse_attribute_id
1723            3, // diverse_results_k
1724            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        // Verify that search was performed and returned some results
1742        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_attribute_id
1752            diverse_results_k,
1753            attribute_provider.clone(),
1754        );
1755
1756        // Test diverse search using the search API
1757        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        // Verify results
1782        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        // Verify that we got some results
1793        assert!(
1794            stats.result_count > 0,
1795            "Expected to get some search results"
1796        );
1797
1798        // Print search results with their attributes
1799        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        // Verify that distances are non-negative and sorted
1814        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        // Verify diversity: Check that we have diverse attribute values in the results
1823        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        // Print attribute distribution
1831        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        // With diverse_results_k = 5, we expect at most 5 results per attribute value
1844        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        // Verify that we have multiple different attribute values (diversity)
1859        // With 3 possible labels and diverse_results_k=5, we should see at least 2 different labels
1860        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    // This case checks expected behavior of unfiltered search.
1873    #[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    // This case validates post-filtering using 2 ids which are not present in the unfiltered result set.
1881    // It is expected that the post-filtering will return an empty result
1882    #[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    // This case validates pre-filtering using 2 ids which are not present in the unfiltered result set.
1890    // It is expected that the pre-filtering will do search over matching ids
1891    #[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    // This case validates post-filtering using 3 ids from the unfiltered result set.
1899    // It is expected that the post-filtering will filter out non-matching ids
1900    #[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    // This case validates pre-filtering using 3 ids from the unfiltered result set.
1908    // It is expected that the pre-filtering will do search over matching ids
1909    #[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        // Exact distances can vary slightly depending on the architecture used
1924        // to compute distances due to different unrolling strategies and SIMD widthd.
1925        //
1926        // This parameter allows for a small margin when matching distances.
1927        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, // beam_width
1966            &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, // beam_width
1991            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; // Set a small IO limit for testing
2024        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        //Verify the IO limit was respected
2073        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        ]; //Expected nodes for query = [1f32; 128] with beam_width=4
2083
2084        // Count matching results
2085        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        // Calculate recall
2093        let recall = (matching_count as f32 / EXPECTED_NODES.len() as f32) * 100.0;
2094
2095        //Verify the recall is above 60%. The threshold her eis arbitrary, just to make sure when
2096        // search hits io_limit that it doesn't break and the recall degrades gracefully
2097        assert!(recall >= 60.0, "Match percentage is below 60%: {}", recall);
2098    }
2099}