use crate::memory::config::WeightProfile;
use crate::memory::types::RetrievalScoreBreakdown;
pub const DEFAULT_FRESHNESS_HALF_LIFE_DAYS: f64 = 7.0;
const MS_PER_DAY: f64 = 86_400_000.0;
pub fn keyword_relevance(query: &str, content: &str) -> f64 {
let q_tokens: std::collections::HashSet<String> = tokenize(query).collect();
if q_tokens.is_empty() {
return 0.0;
}
let c_tokens: std::collections::HashSet<String> = tokenize(content).collect();
if c_tokens.is_empty() {
return 0.0;
}
let matched = q_tokens.iter().filter(|t| c_tokens.contains(*t)).count();
matched as f64 / q_tokens.len() as f64
}
fn tokenize(text: &str) -> impl Iterator<Item = String> + '_ {
text.split(|c: char| !c.is_alphanumeric())
.filter(|t| !t.is_empty())
.map(|t| t.to_lowercase())
}
pub fn freshness(updated_at_ms: i64, now_ms: i64, half_life_days: f64) -> f64 {
if half_life_days <= 0.0 {
return 1.0;
}
let age_days = (now_ms - updated_at_ms) as f64 / MS_PER_DAY;
if age_days <= 0.0 {
return 1.0;
}
0.5_f64.powf(age_days / half_life_days)
}
pub fn hybrid_score(
profile: &WeightProfile,
graph_relevance: f64,
vector_similarity: f64,
keyword_relevance: f64,
freshness: f64,
) -> RetrievalScoreBreakdown {
let final_score = profile.graph * graph_relevance
+ profile.vector * vector_similarity
+ profile.keyword * keyword_relevance
+ profile.freshness * freshness;
RetrievalScoreBreakdown {
keyword_relevance,
vector_similarity,
graph_relevance,
episodic_relevance: 0.0,
freshness,
final_score,
}
}
#[cfg(test)]
#[path = "scoring_tests.rs"]
mod tests;