1use 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
31pub struct HybridExecutor {
33 unified: UnifiedExecutor,
35 vector: VectorExecutor,
37 unified_index: Option<Arc<UnifiedIndex>>,
39}
40
41impl HybridExecutor {
42 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 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 pub fn execute(&self, query: &HybridQuery) -> Result<UnifiedResult, ExecutionError> {
67 let start = std::time::Instant::now();
68
69 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 if let Some(limit) = query.limit {
84 result.records.truncate(limit);
85 }
86
87 result.stats.exec_time_us = start.elapsed().as_micros() as u64;
89
90 Ok(result)
91 }
92
93 fn execute_rerank(
99 &self,
100 query: &HybridQuery,
101 weight: f32,
102 ) -> Result<UnifiedResult, ExecutionError> {
103 let structured_result = self.unified.execute(&query.structured)?;
105
106 if structured_result.is_empty() {
107 return Ok(structured_result);
108 }
109
110 let vector_result = self.vector.execute(&query.vector)?;
112
113 let mut vector_distances: HashMap<String, f32> = HashMap::new();
115 for record in &vector_result.records {
116 for vsr in &record.vector_results {
117 let key = format!("{}:{}", vsr.collection, vsr.id);
119 vector_distances.insert(key, vsr.distance);
120 }
121 }
122
123 let mut scored: Vec<(String, UnifiedRecord, f32)> = structured_result
125 .records
126 .into_iter()
127 .enumerate()
128 .map(|(rank, record)| {
129 let struct_score = 1.0 / (rank as f32 + 1.0);
131
132 let vector_score = self.get_vector_score_for_record(&record, &vector_distances);
134
135 let combined = (1.0 - weight) * struct_score + weight * vector_score;
137 (self.record_to_key(&record), record, combined)
138 })
139 .collect();
140
141 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 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 fn execute_filter_then_search(
163 &self,
164 query: &HybridQuery,
165 ) -> Result<UnifiedResult, ExecutionError> {
166 let structured_result = self.unified.execute(&query.structured)?;
168
169 if structured_result.is_empty() {
170 return Ok(structured_result);
171 }
172
173 let candidate_ids: HashSet<u64> = structured_result
175 .records
176 .iter()
177 .filter_map(|r| {
178 r.get("id").and_then(|v| match v {
180 Value::Integer(i) => Some(*i as u64),
181 _ => None,
182 })
183 })
184 .collect();
185
186 let vector_result = self.vector.execute(&query.vector)?;
188
189 let mut result = UnifiedResult::with_columns(vector_result.columns.clone());
191
192 for record in vector_result.records {
193 let matches = record.vector_results.iter().any(|vsr| {
195 candidate_ids.contains(&vsr.id) ||
196 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 fn execute_search_then_filter(
211 &self,
212 query: &HybridQuery,
213 ) -> Result<UnifiedResult, ExecutionError> {
214 let vector_result = self.vector.execute(&query.vector)?;
216
217 if vector_result.is_empty() {
218 return Ok(vector_result);
219 }
220
221 let structured_result = self.unified.execute(&query.structured)?;
223
224 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 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 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 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 let (structured_result, vector_result) =
296 self.execute_structured_and_vector_parallel(query)?;
297
298 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 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 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 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 existing.vector_results.extend(record.vector_results);
353 } else {
354 record_map.insert(key, record);
355 }
356 }
357
358 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 fn execute_intersection(&self, query: &HybridQuery) -> Result<UnifiedResult, ExecutionError> {
381 let (structured_result, vector_result) =
384 self.execute_structured_and_vector_parallel(query)?;
385
386 let structured_keys: HashSet<String> = structured_result
388 .records
389 .iter()
390 .map(|r| self.record_to_key(r))
391 .collect();
392
393 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 fn execute_union(
409 &self,
410 query: &HybridQuery,
411 struct_weight: f32,
412 vector_weight: f32,
413 ) -> Result<UnifiedResult, ExecutionError> {
414 let (structured_result, vector_result) =
417 self.execute_structured_and_vector_parallel(query)?;
418
419 let mut scored_records: HashMap<String, (UnifiedRecord, f32)> = HashMap::new();
421
422 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 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 *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 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 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 fn record_to_key(&self, record: &UnifiedRecord) -> String {
480 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 let fields: Vec<_> = record.iter_fields().collect();
492 format!("hash:{:?}", fields)
493 }
494
495 fn get_vector_score_for_record(
497 &self,
498 record: &UnifiedRecord,
499 vector_distances: &HashMap<String, f32>,
500 ) -> f32 {
501 if let Some(Value::Integer(id)) = record.get("id") {
503 for (key, distance) in vector_distances {
505 if key.ends_with(&format!(":{}", id)) {
506 return 1.0 / (1.0 + distance);
508 }
509 }
510 }
511
512 if let Some(ref unified_index) = self.unified_index {
514 if let Some(Value::Integer(id)) = record.get("id") {
515 }
519 }
520
521 0.0 }
523}
524
525impl QueryStats {
530 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, }
542 }
543}
544
545use super::vector::InMemoryVectorExecutor;
550
551pub struct InMemoryHybridExecutor {
553 records: HashMap<u64, UnifiedRecord>,
555 vector: InMemoryVectorExecutor,
557}
558
559impl InMemoryHybridExecutor {
560 pub fn new() -> Self {
562 Self {
563 records: HashMap::new(),
564 vector: InMemoryVectorExecutor::new(),
565 }
566 }
567
568 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 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 pub fn execute_with_fusion(
600 &self,
601 structured_ids: &[u64],
602 vector_query: &VectorQuery,
603 fusion: &FusionStrategy,
604 ) -> Result<UnifiedResult, ExecutionError> {
605 let vector_result = self.vector.execute(vector_query)?;
607
608 let structured: Vec<_> = structured_ids
610 .iter()
611 .filter_map(|id| self.records.get(id).cloned())
612 .collect();
613
614 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 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 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 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#[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 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 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)); 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 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 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 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 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 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 let result = executor
907 .execute_with_fusion(
908 &[1, 2, 3, 4], &query, &FusionStrategy::RRF { k: 60 },
911 )
912 .unwrap();
913
914 assert_eq!(result.len(), 4);
915
916 for record in &result.records {
918 assert!(record.contains_column("_rrf_score"));
919 }
920 }
921}