fix(workspace): thread document path through search results (#503)

* fix(workspace): thread document path through search results

Memory search results were showing chunk UUIDs instead of source file
paths. Thread document_path through RankedResult, SearchResult, and the
RRF fusion pipeline so handlers can display the actual file path.

Fixes #481

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* refactor: use into_iter to move values instead of cloning

Address review feedback: consume results with into_iter() to move
String fields directly instead of cloning them.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Zaki Manian
2026-03-03 23:31:30 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent c239a4fc2a
commit d562dc8d90
5 changed files with 26 additions and 11 deletions
+3 -3
View File
@@ -159,10 +159,10 @@ pub async fn memory_search_handler(
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let hits: Vec<SearchHit> = results
.iter()
.into_iter()
.map(|r| SearchHit {
path: r.document_id.to_string(),
content: r.content.clone(),
path: r.document_path,
content: r.content,
score: r.score as f64,
})
.collect();
+6 -4
View File
@@ -515,7 +515,7 @@ impl WorkspaceStore for LibSqlBackend {
let mut rows = conn
.query(
r#"
SELECT c.id, c.document_id, c.content
SELECT c.id, c.document_id, d.path, c.content
FROM memory_chunks_fts fts
JOIN memory_chunks c ON c._rowid = fts.rowid
JOIN memory_documents d ON d.id = c.document_id
@@ -542,7 +542,8 @@ impl WorkspaceStore for LibSqlBackend {
results.push(RankedResult {
chunk_id: get_text(&row, 0).parse().unwrap_or_default(),
document_id: get_text(&row, 1).parse().unwrap_or_default(),
content: get_text(&row, 2),
document_path: get_text(&row, 2),
content: get_text(&row, 3),
rank: results.len() as u32 + 1,
});
}
@@ -563,7 +564,7 @@ impl WorkspaceStore for LibSqlBackend {
let mut rows = conn
.query(
r#"
SELECT c.id, c.document_id, c.content
SELECT c.id, c.document_id, d.path, c.content
FROM vector_top_k('idx_memory_chunks_embedding', vector(?1), ?2) AS top_k
JOIN memory_chunks c ON c._rowid = top_k.id
JOIN memory_documents d ON d.id = c.document_id
@@ -587,7 +588,8 @@ impl WorkspaceStore for LibSqlBackend {
results.push(RankedResult {
chunk_id: get_text(&row, 0).parse().unwrap_or_default(),
document_id: get_text(&row, 1).parse().unwrap_or_default(),
content: get_text(&row, 2),
document_path: get_text(&row, 2),
content: get_text(&row, 3),
rank: results.len() as u32 + 1,
});
}
+4 -2
View File
@@ -95,15 +95,17 @@ impl Tool for MemorySearchTool {
.await
.map_err(|e| ToolError::ExecutionFailed(format!("Search failed: {}", e)))?;
let result_count = results.len();
let output = serde_json::json!({
"query": query,
"results": results.iter().map(|r| serde_json::json!({
"results": results.into_iter().map(|r| serde_json::json!({
"content": r.content,
"score": r.score,
"path": r.document_path,
"document_id": r.document_id.to_string(),
"is_hybrid_match": r.is_hybrid(),
})).collect::<Vec<_>>(),
"result_count": results.len(),
"result_count": result_count,
});
Ok(ToolOutput::success(output, start.elapsed()))
+4 -2
View File
@@ -431,7 +431,7 @@ impl Repository {
let rows = conn
.query(
r#"
SELECT c.id as chunk_id, c.document_id, c.content,
SELECT c.id as chunk_id, c.document_id, d.path as document_path, c.content,
ts_rank_cd(c.content_tsv, plainto_tsquery('english', $3)) as rank
FROM memory_chunks c
JOIN memory_documents d ON d.id = c.document_id
@@ -453,6 +453,7 @@ impl Repository {
.map(|(i, row)| RankedResult {
chunk_id: row.get("chunk_id"),
document_id: row.get("document_id"),
document_path: row.get("document_path"),
content: row.get("content"),
rank: (i + 1) as u32,
})
@@ -473,7 +474,7 @@ impl Repository {
let rows = conn
.query(
r#"
SELECT c.id as chunk_id, c.document_id, c.content,
SELECT c.id as chunk_id, c.document_id, d.path as document_path, c.content,
1 - (c.embedding <=> $3) as similarity
FROM memory_chunks c
JOIN memory_documents d ON d.id = c.document_id
@@ -495,6 +496,7 @@ impl Repository {
.map(|(i, row)| RankedResult {
chunk_id: row.get("chunk_id"),
document_id: row.get("document_id"),
document_path: row.get("document_path"),
content: row.get("content"),
rank: (i + 1) as u32,
})
+9
View File
@@ -81,6 +81,8 @@ impl SearchConfig {
pub struct SearchResult {
/// Document ID containing this chunk.
pub document_id: Uuid,
/// File path of the source document.
pub document_path: String,
/// Chunk ID.
pub chunk_id: Uuid,
/// Chunk content.
@@ -115,6 +117,8 @@ impl SearchResult {
pub struct RankedResult {
pub chunk_id: Uuid,
pub document_id: Uuid,
/// File path of the source document.
pub document_path: String,
pub content: String,
pub rank: u32, // 1-based rank
}
@@ -143,6 +147,7 @@ pub fn reciprocal_rank_fusion(
// Track scores and metadata for each chunk
struct ChunkInfo {
document_id: Uuid,
document_path: String,
content: String,
score: f32,
fts_rank: Option<u32>,
@@ -162,6 +167,7 @@ pub fn reciprocal_rank_fusion(
})
.or_insert(ChunkInfo {
document_id: result.document_id,
document_path: result.document_path,
content: result.content,
score: rrf_score,
fts_rank: Some(result.rank),
@@ -180,6 +186,7 @@ pub fn reciprocal_rank_fusion(
})
.or_insert(ChunkInfo {
document_id: result.document_id,
document_path: result.document_path,
content: result.content,
score: rrf_score,
fts_rank: None,
@@ -192,6 +199,7 @@ pub fn reciprocal_rank_fusion(
.into_iter()
.map(|(chunk_id, info)| SearchResult {
document_id: info.document_id,
document_path: info.document_path,
chunk_id,
content: info.content,
score: info.score,
@@ -235,6 +243,7 @@ mod tests {
RankedResult {
chunk_id,
document_id: doc_id,
document_path: format!("docs/{}.md", doc_id),
content: format!("content for chunk {}", chunk_id),
rank,
}