zeph_memory/semantic/
algorithms.rs1use crate::math::cosine_similarity;
5use crate::types::MessageId;
6
7#[allow(clippy::implicit_hasher)]
8pub fn apply_temporal_decay(
9 ranked: &mut [(MessageId, f64)],
10 timestamps: &std::collections::HashMap<MessageId, i64>,
11 half_life_days: u32,
12) {
13 if half_life_days == 0 {
14 return;
15 }
16 let now = std::time::SystemTime::now()
17 .duration_since(std::time::UNIX_EPOCH)
18 .unwrap_or_default()
19 .as_secs()
20 .cast_signed();
21 let lambda = std::f64::consts::LN_2 / f64::from(half_life_days);
22
23 for (msg_id, score) in ranked.iter_mut() {
24 if let Some(&ts) = timestamps.get(msg_id) {
25 #[allow(clippy::cast_precision_loss)]
26 let age_days = (now - ts).max(0) as f64 / 86400.0;
27 *score *= (-lambda * age_days).exp();
28 }
29 }
30}
31
32#[allow(clippy::implicit_hasher)]
33pub fn apply_mmr(
34 ranked: &[(MessageId, f64)],
35 vectors: &std::collections::HashMap<MessageId, Vec<f32>>,
36 lambda: f32,
37 limit: usize,
38) -> Vec<(MessageId, f64)> {
39 if ranked.is_empty() || limit == 0 {
40 return Vec::new();
41 }
42
43 tracing::debug!(
44 candidates = ranked.len(),
45 limit,
46 lambda = %lambda,
47 "mmr: starting re-ranking"
48 );
49
50 let lambda = f64::from(lambda);
51 let mut selected: Vec<(MessageId, f64)> = Vec::with_capacity(limit);
52 let mut remaining: Vec<(MessageId, f64)> = ranked.to_vec();
53
54 while selected.len() < limit && !remaining.is_empty() {
55 let best_idx = if selected.is_empty() {
56 0
58 } else {
59 let mut best = 0usize;
60 let mut best_score = f64::NEG_INFINITY;
61
62 for (i, &(cand_id, relevance)) in remaining.iter().enumerate() {
63 let max_sim = if let Some(cand_vec) = vectors.get(&cand_id) {
64 selected
65 .iter()
66 .filter_map(|(sel_id, _)| vectors.get(sel_id))
67 .map(|sel_vec| f64::from(cosine_similarity(cand_vec, sel_vec)))
68 .fold(f64::NEG_INFINITY, f64::max)
69 } else {
70 0.0
71 };
72 let max_sim = if max_sim == f64::NEG_INFINITY {
73 0.0
74 } else {
75 max_sim
76 };
77 let mmr_score = lambda * relevance - (1.0 - lambda) * max_sim;
78 if mmr_score > best_score {
79 best_score = mmr_score;
80 best = i;
81 }
82 }
83 best
84 };
85
86 selected.push(remaining.remove(best_idx));
87 }
88
89 tracing::debug!(selected = selected.len(), "mmr: re-ranking complete");
90
91 selected
92}