use std::collections::HashMap;
pub fn rrf_fuse(
legs: &[Vec<(String, f32)>],
k: u32,
weights: Option<&[f32]>,
) -> Vec<(String, f32)> {
let k = k.max(1) as f32;
let w: Vec<f32> = match weights {
Some(ws) if ws.len() == legs.len() => ws.to_vec(),
_ => vec![1.0; legs.len()],
};
let mut acc: HashMap<String, f32> = HashMap::new();
for (leg_idx, leg) in legs.iter().enumerate() {
let weight = w[leg_idx];
for (rank, (id, _score)) in leg.iter().enumerate() {
let rank = (rank + 1) as f32;
*acc.entry(id.clone()).or_insert(0.0) += weight / (k + rank);
}
}
let mut out: Vec<(String, f32)> = acc.into_iter().collect();
out.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
out
}
pub fn dbsf_fuse(legs: &[Vec<(String, f32)>]) -> Vec<(String, f32)> {
let mut normalized: Vec<Vec<(String, f32)>> = Vec::with_capacity(legs.len());
for leg in legs {
if leg.is_empty() {
normalized.push(Vec::new());
continue;
}
let n = leg.len() as f32;
let mean = leg.iter().map(|(_, s)| s).sum::<f32>() / n;
let var = leg.iter().map(|(_, s)| (s - mean).powi(2)).sum::<f32>() / n;
let sigma3 = (3.0 * var).sqrt().max(1e-6);
normalized.push(
leg.iter()
.map(|(id, s)| (id.clone(), (s - mean) / sigma3))
.collect(),
);
}
let mut acc: HashMap<String, f32> = HashMap::new();
for leg in &normalized {
for (id, s) in leg {
*acc.entry(id.clone()).or_insert(0.0) += s;
}
}
let mut out: Vec<(String, f32)> = acc.into_iter().collect();
out.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rrf_rank_fusion_basic() {
let a = vec![("a".into(), 1.0), ("b".into(), 0.9), ("c".into(), 0.8)];
let b = vec![("a".into(), 5.0), ("b".into(), 4.0), ("d".into(), 3.0)];
let fused = rrf_fuse(&[a, b], 60, None);
assert_eq!(fused[0].0, "a");
assert_eq!(fused[1].0, "b");
assert!(
fused[0].1 > fused[1].1,
"rank-1-in-both must beat rank-2-in-both"
);
assert_eq!(fused.len(), 4);
assert!(
(fused[2].1 - fused[3].1).abs() < 1e-9,
"single-leg rank-3 ties"
);
let last_two: Vec<&str> = fused[2..].iter().map(|(id, _)| id.as_str()).collect();
assert!(last_two.contains(&"c") && last_two.contains(&"d"));
}
#[test]
fn rrf_weights_bias_leg() {
let a = vec![("x".into(), 1.0), ("y".into(), 0.5)];
let b = vec![("y".into(), 1.0), ("x".into(), 0.5)];
let tied = rrf_fuse(&[a.clone(), b.clone()], 60, None);
assert!((tied[0].1 - tied[1].1).abs() < 1e-9);
let biased = rrf_fuse(&[a, b], 60, Some(&[1.0, 10.0]));
assert_eq!(biased[0].0, "y");
}
#[test]
fn dbsf_uses_score_magnitude() {
let a = vec![("p".into(), 1.0), ("q".into(), 0.5), ("r".into(), 0.0)];
let fused = dbsf_fuse(&[a]);
assert_eq!(fused[0].0, "p");
assert_eq!(fused[2].0, "r");
}
}