weavatrix_search_vector/hnsw/policy.rs
1use crate::error::SearchError;
2use crate::vector::{MAX_ROUTING_PROBES as VECTOR_MAX_ROUTING_PROBES, ROUTING_PROBES};
3
4/// Completeness policy for filtered approximate search.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
6pub enum FilterSearchPolicy {
7 /// Apply the predicate while traversing HNSW and return the candidates
8 /// reached without a full vector scan.
9 Traversal,
10 /// Use traversal filtering first, then use the exact oracle only when the
11 /// graph cannot supply the requested number of accepted results.
12 #[default]
13 ExactFallback,
14}
15
16/// Per-query approximation policy.
17///
18/// Increasing `expansion` explores more graph nodes. Increasing
19/// `routing_probes` recovers more candidates from nearby deterministic routing
20/// buckets, which is useful when graph expansion alone no longer improves
21/// recall.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct SearchPolicy {
24 /// Minimum number of graph candidates retained per replica.
25 pub expansion: usize,
26 /// Number of deterministic routing buckets probed after graph traversal.
27 ///
28 /// Values from `1` through [`Self::MAX_ROUTING_PROBES`] are supported.
29 pub routing_probes: usize,
30}
31
32impl SearchPolicy {
33 /// Largest supported routing recovery width.
34 pub const MAX_ROUTING_PROBES: usize = VECTOR_MAX_ROUTING_PROBES;
35
36 /// Creates the low-overhead default policy used by
37 /// [`crate::VectorIndex::search`].
38 #[must_use]
39 pub const fn new(expansion: usize) -> Self {
40 Self {
41 expansion,
42 routing_probes: ROUTING_PROBES,
43 }
44 }
45
46 /// Creates a stronger deterministic recovery policy for recall-sensitive
47 /// queries.
48 #[must_use]
49 pub const fn high_recall(expansion: usize) -> Self {
50 Self {
51 expansion,
52 routing_probes: 12,
53 }
54 }
55
56 /// Changes only the routing recovery width.
57 #[must_use]
58 pub const fn with_routing_probes(mut self, routing_probes: usize) -> Self {
59 self.routing_probes = routing_probes;
60 self
61 }
62
63 pub(crate) fn validate(self) -> Result<Self, SearchError> {
64 if self.expansion == 0 {
65 return Err(SearchError::InvalidConfig(
66 "search policy expansion must be greater than zero",
67 ));
68 }
69 if self.routing_probes == 0 || self.routing_probes > Self::MAX_ROUTING_PROBES {
70 return Err(SearchError::InvalidConfig(
71 "search policy routing_probes must be between 1 and 470",
72 ));
73 }
74 Ok(self)
75 }
76}