1use hawdb_plan::{VectorBackendSelectionReason, VectorCandidateSource, VectorPhysicalPlan};
16use std::cmp::Ordering;
17use std::collections::BTreeSet;
18use std::fmt::{Display, Formatter};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum VectorScoreSource {
22 Unavailable,
23 RawVector,
24 AnnApproximate,
25 QuantizedApproximate,
26}
27
28impl VectorScoreSource {
29 pub const fn as_str(self) -> &'static str {
30 match self {
31 Self::Unavailable => "unavailable",
32 Self::RawVector => "raw_vector",
33 Self::AnnApproximate => "ann_approximate",
34 Self::QuantizedApproximate => "quantized_approximate",
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum VectorExecutionBackend {
41 ScalarFlat,
42 AnnProjection,
43 QuantizedProjection,
44 Unavailable,
45}
46
47impl VectorExecutionBackend {
48 pub const fn as_str(self) -> &'static str {
49 match self {
50 Self::ScalarFlat => "scalar_flat",
51 Self::AnnProjection => "ann_projection",
52 Self::QuantizedProjection => "quantized_projection",
53 Self::Unavailable => "unavailable",
54 }
55 }
56
57 const fn from_candidate_source(source: VectorCandidateSource) -> Self {
58 match source {
59 VectorCandidateSource::Scalar => Self::ScalarFlat,
60 VectorCandidateSource::Ann => Self::AnnProjection,
61 VectorCandidateSource::Quantized => Self::QuantizedProjection,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum VectorCompressionMode {
68 Unspecified,
69 Disabled,
70 Preferred,
71 Required,
72}
73
74impl VectorCompressionMode {
75 pub const fn as_str(self) -> &'static str {
76 match self {
77 Self::Unspecified => "unspecified",
78 Self::Disabled => "disabled",
79 Self::Preferred => "preferred",
80 Self::Required => "required",
81 }
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum VectorFallbackReasonCode {
87 VectorDimensionMismatch,
88 VectorIndexEmpty,
89 CompressedVectorProjectionUnavailable,
90 QueryEmbeddingMissing,
91}
92
93impl VectorFallbackReasonCode {
94 pub const fn as_str(self) -> &'static str {
95 match self {
96 Self::VectorDimensionMismatch => "vector_dimension_mismatch",
97 Self::VectorIndexEmpty => "vector_index_empty",
98 Self::CompressedVectorProjectionUnavailable => {
99 "compressed_vector_projection_unavailable"
100 }
101 Self::QueryEmbeddingMissing => "query_embedding_missing",
102 }
103 }
104}
105
106#[derive(Debug, Clone, PartialEq)]
107pub struct VectorCandidate {
108 pub id: String,
109 pub score: f64,
110}
111
112#[derive(Debug, Clone, PartialEq)]
113pub struct VectorCandidateBatch {
114 pub score_source: VectorScoreSource,
115 pub candidates: Vec<VectorCandidate>,
116}
117
118#[derive(Debug, Clone, PartialEq)]
119pub struct VectorRawScore {
120 pub id: String,
121 pub score: f64,
122}
123
124#[derive(Debug, Clone, Copy)]
125pub struct VectorCandidateScanRequest<'a> {
126 pub source: VectorCandidateSource,
127 pub embedding_dimension: usize,
128 pub candidate_limit: usize,
129 pub filter_fields: &'a [String],
130}
131
132#[derive(Debug, Clone, Copy)]
133pub struct VectorRawRerankRequest<'a> {
134 pub embedding_dimension: usize,
135 pub candidates: &'a [VectorCandidate],
136}
137
138#[derive(Debug)]
139pub struct VectorResidualFilterRequest<'a> {
140 pub fields: &'a [String],
141 pub candidates: Vec<VectorCandidate>,
142}
143
144pub trait VectorExecutionSource {
145 type Error;
146
147 fn scan_candidates(
148 &mut self,
149 request: VectorCandidateScanRequest<'_>,
150 ) -> Result<VectorCandidateBatch, Self::Error>;
151
152 fn filter_residual(
153 &mut self,
154 request: VectorResidualFilterRequest<'_>,
155 ) -> Result<Vec<VectorCandidate>, Self::Error>;
156
157 fn rerank_raw(
158 &mut self,
159 request: VectorRawRerankRequest<'_>,
160 ) -> Result<Vec<VectorRawScore>, Self::Error>;
161
162 fn raw_vector_bytes_read(&self) -> u64 {
163 0
164 }
165
166 fn candidate_scan_metrics(&self) -> Option<VectorCandidateScanMetrics> {
167 None
168 }
169
170 fn capture_candidate_ids(&self) -> bool {
171 false
172 }
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub struct VectorCandidateScanMetrics {
177 pub kernel: String,
178 pub worker_count: usize,
179 pub segment_count: usize,
180 pub scanned_segment_count: usize,
181 pub scored_document_count: usize,
182 pub filtered_document_count: usize,
183 pub scanned_block_count: usize,
184 pub skipped_block_count: usize,
185 pub payload_bytes_read: u64,
186 pub admitted_working_bytes: usize,
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub struct VectorExecutionReport {
191 pub backend: VectorExecutionBackend,
192 pub compression_mode: VectorCompressionMode,
193 pub candidate_source: VectorCandidateSource,
194 pub backend_selection_reason: Option<VectorBackendSelectionReason>,
195 pub estimated_raw_vector_bytes: Option<u64>,
196 pub filter_selectivity_per_million: Option<u32>,
197 pub candidate_score_source: VectorScoreSource,
198 pub final_score_source: VectorScoreSource,
199 pub generated_candidate_count: usize,
200 pub descriptor_pruned_count: usize,
201 pub scalar_filtered_count: usize,
202 pub residual_filtered_count: usize,
203 pub candidate_scan_rounds: usize,
204 pub reranked_candidate_count: usize,
205 pub returned_count: usize,
206 pub raw_vector_bytes_read: u64,
207 pub candidate_scan_metrics: Option<VectorCandidateScanMetrics>,
208 pub index_covered_document_count: Option<usize>,
209 pub index_candidate_document_count: Option<usize>,
210 pub index_coverage_complete: Option<bool>,
211 pub fallback_reason_codes: Vec<VectorFallbackReasonCode>,
212}
213
214#[derive(Debug, Clone, PartialEq)]
215pub struct VectorExecutionOutput {
216 pub scores: Vec<VectorRawScore>,
217 pub candidate_ids: Vec<String>,
218 pub report: VectorExecutionReport,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub enum VectorExecutionError<E> {
223 InvalidPlan(&'static str),
224 Source(E),
225 DuplicateCandidate,
226 DuplicateRawScore,
227 NonFiniteCandidateScore,
228 NonFiniteRawScore,
229 ResidualFilterAddedCandidate,
230 RawScoreOutsideCandidateSet,
231}
232
233impl<E: Display> Display for VectorExecutionError<E> {
234 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
235 match self {
236 Self::InvalidPlan(reason) => write!(formatter, "invalid vector plan: {reason}"),
237 Self::Source(error) => write!(formatter, "vector execution source failed: {error}"),
238 Self::DuplicateCandidate => formatter.write_str("vector candidate ids must be unique"),
239 Self::DuplicateRawScore => formatter.write_str("raw vector score ids must be unique"),
240 Self::NonFiniteCandidateScore => {
241 formatter.write_str("vector candidate score must be finite")
242 }
243 Self::NonFiniteRawScore => formatter.write_str("raw vector score must be finite"),
244 Self::ResidualFilterAddedCandidate => {
245 formatter.write_str("residual vector filter may only retain generated candidates")
246 }
247 Self::RawScoreOutsideCandidateSet => {
248 formatter.write_str("raw vector score must reference a generated candidate")
249 }
250 }
251 }
252}
253
254impl<E: std::error::Error + 'static> std::error::Error for VectorExecutionError<E> {}
255
256pub fn execute_vector_plan<S: VectorExecutionSource>(
257 plan: &VectorPhysicalPlan,
258 source: &mut S,
259) -> Result<VectorExecutionOutput, VectorExecutionError<S::Error>> {
260 let VectorPhysicalPlan::TopK {
261 limit: top_k,
262 input,
263 } = plan
264 else {
265 return Err(VectorExecutionError::InvalidPlan("missing TopK root"));
266 };
267 let VectorPhysicalPlan::RawVectorRerank {
268 embedding_dimension: rerank_dimension,
269 input,
270 } = input.as_ref()
271 else {
272 return Err(VectorExecutionError::InvalidPlan(
273 "TopK must consume RawVectorRerank",
274 ));
275 };
276 let (residual_filter, input) = match input.as_ref() {
277 VectorPhysicalPlan::ResidualFilter {
278 fields,
279 initial_candidate_limit,
280 input,
281 } => (
282 Some((fields.as_slice(), *initial_candidate_limit)),
283 input.as_ref(),
284 ),
285 input => (None, input),
286 };
287 let VectorPhysicalPlan::VectorCandidateScan {
288 source: candidate_source,
289 embedding_dimension: candidate_dimension,
290 candidate_limit,
291 input,
292 } = input
293 else {
294 return Err(VectorExecutionError::InvalidPlan(
295 "RawVectorRerank must consume VectorCandidateScan",
296 ));
297 };
298 let VectorPhysicalPlan::Filter { fields } = input.as_ref() else {
299 return Err(VectorExecutionError::InvalidPlan(
300 "VectorCandidateScan must consume Filter",
301 ));
302 };
303 if candidate_dimension != rerank_dimension {
304 return Err(VectorExecutionError::InvalidPlan(
305 "candidate and rerank dimensions differ",
306 ));
307 }
308 if *top_k == 0 || *candidate_limit < *top_k {
309 return Err(VectorExecutionError::InvalidPlan(
310 "candidate and top-k limits are inconsistent",
311 ));
312 }
313 if let Some((_, initial_limit)) = residual_filter
314 && (initial_limit < *top_k || initial_limit > *candidate_limit)
315 {
316 return Err(VectorExecutionError::InvalidPlan(
317 "residual candidate window is outside the top-k candidate budget",
318 ));
319 }
320
321 let mut scan_limit = residual_filter
322 .map(|(_, initial_limit)| initial_limit)
323 .unwrap_or(*candidate_limit);
324 let mut candidate_scan_rounds = 0;
325 let (batch, generated_candidate_count) = loop {
326 candidate_scan_rounds += 1;
327 let mut batch = source
328 .scan_candidates(VectorCandidateScanRequest {
329 source: *candidate_source,
330 embedding_dimension: *candidate_dimension,
331 candidate_limit: scan_limit,
332 filter_fields: fields,
333 })
334 .map_err(VectorExecutionError::Source)?;
335 validate_candidates(&batch.candidates)?;
336 sort_candidates(&mut batch.candidates);
337 batch.candidates.truncate(scan_limit);
338 let generated_candidate_count = batch.candidates.len();
339 if let Some((residual_fields, _)) = residual_filter {
340 let generated_ids = batch
341 .candidates
342 .iter()
343 .map(|candidate| candidate.id.clone())
344 .collect::<BTreeSet<_>>();
345 batch.candidates = source
346 .filter_residual(VectorResidualFilterRequest {
347 fields: residual_fields,
348 candidates: batch.candidates,
349 })
350 .map_err(VectorExecutionError::Source)?;
351 validate_candidates(&batch.candidates)?;
352 if batch
353 .candidates
354 .iter()
355 .any(|candidate| !generated_ids.contains(&candidate.id))
356 {
357 return Err(VectorExecutionError::ResidualFilterAddedCandidate);
358 }
359 }
360 if residual_filter.is_none()
361 || batch.candidates.len() >= *top_k
362 || scan_limit >= *candidate_limit
363 || generated_candidate_count < scan_limit
364 {
365 break (batch, generated_candidate_count);
366 }
367 scan_limit = scan_limit.saturating_mul(2).min(*candidate_limit);
368 };
369 let residual_filtered_count = generated_candidate_count.saturating_sub(batch.candidates.len());
370 let candidate_ids = if source.capture_candidate_ids() {
371 batch
372 .candidates
373 .iter()
374 .map(|candidate| candidate.id.clone())
375 .collect()
376 } else {
377 Vec::new()
378 };
379
380 let mut raw_scores = source
381 .rerank_raw(VectorRawRerankRequest {
382 embedding_dimension: *rerank_dimension,
383 candidates: &batch.candidates,
384 })
385 .map_err(VectorExecutionError::Source)?;
386 validate_raw_scores(&batch.candidates, &raw_scores)?;
387 sort_raw_scores(&mut raw_scores);
388 let reranked_candidate_count = raw_scores.len();
389 raw_scores.truncate(*top_k);
390
391 let final_score_source = if batch.score_source == VectorScoreSource::Unavailable {
392 VectorScoreSource::Unavailable
393 } else {
394 VectorScoreSource::RawVector
395 };
396 Ok(VectorExecutionOutput {
397 candidate_ids,
398 report: VectorExecutionReport {
399 backend: VectorExecutionBackend::from_candidate_source(*candidate_source),
400 compression_mode: VectorCompressionMode::Unspecified,
401 candidate_source: *candidate_source,
402 backend_selection_reason: None,
403 estimated_raw_vector_bytes: None,
404 filter_selectivity_per_million: None,
405 candidate_score_source: batch.score_source,
406 final_score_source,
407 generated_candidate_count,
408 descriptor_pruned_count: 0,
409 scalar_filtered_count: residual_filtered_count,
410 residual_filtered_count,
411 candidate_scan_rounds,
412 reranked_candidate_count,
413 returned_count: raw_scores.len(),
414 raw_vector_bytes_read: source.raw_vector_bytes_read(),
415 candidate_scan_metrics: source.candidate_scan_metrics(),
416 index_covered_document_count: None,
417 index_candidate_document_count: None,
418 index_coverage_complete: None,
419 fallback_reason_codes: Vec::new(),
420 },
421 scores: raw_scores,
422 })
423}
424
425fn validate_candidates<E>(candidates: &[VectorCandidate]) -> Result<(), VectorExecutionError<E>> {
426 let mut ids = BTreeSet::new();
427 for candidate in candidates {
428 if !candidate.score.is_finite() {
429 return Err(VectorExecutionError::NonFiniteCandidateScore);
430 }
431 if !ids.insert(candidate.id.as_str()) {
432 return Err(VectorExecutionError::DuplicateCandidate);
433 }
434 }
435 Ok(())
436}
437
438fn validate_raw_scores<E>(
439 candidates: &[VectorCandidate],
440 raw_scores: &[VectorRawScore],
441) -> Result<(), VectorExecutionError<E>> {
442 let candidate_ids = candidates
443 .iter()
444 .map(|candidate| candidate.id.as_str())
445 .collect::<BTreeSet<_>>();
446 let mut score_ids = BTreeSet::new();
447 for score in raw_scores {
448 if !score.score.is_finite() {
449 return Err(VectorExecutionError::NonFiniteRawScore);
450 }
451 if !candidate_ids.contains(score.id.as_str()) {
452 return Err(VectorExecutionError::RawScoreOutsideCandidateSet);
453 }
454 if !score_ids.insert(score.id.as_str()) {
455 return Err(VectorExecutionError::DuplicateRawScore);
456 }
457 }
458 Ok(())
459}
460
461fn sort_candidates(candidates: &mut [VectorCandidate]) {
462 candidates.sort_by(|left, right| {
463 descending_score_order(left.score, &left.id, right.score, &right.id)
464 });
465}
466
467fn sort_raw_scores(scores: &mut [VectorRawScore]) {
468 scores.sort_by(|left, right| {
469 descending_score_order(left.score, &left.id, right.score, &right.id)
470 });
471}
472
473fn descending_score_order(
474 left_score: f64,
475 left_id: &str,
476 right_score: f64,
477 right_id: &str,
478) -> Ordering {
479 right_score
480 .partial_cmp(&left_score)
481 .unwrap_or(Ordering::Equal)
482 .then_with(|| left_id.cmp(right_id))
483}
484
485#[cfg(test)]
486mod tests;