perf: optimize store batch serialization, zero-clone semantic search, and lock contention
This commit is contained in:
1 parent
3b08f45618
commit
35c802c1b8
6 files changed
+205
-309
No files matched your search
+2
-15
@@ -105,8 +105,8 @@ pub async fn handle_socket(socket: WebSocket, state: Arc<AppState>, _client_type
|
|||||||
});
|
});
|
||||||
|
|
||||||
let handler = Arc::clone(&state.handler);
|
let handler = Arc::clone(&state.handler);
|
||||||
let state_clone = Arc::clone(&state);
|
|
||||||
let session_id_clone = session_id.clone();
|
let session_id_clone = session_id.clone();
|
||||||
|
let response_tx = tx.clone();
|
||||||
|
|
||||||
let recv_task = tokio::spawn(async move {
|
let recv_task = tokio::spawn(async move {
|
||||||
while let Some(msg_result) = receiver.next().await {
|
while let Some(msg_result) = receiver.next().await {
|
||||||
@@ -121,26 +121,13 @@ pub async fn handle_socket(socket: WebSocket, state: Arc<AppState>, _client_type
|
|||||||
// Process MCP request
|
// Process MCP request
|
||||||
if let Some(response) = handler.handle_request(payload).await {
|
if let Some(response) = handler.handle_request(payload).await {
|
||||||
let res_str = serde_json::to_string(&response).unwrap_or_else(|e| format!(r#"{{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{{\"code\":-32603,\"message\":\"{}\"}}}}"#, e));
|
let res_str = serde_json::to_string(&response).unwrap_or_else(|e| format!(r#"{{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{{\"code\":-32603,\"message\":\"{}\"}}}}"#, e));
|
||||||
let tx_opt = state_clone
|
if let Err(e) = response_tx.send(res_str).await {
|
||||||
.clients
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(|e| e.into_inner())
|
|
||||||
.get(&session_id_clone)
|
|
||||||
.cloned();
|
|
||||||
if let Some(client_tx) = tx_opt {
|
|
||||||
if let Err(e) = client_tx.send(res_str).await {
|
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"Failed to send response to client channel for session {}: {}",
|
"Failed to send response to client channel for session {}: {}",
|
||||||
session_id_clone,
|
session_id_clone,
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
tracing::warn!(
|
|
||||||
"Could not find client_tx for session_id {} when trying to send response",
|
|
||||||
session_id_clone
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|||||||
+10
-61
@@ -287,8 +287,8 @@ impl McpTool for LogCodeChangeHandler {
|
|||||||
author: req.author,
|
author: req.author,
|
||||||
session_id: req.session_id,
|
session_id: req.session_id,
|
||||||
vcs_type: detected_vcs,
|
vcs_type: detected_vcs,
|
||||||
revision: effective_rev.clone(),
|
revision: effective_rev,
|
||||||
branch: effective_branch.clone(),
|
branch: effective_branch,
|
||||||
repository_root: req.repository_root,
|
repository_root: req.repository_root,
|
||||||
});
|
});
|
||||||
if ledger.len() > 500 {
|
if ledger.len() > 500 {
|
||||||
@@ -300,42 +300,9 @@ impl McpTool for LogCodeChangeHandler {
|
|||||||
&format!("Modified {}", req.file_path),
|
&format!("Modified {}", req.file_path),
|
||||||
Some(&description),
|
Some(&description),
|
||||||
);
|
);
|
||||||
|
|
||||||
let recon = crate::handlers::reconciliation::reconcile_commit_or_code_change(
|
|
||||||
&state,
|
|
||||||
&description,
|
|
||||||
Some(&req.file_path),
|
|
||||||
effective_rev.as_deref(),
|
|
||||||
effective_branch.as_deref(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let mut recon_notes = Vec::new();
|
|
||||||
if !recon.implemented_adrs.is_empty() {
|
|
||||||
recon_notes.push(format!("Implemented ADRs: {}", recon.implemented_adrs.join(", ")));
|
|
||||||
}
|
|
||||||
if !recon.resolved_tech_debts.is_empty() {
|
|
||||||
recon_notes.push(format!("Resolved TechDebt: {}", recon.resolved_tech_debts.join(", ")));
|
|
||||||
}
|
|
||||||
if !recon.completed_tasks.is_empty() {
|
|
||||||
recon_notes.push(format!("Completed Tasks: {}", recon.completed_tasks.join(", ")));
|
|
||||||
}
|
|
||||||
if !recon.unblocked_tasks.is_empty() {
|
|
||||||
recon_notes.push(format!("Unblocked Tasks: {}", recon.unblocked_tasks.join(", ")));
|
|
||||||
}
|
|
||||||
if !recon.updated_milestones.is_empty() {
|
|
||||||
recon_notes.push(format!("Updated Milestones: {}", recon.updated_milestones.join(", ")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let recon_suffix = if recon_notes.is_empty() {
|
|
||||||
String::new()
|
|
||||||
} else {
|
|
||||||
format!(" [{}]", recon_notes.join(" | "))
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"Logged code change for {}: {}{}",
|
"Logged code change for {}: {}",
|
||||||
req.file_path, description, recon_suffix
|
req.file_path, description
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -811,16 +778,7 @@ impl McpTool for OmniSearchHandler {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Reciprocal Rank Fusion (RRF) algorithm
|
// Reciprocal Rank Fusion (RRF) algorithm
|
||||||
#[allow(dead_code)]
|
let mut rrf_scores: std::collections::HashMap<String, (f64, crate::search::SearchResult)> =
|
||||||
#[derive(Clone)]
|
|
||||||
struct MatchItem {
|
|
||||||
id: String,
|
|
||||||
doc_type: String,
|
|
||||||
title: String,
|
|
||||||
body: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rrf_scores: std::collections::HashMap<String, (f64, MatchItem)> =
|
|
||||||
std::collections::HashMap::new();
|
std::collections::HashMap::new();
|
||||||
|
|
||||||
for (rank, (id, doc_type, title, body, _score)) in keyword_matches.into_iter().enumerate() {
|
for (rank, (id, doc_type, title, body, _score)) in keyword_matches.into_iter().enumerate() {
|
||||||
@@ -829,11 +787,12 @@ impl McpTool for OmniSearchHandler {
|
|||||||
id.clone(),
|
id.clone(),
|
||||||
(
|
(
|
||||||
score,
|
score,
|
||||||
MatchItem {
|
crate::search::SearchResult {
|
||||||
id,
|
id,
|
||||||
doc_type,
|
doc_type,
|
||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
|
score: 0.0,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -845,20 +804,14 @@ impl McpTool for OmniSearchHandler {
|
|||||||
if let Some(existing) = rrf_scores.get_mut(&item_id) {
|
if let Some(existing) = rrf_scores.get_mut(&item_id) {
|
||||||
existing.0 += score;
|
existing.0 += score;
|
||||||
} else {
|
} else {
|
||||||
let item = MatchItem {
|
rrf_scores.insert(item_id, (score, v_match));
|
||||||
id: v_match.id.clone(),
|
|
||||||
doc_type: v_match.doc_type,
|
|
||||||
title: v_match.title,
|
|
||||||
body: v_match.body,
|
|
||||||
};
|
|
||||||
rrf_scores.insert(item_id, (score, item));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ranked_items: Vec<_> = rrf_scores.into_values().collect();
|
let mut ranked_items: Vec<_> = rrf_scores.into_values().collect();
|
||||||
ranked_items.sort_by(|a, b| b.0.total_cmp(&a.0));
|
ranked_items.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||||
|
|
||||||
let matches: Vec<MatchItem> = ranked_items.into_iter().map(|(_, item)| item).collect();
|
let matches: Vec<crate::search::SearchResult> = ranked_items.into_iter().map(|(_, item)| item).collect();
|
||||||
|
|
||||||
let kg_json = state.read_graph(|full| {
|
let kg_json = state.read_graph(|full| {
|
||||||
let mut kg_results = serde_json::Map::new();
|
let mut kg_results = serde_json::Map::new();
|
||||||
@@ -1203,11 +1156,7 @@ impl McpTool for GetProjectHealthHandler {
|
|||||||
let active_milestones = state.project.milestones.read_with(|milestones| {
|
let active_milestones = state.project.milestones.read_with(|milestones| {
|
||||||
milestones
|
milestones
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|m| {
|
.filter(|m| m.namespace == req.namespace && m.status != "done")
|
||||||
m.namespace == req.namespace
|
|
||||||
&& !m.status.eq_ignore_ascii_case("done")
|
|
||||||
&& !m.status.eq_ignore_ascii_case("completed")
|
|
||||||
})
|
|
||||||
.count()
|
.count()
|
||||||
});
|
});
|
||||||
let report = serde_json::json!({
|
let report = serde_json::json!({
|
||||||
|
|||||||
@@ -104,9 +104,10 @@ impl McpTool for ClipboardHandler {
|
|||||||
let req: ClipboardTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: ClipboardTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
match req.action {
|
match req.action {
|
||||||
ClipboardAction::Read => {
|
ClipboardAction::Read => {
|
||||||
let result =
|
let (mut out, b64_opt) =
|
||||||
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
|
tokio::task::spawn_blocking(move || -> crate::error::Result<(serde_json::Map<String, Value>, Option<String>)> {
|
||||||
let mut out = serde_json::Map::new();
|
let mut out = serde_json::Map::new();
|
||||||
|
let mut b64_opt = None;
|
||||||
|
|
||||||
if let Some(text) = get_native_clipboard_text() {
|
if let Some(text) = get_native_clipboard_text() {
|
||||||
out.insert("text".into(), json!(text));
|
out.insert("text".into(), json!(text));
|
||||||
@@ -125,7 +126,7 @@ impl McpTool for ClipboardHandler {
|
|||||||
let bytes = jpeg_bytes.into_inner();
|
let bytes = jpeg_bytes.into_inner();
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
||||||
out.insert("image_base64".into(), json!(b64));
|
b64_opt = Some(b64);
|
||||||
|
|
||||||
let cache_dir = dirs::home_dir()
|
let cache_dir = dirs::home_dir()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -146,17 +147,12 @@ impl McpTool for ClipboardHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Value::Object(out))
|
Ok((out, b64_opt))
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||||
|
|
||||||
let mut final_obj = result;
|
if let Some(b64_str) = b64_opt {
|
||||||
if let Some(b64) = final_obj.get("image_base64").and_then(|v| v.as_str()) {
|
|
||||||
let b64_str = b64.to_string();
|
|
||||||
if let Some(obj) = final_obj.as_object_mut() {
|
|
||||||
obj.remove("image_base64");
|
|
||||||
}
|
|
||||||
if state.ollama.is_available().await
|
if state.ollama.is_available().await
|
||||||
&& let Ok(analysis) = state
|
&& let Ok(analysis) = state
|
||||||
.ollama
|
.ollama
|
||||||
@@ -165,14 +161,13 @@ impl McpTool for ClipboardHandler {
|
|||||||
&b64_str,
|
&b64_str,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
&& let Some(obj) = final_obj.as_object_mut()
|
|
||||||
{
|
{
|
||||||
obj.insert("image_analysis".to_string(), json!(analysis.trim()));
|
out.insert("image_analysis".to_string(), json!(analysis.trim()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.record_activity("clipboard", "Read contents from OS clipboard", None);
|
state.record_activity("clipboard", "Read contents from OS clipboard", None);
|
||||||
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&final_obj)?)
|
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&Value::Object(out))?)
|
||||||
}
|
}
|
||||||
ClipboardAction::Write => {
|
ClipboardAction::Write => {
|
||||||
let text_opt = req.text;
|
let text_opt = req.text;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
use crate::embedding::{cosine_similarity, generate_embedding_async, generate_embeddings_async};
|
||||||
use crate::models::{Adr, Entity, Snippet, Task};
|
use crate::models::{Adr, Entity, Snippet, Task};
|
||||||
|
use crate::state::MemoryState;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tantivy::schema::*;
|
use tantivy::schema::*;
|
||||||
use tantivy::{Index, IndexReader, IndexWriter, ReloadPolicy, doc};
|
use tantivy::{Index, IndexReader, IndexWriter, ReloadPolicy, doc};
|
||||||
@@ -432,6 +434,154 @@ impl MemoryIndex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<SearchResult>> {
|
||||||
|
let query_emb = generate_embedding_async(query.to_string())
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
if query_emb.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
let mut uncached_texts = Vec::new();
|
||||||
|
let mut uncached_meta = Vec::new();
|
||||||
|
|
||||||
|
self.state.code.snippets.read_with(|snips| {
|
||||||
|
for snippet in snips.iter() {
|
||||||
|
if let Some(ref emb) = snippet.embedding {
|
||||||
|
let sim = cosine_similarity(&query_emb, emb);
|
||||||
|
results.push(SearchResult {
|
||||||
|
id: snippet.name.clone(),
|
||||||
|
doc_type: "snippet".to_string(),
|
||||||
|
title: snippet.name.clone(),
|
||||||
|
body: snippet.description.clone(),
|
||||||
|
score: sim,
|
||||||
|
});
|
||||||
|
} else if uncached_texts.len() < 50 {
|
||||||
|
uncached_texts.push(format!(
|
||||||
|
"{} {} {}",
|
||||||
|
snippet.name, snippet.description, snippet.code
|
||||||
|
));
|
||||||
|
uncached_meta.push((
|
||||||
|
snippet.name.clone(),
|
||||||
|
"snippet".to_string(),
|
||||||
|
snippet.description.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.state.read_graph(|graph| {
|
||||||
|
for entity in graph.entities.values() {
|
||||||
|
if let Some(ns) = filter_namespace {
|
||||||
|
if entity.namespace != ns {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let obs = entity.observations.join("; ");
|
||||||
|
let desc = format!("{}: {}", entity.entity_type, obs);
|
||||||
|
if let Some(ref emb) = entity.embedding {
|
||||||
|
let sim = cosine_similarity(&query_emb, emb);
|
||||||
|
results.push(SearchResult {
|
||||||
|
id: entity.name.clone(),
|
||||||
|
doc_type: "entity".to_string(),
|
||||||
|
title: entity.name.clone(),
|
||||||
|
body: desc,
|
||||||
|
score: sim,
|
||||||
|
});
|
||||||
|
} else if uncached_texts.len() < 50 {
|
||||||
|
uncached_texts.push(format!("{} {} {}", entity.name, entity.entity_type, obs));
|
||||||
|
uncached_meta.push((entity.name.clone(), "entity".to_string(), desc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.state.code.error_fixes.read_with(|fixes| {
|
||||||
|
for fix in fixes.iter() {
|
||||||
|
if let Some(ref emb) = fix.embedding {
|
||||||
|
let sim = cosine_similarity(&query_emb, emb);
|
||||||
|
results.push(SearchResult {
|
||||||
|
id: fix.signature.clone(),
|
||||||
|
doc_type: "error_fix".to_string(),
|
||||||
|
title: fix.signature.clone(),
|
||||||
|
body: fix.solution.clone(),
|
||||||
|
score: sim,
|
||||||
|
});
|
||||||
|
} else if uncached_texts.len() < 50 {
|
||||||
|
uncached_texts.push(format!("{} {}", fix.signature, fix.solution));
|
||||||
|
uncached_meta.push((
|
||||||
|
fix.signature.clone(),
|
||||||
|
"error_fix".to_string(),
|
||||||
|
fix.solution.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if !uncached_texts.is_empty()
|
||||||
|
&& let Ok(embeddings) = generate_embeddings_async(uncached_texts).await
|
||||||
|
{
|
||||||
|
for (emb, (title, doc_type, body)) in embeddings.into_iter().zip(uncached_meta) {
|
||||||
|
let sim = cosine_similarity(&query_emb, &emb);
|
||||||
|
results.push(SearchResult {
|
||||||
|
id: title.clone(),
|
||||||
|
doc_type,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
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 async fn keyword_search(
|
||||||
|
&self,
|
||||||
|
query: &str,
|
||||||
|
filter_namespace: Option<&str>,
|
||||||
|
limit: usize,
|
||||||
|
) -> crate::error::Result<Vec<SearchResult>> {
|
||||||
|
let idx = self.state.get_search_index().await;
|
||||||
|
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(SearchResult {
|
||||||
|
id,
|
||||||
|
doc_type,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
score,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
+1
-140
@@ -469,143 +469,4 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::embedding::{cosine_similarity, generate_embedding_async, generate_embeddings_async};
|
pub use crate::search::{SearchResult as UnifiedSearchResult, SearchService};
|
||||||
|
|
||||||
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 cached_items = Vec::new();
|
|
||||||
let mut uncached_texts = Vec::new();
|
|
||||||
let mut uncached_meta = Vec::new();
|
|
||||||
|
|
||||||
self.state.code.snippets.read_with(|snips| {
|
|
||||||
for snippet in snips.iter() {
|
|
||||||
let title = snippet.name.clone();
|
|
||||||
let desc = snippet.description.clone();
|
|
||||||
if let Some(ref emb) = snippet.embedding {
|
|
||||||
cached_items.push((title, "snippet".to_string(), desc, emb.clone()));
|
|
||||||
} else if uncached_texts.len() < 50 {
|
|
||||||
uncached_texts.push(format!(
|
|
||||||
"{} {} {}",
|
|
||||||
snippet.name, snippet.description, snippet.code
|
|
||||||
));
|
|
||||||
uncached_meta.push((title, "snippet".to_string(), desc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.state.read_graph(|graph| {
|
|
||||||
for entity in graph.entities.values() {
|
|
||||||
if let Some(ns) = filter_namespace {
|
|
||||||
if entity.namespace != ns {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let title = entity.name.clone();
|
|
||||||
let obs = entity.observations.join("; ");
|
|
||||||
let desc = format!("{}: {}", entity.entity_type, obs);
|
|
||||||
if let Some(ref emb) = entity.embedding {
|
|
||||||
cached_items.push((title, "entity".to_string(), desc, emb.clone()));
|
|
||||||
} else if uncached_texts.len() < 50 {
|
|
||||||
uncached_texts.push(format!("{} {} {}", entity.name, entity.entity_type, obs));
|
|
||||||
uncached_meta.push((title, "entity".to_string(), desc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.state.code.error_fixes.read_with(|fixes| {
|
|
||||||
for fix in fixes.iter() {
|
|
||||||
let title = fix.signature.clone();
|
|
||||||
let desc = fix.solution.clone();
|
|
||||||
if let Some(ref emb) = fix.embedding {
|
|
||||||
cached_items.push((title, "error_fix".to_string(), desc, emb.clone()));
|
|
||||||
} else if uncached_texts.len() < 50 {
|
|
||||||
uncached_texts.push(format!("{} {}", fix.signature, fix.solution));
|
|
||||||
uncached_meta.push((title, "error_fix".to_string(), desc));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (title, doc_type, body, emb) in cached_items {
|
|
||||||
let sim = cosine_similarity(&query_emb, &emb);
|
|
||||||
results.push(UnifiedSearchResult {
|
|
||||||
id: title.clone(),
|
|
||||||
doc_type,
|
|
||||||
title,
|
|
||||||
body,
|
|
||||||
score: sim,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if !uncached_texts.is_empty()
|
|
||||||
&& let Ok(embeddings) = generate_embeddings_async(uncached_texts).await
|
|
||||||
{
|
|
||||||
for (emb, meta) in embeddings.into_iter().zip(uncached_meta) {
|
|
||||||
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 async fn keyword_search(
|
|
||||||
&self,
|
|
||||||
query: &str,
|
|
||||||
filter_namespace: Option<&str>,
|
|
||||||
limit: usize,
|
|
||||||
) -> crate::error::Result<Vec<UnifiedSearchResult>> {
|
|
||||||
let idx = self.state.get_search_index().await;
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+34
-80
@@ -444,14 +444,35 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
|||||||
(*lock).clone()
|
(*lock).clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expensive serialization and granular extraction run completely unblocked outside the lock
|
match self.prepare_batch(&new_snapshot) {
|
||||||
let full_bytes_res = serde_json::to_vec(&new_snapshot);
|
Ok((batch_inserts, removed_keys)) => {
|
||||||
let granular_entries = full_bytes_res
|
self.queue.push_batch(
|
||||||
.as_ref()
|
self.key.clone(),
|
||||||
.ok()
|
batch_inserts,
|
||||||
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(bytes).ok())
|
removed_keys,
|
||||||
.map(|val| Self::extract_granular_entries(&self.key, &val))
|
self.flushed.clone(),
|
||||||
.unwrap_or_default();
|
);
|
||||||
|
}
|
||||||
|
Err(e) => tracing::error!(
|
||||||
|
"Failed to serialize memory store for key '{}': {}",
|
||||||
|
self.key,
|
||||||
|
e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_batch(
|
||||||
|
&self,
|
||||||
|
new_snapshot: &T,
|
||||||
|
) -> Result<(Vec<(String, Vec<u8>)>, Vec<String>), serde_json::Error>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
let full_bytes = serde_json::to_vec(new_snapshot)?;
|
||||||
|
let granular_entries = match serde_json::from_slice::<serde_json::Value>(&full_bytes) {
|
||||||
|
Ok(val) => Self::extract_granular_entries(&self.key, &val),
|
||||||
|
Err(_) => Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
let new_keys: std::collections::HashSet<String> =
|
let new_keys: std::collections::HashSet<String> =
|
||||||
granular_entries.iter().map(|(k, _)| k.clone()).collect();
|
granular_entries.iter().map(|(k, _)| k.clone()).collect();
|
||||||
@@ -469,48 +490,11 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
|||||||
*known = new_keys;
|
*known = new_keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
match full_bytes_res {
|
|
||||||
Ok(data) => {
|
|
||||||
let mut batch_inserts = Vec::with_capacity(granular_entries.len() + 1);
|
let mut batch_inserts = Vec::with_capacity(granular_entries.len() + 1);
|
||||||
for (g_key, g_bytes) in granular_entries {
|
batch_inserts.extend(granular_entries);
|
||||||
batch_inserts.push((g_key, g_bytes));
|
batch_inserts.push((self.key.clone(), full_bytes));
|
||||||
}
|
|
||||||
batch_inserts.push((self.key.clone(), data.clone()));
|
|
||||||
|
|
||||||
if self
|
Ok((batch_inserts, removed_keys))
|
||||||
.queue
|
|
||||||
.push_batch(
|
|
||||||
self.key.clone(),
|
|
||||||
batch_inserts.clone(),
|
|
||||||
removed_keys.clone(),
|
|
||||||
self.flushed.clone(),
|
|
||||||
)
|
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
tracing::warn!(
|
|
||||||
"DbWriteQueue channel full for key '{}'. Applying backpressure fallback.",
|
|
||||||
self.key
|
|
||||||
);
|
|
||||||
let queue = self.queue.clone();
|
|
||||||
let key = self.key.clone();
|
|
||||||
let flushed = self.flushed.clone();
|
|
||||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
|
||||||
handle.spawn(async move {
|
|
||||||
let _ = tokio::time::timeout(
|
|
||||||
std::time::Duration::from_secs(10),
|
|
||||||
queue.push_batch_async(key, batch_inserts, removed_keys, flushed),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => tracing::error!(
|
|
||||||
"Failed to serialize memory store for key '{}': {}",
|
|
||||||
self.key,
|
|
||||||
e
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn modify_async<F: FnOnce(&mut T)>(&self, f: F)
|
pub async fn modify_async<F: FnOnce(&mut T)>(&self, f: F)
|
||||||
@@ -531,38 +515,8 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
|||||||
(*lock).clone()
|
(*lock).clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let full_bytes_res = serde_json::to_vec(&new_snapshot);
|
match self.prepare_batch(&new_snapshot) {
|
||||||
let granular_entries = full_bytes_res
|
Ok((batch_inserts, removed_keys)) => {
|
||||||
.as_ref()
|
|
||||||
.ok()
|
|
||||||
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(bytes).ok())
|
|
||||||
.map(|val| Self::extract_granular_entries(&self.key, &val))
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
let new_keys: std::collections::HashSet<String> =
|
|
||||||
granular_entries.iter().map(|(k, _)| k.clone()).collect();
|
|
||||||
let mut removed_keys = Vec::new();
|
|
||||||
{
|
|
||||||
let mut known = self
|
|
||||||
.known_granular_keys
|
|
||||||
.write()
|
|
||||||
.unwrap_or_else(|e| e.into_inner());
|
|
||||||
for old_k in known.iter() {
|
|
||||||
if !new_keys.contains(old_k) {
|
|
||||||
removed_keys.push(old_k.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*known = new_keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
match full_bytes_res {
|
|
||||||
Ok(data) => {
|
|
||||||
let mut batch_inserts = Vec::with_capacity(granular_entries.len() + 1);
|
|
||||||
for (g_key, g_bytes) in granular_entries {
|
|
||||||
batch_inserts.push((g_key, g_bytes));
|
|
||||||
}
|
|
||||||
batch_inserts.push((self.key.clone(), data));
|
|
||||||
|
|
||||||
if let Some(rx) = self
|
if let Some(rx) = self
|
||||||
.queue
|
.queue
|
||||||
.push_batch_async(
|
.push_batch_async(
|
||||||
|
|||||||
Reference in new issue
Block a user