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}
38
39impl HybridExecutor {
40 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 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 pub fn execute(&self, query: &HybridQuery) -> Result<UnifiedResult, ExecutionError> {
60 let start = std::time::Instant::now();
61
62 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 if let Some(limit) = query.limit {
77 result.records.truncate(limit);
78 }
79
80 result.stats.exec_time_us = start.elapsed().as_micros() as u64;
82
83 Ok(result)
84 }
85
86 fn execute_rerank(
92 &self,
93 query: &HybridQuery,
94 weight: f32,
95 ) -> Result<UnifiedResult, ExecutionError> {
96 let structured_result = self.unified.execute(&query.structured)?;
98
99 if structured_result.is_empty() {
100 return Ok(structured_result);
101 }
102
103 let vector_result = self.vector.execute(&query.vector)?;
105
106 let mut vector_distances: HashMap<String, f32> = HashMap::new();
108 for record in &vector_result.records {
109 for vsr in &record.vector_results {
110 let key = format!("{}:{}", vsr.collection, vsr.id);
112 vector_distances.insert(key, vsr.distance);
113 }
114 }
115
116 let mut scored: Vec<(String, UnifiedRecord, f32)> = structured_result
118 .records
119 .into_iter()
120 .enumerate()
121 .map(|(rank, record)| {
122 let struct_score = 1.0 / (rank as f32 + 1.0);
124
125 let vector_score = self.get_vector_score_for_record(&record, &vector_distances);
127
128 let combined = (1.0 - weight) * struct_score + weight * vector_score;
130 (self.record_to_key(&record), record, combined)
131 })
132 .collect();
133
134 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 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 fn execute_filter_then_search(
156 &self,
157 query: &HybridQuery,
158 ) -> Result<UnifiedResult, ExecutionError> {
159 let structured_result = self.unified.execute(&query.structured)?;
161
162 if structured_result.is_empty() {
163 return Ok(structured_result);
164 }
165
166 let candidate_ids: HashSet<u64> = structured_result
168 .records
169 .iter()
170 .filter_map(|r| {
171 r.get("id").and_then(|v| match v {
173 Value::Integer(i) => Some(*i as u64),
174 _ => None,
175 })
176 })
177 .collect();
178
179 let vector_result = self.vector.execute(&query.vector)?;
181
182 let mut result = UnifiedResult::with_columns(vector_result.columns.clone());
184
185 for record in vector_result.records {
186 let matches = record.vector_results.iter().any(|vsr| {
188 candidate_ids.contains(&vsr.id) ||
189 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 fn execute_search_then_filter(
204 &self,
205 query: &HybridQuery,
206 ) -> Result<UnifiedResult, ExecutionError> {
207 let vector_result = self.vector.execute(&query.vector)?;
209
210 if vector_result.is_empty() {
211 return Ok(vector_result);
212 }
213
214 let structured_result = self.unified.execute(&query.structured)?;
216
217 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 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 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 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 let (structured_result, vector_result) =
289 self.execute_structured_and_vector_parallel(query)?;
290
291 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 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 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 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 existing.vector_results.extend(record.vector_results);
346 } else {
347 record_map.insert(key, record);
348 }
349 }
350
351 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 fn execute_intersection(&self, query: &HybridQuery) -> Result<UnifiedResult, ExecutionError> {
374 let (structured_result, vector_result) =
377 self.execute_structured_and_vector_parallel(query)?;
378
379 let structured_keys: HashSet<String> = structured_result
381 .records
382 .iter()
383 .map(|r| self.record_to_key(r))
384 .collect();
385
386 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 fn execute_union(
402 &self,
403 query: &HybridQuery,
404 struct_weight: f32,
405 vector_weight: f32,
406 ) -> Result<UnifiedResult, ExecutionError> {
407 let (structured_result, vector_result) =
410 self.execute_structured_and_vector_parallel(query)?;
411
412 let mut scored_records: HashMap<String, (UnifiedRecord, f32)> = HashMap::new();
414
415 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 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 *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 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 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 fn record_to_key(&self, record: &UnifiedRecord) -> String {
473 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 let fields: Vec<_> = record.iter_fields().collect();
485 format!("hash:{:?}", fields)
486 }
487
488 fn get_vector_score_for_record(
490 &self,
491 record: &UnifiedRecord,
492 vector_distances: &HashMap<String, f32>,
493 ) -> f32 {
494 if let Some(Value::Integer(id)) = record.get("id") {
496 for (key, distance) in vector_distances {
498 if key.ends_with(&format!(":{}", id)) {
499 return 1.0 / (1.0 + distance);
501 }
502 }
503 }
504
505 0.0 }
507}
508
509use super::vector::InMemoryVectorExecutor;
514
515pub struct InMemoryHybridExecutor {
517 records: HashMap<u64, UnifiedRecord>,
519 vector: InMemoryVectorExecutor,
521}
522
523impl InMemoryHybridExecutor {
524 pub fn new() -> Self {
526 Self {
527 records: HashMap::new(),
528 vector: InMemoryVectorExecutor::new(),
529 }
530 }
531
532 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 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 pub fn execute_with_fusion(
564 &self,
565 structured_ids: &[u64],
566 vector_query: &VectorQuery,
567 fusion: &FusionStrategy,
568 ) -> Result<UnifiedResult, ExecutionError> {
569 let vector_result = self.vector.execute(vector_query)?;
571
572 let structured: Vec<_> = structured_ids
574 .iter()
575 .filter_map(|id| self.records.get(id).cloned())
576 .collect();
577
578 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 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 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 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#[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 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 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)); 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 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 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 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 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 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 let result = executor
871 .execute_with_fusion(
872 &[1, 2, 3, 4], &query, &FusionStrategy::RRF { k: 60 },
875 )
876 .unwrap();
877
878 assert_eq!(result.len(), 4);
879
880 for record in &result.records {
882 assert!(record.contains_column("_rrf_score"));
883 }
884 }
885}