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