Skip to main content

zeph_memory/semantic/
algorithms.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use 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            // Pick highest relevance first
57            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}