#[derive(Debug, Clone)]
pub struct Ranking {
pub signal: String,
pub ids: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Fused {
pub id: String,
pub score: f64,
pub contributions: Vec<String>,
}
pub const RRF_K: f64 = 60.0;
#[must_use]
pub fn rrf_fuse(rankings: &[Ranking], k: f64) -> Vec<Fused> {
use std::collections::HashMap;
let mut acc: HashMap<&str, (f64, Vec<String>)> = HashMap::new();
let mut ordre: Vec<&str> = Vec::new();
for ranking in rankings {
for (rang, id) in ranking.ids.iter().enumerate() {
#[allow(clippy::cast_precision_loss)]
let contribution = 1.0 / (k + rang as f64);
let entree = acc.entry(id.as_str()).or_insert_with(|| {
ordre.push(id.as_str());
(0.0, Vec::new())
});
entree.0 += contribution;
if !entree.1.contains(&ranking.signal) {
entree.1.push(ranking.signal.clone());
}
}
}
let mut fused: Vec<Fused> = ordre
.into_iter()
.map(|id| {
let (score, contributions) = acc.remove(id).unwrap_or((0.0, Vec::new()));
Fused {
id: id.to_string(),
score,
contributions,
}
})
.collect();
fused.sort_by(|a, b| b.score.total_cmp(&a.score).then_with(|| a.id.cmp(&b.id)));
fused
}