mod basic;
mod rrf;
pub use basic::{BasicWeightedFusion, FusionWeights};
pub use rrf::RRFFusion;
use crate::search::executor_types::{RankedResults, SearchSource};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub trait ScoreFusion: Send + Sync {
fn fuse(
&self,
results: Vec<RankedResults>,
weights: &FusionWeights,
limit: usize,
) -> Vec<FusedResult>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoreBreakdown {
pub fts: f32,
pub vector: f32,
pub graph: f32,
pub recency: f32,
pub churn: f32,
}
impl ScoreBreakdown {
pub fn zero() -> Self {
Self {
fts: 0.0,
vector: 0.0,
graph: 0.0,
recency: 0.0,
churn: 0.0,
}
}
pub fn format_debug(&self) -> String {
format!(
"FTS:{:.3} Vec:{:.3} Graph:{:.3} Recency:{:.3} Churn:{:.3}",
self.fts, self.vector, self.graph, self.recency, self.churn
)
}
pub fn as_percentages(&self) -> Vec<(String, f32)> {
let total = self.fts + self.vector + self.graph + self.recency + self.churn;
if total < 0.0001 {
return vec![];
}
vec![
("FTS".to_string(), (self.fts / total) * 100.0),
("Vector".to_string(), (self.vector / total) * 100.0),
("Graph".to_string(), (self.graph / total) * 100.0),
("Recency".to_string(), (self.recency / total) * 100.0),
("Churn".to_string(), (self.churn / total) * 100.0),
]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FusedResult {
pub chunk_id: i64,
pub score: f32,
pub source_scores: HashMap<SearchSource, f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub breakdown: Option<ScoreBreakdown>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exact_match_multiplier: Option<f32>,
}
impl FusedResult {
pub fn new(chunk_id: i64, score: f32, source_scores: HashMap<SearchSource, f32>) -> Self {
Self {
chunk_id,
score,
source_scores,
breakdown: None,
exact_match_multiplier: None,
}
}
pub fn with_exact_match(
chunk_id: i64,
score: f32,
source_scores: HashMap<SearchSource, f32>,
exact_match_multiplier: Option<f32>,
) -> Self {
Self {
chunk_id,
score,
source_scores,
breakdown: None,
exact_match_multiplier,
}
}
pub fn with_breakdown(
chunk_id: i64,
score: f32,
source_scores: HashMap<SearchSource, f32>,
breakdown: ScoreBreakdown,
) -> Self {
Self {
chunk_id,
score,
source_scores,
breakdown: Some(breakdown),
exact_match_multiplier: None,
}
}
pub fn with_all(
chunk_id: i64,
score: f32,
source_scores: HashMap<SearchSource, f32>,
breakdown: ScoreBreakdown,
exact_match_multiplier: Option<f32>,
) -> Self {
Self {
chunk_id,
score,
source_scores,
breakdown: Some(breakdown),
exact_match_multiplier,
}
}
}