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