use crate::model::Candidate; use std::collections::HashMap; /// Reciprocal Rank Fusion of two ranked lists of entry indices. /// Returns deduped entry indices ordered by fused score desc. pub fn rrf_merge(a: &[usize], b: &[usize], k: usize) -> Vec { let mut score: HashMap = HashMap::new(); for (rank, &id) in a.iter().enumerate() { *score.entry(id).or_default() += 1.0 / (k as f64 + rank as f64 + 1.0); } for (rank, &id) in b.iter().enumerate() { *score.entry(id).or_default() += 1.0 / (k as f64 + rank as f64 + 1.0); } let mut v: Vec<(usize, f64)> = score.into_iter().collect(); v.sort_by(|x, y| y.1.partial_cmp(&x.1).unwrap()); v.into_iter().map(|(id, _)| id).collect() } /// Group candidates by normalized ICD code across all segments. /// Score = max over occurrences; retain all source segments and /// the matched phrase of the best-scoring occurrence. pub fn cross_segment_dedupe(cands: Vec) -> Vec<(DedupCandidate, f32)> { let mut by_code: HashMap = HashMap::new(); for c in cands { let s = c.rerank.or(c.semantic).or(c.lexical).unwrap_or(0.0); let e = by_code.entry(c.icd_code.clone()).or_insert_with(|| DedupCandidate { icd_code: c.icd_code.clone(), best_phrase: c.alpha_text.clone(), best_score: f32::MIN, source_segments: Vec::new(), }); if !e.source_segments.contains(&c.segment_idx) { e.source_segments.push(c.segment_idx); } if s > e.best_score { e.best_score = s; e.best_phrase = c.alpha_text.clone(); } } let mut out: Vec<(DedupCandidate, f32)> = by_code .into_values() .map(|mut d| { d.source_segments.sort(); let s = d.best_score; (d, s) }) .collect(); out.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); out } #[derive(Debug, Clone)] pub struct DedupCandidate { pub icd_code: String, pub best_phrase: String, pub best_score: f32, pub source_segments: Vec, }