Improve evidence search ranking and diagnostics

This commit is contained in:
lix-2026
2026-06-06 00:33:02 +08:00
parent 4dbd9a978b
commit 5157a5960c
6 changed files with 891 additions and 235 deletions
+138 -3
View File
@@ -61,19 +61,33 @@ pub(crate) async fn search_payload(
body.top_k,
)? {
enrich_citation_links(&mut results);
return Ok(json!(EvidenceSearchResponse { ok: true, results }));
return Ok(json!(EvidenceSearchResponse {
ok: true,
results,
diagnostics: None
}));
}
}
if let Some(mut results) = local_search_index::query_evidence_sqlite_results(
if let Some(mut results) = local_search_index::query_evidence_sqlite_results_with_mode(
&root_path,
&query,
evidence_owner_filter,
body.top_k,
false,
)? {
enrich_sqlite_evidence_results(&mut results, &root_path, &query);
enrich_citation_links(&mut results);
if !results.is_empty() || !query.is_empty() {
let response = EvidenceSearchResponse { ok: true, results };
let diagnostics = Some(evidence_search_diagnostics(
&query,
&results,
"evidence_sqlite",
));
let response = EvidenceSearchResponse {
ok: true,
results,
diagnostics,
};
return Ok(json!(response));
}
}
@@ -97,6 +111,7 @@ pub(crate) async fn search_payload(
enrich_citation_links(&mut results);
results
},
diagnostics: None,
};
Ok(json!(response))
}
@@ -154,6 +169,78 @@ fn enrich_citation_links(results: &mut [EvidenceSearchResult]) {
}
}
fn evidence_search_diagnostics(
query: &str,
results: &[EvidenceSearchResult],
backend: &str,
) -> Value {
let query_tokens = evidence_query_tokens(query);
let strong_match_count = results
.iter()
.filter(|result| result.match_info.as_ref().is_some_and(|info| info.strong))
.count();
let weak_match_count = results.len().saturating_sub(strong_match_count);
let reason = if results.is_empty() {
"no_evidence_match"
} else if strong_match_count == 0 {
"only_weak_partial_matches"
} else {
"ranked_evidence_matches"
};
json!({
"backend": backend,
"queryTokens": query_tokens,
"resultCount": results.len(),
"strongMatchCount": strong_match_count,
"weakMatchCount": weak_match_count,
"reason": reason,
"suggestedQueries": suggested_evidence_queries(query),
})
}
fn evidence_query_tokens(query: &str) -> Vec<String> {
query
.split(|ch: char| {
ch.is_whitespace()
|| matches!(
ch,
',' | ';'
| ':'
| ''
| ''
| ''
| '。'
| '、'
| '('
| ')'
| '['
| ']'
| '{'
| '}'
)
})
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.collect()
}
fn suggested_evidence_queries(query: &str) -> Vec<String> {
let tokens = evidence_query_tokens(query);
let mut suggestions = Vec::new();
if tokens.len() > 1 {
suggestions.push(tokens.join(" "));
}
for token in tokens {
if token.chars().count() >= 3 {
suggestions.push(token.chars().take(2).collect::<String>());
}
}
suggestions.sort();
suggestions.dedup();
suggestions
}
fn citation_markdown_for_locator(locator: &EvidenceLocator) -> String {
let label = citation_label_for_locator(locator);
let url = citation_url_for_locator(locator);
@@ -537,6 +624,7 @@ fn source_map_block_result(
quote: block.text.clone(),
score,
source,
match_info: None,
citation_url: None,
citation_label: None,
citation_markdown: None,
@@ -747,6 +835,11 @@ pub(crate) fn evidence_results_from_local_search(
.to_string(),
score: result.get("score").and_then(Value::as_f64).unwrap_or(0.0),
source: locator,
match_info: result
.get("evidence")
.and_then(|value| value.get("matchInfo"))
.cloned()
.and_then(|value| serde_json::from_value(value).ok()),
citation_url: None,
citation_label: None,
citation_markdown: None,
@@ -1251,6 +1344,7 @@ mod tests {
quote: "NeedleToken 原文定位".into(),
score: 1.0,
source: locator,
match_info: None,
citation_url: None,
citation_label: None,
citation_markdown: None,
@@ -1285,6 +1379,47 @@ mod tests {
.contains("spec.pdf"));
}
#[test]
fn evidence_search_diagnostics_marks_partial_matches() {
let results = vec![EvidenceSearchResult {
evidence_id: "ev1".into(),
quote: "羧酸需要被保护".into(),
score: 0.7,
source: EvidenceLocator::new(
"file:///workspace",
"local-md:docs~2FPage.md",
"docs/Page.md",
EvidenceResourceKind::Markdown,
EvidenceOpenAction {
action_type: "mnote.open_resource_locator".into(),
url: "/documents/local-md:docs~2FPage.md".into(),
params: json!({}),
},
),
match_info: Some(core_protocol::EvidenceSearchMatchInfo {
rank: Some(1),
matched_terms: vec!["羧酸".into(), "保护".into()],
missing_terms: vec!["保护基".into()],
match_mode: "cjk_partial".into(),
match_scope: "block".into(),
strong: false,
}),
citation_url: None,
citation_label: None,
citation_markdown: None,
}];
let diagnostics = evidence_search_diagnostics("羧酸 保护基", &results, "evidence_sqlite");
assert_eq!(diagnostics["backend"].as_str(), Some("evidence_sqlite"));
assert_eq!(diagnostics["queryTokens"].as_array().map(Vec::len), Some(2));
assert_eq!(diagnostics["strongMatchCount"].as_u64(), Some(0));
assert_eq!(diagnostics["weakMatchCount"].as_u64(), Some(1));
assert_eq!(
diagnostics["reason"].as_str(),
Some("only_weak_partial_matches")
);
}
#[test]
fn evidence_read_uses_source_map_context_and_section_blocks() {
let root = std::env::temp_dir().join(format!(