use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct ScoreBreakdown {
pub bm25: Option<f32>,
pub vector: Option<f32>,
pub hybrid: f32,
pub fusion_path: String,
}
#[derive(Debug, Clone)]
pub struct ConfidenceComponents {
pub percentile: f32,
pub margin: f32,
pub agreement: f32,
}
#[derive(Debug, Clone)]
pub struct HybridResult {
pub id: String,
pub bm25_score: Option<f32>,
pub vector_score: Option<f32>,
pub hybrid_score: f32,
pub score_breakdown: ScoreBreakdown,
pub confidence_score: Option<f32>,
pub confidence_components: Option<ConfidenceComponents>,
pub metadata: HashMap<String, serde_json::Value>,
}
impl HybridResult {
pub fn new(
id: impl Into<String>,
hybrid_score: f32,
bm25_score: Option<f32>,
vector_score: Option<f32>,
fusion_path: impl Into<String>,
) -> Self {
let breakdown = ScoreBreakdown {
bm25: bm25_score,
vector: vector_score,
hybrid: hybrid_score,
fusion_path: fusion_path.into(),
};
Self {
id: id.into(),
bm25_score,
vector_score,
hybrid_score,
score_breakdown: breakdown,
confidence_score: None,
confidence_components: None,
metadata: HashMap::new(),
}
}
pub fn has_both_sources(&self) -> bool {
self.bm25_score.is_some() && self.vector_score.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn result(hybrid: f32, bm25: Option<f32>, vec: Option<f32>) -> HybridResult {
HybridResult::new("id", hybrid, bm25, vec, "rrf")
}
#[test]
fn new_sets_all_fields() {
let r = result(0.8, Some(0.5), Some(0.9));
assert_eq!(r.id, "id");
assert!((r.hybrid_score - 0.8).abs() < 1e-6);
assert_eq!(r.bm25_score, Some(0.5));
assert_eq!(r.vector_score, Some(0.9));
assert_eq!(r.score_breakdown.fusion_path, "rrf");
}
#[test]
fn has_both_sources_true_when_both_present() {
assert!(result(0.5, Some(0.3), Some(0.7)).has_both_sources());
}
#[test]
fn has_both_sources_false_when_only_bm25() {
assert!(!result(0.5, Some(0.3), None).has_both_sources());
}
#[test]
fn has_both_sources_false_when_only_vector() {
assert!(!result(0.5, None, Some(0.7)).has_both_sources());
}
#[test]
fn confidence_is_none_by_default() {
assert!(result(0.5, Some(0.3), Some(0.7)).confidence_score.is_none());
}
#[test]
fn breakdown_carries_component_scores() {
let r = result(0.8, Some(0.6), Some(0.9));
assert_eq!(r.score_breakdown.bm25, Some(0.6));
assert_eq!(r.score_breakdown.vector, Some(0.9));
assert!((r.score_breakdown.hybrid - 0.8).abs() < 1e-6);
}
#[test]
fn metadata_is_empty_by_default() {
assert!(result(0.5, None, None).metadata.is_empty());
}
}