lean_ctx/core/session_summary/
recall.rs1use super::record::SummaryRecord;
5use super::store::SummaryStore;
6
7#[derive(Debug, Clone)]
9pub struct RecallHit {
10 pub record: SummaryRecord,
11 pub score: f32,
12 pub mode: &'static str,
13}
14
15pub fn recall(project_root: &str, query: &str, top_k: usize) -> Vec<RecallHit> {
17 let store = SummaryStore::load_or_create(project_root);
18 if store.summaries.is_empty() || query.trim().is_empty() {
19 return Vec::new();
20 }
21 #[cfg(feature = "embeddings")]
22 {
23 if let Some(hits) = semantic(&store, query, top_k) {
24 return hits;
25 }
26 }
27 lexical(&store, query, top_k)
28}
29
30fn lexical(store: &SummaryStore, query: &str, top_k: usize) -> Vec<RecallHit> {
31 store
32 .search_lexical(query, top_k)
33 .into_iter()
34 .map(|(i, score)| RecallHit {
35 record: store.summaries[i].clone(),
36 score: score as f32,
37 mode: "lexical",
38 })
39 .collect()
40}
41
42#[cfg(feature = "embeddings")]
45fn semantic(store: &SummaryStore, query: &str, top_k: usize) -> Option<Vec<RecallHit>> {
46 let cfg = crate::core::config::Config::load();
47 let profile = crate::core::config::MemoryProfile::effective(&cfg);
48 if !profile.embeddings_enabled() {
49 return None;
50 }
51 let engine = crate::core::embeddings::try_shared_engine()?;
53 let q = engine.embed_query(query).ok()?;
54
55 let mut scored: Vec<RecallHit> = Vec::new();
56 for rec in &store.summaries {
57 if let Ok(emb) = engine.embed_query(&rec.searchable_text()) {
58 scored.push(RecallHit {
59 record: rec.clone(),
60 score: cosine(&q, &emb),
61 mode: "semantic",
62 });
63 }
64 }
65 if scored.is_empty() {
66 return None;
67 }
68 scored.sort_by(|a, b| {
69 b.score
70 .partial_cmp(&a.score)
71 .unwrap_or(std::cmp::Ordering::Equal)
72 .then_with(|| b.record.created_at.cmp(&a.record.created_at))
73 });
74 scored.truncate(top_k);
75 Some(scored)
76}
77
78#[cfg(feature = "embeddings")]
79fn cosine(a: &[f32], b: &[f32]) -> f32 {
80 if a.len() != b.len() {
81 return 0.0;
82 }
83 let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
84 let na: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
85 let nb: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
86 if na == 0.0 || nb == 0.0 {
87 0.0
88 } else {
89 dot / (na * nb)
90 }
91}