Skip to content

Commit 7e2db8b

Browse files
committed
feat(search): confidence % display, date coverage in status
1 parent 94606e1 commit 7e2db8b

3 files changed

Lines changed: 41 additions & 8 deletions

File tree

src/context.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,7 @@ mod tests {
10331033
file_path: "result.md".into(),
10341034
file_id: 1,
10351035
score: 0.85,
1036+
confidence: 100.0,
10361037
heading: Some("# Result".into()),
10371038
snippet: "relevant content".into(),
10381039
docid: Some("aaa111".into()),
@@ -1066,6 +1067,7 @@ mod tests {
10661067
file_path: "long.md".into(),
10671068
file_id: 1,
10681069
score: 0.9,
1070+
confidence: 100.0,
10691071
heading: None,
10701072
snippet: "word word".into(),
10711073
docid: Some("aaa111".into()),
@@ -1103,6 +1105,7 @@ mod tests {
11031105
file_path: "main.md".into(),
11041106
file_id: f1,
11051107
score: 0.8,
1108+
confidence: 100.0,
11061109
heading: None,
11071110
snippet: "Main".into(),
11081111
docid: Some("aaa111".into()),

src/fusion.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub struct FusedResult {
2525
pub snippet: String,
2626
pub docid: Option<String>,
2727
pub lane_contributions: Vec<LaneContribution>,
28+
pub confidence: f64, // 0-100% normalized score
2829
}
2930

3031
/// Per-lane contribution details for --explain output.
@@ -110,6 +111,7 @@ pub fn rrf_fuse(lanes: &[(&str, &[RankedResult], f64)], k: usize) -> Vec<FusedRe
110111
snippet: a.snippet,
111112
docid: a.docid,
112113
lane_contributions: a.lane_contributions,
114+
confidence: 0.0,
113115
})
114116
.collect();
115117

@@ -120,6 +122,16 @@ pub fn rrf_fuse(lanes: &[(&str, &[RankedResult], f64)], k: usize) -> Vec<FusedRe
120122
.unwrap_or(std::cmp::Ordering::Equal)
121123
});
122124

125+
// Normalize confidence as percentage of max score
126+
let max_score = results.first().map(|r| r.rrf_score).unwrap_or(1.0);
127+
for r in &mut results {
128+
r.confidence = if max_score > 0.0 {
129+
(r.rrf_score / max_score) * 100.0
130+
} else {
131+
0.0
132+
};
133+
}
134+
123135
results
124136
}
125137

@@ -236,6 +248,7 @@ mod tests {
236248
heading: None,
237249
snippet: "test".to_string(),
238250
docid: None,
251+
confidence: 100.0,
239252
lane_contributions: vec![
240253
LaneContribution {
241254
lane_name: "semantic".to_string(),

src/search.rs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ fn orchestration_cache_key(query: &str) -> String {
1919
/// A single search result with metadata.
2020
pub struct SearchResult {
2121
pub score: f32,
22+
pub confidence: f64,
2223
pub file_path: String,
2324
pub heading: Option<String>,
2425
pub snippet: String,
@@ -31,6 +32,7 @@ pub struct InternalSearchResult {
3132
pub file_path: String,
3233
pub file_id: i64,
3334
pub score: f64,
35+
pub confidence: f64,
3436
pub heading: Option<String>,
3537
pub snippet: String,
3638
pub docid: Option<String>,
@@ -346,6 +348,7 @@ pub fn search_with_intelligence(
346348
file_path: f.file_path.clone(),
347349
file_id: f.file_id,
348350
score: f.rrf_score,
351+
confidence: f.confidence,
349352
heading: f.heading.clone(),
350353
snippet: f.snippet.clone(),
351354
docid: f.docid.clone(),
@@ -457,6 +460,7 @@ pub fn run_search(
457460
.iter()
458461
.map(|r| SearchResult {
459462
score: r.score as f32,
463+
confidence: r.confidence,
460464
file_path: r.file_path.clone(),
461465
heading: r.heading.clone(),
462466
snippet: r.snippet.clone(),
@@ -488,6 +492,7 @@ pub fn run_status(json: bool, data_dir: &Path) -> Result<()> {
488492
let db_path = data_dir.join("engraph.db");
489493
let store = Store::open(&db_path).context("opening store")?;
490494
let stats = store.stats()?;
495+
let date_count = store.count_files_with_dates().unwrap_or(0);
491496

492497
// Compute index size on disk (sqlite db file).
493498
let index_size = std::fs::metadata(&db_path).map(|m| m.len()).unwrap_or(0);
@@ -501,7 +506,7 @@ pub fn run_status(json: bool, data_dir: &Path) -> Result<()> {
501506
"disabled"
502507
};
503508

504-
let output = format_status(&stats, index_size, model_name, intelligence, json);
509+
let output = format_status(&stats, index_size, model_name, intelligence, date_count, json);
505510
print!("{output}");
506511
Ok(())
507512
}
@@ -526,6 +531,7 @@ pub fn format_results(results: &[SearchResult], json: bool) -> String {
526531
json!({
527532
"rank": i + 1,
528533
"score": score_rounded,
534+
"confidence": r.confidence,
529535
"file": r.file_path,
530536
"heading": r.heading,
531537
"snippet": r.snippet,
@@ -547,9 +553,9 @@ pub fn format_results(results: &[SearchResult], json: bool) -> String {
547553
};
548554
let snippet = truncate_snippet(&r.snippet, 200);
549555
out.push_str(&format!(
550-
"{:>2}. [{:.2}] {}{}{}\n {}\n",
556+
"{:>2}. [{:>3.0}%] {}{}{}\n {}\n",
551557
i + 1,
552-
r.score,
558+
r.confidence,
553559
r.file_path,
554560
heading_part,
555561
docid_part,
@@ -566,6 +572,7 @@ pub fn format_status(
566572
index_size: u64,
567573
model_name: &str,
568574
intelligence: &str,
575+
date_count: usize,
569576
json: bool,
570577
) -> String {
571578
let vault = stats.vault_path.as_deref().unwrap_or("<not set>");
@@ -581,6 +588,7 @@ pub fn format_status(
581588
"index_size": index_size,
582589
"model": model_name,
583590
"intelligence": intelligence,
591+
"files_with_dates": date_count,
584592
});
585593
if let (Some(edges), Some(wl), Some(mn)) =
586594
(stats.edge_count, stats.wikilink_count, stats.mention_count)
@@ -606,11 +614,14 @@ pub fn format_status(
606614
));
607615
}
608616
out.push_str(&format!(
609-
"Tombstones: {} (pending cleanup)\n\
617+
"Dates: {}/{} files\n\
618+
Tombstones: {} (pending cleanup)\n\
610619
Last index: {}\n\
611620
Index size: {}\n\
612621
Model: {}\n\
613622
Intelligence: {}\n",
623+
date_count,
624+
stats.file_count,
614625
stats.tombstone_count,
615626
last_indexed,
616627
format_bytes(index_size),
@@ -660,6 +671,7 @@ mod tests {
660671
fn test_format_human_result() {
661672
let results = vec![SearchResult {
662673
score: 0.87,
674+
confidence: 100.0,
663675
file_path: "foo.md".to_string(),
664676
heading: Some("## Bar".to_string()),
665677
snippet: "Some text...".to_string(),
@@ -668,27 +680,29 @@ mod tests {
668680
let output = format_results(&results, false);
669681
assert_eq!(
670682
output,
671-
" 1. [0.87] foo.md > ## Bar #ab12cd\n Some text...\n"
683+
" 1. [100%] foo.md > ## Bar #ab12cd\n Some text...\n"
672684
);
673685
}
674686

675687
#[test]
676688
fn test_format_human_result_no_docid() {
677689
let results = vec![SearchResult {
678690
score: 0.87,
691+
confidence: 100.0,
679692
file_path: "foo.md".to_string(),
680693
heading: Some("## Bar".to_string()),
681694
snippet: "Some text...".to_string(),
682695
docid: None,
683696
}];
684697
let output = format_results(&results, false);
685-
assert_eq!(output, " 1. [0.87] foo.md > ## Bar\n Some text...\n");
698+
assert_eq!(output, " 1. [100%] foo.md > ## Bar\n Some text...\n");
686699
}
687700

688701
#[test]
689702
fn test_format_json_result() {
690703
let results = vec![SearchResult {
691704
score: 0.87,
705+
confidence: 100.0,
692706
file_path: "foo.md".to_string(),
693707
heading: Some("## Bar".to_string()),
694708
snippet: "Some text...".to_string(),
@@ -699,6 +713,7 @@ mod tests {
699713
assert_eq!(parsed.len(), 1);
700714
assert_eq!(parsed[0]["rank"], 1);
701715
assert_eq!(parsed[0]["score"], 0.87);
716+
assert_eq!(parsed[0]["confidence"], 100.0);
702717
assert_eq!(parsed[0]["file"], "foo.md");
703718
assert_eq!(parsed[0]["heading"], "## Bar");
704719
assert_eq!(parsed[0]["snippet"], "Some text...");
@@ -726,11 +741,12 @@ mod tests {
726741
wikilink_count: None,
727742
mention_count: None,
728743
};
729-
let output = format_status(&stats, 2_516_582, "all-MiniLM-L6-v2", "disabled", false);
744+
let output = format_status(&stats, 2_516_582, "all-MiniLM-L6-v2", "disabled", 30, false);
730745

731746
assert!(output.contains("/path/to/vault"), "missing vault path");
732747
assert!(output.contains("42"), "missing file count");
733748
assert!(output.contains("187"), "missing chunk count");
749+
assert!(output.contains("30/42 files"), "missing date coverage");
734750
assert!(output.contains("3"), "missing tombstone count");
735751
assert!(output.contains("2026-03-19 14:30:00"), "missing last index");
736752
assert!(output.contains("2.4 MB"), "missing index size");
@@ -750,7 +766,7 @@ mod tests {
750766
wikilink_count: None,
751767
mention_count: None,
752768
};
753-
let output = format_status(&stats, 2_516_582, "all-MiniLM-L6-v2", "enabled", true);
769+
let output = format_status(&stats, 2_516_582, "all-MiniLM-L6-v2", "enabled", 30, true);
754770
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
755771

756772
assert_eq!(parsed["vault"], "/path/to/vault");
@@ -761,6 +777,7 @@ mod tests {
761777
assert_eq!(parsed["index_size"], 2_516_582);
762778
assert_eq!(parsed["model"], "all-MiniLM-L6-v2");
763779
assert_eq!(parsed["intelligence"], "enabled");
780+
assert_eq!(parsed["files_with_dates"], 30);
764781
}
765782

766783
#[test]

0 commit comments

Comments
 (0)