1use std::collections::HashSet;
2
3use crate::errors::{Result, VecDbError};
4use crate::hybrid::FusionStrategy;
5use crate::storage::Storage;
6use crate::types::{SearchResult, Vector};
7
8use super::plan::{LogicalNode, PhysicalPlan};
9
10pub struct PlanExecutor<'a> {
15 storage: &'a Storage,
16}
17
18impl<'a> PlanExecutor<'a> {
19 pub fn new(storage: &'a Storage) -> Self {
20 Self { storage }
21 }
22
23 pub fn execute(
33 &self,
34 plan: &PhysicalPlan,
35 query_vector: Option<&Vector>,
36 query_text: Option<&str>,
37 ) -> Result<Vec<SearchResult>> {
38 let allowed_ids: Option<HashSet<String>> = if plan.pre_filter {
40 plan.filter_predicate.as_deref().and_then(|pred| {
41 match serde_json::from_str::<serde_json::Value>(pred) {
42 Ok(fv) => match self
43 .storage
44 .metadata
45 .filter_ids(&fv, &self.storage.config.indexed_payload_fields)
46 {
47 Ok(ids) => Some(ids.into_iter().collect()),
48 Err(e) => {
49 tracing::warn!("pre-filter failed ({e}), falling back to post-filter");
50 None
51 }
52 },
53 Err(e) => {
54 tracing::warn!("pre-filter: could not parse predicate ({e}), skipping");
55 None
56 }
57 }
58 })
59 } else {
60 None
61 };
62
63 let mut results: Vec<SearchResult> = if plan.use_hybrid
65 || matches!(
66 find_scan_node(&plan.root),
67 Some(LogicalNode::HybridScan { .. })
68 ) {
69 self.storage.search_hybrid(
70 query_vector,
71 query_text,
72 plan.candidate_k,
73 extract_alpha(plan),
74 extract_strategy(plan),
75 )?
76 } else {
77 let scan = find_scan_node(&plan.root);
78 let is_sparse = matches!(scan, Some(LogicalNode::SparseScan { .. }));
79
80 if is_sparse {
81 let qt = query_text.ok_or_else(|| {
82 VecDbError::InvalidQuery("sparse search requires query_text".into())
83 })?;
84 self.storage.search_sparse(qt, plan.candidate_k)?
85 } else {
86 let qv = query_vector.ok_or_else(|| {
87 VecDbError::InvalidQuery("dense search requires query_vector".into())
88 })?;
89 match plan.filter_predicate.as_deref() {
94 Some(pred) if plan.pre_filter => {
95 match serde_json::from_str::<serde_json::Value>(pred) {
96 Ok(fv) => {
97 self.storage
98 .search_dense_filtered(qv, plan.candidate_k, &fv)?
99 }
100 Err(_) => self.storage.search_dense(qv, plan.candidate_k)?,
101 }
102 }
103 _ => self.storage.search_dense(qv, plan.candidate_k)?,
104 }
105 }
106 };
107
108 if let Some(ref ids) = allowed_ids {
110 results.retain(|r| ids.contains(&r.id));
111 }
112
113 if let Some(ref pred) = plan.filter_predicate {
115 match serde_json::from_str::<serde_json::Value>(pred) {
116 Ok(fv) => results.retain(|r| apply_json_filter(r, &fv)),
117 Err(e) => {
118 tracing::warn!(
119 "could not parse filter predicate as JSON ({e}), skipping filter"
120 );
121 }
122 }
123 }
124
125 results.sort_unstable_by(|a, b| {
127 b.score
128 .partial_cmp(&a.score)
129 .unwrap_or(std::cmp::Ordering::Equal)
130 });
131
132 results.truncate(plan.output_k);
134
135 results = apply_projection(results, &plan.output_columns);
137
138 Ok(results)
139 }
140}
141
142fn find_scan_node(node: &LogicalNode) -> Option<&LogicalNode> {
147 match node {
148 LogicalNode::VectorScan { .. }
149 | LogicalNode::SparseScan { .. }
150 | LogicalNode::HybridScan { .. } => Some(node),
151 LogicalNode::Filter { input, .. }
152 | LogicalNode::Sort { input, .. }
153 | LogicalNode::Limit { input, .. }
154 | LogicalNode::Project { input, .. } => find_scan_node(input),
155 LogicalNode::Join { left, .. } => find_scan_node(left),
156 }
157}
158
159fn extract_alpha(plan: &PhysicalPlan) -> f32 {
160 fn from_node(node: &LogicalNode) -> Option<f32> {
161 match node {
162 LogicalNode::HybridScan { alpha, .. } | LogicalNode::VectorScan { alpha, .. } => {
163 Some(*alpha)
164 }
165 LogicalNode::Filter { input, .. }
166 | LogicalNode::Sort { input, .. }
167 | LogicalNode::Limit { input, .. }
168 | LogicalNode::Project { input, .. } => from_node(input),
169 LogicalNode::Join { left, .. } => from_node(left),
170 LogicalNode::SparseScan { .. } => None,
171 }
172 }
173 from_node(&plan.root).unwrap_or(0.7)
174}
175
176fn extract_strategy(plan: &PhysicalPlan) -> FusionStrategy {
177 fn from_node(node: &LogicalNode) -> Option<FusionStrategy> {
178 match node {
179 LogicalNode::HybridScan { strategy, .. } | LogicalNode::VectorScan { strategy, .. } => {
180 Some(strategy.clone())
181 }
182 LogicalNode::Filter { input, .. }
183 | LogicalNode::Sort { input, .. }
184 | LogicalNode::Limit { input, .. }
185 | LogicalNode::Project { input, .. } => from_node(input),
186 LogicalNode::Join { left, .. } => from_node(left),
187 LogicalNode::SparseScan { .. } => None,
188 }
189 }
190 from_node(&plan.root).unwrap_or_default()
191}
192
193pub(crate) fn apply_json_filter(record: &SearchResult, filter: &serde_json::Value) -> bool {
204 apply_filter_with(filter, &|field| resolve_field(record, field))
205}
206
207pub(crate) fn apply_filter_with<R>(filter: &serde_json::Value, resolve: &R) -> bool
214where
215 R: Fn(&str) -> Option<serde_json::Value>,
216{
217 match filter {
218 serde_json::Value::Array(conditions) => {
219 conditions.iter().all(|c| apply_filter_with(c, resolve))
220 }
221 serde_json::Value::Object(obj) => {
222 if obj.contains_key("field") && obj.contains_key("op") && obj.contains_key("value") {
223 let (Some(field), Some(op), Some(filter_val)) = (
224 obj.get("field").and_then(|v| v.as_str()),
225 obj.get("op").and_then(|v| v.as_str()),
226 obj.get("value"),
227 ) else {
228 return false;
229 };
230 resolve(field)
231 .map(|rv| eval_op(&rv, op, filter_val))
232 .unwrap_or(false)
233 } else {
234 obj.iter()
236 .all(|(k, v)| resolve(k).map(|rv| eq_values(&rv, v)).unwrap_or(false))
237 }
238 }
239 _ => false,
240 }
241}
242
243pub(crate) fn collect_filter_fields(
246 filter: &serde_json::Value,
247 out: &mut std::collections::BTreeSet<String>,
248) -> bool {
249 match filter {
250 serde_json::Value::Array(cs) => cs.iter().all(|c| collect_filter_fields(c, out)),
251 serde_json::Value::Object(obj) => {
252 if obj.contains_key("field") && obj.contains_key("op") && obj.contains_key("value") {
253 match obj.get("field").and_then(|v| v.as_str()) {
254 Some(f) => {
255 out.insert(f.to_string());
256 true
257 }
258 None => false,
259 }
260 } else {
261 for k in obj.keys() {
262 out.insert(k.to_string());
263 }
264 true
265 }
266 }
267 _ => false,
268 }
269}
270
271fn resolve_field(record: &SearchResult, field: &str) -> Option<serde_json::Value> {
276 if field == "id" {
277 return Some(serde_json::Value::String(record.id.clone()));
278 }
279 let mut current = &record.payload;
280 for part in field.split('.') {
281 current = current.get(part)?;
282 }
283 Some(current.clone())
284}
285
286fn eval_op(rv: &serde_json::Value, op: &str, fv: &serde_json::Value) -> bool {
287 match op {
288 "=" => eq_values(rv, fv),
289 "!=" => !eq_values(rv, fv),
290 "<" | "<=" | ">" | ">=" => {
291 let Some(r) = to_f64(rv) else {
292 return false;
293 };
294 let Some(f) = to_f64(fv) else {
295 return false;
296 };
297 match op {
298 "<" => r < f,
299 "<=" => r <= f,
300 ">" => r > f,
301 _ => r >= f,
302 }
303 }
304 "LIKE" => {
305 let (Some(s), Some(p)) = (rv.as_str(), fv.as_str()) else {
306 return false;
307 };
308 like_match(s.as_bytes(), p.as_bytes())
309 }
310 _ => false,
311 }
312}
313
314fn eq_values(a: &serde_json::Value, b: &serde_json::Value) -> bool {
315 match (a, b) {
316 (serde_json::Value::String(s1), serde_json::Value::String(s2)) => s1 == s2,
317 (serde_json::Value::Bool(b1), serde_json::Value::Bool(b2)) => b1 == b2,
318 _ => match (to_f64(a), to_f64(b)) {
319 (Some(fa), Some(fb)) => fa == fb,
320 _ => a == b,
321 },
322 }
323}
324
325fn to_f64(v: &serde_json::Value) -> Option<f64> {
326 match v {
327 serde_json::Value::Number(n) => n.as_f64(),
328 serde_json::Value::String(s) => s.parse::<f64>().ok(),
329 _ => None,
330 }
331}
332
333fn like_match(text: &[u8], pattern: &[u8]) -> bool {
335 match pattern.first() {
336 None => text.is_empty(),
337 Some(b'%') => (0..=text.len()).any(|i| like_match(&text[i..], &pattern[1..])),
338 Some(&pc) => text
339 .first()
340 .map(|&tc| tc == pc && like_match(&text[1..], &pattern[1..]))
341 .unwrap_or(false),
342 }
343}
344
345fn apply_projection(results: Vec<SearchResult>, columns: &[String]) -> Vec<SearchResult> {
356 if columns.is_empty() || columns.iter().any(|c| c == "*") {
357 return results;
358 }
359 let keep_text = columns.iter().any(|c| c == "text");
360 results
361 .into_iter()
362 .map(|mut r| {
363 if let serde_json::Value::Object(ref orig) = r.payload.clone() {
364 let mut new_payload = serde_json::Map::new();
365 for col in columns {
366 match col.as_str() {
367 "id" | "score" | "similarity" | "text" => {}
368 key => {
369 if let Some(v) = orig.get(key) {
370 new_payload.insert(key.to_string(), v.clone());
371 }
372 }
373 }
374 }
375 r.payload = serde_json::Value::Object(new_payload);
376 }
377 if !keep_text {
378 r.text = None;
379 }
380 r
381 })
382 .collect()
383}