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