Skip to main content

diskann_disk/search/
search_mode.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! Top-level disk search mode.
7//!
8//! Encodes the algorithm + filter combination for `DiskIndexSearcher::search`
9//! as a sum type so that invalid combinations (flat scan with adaptive L,
10//! inline filter without a predicate) are unrepresentable at the API boundary.
11
12use diskann::graph::ext::labeled::QueryLabelProvider;
13use diskann::graph::search::AdaptiveL;
14use diskann_providers::model::graph::provider::DeterminantDiversityParams;
15
16/// Owned closure used to filter vector IDs during disk search.
17///
18/// The `&u32` argument is the disk-side internal/external ID (they coincide
19/// on the disk path by construction).
20pub type SearchPredicate<'a> = Box<dyn Fn(&u32) -> bool + Send + Sync + 'a>;
21
22/// Top-level disk search mode.
23///
24/// Three variants encode the algorithm + filter combination:
25///
26/// * `FlatScan` — brute-force linear scan, with or without an inline filter.
27/// * `Graph` — plain greedy beam search; the optional filter is applied as a
28///   hard post-filter during reranking (no traversal-time effect).
29/// * `InlineFilter` — label-filtered graph search; the predicate is consulted
30///   at visit time (not just during rerank). `adaptive_l = Some(_)` grows the
31///   beam mid-search if the observed match specificity is low.
32/// * `DiverseGraph` — greedy graph search with determinant-diversity
33///   post-processing; selects a maximally diverse top-k from the candidate
34///   pool using `DeterminantDiversityParams`. Optional hard post-filter is
35///   applied during the diversity selection step.
36pub enum SearchMode<'a> {
37    FlatScan {
38        filter: Option<SearchPredicate<'a>>,
39    },
40
41    Graph {
42        filter: Option<SearchPredicate<'a>>,
43    },
44
45    InlineFilter {
46        filter: Box<dyn QueryLabelProvider<u32> + 'a>,
47        adaptive_l: Option<AdaptiveL>,
48    },
49
50    DiverseGraph {
51        filter: Option<SearchPredicate<'a>>,
52        params: DeterminantDiversityParams,
53    },
54}
55
56impl<'a> SearchMode<'a> {
57    /// Flat scan over all vectors. Recall baseline.
58    pub fn flat() -> Self {
59        Self::FlatScan { filter: None }
60    }
61
62    /// Flat scan restricted to vectors that satisfy `predicate`.
63    pub fn flat_filtered<F>(predicate: F) -> Self
64    where
65        F: Fn(&u32) -> bool + Send + Sync + 'a,
66    {
67        Self::FlatScan {
68            filter: Some(Box::new(predicate)),
69        }
70    }
71
72    /// Plain greedy graph search; no filter.
73    pub fn graph() -> Self {
74        Self::Graph { filter: None }
75    }
76
77    /// Plain greedy graph search with a hard post-filter applied during
78    /// reranking. Traversal is unaffected.
79    pub fn graph_filtered<F>(predicate: F) -> Self
80    where
81        F: Fn(&u32) -> bool + Send + Sync + 'a,
82    {
83        Self::Graph {
84            filter: Some(Box::new(predicate)),
85        }
86    }
87
88    /// Inline label-filtered graph search. `adaptive_l = Some(_)` enables
89    /// mid-search beam widening; `None` runs inline tracking only (no
90    /// resizing).
91    ///
92    /// The closure is wrapped in a generic adapter (`FnLabelProvider<F>`)
93    /// that implements `QueryLabelProvider<u32>`.
94    pub fn inline_filter<F>(predicate: F, adaptive_l: Option<AdaptiveL>) -> Self
95    where
96        F: Fn(&u32) -> bool + Send + Sync + 'a,
97    {
98        struct FnLabelProvider<F>(F);
99
100        impl<F> std::fmt::Debug for FnLabelProvider<F> {
101            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102                f.debug_struct("FnLabelProvider").finish_non_exhaustive()
103            }
104        }
105
106        impl<F> QueryLabelProvider<u32> for FnLabelProvider<F>
107        where
108            F: Fn(&u32) -> bool + Send + Sync,
109        {
110            fn is_match(&self, vec_id: u32) -> bool {
111                (self.0)(&vec_id)
112            }
113        }
114
115        Self::InlineFilter {
116            filter: Box::new(FnLabelProvider(predicate)),
117            adaptive_l,
118        }
119    }
120
121    /// Greedy graph search with determinant-diversity post-processing.
122    /// Selects a diverse top-k from the candidate pool found at L.
123    pub fn diverse_graph(params: DeterminantDiversityParams) -> Self {
124        Self::DiverseGraph {
125            filter: None,
126            params,
127        }
128    }
129
130    /// Greedy graph search with determinant-diversity post-processing and a
131    /// hard post-filter. The filter is honored during the diverse-selection
132    /// step (non-matching IDs are excluded from the final top-k).
133    pub fn diverse_graph_filtered<F>(predicate: F, params: DeterminantDiversityParams) -> Self
134    where
135        F: Fn(&u32) -> bool + Send + Sync + 'a,
136    {
137        Self::DiverseGraph {
138            filter: Some(Box::new(predicate)),
139            params,
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn flat_no_filter_constructor() {
150        let mode = SearchMode::flat();
151        assert!(matches!(mode, SearchMode::FlatScan { filter: None }));
152    }
153
154    #[test]
155    fn flat_filtered_constructor() {
156        let mode = SearchMode::flat_filtered(|id| *id == 5);
157        match &mode {
158            SearchMode::FlatScan { filter: Some(p) } => {
159                assert!(p(&5));
160                assert!(!p(&4));
161            }
162            _ => panic!("expected FlatScan with filter"),
163        }
164    }
165
166    #[test]
167    fn graph_no_filter_constructor() {
168        let mode = SearchMode::graph();
169        assert!(matches!(mode, SearchMode::Graph { filter: None }));
170    }
171
172    #[test]
173    fn graph_filtered_constructor() {
174        let mode = SearchMode::graph_filtered(|id| *id == 7);
175        match &mode {
176            SearchMode::Graph { filter: Some(p) } => {
177                assert!(p(&7));
178                assert!(!p(&6));
179            }
180            _ => panic!("expected Graph with filter"),
181        }
182    }
183
184    #[test]
185    fn inline_filter_constructor_without_adaptive_l() {
186        let mode = SearchMode::inline_filter(|id| *id == 3, None);
187        match &mode {
188            SearchMode::InlineFilter {
189                filter,
190                adaptive_l: None,
191            } => {
192                assert!(filter.is_match(3));
193                assert!(!filter.is_match(2));
194            }
195            _ => panic!("expected InlineFilter with adaptive_l = None"),
196        }
197    }
198
199    #[test]
200    fn inline_filter_constructor_with_adaptive_l() {
201        let adaptive = AdaptiveL::new(5, 16.0).expect("valid AdaptiveL");
202        let mode = SearchMode::inline_filter(|id| *id == 11, Some(adaptive));
203        match &mode {
204            SearchMode::InlineFilter {
205                adaptive_l: Some(_),
206                ..
207            } => {}
208            _ => panic!("expected InlineFilter with adaptive_l = Some"),
209        }
210    }
211}