Skip to main content

reddb_server/storage/query/executors/
hybrid.rs

1//! Hybrid Query Executor
2//!
3//! Executes HYBRID queries that combine structured (SQL/Graph) queries with
4//! vector similarity search, using various fusion strategies to merge results.
5//!
6//! # Fusion Strategies
7//!
8//! - **Rerank**: Re-ranks structured results by vector similarity
9//! - **FilterThenSearch**: Filters first, then searches vectors
10//! - **SearchThenFilter**: Searches vectors first, then applies structured filter
11//! - **RRF (Reciprocal Rank Fusion)**: Combines rankings fairly
12//! - **Intersection**: Only returns results matching both queries
13//! - **Union**: Returns results from either query with combined scores
14
15use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17use std::thread;
18
19use crate::storage::engine::graph_store::GraphStore;
20use crate::storage::engine::graph_table_index::GraphTableIndex;
21use crate::storage::engine::unified_index::UnifiedIndex;
22use crate::storage::engine::vector_store::VectorStore;
23use crate::storage::query::unified::{
24    ExecutionError, QueryStats, UnifiedExecutor, UnifiedRecord, UnifiedResult,
25};
26use reddb_rql::ast::{FusionStrategy, HybridQuery, VectorQuery};
27use reddb_types::Value;
28
29use super::vector::VectorExecutor;
30
31/// Hybrid query executor that combines structured and vector results
32pub struct HybridExecutor {
33    /// Structured query executor
34    unified: UnifiedExecutor,
35    /// Vector search executor
36    vector: VectorExecutor,
37}
38
39impl HybridExecutor {
40    /// Create a new hybrid executor
41    pub fn new(
42        graph: Arc<GraphStore>,
43        index: Arc<GraphTableIndex>,
44        vector_store: Arc<VectorStore>,
45    ) -> Self {
46        let unified = UnifiedExecutor::new(Arc::clone(&graph), Arc::clone(&index));
47        let vector = VectorExecutor::new(vector_store);
48
49        Self { unified, vector }
50    }
51
52    /// Add cross-reference support
53    pub fn with_unified_index(mut self, index: Arc<UnifiedIndex>) -> Self {
54        self.vector = self.vector.with_unified_index(index);
55        self
56    }
57
58    /// Execute a hybrid query
59    pub fn execute(&self, query: &HybridQuery) -> Result<UnifiedResult, ExecutionError> {
60        let start = std::time::Instant::now();
61
62        // Execute based on fusion strategy
63        let mut result = match &query.fusion {
64            FusionStrategy::Rerank { weight } => self.execute_rerank(query, *weight)?,
65            FusionStrategy::FilterThenSearch => self.execute_filter_then_search(query)?,
66            FusionStrategy::SearchThenFilter => self.execute_search_then_filter(query)?,
67            FusionStrategy::RRF { k } => self.execute_rrf(query, *k)?,
68            FusionStrategy::Intersection => self.execute_intersection(query)?,
69            FusionStrategy::Union {
70                structured_weight,
71                vector_weight,
72            } => self.execute_union(query, *structured_weight, *vector_weight)?,
73        };
74
75        // Apply limit if specified
76        if let Some(limit) = query.limit {
77            result.records.truncate(limit);
78        }
79
80        // Update stats
81        result.stats.exec_time_us = start.elapsed().as_micros() as u64;
82
83        Ok(result)
84    }
85
86    // =========================================================================
87    // Fusion Strategies
88    // =========================================================================
89
90    /// Rerank: Execute structured query, then re-rank by vector similarity
91    fn execute_rerank(
92        &self,
93        query: &HybridQuery,
94        weight: f32,
95    ) -> Result<UnifiedResult, ExecutionError> {
96        // 1. Execute structured query
97        let structured_result = self.unified.execute(&query.structured)?;
98
99        if structured_result.is_empty() {
100            return Ok(structured_result);
101        }
102
103        // 2. Execute vector query
104        let vector_result = self.vector.execute(&query.vector)?;
105
106        // 3. Build vector distance lookup
107        let mut vector_distances: HashMap<String, f32> = HashMap::new();
108        for record in &vector_result.records {
109            for vsr in &record.vector_results {
110                // Use vector ID as key
111                let key = format!("{}:{}", vsr.collection, vsr.id);
112                vector_distances.insert(key, vsr.distance);
113            }
114        }
115
116        // 4. Score and rerank structured results
117        let mut scored: Vec<(String, UnifiedRecord, f32)> = structured_result
118            .records
119            .into_iter()
120            .enumerate()
121            .map(|(rank, record)| {
122                // Structured score: inverse rank (higher = better)
123                let struct_score = 1.0 / (rank as f32 + 1.0);
124
125                // Vector score: try to find matching vector via cross-reference
126                let vector_score = self.get_vector_score_for_record(&record, &vector_distances);
127
128                // Combined score
129                let combined = (1.0 - weight) * struct_score + weight * vector_score;
130                (self.record_to_key(&record), record, combined)
131            })
132            .collect();
133
134        // Sort by combined score (descending), then deterministic key
135        scored.sort_by(
136            |a, b| match b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal) {
137                std::cmp::Ordering::Equal => a.0.cmp(&b.0),
138                ordering => ordering,
139            },
140        );
141
142        // Build result
143        let mut result = UnifiedResult::with_columns(structured_result.columns);
144        result.stats = structured_result.stats;
145
146        for (_key, mut record, score) in scored {
147            record.set_arc(Arc::from("_hybrid_score"), Value::Float(score as f64));
148            result.push(record);
149        }
150
151        Ok(result)
152    }
153
154    /// FilterThenSearch: Use structured results to filter vector search space
155    fn execute_filter_then_search(
156        &self,
157        query: &HybridQuery,
158    ) -> Result<UnifiedResult, ExecutionError> {
159        // 1. Execute structured query to get filter candidates
160        let structured_result = self.unified.execute(&query.structured)?;
161
162        if structured_result.is_empty() {
163            return Ok(structured_result);
164        }
165
166        // 2. Extract IDs from structured results for filtering
167        let candidate_ids: HashSet<u64> = structured_result
168            .records
169            .iter()
170            .filter_map(|r| {
171                // Try to get ID from values
172                r.get("id").and_then(|v| match v {
173                    Value::Integer(i) => Some(*i as u64),
174                    _ => None,
175                })
176            })
177            .collect();
178
179        // 3. Execute vector query
180        let vector_result = self.vector.execute(&query.vector)?;
181
182        // 4. Filter vector results to only include structured candidates
183        let mut result = UnifiedResult::with_columns(vector_result.columns.clone());
184
185        for record in vector_result.records {
186            // Check if this vector result matches any structured candidate
187            let matches = record.vector_results.iter().any(|vsr| {
188                candidate_ids.contains(&vsr.id) ||
189                // Also check linked row if available
190                vsr.linked_row.as_ref().map(|(_, row_id)| candidate_ids.contains(row_id)).unwrap_or(false)
191            });
192
193            if matches {
194                result.push(record);
195            }
196        }
197
198        result.stats = QueryStats::merge(&structured_result.stats, &vector_result.stats);
199        Ok(result)
200    }
201
202    /// SearchThenFilter: Vector search first, then apply structured filter
203    fn execute_search_then_filter(
204        &self,
205        query: &HybridQuery,
206    ) -> Result<UnifiedResult, ExecutionError> {
207        // 1. Execute vector query first
208        let vector_result = self.vector.execute(&query.vector)?;
209
210        if vector_result.is_empty() {
211            return Ok(vector_result);
212        }
213
214        // 2. Execute structured query
215        let structured_result = self.unified.execute(&query.structured)?;
216
217        // 3. Extract IDs from structured results
218        let structured_ids: HashSet<u64> = structured_result
219            .records
220            .iter()
221            .filter_map(|r| {
222                r.get("id").and_then(|v| match v {
223                    Value::Integer(i) => Some(*i as u64),
224                    _ => None,
225                })
226            })
227            .collect();
228
229        // 4. Filter vector results to match structured query
230        let mut result = UnifiedResult::with_columns(vector_result.columns.clone());
231
232        for record in vector_result.records {
233            let matches = record.vector_results.iter().any(|vsr| {
234                structured_ids.contains(&vsr.id)
235                    || vsr
236                        .linked_row
237                        .as_ref()
238                        .map(|(_, row_id)| structured_ids.contains(row_id))
239                        .unwrap_or(false)
240            });
241
242            if matches {
243                result.push(record);
244            }
245        }
246
247        result.stats = QueryStats::merge(&vector_result.stats, &structured_result.stats);
248        Ok(result)
249    }
250
251    /// RRF: Reciprocal Rank Fusion
252    /// Combines rankings using: RRF(d) = Σ(1 / (k + rank(d)))
253    /// Execute structured and vector arms concurrently via
254    /// [`std::thread::scope`].
255    ///
256    /// Used by fusion strategies that always run both arms to completion
257    /// (RRF, Intersection, Union). Short-circuiting strategies (Rerank,
258    /// FilterThenSearch, SearchThenFilter) keep serial execution because
259    /// they check for early-exit conditions on the first arm before
260    /// deciding whether to run the second.
261    ///
262    /// Worst-case total latency collapses from `structured + vector` to
263    /// `max(structured, vector)` — the planner's pessimistic estimate for
264    /// hybrid queries is now tight when both arms dominate.
265    fn execute_structured_and_vector_parallel(
266        &self,
267        query: &HybridQuery,
268    ) -> Result<(UnifiedResult, UnifiedResult), ExecutionError> {
269        thread::scope(|s| {
270            let structured_handle = s.spawn(|| self.unified.execute(&query.structured));
271            let vector_handle = s.spawn(|| self.vector.execute(&query.vector));
272
273            // `join` returns `Result<T, Box<dyn Any + Send>>`; a panic in
274            // either arm is surfaced as an `ExecutionError` so callers
275            // don't see a raw thread panic.
276            let structured = structured_handle
277                .join()
278                .map_err(|_| ExecutionError::new("hybrid: structured arm panicked"))??;
279            let vector = vector_handle
280                .join()
281                .map_err(|_| ExecutionError::new("hybrid: vector arm panicked"))??;
282            Ok((structured, vector))
283        })
284    }
285
286    fn execute_rrf(&self, query: &HybridQuery, k: u32) -> Result<UnifiedResult, ExecutionError> {
287        // 1. Execute both queries in parallel — RRF always consumes both.
288        let (structured_result, vector_result) =
289            self.execute_structured_and_vector_parallel(query)?;
290
291        // 2. Build rank maps (lower rank = better, starting from 1)
292        let mut structured_ranks: HashMap<String, u32> = HashMap::new();
293        for (rank, record) in structured_result.records.iter().enumerate() {
294            let key = self.record_to_key(record);
295            structured_ranks.insert(key, (rank + 1) as u32);
296        }
297
298        let mut vector_ranks: HashMap<String, u32> = HashMap::new();
299        for (rank, record) in vector_result.records.iter().enumerate() {
300            let key = self.record_to_key(record);
301            vector_ranks.insert(key, (rank + 1) as u32);
302        }
303
304        // 3. Calculate RRF scores for all unique records
305        let all_keys: HashSet<_> = structured_ranks
306            .keys()
307            .chain(vector_ranks.keys())
308            .cloned()
309            .collect();
310
311        let k_f64 = k as f64;
312        let mut rrf_scores: Vec<(String, f64)> = all_keys
313            .into_iter()
314            .map(|key| {
315                let struct_contrib = structured_ranks
316                    .get(&key)
317                    .map(|r| 1.0 / (k_f64 + *r as f64))
318                    .unwrap_or(0.0);
319                let vector_contrib = vector_ranks
320                    .get(&key)
321                    .map(|r| 1.0 / (k_f64 + *r as f64))
322                    .unwrap_or(0.0);
323                (key, struct_contrib + vector_contrib)
324            })
325            .collect();
326
327        // Sort by RRF score (descending)
328        rrf_scores.sort_by(|a, b| {
329            match b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal) {
330                std::cmp::Ordering::Equal => a.0.cmp(&b.0),
331                ordering => ordering,
332            }
333        });
334
335        // 4. Build result from scored records
336        let mut record_map: HashMap<String, UnifiedRecord> = HashMap::new();
337        for record in structured_result.records {
338            let key = self.record_to_key(&record);
339            record_map.insert(key, record);
340        }
341        for record in vector_result.records {
342            let key = self.record_to_key(&record);
343            if let Some(existing) = record_map.get_mut(&key) {
344                // Merge vector results
345                existing.vector_results.extend(record.vector_results);
346            } else {
347                record_map.insert(key, record);
348            }
349        }
350
351        // Build final result in RRF order
352        let mut columns = structured_result.columns.clone();
353        for col in &vector_result.columns {
354            if !columns.contains(col) {
355                columns.push(col.clone());
356            }
357        }
358
359        let mut result = UnifiedResult::with_columns(columns);
360        result.stats = QueryStats::merge(&structured_result.stats, &vector_result.stats);
361
362        for (key, score) in rrf_scores {
363            if let Some(mut record) = record_map.remove(&key) {
364                record.set_arc(Arc::from("_rrf_score"), Value::Float(score));
365                result.push(record);
366            }
367        }
368
369        Ok(result)
370    }
371
372    /// Intersection: Only return results present in both
373    fn execute_intersection(&self, query: &HybridQuery) -> Result<UnifiedResult, ExecutionError> {
374        // 1. Execute both queries in parallel — intersection needs both
375        //    result sets before it can filter.
376        let (structured_result, vector_result) =
377            self.execute_structured_and_vector_parallel(query)?;
378
379        // 2. Build key sets
380        let structured_keys: HashSet<String> = structured_result
381            .records
382            .iter()
383            .map(|r| self.record_to_key(r))
384            .collect();
385
386        // 3. Filter vector results to only those in structured
387        let mut result = UnifiedResult::with_columns(vector_result.columns.clone());
388
389        for record in vector_result.records {
390            let key = self.record_to_key(&record);
391            if structured_keys.contains(&key) {
392                result.push(record);
393            }
394        }
395
396        result.stats = QueryStats::merge(&structured_result.stats, &vector_result.stats);
397        Ok(result)
398    }
399
400    /// Union: Combine results with weighted scores
401    fn execute_union(
402        &self,
403        query: &HybridQuery,
404        struct_weight: f32,
405        vector_weight: f32,
406    ) -> Result<UnifiedResult, ExecutionError> {
407        // 1. Execute both queries in parallel — union merges both result
408        //    sets with weighted scores, so neither arm can be skipped.
409        let (structured_result, vector_result) =
410            self.execute_structured_and_vector_parallel(query)?;
411
412        // 2. Score and collect all records
413        let mut scored_records: HashMap<String, (UnifiedRecord, f32)> = HashMap::new();
414
415        // Add structured results with score based on rank
416        for (rank, record) in structured_result.records.into_iter().enumerate() {
417            let key = self.record_to_key(&record);
418            let score = struct_weight * (1.0 / (rank as f32 + 1.0));
419            scored_records.insert(key, (record, score));
420        }
421
422        // Add/merge vector results
423        for (rank, record) in vector_result.records.into_iter().enumerate() {
424            let key = self.record_to_key(&record);
425            let vector_score = vector_weight * (1.0 / (rank as f32 + 1.0));
426
427            if let Some((existing, score)) = scored_records.get_mut(&key) {
428                // Merge: add vector score and vector results
429                *score += vector_score;
430                existing.vector_results.extend(record.vector_results);
431            } else {
432                scored_records.insert(key, (record, vector_score));
433            }
434        }
435
436        // 3. Sort by combined score
437        let mut sorted: Vec<(String, UnifiedRecord, f32)> = scored_records
438            .into_iter()
439            .map(|(key, (record, score))| (key, record, score))
440            .collect();
441        sorted.sort_by(
442            |a, b| match b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal) {
443                std::cmp::Ordering::Equal => a.0.cmp(&b.0),
444                ordering => ordering,
445            },
446        );
447
448        // 4. Build result
449        let mut columns = structured_result.columns.clone();
450        for col in &vector_result.columns {
451            if !columns.contains(col) {
452                columns.push(col.clone());
453            }
454        }
455
456        let mut result = UnifiedResult::with_columns(columns);
457        result.stats = QueryStats::merge(&structured_result.stats, &vector_result.stats);
458
459        for (_key, mut record, score) in sorted {
460            record.set_arc(Arc::from("_union_score"), Value::Float(score as f64));
461            result.push(record);
462        }
463
464        Ok(result)
465    }
466
467    // =========================================================================
468    // Helper Methods
469    // =========================================================================
470
471    /// Get a unique key for a record (for deduplication)
472    fn record_to_key(&self, record: &UnifiedRecord) -> String {
473        // Try various ways to identify the record
474        if let Some(Value::Integer(id)) = record.get("id") {
475            return format!("row:{}", id);
476        }
477        if let Some(first_node) = record.nodes.values().next() {
478            return format!("node:{}", first_node.id);
479        }
480        if let Some(first_vsr) = record.vector_results.first() {
481            return format!("vec:{}:{}", first_vsr.collection, first_vsr.id);
482        }
483        // Fallback: hash of all visible fields
484        let fields: Vec<_> = record.iter_fields().collect();
485        format!("hash:{:?}", fields)
486    }
487
488    /// Get vector similarity score for a structured record
489    fn get_vector_score_for_record(
490        &self,
491        record: &UnifiedRecord,
492        vector_distances: &HashMap<String, f32>,
493    ) -> f32 {
494        // Try to find matching vector via ID
495        if let Some(Value::Integer(id)) = record.get("id") {
496            // Check all collections in vector_distances
497            for (key, distance) in vector_distances {
498                if key.ends_with(&format!(":{}", id)) {
499                    // Convert distance to similarity (lower distance = higher similarity)
500                    return 1.0 / (1.0 + distance);
501                }
502            }
503        }
504
505        0.0 // No vector match found
506    }
507}
508
509// ============================================================================
510// In-Memory Hybrid Executor for Testing
511// ============================================================================
512
513use super::vector::InMemoryVectorExecutor;
514
515/// In-memory hybrid executor for testing
516pub struct InMemoryHybridExecutor {
517    /// Records keyed by ID
518    records: HashMap<u64, UnifiedRecord>,
519    /// Vector executor
520    vector: InMemoryVectorExecutor,
521}
522
523impl InMemoryHybridExecutor {
524    /// Create a new in-memory hybrid executor
525    pub fn new() -> Self {
526        Self {
527            records: HashMap::new(),
528            vector: InMemoryVectorExecutor::new(),
529        }
530    }
531
532    /// Add a structured record
533    pub fn add_record(&mut self, id: u64, values: HashMap<String, Value>) {
534        let mut record = UnifiedRecord::new();
535        for (k, v) in values {
536            record.set_owned(k, v);
537        }
538        record.set_arc(Arc::from("id"), Value::Integer(id as i64));
539        self.records.insert(id, record);
540    }
541
542    /// Add a vector with optional link to record
543    pub fn add_vector(
544        &mut self,
545        collection: &str,
546        id: u64,
547        vector: Vec<f32>,
548        linked_record_id: Option<u64>,
549    ) {
550        use crate::storage::engine::vector_metadata::MetadataValue;
551        let mut meta = HashMap::new();
552        if let Some(record_id) = linked_record_id {
553            meta.insert(
554                "_linked_record".to_string(),
555                MetadataValue::Integer(record_id as i64),
556            );
557        }
558        let meta = if meta.is_empty() { None } else { Some(meta) };
559        self.vector.add_vector(collection, id, vector, meta);
560    }
561
562    /// Execute a hybrid query with manual fusion
563    pub fn execute_with_fusion(
564        &self,
565        structured_ids: &[u64],
566        vector_query: &VectorQuery,
567        fusion: &FusionStrategy,
568    ) -> Result<UnifiedResult, ExecutionError> {
569        // Execute vector query
570        let vector_result = self.vector.execute(vector_query)?;
571
572        // Get structured records
573        let structured: Vec<_> = structured_ids
574            .iter()
575            .filter_map(|id| self.records.get(id).cloned())
576            .collect();
577
578        // Apply fusion strategy
579        match fusion {
580            FusionStrategy::Rerank { weight } => {
581                self.fuse_rerank(structured, vector_result, *weight)
582            }
583            FusionStrategy::Intersection => self.fuse_intersection(structured, vector_result),
584            FusionStrategy::RRF { k } => self.fuse_rrf(structured, vector_result, *k),
585            _ => {
586                // Default: just return vector results
587                Ok(vector_result)
588            }
589        }
590    }
591
592    fn fuse_rerank(
593        &self,
594        structured: Vec<UnifiedRecord>,
595        vector_result: UnifiedResult,
596        weight: f32,
597    ) -> Result<UnifiedResult, ExecutionError> {
598        let mut scored: Vec<(String, UnifiedRecord, f32)> = Vec::new();
599
600        for (rank, record) in structured.into_iter().enumerate() {
601            let struct_score = 1.0 / (rank as f32 + 1.0);
602            let vector_score = self.get_vector_score(&record, &vector_result);
603            let combined = (1.0 - weight) * struct_score + weight * vector_score;
604            let key = self.record_to_key_in_memory(&record);
605            scored.push((key, record, combined));
606        }
607
608        scored.sort_by(
609            |a, b| match b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal) {
610                std::cmp::Ordering::Equal => a.0.cmp(&b.0),
611                ordering => ordering,
612            },
613        );
614
615        let mut result = UnifiedResult::with_columns(vec!["id".to_string()]);
616        for (_key, mut record, score) in scored {
617            record.set_arc(Arc::from("_hybrid_score"), Value::Float(score as f64));
618            result.push(record);
619        }
620
621        Ok(result)
622    }
623
624    fn fuse_intersection(
625        &self,
626        structured: Vec<UnifiedRecord>,
627        vector_result: UnifiedResult,
628    ) -> Result<UnifiedResult, ExecutionError> {
629        let struct_ids: HashSet<i64> = structured
630            .iter()
631            .filter_map(|r| match r.get("id") {
632                Some(Value::Integer(i)) => Some(*i),
633                _ => None,
634            })
635            .collect();
636
637        let mut result = UnifiedResult::with_columns(vector_result.columns.clone());
638
639        for record in vector_result.records {
640            if let Some(vsr) = record.vector_results.first() {
641                if struct_ids.contains(&(vsr.id as i64)) {
642                    result.push(record);
643                }
644            }
645        }
646
647        Ok(result)
648    }
649
650    fn fuse_rrf(
651        &self,
652        structured: Vec<UnifiedRecord>,
653        vector_result: UnifiedResult,
654        k: u32,
655    ) -> Result<UnifiedResult, ExecutionError> {
656        let k_f64 = k as f64;
657
658        // Build ID -> structured rank map
659        let struct_ranks: HashMap<i64, u32> = structured
660            .iter()
661            .enumerate()
662            .filter_map(|(rank, r)| match r.get("id") {
663                Some(Value::Integer(i)) => Some((*i, (rank + 1) as u32)),
664                _ => None,
665            })
666            .collect();
667
668        // Calculate RRF scores for vector results
669        let mut scored: Vec<(String, UnifiedRecord, f64)> = Vec::new();
670
671        for (rank, record) in vector_result.records.into_iter().enumerate() {
672            let vector_contrib = 1.0 / (k_f64 + (rank + 1) as f64);
673
674            let struct_contrib = record
675                .vector_results
676                .first()
677                .and_then(|vsr| struct_ranks.get(&(vsr.id as i64)))
678                .map(|r| 1.0 / (k_f64 + *r as f64))
679                .unwrap_or(0.0);
680
681            let rrf_score = struct_contrib + vector_contrib;
682            let key = self.record_to_key_in_memory(&record);
683            scored.push((key, record, rrf_score));
684        }
685
686        scored.sort_by(
687            |a, b| match b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal) {
688                std::cmp::Ordering::Equal => a.0.cmp(&b.0),
689                ordering => ordering,
690            },
691        );
692
693        let mut result =
694            UnifiedResult::with_columns(vec!["id".to_string(), "distance".to_string()]);
695        for (_key, mut record, score) in scored {
696            record.set_arc(Arc::from("_rrf_score"), Value::Float(score));
697            result.push(record);
698        }
699
700        Ok(result)
701    }
702
703    fn get_vector_score(&self, record: &UnifiedRecord, vector_result: &UnifiedResult) -> f32 {
704        if let Some(Value::Integer(id)) = record.get("id") {
705            for vr in &vector_result.records {
706                for vsr in &vr.vector_results {
707                    if vsr.id == *id as u64 {
708                        return 1.0 / (1.0 + vsr.distance);
709                    }
710                }
711            }
712        }
713        0.0
714    }
715
716    fn record_to_key_in_memory(&self, record: &UnifiedRecord) -> String {
717        if let Some(Value::Integer(id)) = record.get("id") {
718            return format!("row:{}", id);
719        }
720        if let Some(first_vsr) = record.vector_results.first() {
721            return format!("vec:{}:{}", first_vsr.collection, first_vsr.id);
722        }
723        let fields: Vec<_> = record.iter_fields().collect();
724        format!("hash:{:?}", fields)
725    }
726}
727
728impl Default for InMemoryHybridExecutor {
729    fn default() -> Self {
730        Self::new()
731    }
732}
733
734// ============================================================================
735// Tests
736// ============================================================================
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741    use crate::storage::engine::distance::DistanceMetric;
742    use reddb_rql::ast::VectorSource;
743
744    #[test]
745    fn test_in_memory_hybrid_rerank() {
746        let mut executor = InMemoryHybridExecutor::new();
747
748        // Add structured records
749        let mut vals1 = HashMap::new();
750        vals1.insert("name".to_string(), Value::text("host1".to_string()));
751        executor.add_record(1, vals1);
752
753        let mut vals2 = HashMap::new();
754        vals2.insert("name".to_string(), Value::text("host2".to_string()));
755        executor.add_record(2, vals2);
756
757        let mut vals3 = HashMap::new();
758        vals3.insert("name".to_string(), Value::text("host3".to_string()));
759        executor.add_record(3, vals3);
760
761        // Add vectors (host3 is most similar to query)
762        executor.add_vector("hosts", 1, vec![0.1, 0.0], Some(1));
763        executor.add_vector("hosts", 2, vec![0.5, 0.5], Some(2));
764        executor.add_vector("hosts", 3, vec![0.99, 0.0], Some(3)); // Closest to query
765
766        let query = VectorQuery {
767            mode: Default::default(),
768            alias: None,
769            collection: "hosts".to_string(),
770            query_vector: VectorSource::Literal(vec![1.0, 0.0]),
771            k: 3,
772            filter: None,
773            metric: Some(DistanceMetric::L2),
774            include_vectors: false,
775            include_metadata: false,
776            threshold: None,
777        };
778
779        // With pure structural ranking (weight=0), order should be 1, 2, 3
780        let result = executor
781            .execute_with_fusion(&[1, 2, 3], &query, &FusionStrategy::Rerank { weight: 0.0 })
782            .unwrap();
783
784        assert_eq!(result.len(), 3);
785        assert_eq!(result.records[0].get("id"), Some(&Value::Integer(1)));
786
787        // With pure vector ranking (weight=1), order should be 3, 1, 2
788        let result = executor
789            .execute_with_fusion(&[1, 2, 3], &query, &FusionStrategy::Rerank { weight: 1.0 })
790            .unwrap();
791
792        assert_eq!(result.len(), 3);
793        assert_eq!(result.records[0].get("id"), Some(&Value::Integer(3)));
794    }
795
796    #[test]
797    fn test_in_memory_hybrid_intersection() {
798        let mut executor = InMemoryHybridExecutor::new();
799
800        // Add records 1-5
801        for i in 1..=5 {
802            let mut vals = HashMap::new();
803            vals.insert("name".to_string(), Value::text(format!("host{}", i)));
804            executor.add_record(i, vals);
805        }
806
807        // Add vectors for only 2, 3, 4
808        executor.add_vector("hosts", 2, vec![0.1, 0.0], Some(2));
809        executor.add_vector("hosts", 3, vec![0.5, 0.5], Some(3));
810        executor.add_vector("hosts", 4, vec![0.9, 0.0], Some(4));
811
812        let query = VectorQuery {
813            mode: Default::default(),
814            alias: None,
815            collection: "hosts".to_string(),
816            query_vector: VectorSource::Literal(vec![1.0, 0.0]),
817            k: 10,
818            filter: None,
819            metric: Some(DistanceMetric::L2),
820            include_vectors: false,
821            include_metadata: false,
822            threshold: None,
823        };
824
825        // Intersection of structured [1,2,3] and vectors [2,3,4] should be [2,3]
826        let result = executor
827            .execute_with_fusion(&[1, 2, 3], &query, &FusionStrategy::Intersection)
828            .unwrap();
829
830        assert_eq!(result.len(), 2);
831
832        let ids: HashSet<i64> = result
833            .records
834            .iter()
835            .filter_map(|r| match r.get("id") {
836                Some(Value::Integer(i)) => Some(*i),
837                _ => None,
838            })
839            .collect();
840
841        assert!(ids.contains(&2));
842        assert!(ids.contains(&3));
843    }
844
845    #[test]
846    fn test_in_memory_hybrid_rrf() {
847        let mut executor = InMemoryHybridExecutor::new();
848
849        for i in 1..=4 {
850            let mut vals = HashMap::new();
851            vals.insert("name".to_string(), Value::text(format!("host{}", i)));
852            executor.add_record(i, vals);
853            executor.add_vector("hosts", i, vec![i as f32 * 0.25, 0.0], Some(i));
854        }
855
856        let query = VectorQuery {
857            mode: Default::default(),
858            alias: None,
859            collection: "hosts".to_string(),
860            query_vector: VectorSource::Literal(vec![1.0, 0.0]),
861            k: 4,
862            filter: None,
863            metric: Some(DistanceMetric::L2),
864            include_vectors: false,
865            include_metadata: false,
866            threshold: None,
867        };
868
869        // RRF with k=60
870        let result = executor
871            .execute_with_fusion(
872                &[1, 2, 3, 4], // Structured order: 1, 2, 3, 4
873                &query,        // Vector order: 4, 3, 2, 1 (by distance to [1.0, 0.0])
874                &FusionStrategy::RRF { k: 60 },
875            )
876            .unwrap();
877
878        assert_eq!(result.len(), 4);
879
880        // All records should have RRF scores
881        for record in &result.records {
882            assert!(record.contains_column("_rrf_score"));
883        }
884    }
885}