Skip to main content

hawdb_plan/
vector.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum VectorCandidateSource {
17    Scalar,
18    Ann,
19    Quantized,
20}
21
22impl VectorCandidateSource {
23    pub const fn as_str(self) -> &'static str {
24        match self {
25            Self::Scalar => "scalar",
26            Self::Ann => "ann",
27            Self::Quantized => "quantized",
28        }
29    }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum VectorBackendSelectionReason {
34    CompressionDisabled,
35    RecallValidationProbe,
36    SmallFilteredCandidateSet,
37    HighFilterSelectivity,
38    RawVectorsWithinMemoryBudget,
39    QuantizedPreferred,
40    QuantizedRequired,
41    QuantizedProjectionUnavailable,
42    QuantizedProjectionCoverageIncomplete,
43}
44
45impl VectorBackendSelectionReason {
46    pub const fn as_str(self) -> &'static str {
47        match self {
48            Self::CompressionDisabled => "compression_disabled",
49            Self::RecallValidationProbe => "recall_validation_probe",
50            Self::SmallFilteredCandidateSet => "small_filtered_candidate_set",
51            Self::HighFilterSelectivity => "high_filter_selectivity",
52            Self::RawVectorsWithinMemoryBudget => "raw_vectors_within_memory_budget",
53            Self::QuantizedPreferred => "quantized_preferred",
54            Self::QuantizedRequired => "quantized_required",
55            Self::QuantizedProjectionUnavailable => "quantized_projection_unavailable",
56            Self::QuantizedProjectionCoverageIncomplete => {
57                "quantized_projection_coverage_incomplete"
58            }
59        }
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct VectorSearchLogicalPlan {
65    pub embedding_dimension: usize,
66    pub filter_fields: Vec<String>,
67    pub residual_filter_fields: Vec<String>,
68    pub initial_candidate_limit: usize,
69    pub candidate_source: VectorCandidateSource,
70    pub candidate_limit: usize,
71    pub top_k: usize,
72}
73
74/// Planner-owned resource ceilings carried with a vector physical plan.
75///
76/// Runtime admission may grant fewer resources, but execution must never
77/// exceed these limits or silently replace them with process defaults.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct VectorExecutionResourceProfile {
80    pub priority: u8,
81    pub max_parallelism: usize,
82    pub max_working_memory_bytes: Option<u64>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub enum VectorPhysicalPlan {
87    Filter {
88        fields: Vec<String>,
89    },
90    VectorCandidateScan {
91        source: VectorCandidateSource,
92        embedding_dimension: usize,
93        candidate_limit: usize,
94        input: Box<VectorPhysicalPlan>,
95    },
96    ResidualFilter {
97        fields: Vec<String>,
98        initial_candidate_limit: usize,
99        input: Box<VectorPhysicalPlan>,
100    },
101    RawVectorRerank {
102        embedding_dimension: usize,
103        input: Box<VectorPhysicalPlan>,
104    },
105    TopK {
106        limit: usize,
107        input: Box<VectorPhysicalPlan>,
108    },
109}
110
111impl VectorPhysicalPlan {
112    pub fn operator_name(&self) -> &'static str {
113        match self {
114            Self::Filter { .. } => "Filter",
115            Self::VectorCandidateScan { .. } => "VectorCandidateScan",
116            Self::ResidualFilter { .. } => "ResidualFilter",
117            Self::RawVectorRerank { .. } => "RawVectorRerank",
118            Self::TopK { .. } => "TopK",
119        }
120    }
121
122    pub fn input(&self) -> Option<&Self> {
123        match self {
124            Self::Filter { .. } => None,
125            Self::VectorCandidateScan { input, .. }
126            | Self::ResidualFilter { input, .. }
127            | Self::RawVectorRerank { input, .. }
128            | Self::TopK { input, .. } => Some(input),
129        }
130    }
131
132    pub fn operator_pipeline(&self) -> Vec<&'static str> {
133        let mut operators = Vec::new();
134        let mut current = Some(self);
135        while let Some(plan) = current {
136            operators.push(plan.operator_name());
137            current = plan.input();
138        }
139        operators.reverse();
140        operators
141    }
142
143    pub fn explain_summary(&self) -> String {
144        match self {
145            Self::TopK { limit, input } => format!(
146                "pipeline={} top_k={limit} {}",
147                self.operator_pipeline().join("->"),
148                input.explain_details()
149            ),
150            _ => format!("pipeline={}", self.operator_pipeline().join("->")),
151        }
152    }
153
154    pub fn fingerprint(&self) -> String {
155        match self {
156            Self::Filter { fields } => format!("Filter({})", fields.join(",")),
157            Self::VectorCandidateScan {
158                source,
159                embedding_dimension,
160                candidate_limit,
161                input,
162            } => format!(
163                "VectorCandidateScan({}:{}:{}:{})",
164                source.as_str(),
165                embedding_dimension,
166                candidate_limit,
167                input.fingerprint()
168            ),
169            Self::ResidualFilter {
170                fields,
171                initial_candidate_limit,
172                input,
173            } => format!(
174                "ResidualFilter({}:{}:{})",
175                fields.join(","),
176                initial_candidate_limit,
177                input.fingerprint()
178            ),
179            Self::RawVectorRerank {
180                embedding_dimension,
181                input,
182            } => format!(
183                "RawVectorRerank({embedding_dimension}:{})",
184                input.fingerprint()
185            ),
186            Self::TopK { limit, input } => {
187                format!("TopK({limit}:{})", input.fingerprint())
188            }
189        }
190    }
191
192    fn explain_details(&self) -> String {
193        match self {
194            Self::RawVectorRerank {
195                embedding_dimension,
196                input,
197            } => format!(
198                "dimension={embedding_dimension} {}",
199                input.explain_details()
200            ),
201            Self::ResidualFilter {
202                fields,
203                initial_candidate_limit,
204                input,
205            } => format!(
206                "residual_fields={fields:?} initial_candidates={initial_candidate_limit} {}",
207                input.explain_details()
208            ),
209            Self::VectorCandidateScan {
210                source,
211                candidate_limit,
212                input,
213                ..
214            } => format!(
215                "source={} candidates={candidate_limit} {}",
216                source.as_str(),
217                input.explain_details()
218            ),
219            Self::Filter { fields } => format!("filter_fields={fields:?}"),
220            Self::TopK { input, .. } => input.explain_details(),
221        }
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::VectorBackendSelectionReason;
228
229    #[test]
230    fn backend_selection_reasons_have_stable_names() {
231        assert_eq!(
232            VectorBackendSelectionReason::CompressionDisabled.as_str(),
233            "compression_disabled"
234        );
235        assert_eq!(
236            VectorBackendSelectionReason::RecallValidationProbe.as_str(),
237            "recall_validation_probe"
238        );
239        assert_eq!(
240            VectorBackendSelectionReason::SmallFilteredCandidateSet.as_str(),
241            "small_filtered_candidate_set"
242        );
243        assert_eq!(
244            VectorBackendSelectionReason::HighFilterSelectivity.as_str(),
245            "high_filter_selectivity"
246        );
247        assert_eq!(
248            VectorBackendSelectionReason::RawVectorsWithinMemoryBudget.as_str(),
249            "raw_vectors_within_memory_budget"
250        );
251        assert_eq!(
252            VectorBackendSelectionReason::QuantizedPreferred.as_str(),
253            "quantized_preferred"
254        );
255        assert_eq!(
256            VectorBackendSelectionReason::QuantizedRequired.as_str(),
257            "quantized_required"
258        );
259        assert_eq!(
260            VectorBackendSelectionReason::QuantizedProjectionUnavailable.as_str(),
261            "quantized_projection_unavailable"
262        );
263        assert_eq!(
264            VectorBackendSelectionReason::QuantizedProjectionCoverageIncomplete.as_str(),
265            "quantized_projection_coverage_incomplete"
266        );
267    }
268}