refactor: Implement unified search abstraction, MemoryState refactoring, error handling, and unit test expansion

This commit is contained in:
Riz Ashraf committed 2026-10-01 08:37:52 +01:00
1 parent a34554b7ff
commit 462f65f66d
21 files changed
+425 -535

No files matched your search

+100 -1
View File
@@ -164,6 +164,10 @@ impl MemoryState {
.clone()
}
pub fn search(self: &Arc<Self>) -> SearchService {
SearchService::new(self.clone())
}
pub async fn rebuild_index(self: &Arc<Self>) {
let idx = self.search_index.read().unwrap().clone();
idx.delete_all();
@@ -258,7 +262,7 @@ mod tests {
// idx.reader.searcher().num_docs()
// );
let all_docs = idx.search("Test", None).expect("Search failed");
let _all_docs = idx.search("Test", None).expect("Search failed");
// tracing::info!("All docs for 'Test': {:?}", all_docs);
// Verify the task added synchronously is actually searchable
@@ -271,3 +275,98 @@ mod tests {
assert_eq!(results[0].1, "task", "Expected document type to be task");
}
}
use crate::embedding::{generate_embedding_async, generate_embeddings_async, cosine_similarity};
pub struct UnifiedSearchResult {
pub id: String,
pub doc_type: String,
pub title: String,
pub body: String,
pub score: f32,
}
pub struct SearchService {
state: Arc<MemoryState>,
}
impl SearchService {
pub fn new(state: Arc<MemoryState>) -> Self {
Self { state }
}
pub async fn semantic_search(&self, query: &str, _filter_namespace: Option<&str>, limit: usize) -> crate::error::Result<Vec<UnifiedSearchResult>> {
let query_emb = generate_embedding_async(query.to_string()).await.unwrap_or_default();
let mut results = Vec::new();
let mut vdb_search = false;
if let Some(vdb) = &*self.state.vector_db.read().await {
vdb_search = true;
if let Ok(search_results) = vdb.search(query_emb.clone(), limit as u64).await {
for res in search_results {
results.push(UnifiedSearchResult {
id: res.id.clone(),
doc_type: res.doc_type.clone(),
title: res.id,
body: res.text,
score: res.score,
});
}
}
}
if !vdb_search {
let mut texts_to_embed = Vec::new();
let mut metadata = Vec::new();
let snippets = self.state.code.snippets.read_with(|snips| snips.clone());
for snippet in snippets {
let combined = format!("{} {} {}", snippet.name, snippet.description, snippet.code);
texts_to_embed.push(combined);
metadata.push((snippet.name, "snippet".to_string(), snippet.description));
}
let sticky = self.state.code.sticky.read_with(|s| s.clone());
for note in sticky {
texts_to_embed.push(note.content.clone());
metadata.push(("StickyNote".to_string(), "sticky".to_string(), note.content.chars().take(200).collect::<String>()));
}
if let Ok(embeddings) = generate_embeddings_async(texts_to_embed).await {
for (emb, meta) in embeddings.into_iter().zip(metadata) {
let sim = cosine_similarity(&query_emb, &emb);
results.push(UnifiedSearchResult {
id: meta.0.clone(),
doc_type: meta.1.clone(),
title: meta.0,
body: meta.2,
score: sim,
});
}
}
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(limit);
}
Ok(results)
}
pub fn keyword_search(&self, query: &str, filter_namespace: Option<&str>, limit: usize) -> crate::error::Result<Vec<UnifiedSearchResult>> {
let idx = self.state.get_search_index();
let matches = idx.search(query, filter_namespace).map_err(|e| crate::error::AppError::Internal(e.to_string()))?;
let mut results = Vec::new();
for (id, doc_type, title, body, score) in matches.into_iter().take(limit) {
results.push(UnifiedSearchResult {
id,
doc_type,
title,
body,
score,
});
}
Ok(results)
}
}