Improve evidence search ranking and diagnostics
This commit is contained in:
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user