use std::collections::HashMap;
use uuid::Uuid;
pub fn weighted_reciprocal_rank_fusion(
ranked_lists: &[Vec<(Uuid, f32)>],
k: f32,
weights: &[f32],
) -> Vec<(Uuid, f32)> {
let mut scores: HashMap<Uuid, f32> = HashMap::new();
for (i, list) in ranked_lists.iter().enumerate() {
let w = weights.get(i).copied().unwrap_or(1.0);
for (rank, (id, _original_score)) in list.iter().enumerate() {
*scores.entry(*id).or_insert(0.0) += w / (k + rank as f32 + 1.0);
}
}
let mut fused: Vec<(Uuid, f32)> = scores.into_iter().collect();
fused.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
fused
}
pub fn reciprocal_rank_fusion(ranked_lists: &[Vec<(Uuid, f32)>], k: f32) -> Vec<(Uuid, f32)> {
weighted_reciprocal_rank_fusion(ranked_lists, k, &[])
}
pub fn recency_score(created_at: &str, half_life_hours: f64) -> f32 {
let now = chrono::Utc::now();
let created = match chrono::DateTime::parse_from_rfc3339(created_at) {
Ok(dt) => dt.with_timezone(&chrono::Utc),
Err(_) => return 0.5, };
let age_hours = (now - created).num_seconds() as f64 / 3600.0;
if age_hours < 0.0 {
return 1.0; }
let decay = (-age_hours * (2.0_f64.ln()) / half_life_hours).exp();
decay as f32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rrf_basic() {
let id1 = Uuid::now_v7();
let id2 = Uuid::now_v7();
let id3 = Uuid::now_v7();
let list1 = vec![(id1, 0.9), (id2, 0.8), (id3, 0.7)];
let list2 = vec![(id2, 0.95), (id1, 0.85), (id3, 0.75)];
let fused = reciprocal_rank_fusion(&[list1, list2], 60.0);
assert_eq!(fused.len(), 3);
assert!(fused[0].1 > 0.0);
}
#[test]
fn test_rrf_disjoint() {
let id1 = Uuid::now_v7();
let id2 = Uuid::now_v7();
let list1 = vec![(id1, 0.9)];
let list2 = vec![(id2, 0.8)];
let fused = reciprocal_rank_fusion(&[list1, list2], 60.0);
assert_eq!(fused.len(), 2);
assert!((fused[0].1 - fused[1].1).abs() < 0.0001);
}
#[test]
fn test_rrf_single_list() {
let id1 = Uuid::now_v7();
let id2 = Uuid::now_v7();
let list1 = vec![(id1, 0.9), (id2, 0.8)];
let fused = reciprocal_rank_fusion(&[list1], 60.0);
assert_eq!(fused.len(), 2);
assert!(fused[0].1 > fused[1].1);
}
#[test]
fn test_weighted_rrf() {
let id1 = Uuid::now_v7();
let id2 = Uuid::now_v7();
let list1 = vec![(id1, 0.9), (id2, 0.8)];
let list2 = vec![(id2, 0.95), (id1, 0.85)];
let fused =
weighted_reciprocal_rank_fusion(&[list1.clone(), list2.clone()], 60.0, &[2.0, 1.0]);
assert_eq!(fused.len(), 2);
assert_eq!(fused[0].0, id1);
}
#[test]
fn test_recency_score() {
let now = chrono::Utc::now().to_rfc3339();
let score = recency_score(&now, 168.0);
assert!(score > 0.99);
let old = (chrono::Utc::now() - chrono::Duration::days(365)).to_rfc3339();
let score = recency_score(&old, 168.0);
assert!(score < 0.01);
let week_ago = (chrono::Utc::now() - chrono::Duration::hours(168)).to_rfc3339();
let score = recency_score(&week_ago, 168.0);
assert!((score - 0.5).abs() < 0.05);
}
}