Improve evidence search ranking and diagnostics
This commit is contained in:
@@ -9,14 +9,14 @@ use crate::routes::query_support::{
|
||||
use crate::routes::web_shell::load_sidebar_tree_html;
|
||||
use crate::routes::{evidence, local_folder_source, local_search_index};
|
||||
use crate::ssr::pages::search::SearchPage;
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::Json;
|
||||
use bridge_runtime::RuntimeQueryEnvelopeWire;
|
||||
use core_protocol::{EvidenceSearchMode, EvidenceSearchResult};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||
const HEADER_QUERY_NAME: &str = "x-query-name";
|
||||
@@ -239,6 +239,16 @@ pub async fn documents(
|
||||
filters.exact.unwrap_or(false),
|
||||
)?
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|evidence| {
|
||||
let path = evidence
|
||||
.source
|
||||
.resource_path
|
||||
.as_deref()
|
||||
.unwrap_or(evidence.source.owner_document_path.as_str());
|
||||
local_search_index::local_index_relative_path_is_included(path, &user_settings)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -438,12 +448,6 @@ pub async fn update_local_index_settings(
|
||||
&effective_workspace_id,
|
||||
&root_path,
|
||||
)?;
|
||||
let refreshed = local_search_index::refresh_local_search_index_with_settings(
|
||||
&root_path,
|
||||
root_uri,
|
||||
&effective_workspace_id,
|
||||
&effective_settings,
|
||||
)?;
|
||||
let status = local_search_index::local_index_status_with_settings(
|
||||
&root_path,
|
||||
root_uri,
|
||||
@@ -459,7 +463,6 @@ pub async fn update_local_index_settings(
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"settings": settings,
|
||||
"index": refreshed,
|
||||
"result": status,
|
||||
"meta": {
|
||||
"owner": "mnote-web",
|
||||
@@ -481,26 +484,72 @@ fn merge_local_search_with_evidence_results(
|
||||
if direct_evidence_results.is_empty() {
|
||||
return (result, evidence_results);
|
||||
}
|
||||
let mut result_items = result
|
||||
let original_items = result
|
||||
.get("results")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let mut seen_evidence_ids = evidence_results
|
||||
.iter()
|
||||
.map(|item| item.evidence_id.clone())
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
let original_evidence_results = std::mem::take(&mut evidence_results);
|
||||
let mut result_items = Vec::new();
|
||||
let mut merged_evidence_results = Vec::new();
|
||||
let mut seen_result_ids = std::collections::HashSet::new();
|
||||
let mut seen_evidence_ids = std::collections::HashSet::new();
|
||||
let mut seen_paths = std::collections::HashSet::new();
|
||||
for evidence in direct_evidence_results {
|
||||
if result_items.len() >= limit || !seen_evidence_ids.insert(evidence.evidence_id.clone()) {
|
||||
continue;
|
||||
}
|
||||
result_items.push(search_result_from_evidence(&evidence));
|
||||
evidence_results.push(evidence);
|
||||
let item = search_result_from_evidence(&evidence);
|
||||
if let Some(id) = item.get("id").and_then(Value::as_str) {
|
||||
seen_result_ids.insert(id.to_string());
|
||||
}
|
||||
if let Some(path) = search_result_dedupe_path(&item) {
|
||||
seen_paths.insert(path);
|
||||
}
|
||||
result_items.push(item);
|
||||
merged_evidence_results.push(evidence);
|
||||
}
|
||||
for (index, item) in original_items.into_iter().enumerate() {
|
||||
if result_items.len() >= limit {
|
||||
break;
|
||||
}
|
||||
let item_id = item
|
||||
.get("id")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_default();
|
||||
if !item_id.is_empty() && !seen_result_ids.insert(item_id) {
|
||||
continue;
|
||||
}
|
||||
if let Some(path) = search_result_dedupe_path(&item) {
|
||||
if !seen_paths.insert(path) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(evidence) = original_evidence_results.get(index).cloned() {
|
||||
merged_evidence_results.push(evidence);
|
||||
}
|
||||
result_items.push(item);
|
||||
}
|
||||
if let Some(map) = result.as_object_mut() {
|
||||
map.insert("results".into(), Value::Array(result_items));
|
||||
}
|
||||
(result, evidence_results)
|
||||
(result, merged_evidence_results)
|
||||
}
|
||||
|
||||
fn search_result_dedupe_path(item: &Value) -> Option<String> {
|
||||
let source_kind = item
|
||||
.get("sourceKind")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
if source_kind != "local_folder" {
|
||||
return None;
|
||||
}
|
||||
item.get("path")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
fn search_result_from_evidence(evidence: &EvidenceSearchResult) -> Value {
|
||||
@@ -533,6 +582,7 @@ fn search_result_from_evidence(evidence: &EvidenceSearchResult) -> Value {
|
||||
"rootUri": source.root_uri,
|
||||
"snippet": evidence.quote,
|
||||
"score": evidence.score,
|
||||
"matchInfo": evidence.match_info,
|
||||
"publicPath": source.open_action.url,
|
||||
"evidence": evidence_value,
|
||||
"source": {
|
||||
@@ -890,12 +940,12 @@ fn stamp_search_headers(headers: &mut HeaderMap) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::{build_app, AppConfig, AppState};
|
||||
use crate::app::{AppConfig, AppState, build_app};
|
||||
use crate::routes::local_search_index;
|
||||
use axum::body::{to_bytes, Body};
|
||||
use axum::body::{Body, to_bytes};
|
||||
use axum::http::{Request, StatusCode};
|
||||
use control_plane::{DirectoryGrantInput, UpsertUserInput};
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
use std::fs;
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
@@ -1152,26 +1202,33 @@ mod tests {
|
||||
Some("README.md")
|
||||
);
|
||||
assert!(!payload["evidence"].as_array().expect("evidence").is_empty());
|
||||
assert!(home["tags"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|tag| tag.as_str() == Some("alpha")));
|
||||
assert!(home["backlinks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|link| link.as_str() == Some("Daily")));
|
||||
assert!(home["resourceRefs"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|reference| reference.as_str() == Some("assets/spec.pdf")));
|
||||
assert!(root
|
||||
.join(".mnote")
|
||||
.join("index")
|
||||
.join("search-index.json")
|
||||
.exists());
|
||||
assert!(
|
||||
home["tags"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|tag| tag.as_str() == Some("alpha"))
|
||||
);
|
||||
assert!(
|
||||
home["backlinks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|link| link.as_str() == Some("Daily"))
|
||||
);
|
||||
assert!(
|
||||
home["resourceRefs"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|reference| reference.as_str() == Some("assets/spec.pdf"))
|
||||
);
|
||||
assert!(
|
||||
root.join(".mnote")
|
||||
.join("index")
|
||||
.join("search-index.json")
|
||||
.exists()
|
||||
);
|
||||
let evidence_db = root.join(".mnote").join("index").join("evidence.sqlite");
|
||||
assert!(evidence_db.exists(), "evidence sqlite should be built");
|
||||
let connection = rusqlite::Connection::open(&evidence_db).expect("open evidence sqlite");
|
||||
@@ -1201,11 +1258,13 @@ mod tests {
|
||||
locator["schema"].as_str(),
|
||||
Some("mnote.evidence_locator.v1")
|
||||
);
|
||||
assert!(payload["recent"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item["documentId"].as_str() == Some("local-md:README.md")));
|
||||
assert!(
|
||||
payload["recent"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item["documentId"].as_str() == Some("local-md:README.md"))
|
||||
);
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
@@ -1409,10 +1468,32 @@ mod tests {
|
||||
.await
|
||||
.expect("create settings response");
|
||||
assert_eq!(create_response.status(), StatusCode::OK);
|
||||
let create_refresh_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/search/local-index/refresh")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_test")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"workspaceId": "local-ws-settings-delete",
|
||||
"rootUri": root_uri
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("create refresh request"),
|
||||
)
|
||||
.await
|
||||
.expect("create refresh response");
|
||||
assert_eq!(create_refresh_response.status(), StatusCode::OK);
|
||||
assert!(root.join(".mnote/index/search-index.json").exists());
|
||||
assert!(root.join(".mnote/index/evidence.sqlite").exists());
|
||||
|
||||
let delete_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("PUT")
|
||||
@@ -1440,18 +1521,36 @@ mod tests {
|
||||
.await
|
||||
.expect("delete body");
|
||||
let delete_payload: Value = serde_json::from_slice(&delete_body).expect("delete json");
|
||||
assert_eq!(
|
||||
delete_payload["index"]["indexedPaths"]
|
||||
.as_array()
|
||||
.map(Vec::len),
|
||||
Some(0)
|
||||
);
|
||||
assert_eq!(
|
||||
delete_payload["result"]["settings"]["includePaths"]
|
||||
.as_array()
|
||||
.map(Vec::len),
|
||||
Some(0)
|
||||
);
|
||||
assert!(root.join(".mnote/index/search-index.json").exists());
|
||||
assert!(root.join(".mnote/index/evidence.sqlite").exists());
|
||||
|
||||
let delete_refresh_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/search/local-index/refresh")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_test")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"workspaceId": "local-ws-settings-delete",
|
||||
"rootUri": root_uri
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("delete refresh request"),
|
||||
)
|
||||
.await
|
||||
.expect("delete refresh response");
|
||||
assert_eq!(delete_refresh_response.status(), StatusCode::OK);
|
||||
assert!(!root.join(".mnote/index/search-index.json").exists());
|
||||
assert!(!root.join(".mnote/index/evidence.sqlite").exists());
|
||||
|
||||
@@ -1508,11 +1607,13 @@ mod tests {
|
||||
backlinks_payload["meta"]["queryName"].as_str(),
|
||||
Some("search.local_index.backlinks")
|
||||
);
|
||||
assert!(backlinks_payload["result"]["backlinks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item["documentId"].as_str() == Some("local-md:README.md")));
|
||||
assert!(
|
||||
backlinks_payload["result"]["backlinks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|item| item["documentId"].as_str() == Some("local-md:README.md"))
|
||||
);
|
||||
|
||||
let tags_response = app()
|
||||
.oneshot(
|
||||
|
||||
Reference in New Issue
Block a user