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::{
35        object_pool::{ObjectPool, PoolOption, TryAsPooled},
36        IntoUsize, VectorRepr,
37    },
38    ANNError, ANNResult,
39};
40use diskann_providers::storage::StorageReadProvider;
41use diskann_providers::{
42    model::{compute_pq_distance, compute_pq_distance_for_pq_coordinates},
43    storage::{get_compressed_pq_file, get_disk_index_file, get_pq_pivot_file, LoadWith},
44};
45
46use crate::search::pq::{quantizer_preprocess, PQData, PQScratch};
47use diskann_vector::{distance::Metric, DistanceFunction, PreprocessedDistanceFunction};
48use futures_util::future;
49use tokio::runtime::Runtime;
50use tracing::debug;
51
52use crate::{
53    data_model::{CachingStrategy, GraphHeader},
54    filter_parameter::{default_vector_filter, VectorFilter},
55    search::{
56        provider::disk_vertex_provider_factory::DiskVertexProviderFactory,
57        traits::{VertexProvider, VertexProviderFactory},
58    },
59    storage::{api::AsyncDiskLoadContext, disk_index_reader::DiskIndexReader},
60    utils::AlignedFileReaderFactory,
61    utils::QueryStatistics,
62};
63
64///////////////////
65// Disk Provider //
66///////////////////
67
68/// The DiskProvider is a data provider that loads data from disk using the disk readers
69/// The data format for disk is different from that of the in-memory providers.
70/// The disk format stores both the vectors and the adjacency list next to each other for
71/// better locality for quicker access.
72/// Please refer to the RFC documentation at [`docs\rfcs\cy2025\disk_provider_for_async_index.md`] for design details.
73pub struct DiskProvider<Data>
74where
75    Data: GraphDataType<VectorIdType = u32>,
76{
77    /// Holds the graph header information that contains metadata about disk-index file.
78    graph_header: GraphHeader,
79
80    // Full precision distance comparer used in post_process to reorder results.
81    distance_comparer: <Data::VectorDataType as VectorRepr>::Distance,
82
83    /// The PQ data used for quantization.
84    pq_data: Arc<PQData>,
85
86    /// The number of points in the graph.
87    num_points: usize,
88
89    /// Metric used for distance computation.
90    metric: Metric,
91
92    /// The number of IO operations that can be done in parallel.
93    search_io_limit: usize,
94}
95
96impl<Data> DataProvider for DiskProvider<Data>
97where
98    Data: GraphDataType<VectorIdType = u32>,
99{
100    type Context = DefaultContext;
101
102    type InternalId = u32;
103
104    type ExternalId = u32;
105
106    type Guard = NoopGuard<u32>;
107
108    type Error = ANNError;
109
110    /// Translate an external id to its corresponding internal id.
111    fn to_internal_id(
112        &self,
113        _context: &DefaultContext,
114        gid: &Self::ExternalId,
115    ) -> Result<Self::InternalId, Self::Error> {
116        Ok(*gid)
117    }
118
119    /// Translate an internal id its corresponding external id.
120    fn to_external_id(
121        &self,
122        _context: &DefaultContext,
123        id: Self::InternalId,
124    ) -> Result<Self::ExternalId, Self::Error> {
125        Ok(id)
126    }
127}
128
129impl<Data> LoadWith<AsyncDiskLoadContext> for DiskProvider<Data>
130where
131    Data: GraphDataType<VectorIdType = u32>,
132{
133    type Error = ANNError;
134
135    async fn load_with<P>(provider: &P, ctx: &AsyncDiskLoadContext) -> ANNResult<Self>
136    where
137        P: StorageReadProvider,
138    {
139        debug!(
140            "DiskProvider::load_with() called with file: {:?}",
141            get_disk_index_file(ctx.quant_load_context.metadata.prefix())
142        );
143
144        let graph_header = {
145            let aligned_reader_factory = AlignedFileReaderFactory::new(get_disk_index_file(
146                ctx.quant_load_context.metadata.prefix(),
147            ));
148
149            let caching_strategy = if ctx.num_nodes_to_cache > 0 {
150                CachingStrategy::StaticCacheWithBfsNodes(ctx.num_nodes_to_cache)
151            } else {
152                CachingStrategy::None
153            };
154
155            let vertex_provider_factory = DiskVertexProviderFactory::<Data, _>::new(
156                aligned_reader_factory,
157                caching_strategy,
158            )?;
159            VertexProviderFactory::get_header(&vertex_provider_factory)?
160        };
161
162        let metric = ctx.quant_load_context.metric;
163        let num_points = ctx.num_points;
164
165        let index_path_prefix = ctx.quant_load_context.metadata.prefix();
166        let index_reader = DiskIndexReader::<<Data as GraphDataType>::VectorDataType>::new(
167            get_pq_pivot_file(index_path_prefix),
168            get_compressed_pq_file(index_path_prefix),
169            provider,
170        )?;
171
172        Self::new(
173            &index_reader,
174            graph_header,
175            metric,
176            num_points,
177            ctx.search_io_limit,
178        )
179    }
180}
181
182impl<Data> DiskProvider<Data>
183where
184    Data: GraphDataType<VectorIdType = u32>,
185{
186    fn new(
187        disk_index_reader: &DiskIndexReader<Data::VectorDataType>,
188        graph_header: GraphHeader,
189        metric: Metric,
190        num_points: usize,
191        search_io_limit: usize,
192    ) -> ANNResult<Self> {
193        let distance_comparer =
194            Data::VectorDataType::distance(metric, Some(graph_header.metadata().dims));
195
196        let pq_data = disk_index_reader.get_pq_data();
197
198        Ok(Self {
199            graph_header,
200            distance_comparer,
201            pq_data,
202            num_points,
203            metric,
204            search_io_limit,
205        })
206    }
207}
208
209/// The search strategy for the disk provider. This is used to create the search accessor
210/// for use in search in quant space and post_process function to reorder with full precision vectors.
211///
212/// # Why vertex_provider_factory and scratch_pool are here instead of DiskProvider
213///
214/// The DataProvider trait requires 'static bounds for multi-threaded async contexts,
215/// but vertex_provider_factory may have non-'static lifetime bounds (e.g., borrowing
216/// from local data structures). Moving these components to the search strategy allows
217/// DiskProvider to satisfy 'static constraints while enabling flexible per-search
218/// resource management.
219pub struct DiskSearchStrategy<'a, Data, ProviderFactory>
220where
221    Data: GraphDataType<VectorIdType = u32>,
222    ProviderFactory: VertexProviderFactory<Data>,
223{
224    // This needs to be Arc instead of Rc because DiskSearchStrategy has "Send" trait bound, though this is not expected to be shared across threads.
225    io_tracker: IOTracker,
226    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.
227    query: &'a [Data::VectorDataType],
228
229    /// The vertex provider factory is used to create the vertex provider for each search instance.
230    vertex_provider_factory: &'a ProviderFactory,
231
232    /// Scratch pool for disk search operations that need allocations.
233    scratch_pool: &'a Arc<ObjectPool<DiskSearchScratch<Data, ProviderFactory::VertexProviderType>>>,
234}
235
236// Struct to track IO. This is used by single thread, but needs to be Atomic as the Strategy has "Send" trait bound.
237// There should be minimal to no overhead compared to using a raw reference.
238struct IOTracker {
239    io_time_us: AtomicU64,
240    preprocess_time_us: AtomicU64,
241    io_count: AtomicUsize,
242}
243
244impl Default for IOTracker {
245    fn default() -> Self {
246        Self {
247            io_time_us: AtomicU64::new(0),
248            preprocess_time_us: AtomicU64::new(0),
249            io_count: AtomicUsize::new(0),
250        }
251    }
252}
253
254impl IOTracker {
255    fn add_time(category: &AtomicU64, time: u64) {
256        category.fetch_add(time, std::sync::atomic::Ordering::Relaxed);
257    }
258
259    fn time(category: &AtomicU64) -> u64 {
260        category.load(std::sync::atomic::Ordering::Relaxed)
261    }
262
263    fn add_io_count(&self, count: usize) {
264        self.io_count
265            .fetch_add(count, std::sync::atomic::Ordering::Relaxed);
266    }
267
268    fn io_count(&self) -> usize {
269        self.io_count.load(std::sync::atomic::Ordering::Relaxed)
270    }
271}
272
273#[derive(Clone, Copy)]
274pub struct RerankAndFilter<'a> {
275    filter: &'a (dyn Fn(&u32) -> bool + Send + Sync),
276}
277
278impl<'a> RerankAndFilter<'a> {
279    fn new(filter: &'a (dyn Fn(&u32) -> bool + Send + Sync)) -> Self {
280        Self { filter }
281    }
282}
283
284impl<Data, VP>
285    SearchPostProcess<
286        DiskAccessor<'_, Data, VP>,
287        &[Data::VectorDataType],
288        (
289            <DiskProvider<Data> as DataProvider>::InternalId,
290            Data::AssociatedDataType,
291        ),
292    > for RerankAndFilter<'_>
293where
294    Data: GraphDataType<VectorIdType = u32>,
295    VP: VertexProvider<Data>,
296{
297    type Error = ANNError;
298    async fn post_process<I, B>(
299        &self,
300        accessor: &mut DiskAccessor<'_, Data, VP>,
301        query: &[Data::VectorDataType],
302        _computer: &DiskQueryComputer,
303        candidates: I,
304        output: &mut B,
305    ) -> Result<usize, Self::Error>
306    where
307        I: Iterator<Item = Neighbor<u32>> + Send,
308        B: search_output_buffer::SearchOutputBuffer<(u32, Data::AssociatedDataType)>
309            + Send
310            + ?Sized,
311    {
312        let provider = accessor.provider;
313
314        let mut uncached_ids = Vec::new();
315        let mut reranked = candidates
316            .map(|n| n.id)
317            .filter(|id| (self.filter)(id))
318            .filter_map(|n| {
319                if let Some(entry) = accessor.scratch.distance_cache.get(&n) {
320                    Some(Ok::<((u32, _), f32), ANNError>(((n, entry.1), entry.0)))
321                } else {
322                    uncached_ids.push(n);
323                    None
324                }
325            })
326            .collect::<Result<Vec<_>, _>>()?;
327        if !uncached_ids.is_empty() {
328            ensure_vertex_loaded(&mut accessor.scratch.vertex_provider, &uncached_ids)?;
329            for n in &uncached_ids {
330                let v = accessor.scratch.vertex_provider.get_vector(n)?;
331                let d = provider.distance_comparer.evaluate_similarity(query, v);
332                let a = accessor.scratch.vertex_provider.get_associated_data(n)?;
333                reranked.push(((*n, *a), d));
334            }
335        }
336
337        // Sort the full precision distances.
338        reranked
339            .sort_unstable_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
340        // Store the reranked results.
341        Ok(output.extend(reranked))
342    }
343}
344
345impl<'this, Data, ProviderFactory> SearchStrategy<DiskProvider<Data>, &[Data::VectorDataType]>
346    for DiskSearchStrategy<'this, Data, ProviderFactory>
347where
348    Data: GraphDataType<VectorIdType = u32>,
349    ProviderFactory: VertexProviderFactory<Data>,
350{
351    type QueryComputer = DiskQueryComputer;
352    type SearchAccessor<'a> = DiskAccessor<'a, Data, ProviderFactory::VertexProviderType>;
353    type SearchAccessorError = ANNError;
354
355    fn search_accessor<'a>(
356        &'a self,
357        provider: &'a DiskProvider<Data>,
358        _context: &DefaultContext,
359    ) -> Result<Self::SearchAccessor<'a>, Self::SearchAccessorError> {
360        DiskAccessor::new(
361            provider,
362            &self.io_tracker,
363            self.query,
364            self.vertex_provider_factory,
365            self.scratch_pool,
366        )
367    }
368}
369
370impl<'this, Data, ProviderFactory>
371    DefaultPostProcessor<
372        DiskProvider<Data>,
373        &[Data::VectorDataType],
374        (
375            <DiskProvider<Data> as DataProvider>::InternalId,
376            Data::AssociatedDataType,
377        ),
378    > for DiskSearchStrategy<'this, Data, ProviderFactory>
379where
380    Data: GraphDataType<VectorIdType = u32>,
381    ProviderFactory: VertexProviderFactory<Data>,
382{
383    type Processor = RerankAndFilter<'this>;
384
385    fn default_post_processor(&self) -> Self::Processor {
386        RerankAndFilter::new(self.vector_filter)
387    }
388}
389
390/// The query computer for the disk provider. This is used to compute the distance between the query vector and the PQ coordinates.
391pub struct DiskQueryComputer {
392    num_pq_chunks: usize,
393    query_centroid_l2_distance: Vec<f32>,
394}
395
396impl PreprocessedDistanceFunction<&[u8], f32> for DiskQueryComputer {
397    fn evaluate_similarity(&self, changing: &[u8]) -> f32 {
398        let mut dist = 0.0f32;
399        #[allow(clippy::expect_used)]
400        compute_pq_distance_for_pq_coordinates(
401            changing,
402            self.num_pq_chunks,
403            &self.query_centroid_l2_distance,
404            std::slice::from_mut(&mut dist),
405        )
406        .expect("PQ distance compute for PQ coordinates is expected to succeed");
407        dist
408    }
409}
410
411impl<Data, VP> BuildQueryComputer<&[Data::VectorDataType]> for DiskAccessor<'_, Data, VP>
412where
413    Data: GraphDataType<VectorIdType = u32>,
414    VP: VertexProvider<Data>,
415{
416    type QueryComputerError = ANNError;
417    type QueryComputer = DiskQueryComputer;
418
419    fn build_query_computer(
420        &self,
421        _from: &[Data::VectorDataType],
422    ) -> Result<Self::QueryComputer, Self::QueryComputerError> {
423        Ok(DiskQueryComputer {
424            num_pq_chunks: self.provider.pq_data.get_num_chunks(),
425            query_centroid_l2_distance: self
426                .scratch
427                .pq_scratch
428                .aligned_pqtable_dist_scratch
429                .to_vec(),
430        })
431    }
432
433    async fn distances_unordered<Itr, F>(
434        &mut self,
435        vec_id_itr: Itr,
436        _computer: &Self::QueryComputer,
437        f: F,
438    ) -> Result<(), Self::GetError>
439    where
440        F: Send + FnMut(f32, Self::Id),
441        Itr: Iterator<Item = Self::Id>,
442    {
443        self.pq_distances(&vec_id_itr.collect::<Box<[_]>>(), f)
444    }
445}
446
447impl<Data, VP> ExpandBeam<&[Data::VectorDataType]> for DiskAccessor<'_, Data, VP>
448where
449    Data: GraphDataType<VectorIdType = u32>,
450    VP: VertexProvider<Data>,
451{
452    fn expand_beam<Itr, P, F>(
453        &mut self,
454        ids: Itr,
455        _computer: &Self::QueryComputer,
456        mut pred: P,
457        mut f: F,
458    ) -> impl std::future::Future<Output = Result<(), Self::GetError>> + Send
459    where
460        Itr: Iterator<Item = Self::Id> + Send,
461        P: glue::HybridPredicate<Self::Id> + Send + Sync,
462        F: FnMut(f32, Self::Id) + Send,
463    {
464        let result = (|| {
465            let io_limit = self.provider.search_io_limit - self.io_tracker.io_count();
466            let load_ids: Box<[_]> = ids.take(io_limit).collect();
467
468            self.ensure_loaded(&load_ids)?;
469            let mut ids = Vec::new();
470            for i in load_ids {
471                ids.clear();
472                ids.extend(
473                    self.scratch
474                        .vertex_provider
475                        .get_adjacency_list(&i)?
476                        .iter()
477                        .copied()
478                        .filter(|id| pred.eval_mut(id)),
479                );
480
481                self.pq_distances(&ids, &mut f)?;
482            }
483
484            Ok(())
485        })();
486
487        std::future::ready(result)
488    }
489}
490
491// Scratch space for disk search operations that need allocations.
492// These allocations are amortized across searches using the scratch pool.
493struct DiskSearchScratch<Data, VP>
494where
495    Data: GraphDataType<VectorIdType = u32>,
496    VP: VertexProvider<Data>,
497{
498    distance_cache: HashMap<u32, (f32, Data::AssociatedDataType)>,
499    pq_scratch: PQScratch,
500    vertex_provider: VP,
501}
502
503#[derive(Clone)]
504struct DiskSearchScratchArgs<'a, ProviderFactory> {
505    graph_degree: usize,
506    dim: usize,
507    num_pq_chunks: usize,
508    num_pq_centers: usize,
509    vertex_factory: &'a ProviderFactory,
510    graph_header: &'a GraphHeader,
511}
512
513impl<Data, ProviderFactory> TryAsPooled<&DiskSearchScratchArgs<'_, ProviderFactory>>
514    for DiskSearchScratch<Data, ProviderFactory::VertexProviderType>
515where
516    Data: GraphDataType<VectorIdType = u32>,
517    ProviderFactory: VertexProviderFactory<Data>,
518{
519    type Error = ANNError;
520
521    fn try_create(args: &DiskSearchScratchArgs<ProviderFactory>) -> Result<Self, Self::Error> {
522        let pq_scratch = PQScratch::new(
523            args.graph_degree,
524            args.dim,
525            args.num_pq_chunks,
526            args.num_pq_centers,
527        )?;
528
529        const DEFAULT_BEAM_WIDTH: usize = 0; // Setting as 0 to avoid preallocation of memory.
530        let vertex_provider = args
531            .vertex_factory
532            .create_vertex_provider(DEFAULT_BEAM_WIDTH, args.graph_header)?;
533
534        Ok(Self {
535            distance_cache: HashMap::new(),
536            pq_scratch,
537            vertex_provider,
538        })
539    }
540
541    fn try_modify(
542        &mut self,
543        _args: &DiskSearchScratchArgs<ProviderFactory>,
544    ) -> Result<(), Self::Error> {
545        self.distance_cache.clear();
546        self.vertex_provider.clear();
547        Ok(())
548    }
549}
550
551pub struct DiskAccessor<'a, Data, VP>
552where
553    Data: GraphDataType<VectorIdType = u32>,
554    VP: VertexProvider<Data>,
555{
556    provider: &'a DiskProvider<Data>,
557    io_tracker: &'a IOTracker,
558    scratch: PoolOption<DiskSearchScratch<Data, VP>>,
559    query: &'a [Data::VectorDataType],
560}
561
562impl<Data, VP> DiskAccessor<'_, Data, VP>
563where
564    Data: GraphDataType<VectorIdType = u32>,
565    VP: VertexProvider<Data>,
566{
567    // Compute the PQ distance between each ID in `ids` and the distance table stored in
568    // `self`, invoking the callback with the results of each computation in order.
569    fn pq_distances<F>(&mut self, ids: &[u32], mut f: F) -> ANNResult<()>
570    where
571        F: FnMut(f32, u32),
572    {
573        let pq_scratch = &mut self.scratch.pq_scratch;
574        compute_pq_distance(
575            ids,
576            self.provider.pq_data.get_num_chunks(),
577            &pq_scratch.aligned_pqtable_dist_scratch,
578            self.provider.pq_data.pq_compressed_data().as_slice(),
579            &mut pq_scratch.aligned_pq_coord_scratch,
580            &mut pq_scratch.aligned_dist_scratch,
581        )?;
582
583        for (i, id) in ids.iter().enumerate() {
584            let distance = self.scratch.pq_scratch.aligned_dist_scratch[i];
585            f(distance, *id);
586        }
587
588        Ok(())
589    }
590}
591
592impl<Data, VP> SearchExt for DiskAccessor<'_, Data, VP>
593where
594    Data: GraphDataType<VectorIdType = u32>,
595    VP: VertexProvider<Data>,
596{
597    async fn starting_points(&self) -> ANNResult<Vec<u32>> {
598        let start_vertex_id = self.provider.graph_header.metadata().medoid as u32;
599        Ok(vec![start_vertex_id])
600    }
601
602    fn terminate_early(&mut self) -> bool {
603        self.io_tracker.io_count() > self.provider.search_io_limit
604    }
605}
606
607impl<'a, Data, VP> DiskAccessor<'a, Data, VP>
608where
609    Data: GraphDataType<VectorIdType = u32>,
610    VP: VertexProvider<Data>,
611{
612    fn new<VPF>(
613        provider: &'a DiskProvider<Data>,
614        io_tracker: &'a IOTracker,
615        query: &'a [Data::VectorDataType],
616        vertex_provider_factory: &'a VPF,
617        scratch_pool: &'a Arc<ObjectPool<DiskSearchScratch<Data, VP>>>,
618    ) -> ANNResult<Self>
619    where
620        VPF: VertexProviderFactory<Data, VertexProviderType = VP>,
621    {
622        let mut scratch = PoolOption::try_pooled(
623            scratch_pool,
624            &DiskSearchScratchArgs {
625                graph_degree: provider.graph_header.max_degree::<Data::VectorDataType>()?,
626                dim: provider.graph_header.metadata().dims,
627                num_pq_chunks: provider.pq_data.get_num_chunks(),
628                num_pq_centers: provider.pq_data.get_num_centers(),
629                vertex_factory: vertex_provider_factory,
630                graph_header: &provider.graph_header,
631            },
632        )?;
633
634        scratch
635            .pq_scratch
636            .set(provider.graph_header.metadata().dims, query)?;
637        let start_vertex_id = provider.graph_header.metadata().medoid as u32;
638
639        let timer = Instant::now();
640        quantizer_preprocess(
641            &mut scratch.pq_scratch,
642            &provider.pq_data,
643            provider.metric,
644            &[start_vertex_id],
645        )?;
646        IOTracker::add_time(
647            &io_tracker.preprocess_time_us,
648            timer.elapsed().as_micros() as u64,
649        );
650
651        Ok(Self {
652            provider,
653            io_tracker,
654            scratch,
655            query,
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            dim: graph_header.metadata().dims,
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, |(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, |(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}