Skip to main content

weavatrix_search_vector/hnsw/
index_search.rs

1use super::graph_helpers::from_node;
2use super::{FilterSearchPolicy, SearchPolicy, SearchScratch, VectorIndex};
3use crate::error::SearchError;
4use crate::hit::SearchHit;
5use crate::vector::Candidate;
6
7impl VectorIndex {
8    /// Returns approximate top-K hits ordered by exact distance and key.
9    ///
10    /// # Errors
11    ///
12    /// Returns a typed error for an invalid query or scratch allocation.
13    pub fn search(&self, query: &[f32], count: usize) -> Result<Vec<SearchHit>, SearchError> {
14        self.search_with_policy(query, count, SearchPolicy::new(self.config.expansion_query))
15    }
16
17    /// Returns approximate top-K hits with a per-query recall policy.
18    ///
19    /// This separates graph expansion from deterministic routing recovery so
20    /// callers can raise recall without rebuilding the index.
21    ///
22    /// # Errors
23    ///
24    /// Returns a typed error for an invalid policy, query, or scratch
25    /// allocation.
26    pub fn search_with_policy(
27        &self,
28        query: &[f32],
29        count: usize,
30        policy: SearchPolicy,
31    ) -> Result<Vec<SearchHit>, SearchError> {
32        let policy = policy.validate()?;
33        let mut scratch = SearchScratch::new(self.len())?;
34        self.search_with_scratch(query, count, policy, &mut scratch)
35    }
36
37    /// Returns exact top-K results over the same normalized vectors.
38    ///
39    /// This is intended as a correctness and recall oracle.
40    ///
41    /// # Errors
42    ///
43    /// Returns a typed error for an invalid query.
44    pub fn search_exact(&self, query: &[f32], count: usize) -> Result<Vec<SearchHit>, SearchError> {
45        self.vectors.exact(query, count)
46    }
47
48    /// Returns exact top-K results accepted by `filter`.
49    ///
50    /// # Errors
51    ///
52    /// Returns a typed error for an invalid query.
53    pub fn search_exact_filtered<F>(
54        &self,
55        query: &[f32],
56        count: usize,
57        filter: F,
58    ) -> Result<Vec<SearchHit>, SearchError>
59    where
60        F: Fn(u64) -> bool,
61    {
62        self.vectors.exact_filtered(query, count, filter)
63    }
64
65    /// Searches HNSW candidates accepted by `filter`, with an exact fallback
66    /// when selective filters do not yield enough approximate candidates.
67    ///
68    /// # Errors
69    ///
70    /// Returns a typed error for an invalid query or scratch allocation.
71    pub fn search_filtered<F>(
72        &self,
73        query: &[f32],
74        count: usize,
75        filter: F,
76    ) -> Result<Vec<SearchHit>, SearchError>
77    where
78        F: Fn(u64) -> bool,
79    {
80        self.search_filtered_with_policy(query, count, filter, FilterSearchPolicy::ExactFallback)
81    }
82
83    /// Searches with the predicate applied during graph traversal.
84    ///
85    /// `Traversal` avoids a full scan and may return fewer than `count` hits.
86    /// `ExactFallback` preserves completeness when the graph cannot find
87    /// enough accepted candidates.
88    ///
89    /// # Errors
90    ///
91    /// Returns a typed error for an invalid query or scratch allocation.
92    pub fn search_filtered_with_policy<F>(
93        &self,
94        query: &[f32],
95        count: usize,
96        filter: F,
97        policy: FilterSearchPolicy,
98    ) -> Result<Vec<SearchHit>, SearchError>
99    where
100        F: Fn(u64) -> bool,
101    {
102        let requested = count.min(self.len());
103        if requested == 0 {
104            self.vectors.query_squared_norm(query)?;
105            return Ok(Vec::new());
106        }
107        let mut scratch = SearchScratch::new(self.len())?;
108        let hits = self.search_with_scratch_where(
109            query,
110            requested,
111            SearchPolicy::new(self.config.expansion_query),
112            &mut scratch,
113            &filter,
114        )?;
115        if hits.len() == requested || policy == FilterSearchPolicy::Traversal {
116            return Ok(hits);
117        }
118        self.vectors.exact_filtered(query, requested, filter)
119    }
120
121    pub(super) fn search_with_scratch(
122        &self,
123        query: &[f32],
124        count: usize,
125        policy: SearchPolicy,
126        scratch: &mut SearchScratch,
127    ) -> Result<Vec<SearchHit>, SearchError> {
128        self.search_with_scratch_where(query, count, policy, scratch, &|_| true)
129    }
130
131    fn search_with_scratch_where<F>(
132        &self,
133        query: &[f32],
134        count: usize,
135        policy: SearchPolicy,
136        scratch: &mut SearchScratch,
137        accepts: &F,
138    ) -> Result<Vec<SearchHit>, SearchError>
139    where
140        F: Fn(u64) -> bool,
141    {
142        let query_norm = self.vectors.query_squared_norm(query)?;
143        let limit = count.min(self.len());
144        if limit == 0 {
145            return Ok(Vec::new());
146        }
147        let expansion = policy.expansion.max(limit);
148        let mut merged = std::mem::take(&mut scratch.merged);
149        merged.clear();
150        merged
151            .try_reserve(self.graphs.len().saturating_mul(expansion))
152            .map_err(|_| SearchError::AllocationFailed)?;
153        for graph in &self.graphs {
154            graph.search_into(
155                &self.vectors,
156                query,
157                query_norm,
158                expansion,
159                scratch,
160                &mut merged,
161                accepts,
162            );
163        }
164        self.append_routing_candidates(query, query_norm, policy, scratch, accepts, &mut merged)?;
165        merged.sort_unstable();
166        let hits = collect_unique_hits(&self.vectors, &merged, limit)?;
167        merged.clear();
168        scratch.merged = merged;
169        Ok(hits)
170    }
171
172    fn append_routing_candidates<F>(
173        &self,
174        query: &[f32],
175        query_norm: f32,
176        policy: SearchPolicy,
177        scratch: &mut SearchScratch,
178        accepts: &F,
179        merged: &mut Vec<Candidate>,
180    ) -> Result<(), SearchError>
181    where
182        F: Fn(u64) -> bool,
183    {
184        let mut routing_nodes = std::mem::take(&mut scratch.routing_nodes);
185        routing_nodes.clear();
186        self.vectors.routing_probes_into(
187            query,
188            policy.routing_probes,
189            &mut scratch.routing_probes,
190            &mut scratch.routing_probe_heap,
191        )?;
192        self.routing
193            .append_candidates(&scratch.routing_probes, &mut routing_nodes);
194        merged
195            .try_reserve(routing_nodes.len())
196            .map_err(|_| SearchError::AllocationFailed)?;
197        merged.extend(
198            routing_nodes
199                .iter()
200                .copied()
201                .map(from_node)
202                .filter(|index| accepts(self.vectors.key(*index)))
203                .map(|index| {
204                    Candidate::new(self.vectors.distance_query(index, query, query_norm), index)
205                }),
206        );
207        routing_nodes.clear();
208        scratch.routing_nodes = routing_nodes;
209        Ok(())
210    }
211}
212
213fn collect_unique_hits(
214    vectors: &crate::vector::VectorStore,
215    candidates: &[Candidate],
216    limit: usize,
217) -> Result<Vec<SearchHit>, SearchError> {
218    let mut hits = Vec::new();
219    hits.try_reserve_exact(limit)
220        .map_err(|_| SearchError::AllocationFailed)?;
221    for candidate in candidates.iter().copied() {
222        let key = vectors.key(candidate.index());
223        if hits.iter().any(|hit: &SearchHit| hit.key == key) {
224            continue;
225        }
226        hits.push(SearchHit {
227            key,
228            distance: candidate.distance,
229        });
230        if hits.len() == limit {
231            break;
232        }
233    }
234    Ok(hits)
235}