1#[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#[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}