use crate::base::types::RecallResult;
use std::collections::HashMap;
pub(crate) const KEYWORD_RESERVE_SLOTS: usize = 3;
pub(crate) const KEYWORD_RESERVE_MIN_SIM: f64 = 0.25;
pub(crate) const KEYWORD_RESERVE_MIN_LEX: f64 = 0.5;
pub(crate) const LEX_STRONG: f64 = 0.75;
pub(crate) fn lexical_strengths(ranked: &[(String, f64)]) -> HashMap<String, f64> {
let mut best: HashMap<String, f64> = HashMap::new();
for (rid, rank) in ranked {
best.entry(rid.clone())
.and_modify(|r| *r = r.min(*rank))
.or_insert(*rank);
}
if best.is_empty() {
return best;
}
let top = best.values().fold(f64::INFINITY, |a, &r| a.min(r));
if top >= -1e-9 {
for v in best.values_mut() {
*v = 1.0;
}
return best;
}
for v in best.values_mut() {
*v = (*v / top).clamp(0.0, 1.0);
}
best
}
pub(crate) fn quantize_score(s: f64) -> f64 {
(s * 1e6).round()
}
pub(crate) fn rank_cmp(a: &RecallResult, b: &RecallResult) -> std::cmp::Ordering {
quantize_score(b.score)
.total_cmp(&quantize_score(a.score))
.then_with(|| a.rid.cmp(&b.rid))
}
pub(crate) fn keyword_lane_boost(keyword_boost: f64, sim: f64, lex: f64) -> f64 {
let t = crate::base::tuning::tuning();
let kb = if t.keyword_boost_override >= 0.0 {
t.keyword_boost_override
} else {
keyword_boost
};
kb * lex.clamp(0.0, 1.0) * (1.0 - sim).max(0.2)
}
pub(crate) fn apply_keyword_reserve(
scored: &mut [RecallResult],
lex_by_rid: &HashMap<String, f64>,
top_k: usize,
) {
scored.sort_by(rank_cmp);
let quant = quantize_score;
let cutoff_idx = top_k.min(scored.len()).saturating_sub(1);
let cutoff_score = scored.get(cutoff_idx).map(|r| r.score).unwrap_or(0.0);
let mut kw_below: Vec<(usize, f64, f64)> = scored
.iter()
.enumerate()
.filter(|(_, r)| {
r.why_retrieved
.iter()
.any(|w| w == "keyword_match" || w.starts_with("claims_match"))
&& quant(r.score) < quant(cutoff_score)
})
.map(|(i, r)| {
let lex = crate::engine::claims_lane::claims_lex_strength(&r.why_retrieved)
.unwrap_or_else(|| lex_by_rid.get(r.rid.as_str()).copied().unwrap_or(0.0));
(i, lex, r.scores.similarity)
})
.filter(|(_, lex, sim)| *lex >= KEYWORD_RESERVE_MIN_LEX || *sim >= KEYWORD_RESERVE_MIN_SIM)
.collect();
{
let snapshot: &[RecallResult] = scored;
kw_below.sort_by(|a, b| {
b.1.total_cmp(&a.1)
.then(b.2.total_cmp(&a.2))
.then_with(|| snapshot[a.0].rid.cmp(&snapshot[b.0].rid))
});
}
for (pos, (idx, _, _)) in kw_below.into_iter().take(KEYWORD_RESERVE_SLOTS).enumerate() {
scored[idx].score = cutoff_score + 0.001 * (KEYWORD_RESERVE_SLOTS - pos) as f64;
scored[idx]
.why_retrieved
.push("keyword_reserved".to_string());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::types::{RecallResult, ScoreBreakdown, ScoreContributions};
fn hit(rid: &str, score: f64, sim: f64, why: &[&str]) -> RecallResult {
RecallResult {
rid: rid.to_string(),
memory_type: "semantic".to_string(),
text: String::new(),
created_at: 0.0,
importance: 0.5,
valence: 0.0,
score,
scores: ScoreBreakdown {
similarity: sim,
decay: 0.0,
recency: 0.0,
importance: 0.5,
graph_proximity: 0.0,
contributions: ScoreContributions {
similarity: 0.0,
decay: 0.0,
recency: 0.0,
importance: 0.0,
graph_proximity: 0.0,
},
valence_multiplier: 1.0,
},
why_retrieved: why.iter().map(|s| s.to_string()).collect(),
metadata: serde_json::Value::Null,
namespace: "default".to_string(),
certainty: 0.8,
domain: "general".to_string(),
source: "user".to_string(),
emotional_state: None,
current_status: Default::default(),
superseded_by: None,
disputed_with: Vec::new(),
aged_last_verified: None,
best_span: None,
pack: None,
event_time_min: None,
event_time_max: None,
}
}
#[test]
fn strengths_normalize_best_to_one_and_noise_below_reserve_floor() {
let ranked = vec![
("verbatim".to_string(), -12.4),
("noise1".to_string(), -2.1),
("noise2".to_string(), -1.7),
];
let lex = lexical_strengths(&ranked);
assert_eq!(lex["verbatim"], 1.0);
assert!(lex["noise1"] < KEYWORD_RESERVE_MIN_LEX);
assert!(lex["noise2"] < lex["noise1"]);
}
#[test]
fn strengths_keep_best_rank_across_duplicate_rows() {
let ranked = vec![
("a".to_string(), -4.0),
("a".to_string(), -9.0),
("b".to_string(), -9.0),
];
let lex = lexical_strengths(&ranked);
assert_eq!(lex["a"], 1.0);
assert_eq!(lex["b"], 1.0);
}
#[test]
fn uniform_ranks_reproduce_the_pre_fusion_lane() {
let ranked = vec![("x".to_string(), -3.0), ("y".to_string(), -3.0)];
let lex = lexical_strengths(&ranked);
for rid in ["x", "y"] {
let old = 0.31 * (1.0f64 - 0.4).max(0.2);
assert_eq!(keyword_lane_boost(0.31, 0.4, lex[rid]), old);
}
}
#[test]
fn degenerate_non_negative_ranks_do_not_divide_by_zero() {
let ranked = vec![("a".to_string(), 0.0), ("b".to_string(), 0.0)];
let lex = lexical_strengths(&ranked);
assert_eq!(lex["a"], 1.0);
assert_eq!(lex["b"], 1.0);
assert!(lexical_strengths(&[]).is_empty());
}
#[test]
fn reserve_admits_diluted_exact_match_by_lexical_strength() {
let mut scored = vec![
hit("top1", 0.90, 0.60, &[]),
hit("top2", 0.80, 0.55, &[]),
hit("top3", 0.70, 0.50, &[]),
hit("verbatim", 0.10, 0.057, &["keyword_match"]),
];
let mut lex = HashMap::new();
lex.insert("verbatim".to_string(), 1.0);
apply_keyword_reserve(&mut scored, &lex, 3);
let v = scored.iter().find(|r| r.rid == "verbatim").unwrap();
assert!(
v.why_retrieved.iter().any(|w| w == "keyword_reserved"),
"lexical-top match must win a reserve slot at any cosine"
);
assert!(v.score > 0.70, "lifted just above the cutoff");
}
#[test]
fn reserve_still_admits_by_cosine_and_refuses_weak_noise() {
let mut scored = vec![
hit("top1", 0.90, 0.60, &[]),
hit("top2", 0.80, 0.55, &[]),
hit("top3", 0.70, 0.50, &[]),
hit("cosine_ok", 0.20, 0.30, &["keyword_match"]),
hit("weak_noise", 0.15, 0.10, &["keyword_match"]),
];
let mut lex = HashMap::new();
lex.insert("cosine_ok".to_string(), 0.2);
lex.insert("weak_noise".to_string(), 0.2);
apply_keyword_reserve(&mut scored, &lex, 3);
assert!(scored
.iter()
.find(|r| r.rid == "cosine_ok")
.unwrap()
.why_retrieved
.iter()
.any(|w| w == "keyword_reserved"));
assert!(!scored
.iter()
.find(|r| r.rid == "weak_noise")
.unwrap()
.why_retrieved
.iter()
.any(|w| w == "keyword_reserved"));
}
#[test]
fn reserve_ranks_by_lexical_strength_before_cosine() {
let mut scored = vec![
hit("top1", 0.90, 0.60, &[]),
hit("top2", 0.85, 0.55, &[]),
hit("top3", 0.80, 0.50, &[]),
hit("lex_hi", 0.10, 0.05, &["keyword_match"]),
hit("lex_mid", 0.10, 0.06, &["keyword_match"]),
hit("lex_low_cos_hi", 0.10, 0.40, &["keyword_match"]),
hit("lex_hi2", 0.10, 0.07, &["keyword_match"]),
];
let mut lex = HashMap::new();
lex.insert("lex_hi".to_string(), 1.0);
lex.insert("lex_mid".to_string(), 0.8);
lex.insert("lex_hi2".to_string(), 0.9);
lex.insert("lex_low_cos_hi".to_string(), 0.1);
apply_keyword_reserve(&mut scored, &lex, 3);
let reserved: Vec<&str> = scored
.iter()
.filter(|r| r.why_retrieved.iter().any(|w| w == "keyword_reserved"))
.map(|r| r.rid.as_str())
.collect();
assert_eq!(reserved.len(), KEYWORD_RESERVE_SLOTS);
assert!(reserved.contains(&"lex_hi"));
assert!(reserved.contains(&"lex_hi2"));
assert!(reserved.contains(&"lex_mid"));
assert!(!reserved.contains(&"lex_low_cos_hi"));
}
}