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