codemem_engine/
scoring.rs1use crate::bm25;
4use chrono::{DateTime, Utc};
5use codemem_core::{MemoryNode, ScoreBreakdown};
6use codemem_storage::graph::GraphEngine;
7
8pub fn graph_strength_for_memory(graph: &GraphEngine, memory_id: &str) -> f64 {
18 let metrics = match graph.raw_graph_metrics_for_memory(memory_id) {
19 Some(m) => m,
20 None => return 0.0,
21 };
22
23 let code_score = if metrics.code_neighbor_count > 0 {
25 let connectivity = (metrics.code_neighbor_count as f64 / 5.0).min(1.0);
26 let avg_edge_w = (metrics.total_edge_weight / metrics.code_neighbor_count as f64).min(1.0);
27 0.4 * metrics.max_pagerank
28 + 0.3 * metrics.max_betweenness
29 + 0.2 * connectivity
30 + 0.1 * avg_edge_w
31 } else {
32 0.0
33 };
34
35 let memory_score = if metrics.memory_neighbor_count > 0 {
37 let connectivity = (metrics.memory_neighbor_count as f64 / 10.0).min(1.0);
38 let avg_edge_w =
39 (metrics.memory_edge_weight / metrics.memory_neighbor_count as f64).min(1.0);
40 0.6 * connectivity + 0.4 * avg_edge_w
41 } else {
42 0.0
43 };
44
45 let score = match (
48 metrics.code_neighbor_count > 0,
49 metrics.memory_neighbor_count > 0,
50 ) {
51 (true, true) => 0.7 * code_score + 0.3 * memory_score,
52 (true, false) => code_score,
53 (false, true) => memory_score,
54 (false, false) => 0.0,
55 };
56
57 score.min(1.0)
58}
59
60pub fn truncate_content(s: &str, max: usize) -> String {
63 if s.len() <= max {
64 s.to_string()
65 } else {
66 let mut end = max;
67 while end > 0 && !s.is_char_boundary(end) {
68 end -= 1;
69 }
70 format!("{}...", &s[..end])
71 }
72}
73
74pub fn compute_score(
80 memory: &MemoryNode,
81 query_tokens: &[&str],
82 vector_similarity: f64,
83 graph: &GraphEngine,
84 bm25: &bm25::Bm25Index,
85 now: DateTime<Utc>,
86) -> ScoreBreakdown {
87 let token_overlap = if query_tokens.is_empty() {
90 0.0
91 } else {
92 let indexed_score = bm25.score_with_tokens_str(query_tokens, &memory.id);
95 if indexed_score > 0.0 {
96 indexed_score
97 } else {
98 bm25.score_text_with_tokens_str(query_tokens, &memory.content)
99 }
100 };
101
102 let age_hours = (now - memory.updated_at).num_hours().max(0) as f64;
104 let temporal = (-age_hours / (30.0 * 24.0)).exp();
105
106 let tag_matching = if !query_tokens.is_empty() {
110 let tag_str: String = memory.tags.join(" ").to_lowercase();
111 let matches = query_tokens
112 .iter()
113 .filter(|qt| tag_str.contains(**qt))
114 .count();
115 matches as f64 / query_tokens.len() as f64
116 } else {
117 0.0
118 };
119
120 let access_hours = (now - memory.last_accessed_at).num_hours().max(0) as f64;
122 let recency = (-access_hours / (7.0 * 24.0)).exp();
123
124 let graph_strength = graph_strength_for_memory(graph, &memory.id);
129
130 ScoreBreakdown {
131 vector_similarity,
132 graph_strength,
133 token_overlap,
134 temporal,
135 tag_matching,
136 importance: memory.importance,
137 confidence: memory.confidence,
138 recency,
139 }
140}
141
142#[cfg(test)]
143#[path = "tests/scoring_tests.rs"]
144mod tests;