1use crate::util::expand_tilde;
34use echo_core::error::{MemoryError, Result};
35pub use echo_core::memory::embedder::Embedder;
36pub use echo_core::memory::store::{SearchMode, SearchQuery, Store, StoreItem};
37use futures::future::BoxFuture;
38use serde_json::Value;
39use std::collections::HashMap;
40use std::path::{Path, PathBuf};
41use std::sync::Arc;
42use tokio::sync::RwLock;
43use tracing::{debug, info, warn};
44
45#[derive(Default)]
49struct VecIndex {
50 data: HashMap<String, HashMap<String, Vec<f32>>>,
52}
53
54impl VecIndex {
55 fn insert(&mut self, ns_key: &str, key: &str, vec: Vec<f32>) {
56 self.data
57 .entry(ns_key.to_string())
58 .or_default()
59 .insert(key.to_string(), vec);
60 }
61
62 fn remove(&mut self, ns_key: &str, key: &str) {
63 if let Some(ns) = self.data.get_mut(ns_key) {
64 ns.remove(key);
65 }
66 }
67
68 fn get_namespace(&self, ns_key: &str) -> Option<&HashMap<String, Vec<f32>>> {
69 self.data.get(ns_key)
70 }
71}
72
73pub struct EmbeddingStore {
81 inner: Arc<dyn Store>,
82 embedder: Arc<dyn Embedder>,
83 index: RwLock<VecIndex>,
84 vec_path: Option<PathBuf>,
85 max_candidates: usize,
89}
90
91impl EmbeddingStore {
92 pub fn new(inner: Arc<dyn Store>, embedder: Arc<dyn Embedder>) -> Self {
94 info!("🧠 EmbeddingStore 初始化(内存索引)");
95 Self {
96 inner,
97 embedder,
98 index: RwLock::new(VecIndex::default()),
99 vec_path: None,
100 max_candidates: 10_000,
101 }
102 }
103
104 pub fn with_persistence(
108 inner: Arc<dyn Store>,
109 embedder: Arc<dyn Embedder>,
110 vec_path: impl AsRef<Path>,
111 ) -> Result<Self> {
112 let path = expand_tilde(vec_path.as_ref());
113 if let Some(parent) = path.parent() {
114 std::fs::create_dir_all(parent).map_err(|e| MemoryError::IoError(e.to_string()))?;
115 }
116 let index = if path.exists() {
117 let raw =
118 std::fs::read_to_string(&path).map_err(|e| MemoryError::IoError(e.to_string()))?;
119 let data: HashMap<String, HashMap<String, Vec<f32>>> = serde_json::from_str(&raw)
120 .unwrap_or_else(|e| {
121 warn!("向量索引文件解析失败,从空索引开始: {e}");
122 HashMap::new()
123 });
124 let entry_count: usize = data.values().map(|m| m.len()).sum();
125 info!(
126 path = %path.display(),
127 entries = entry_count,
128 "🧠 EmbeddingStore 初始化(持久化索引,已加载 {} 条向量)",
129 entry_count,
130 );
131 VecIndex { data }
132 } else {
133 info!(path = %path.display(), "🧠 EmbeddingStore 初始化(空索引)");
134 VecIndex::default()
135 };
136 Ok(Self {
137 inner,
138 embedder,
139 index: RwLock::new(index),
140 vec_path: Some(path),
141 max_candidates: 10_000,
142 })
143 }
144
145 pub fn with_max_candidates(mut self, max: usize) -> Self {
149 self.max_candidates = max;
150 self
151 }
152
153 fn extract_text(value: &Value) -> String {
158 if let Value::Object(map) = value
159 && let Some(content) = map.get("content").and_then(|v| v.as_str())
160 {
161 let tags: String = map
162 .get("tags")
163 .and_then(|v| v.as_array())
164 .map(|arr| {
165 arr.iter()
166 .filter_map(|t| t.as_str())
167 .collect::<Vec<_>>()
168 .join(" ")
169 })
170 .unwrap_or_default();
171 return if tags.is_empty() {
172 content.to_string()
173 } else {
174 format!("{content} {tags}")
175 };
176 }
177
178 value_to_text(value)
179 }
180
181 async fn flush_index(&self) -> Result<()> {
182 let Some(ref path) = self.vec_path else {
183 return Ok(());
184 };
185 let index = self.index.read().await;
186 let json = serde_json::to_string(&index.data)
187 .map_err(|e| MemoryError::SerializationError(format!("向量索引序列化失败: {e}")))?;
188 tokio::fs::write(path, json)
189 .await
190 .map_err(|e| MemoryError::IoError(e.to_string()))?;
191 debug!(path = %path.display(), "💾 向量索引已持久化");
192 Ok(())
193 }
194
195 pub async fn flush_vector_index(&self) -> Result<()> {
199 self.flush_index().await
200 }
201
202 async fn semantic_search_impl(
203 &self,
204 namespace: &[&str],
205 query: &str,
206 limit: usize,
207 ) -> Result<Vec<StoreItem>> {
208 let ns_key = namespace.join("/");
209
210 let query_vec = match self.embedder.embed(query).await {
211 Ok(v) => v,
212 Err(e) => {
213 warn!(error = %e, "⚠️ 查询嵌入计算失败,回退到关键词检索");
214 return self.inner.search(namespace, query, limit).await;
215 }
216 };
217
218 let scored: Vec<(f32, String)> = {
219 let index = self.index.read().await;
220 let Some(ns_vecs) = index.get_namespace(&ns_key) else {
221 debug!(ns = %ns_key, "向量索引为空,回退到关键词检索");
222 drop(index);
223 return self.inner.search(namespace, query, limit).await;
224 };
225 let mut scored: Vec<(f32, String)> = ns_vecs
226 .iter()
227 .take(self.max_candidates)
228 .map(|(key, vec)| (cosine_similarity(&query_vec, vec), key.clone()))
229 .collect();
230 scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
231 scored.truncate(limit);
232 scored
233 };
234
235 if scored.is_empty() {
236 return Ok(vec![]);
237 }
238
239 debug!(ns = %ns_key, query = %query, hits = scored.len(), "🔍 语义检索完成");
240
241 let mut results = Vec::with_capacity(scored.len());
242 for (score, key) in scored {
243 if let Ok(Some(mut item)) = self.inner.get(namespace, &key).await {
244 item.score = Some(score);
245 results.push(item);
246 }
247 }
248 Ok(results)
249 }
250}
251
252impl Store for EmbeddingStore {
253 fn put<'a>(
254 &'a self,
255 namespace: &'a [&'a str],
256 key: &'a str,
257 value: Value,
258 ) -> BoxFuture<'a, Result<()>> {
259 Box::pin(async move {
260 self.inner.put(namespace, key, value.clone()).await?;
261
262 let text = Self::extract_text(&value);
264 match self.embedder.embed(&text).await {
265 Ok(vec) => {
266 let ns_key = namespace.join("/");
267 debug!(ns = %ns_key, key = %key, dims = vec.len(), "📌 向量索引已更新");
268 self.index.write().await.insert(&ns_key, key, vec);
269 }
273 Err(e) => {
274 warn!(key = %key, error = %e, "⚠️ 嵌入计算失败,该条目不加入向量索引");
275 }
276 }
277 Ok(())
278 })
279 }
280
281 fn get<'a>(
282 &'a self,
283 namespace: &'a [&'a str],
284 key: &'a str,
285 ) -> BoxFuture<'a, Result<Option<StoreItem>>> {
286 Box::pin(async move { self.inner.get(namespace, key).await })
287 }
288
289 fn search<'a>(
290 &'a self,
291 namespace: &'a [&'a str],
292 query: &'a str,
293 limit: usize,
294 ) -> BoxFuture<'a, Result<Vec<StoreItem>>> {
295 Box::pin(async move { self.inner.search(namespace, query, limit).await })
296 }
297
298 fn delete<'a>(&'a self, namespace: &'a [&'a str], key: &'a str) -> BoxFuture<'a, Result<bool>> {
299 Box::pin(async move {
300 let found = self.inner.delete(namespace, key).await?;
301 if found {
302 let ns_key = namespace.join("/");
303 self.index.write().await.remove(&ns_key, key);
304 }
306 Ok(found)
307 })
308 }
309
310 fn list_namespaces<'a>(
311 &'a self,
312 prefix: Option<&'a [&'a str]>,
313 ) -> BoxFuture<'a, Result<Vec<Vec<String>>>> {
314 Box::pin(async move { self.inner.list_namespaces(prefix).await })
315 }
316
317 fn list<'a>(&'a self, namespace: &'a [&'a str]) -> BoxFuture<'a, Result<Vec<StoreItem>>> {
318 Box::pin(async move { self.inner.list(namespace).await })
319 }
320
321 fn search_with<'a>(
322 &'a self,
323 namespace: &'a [&'a str],
324 query: SearchQuery<'a>,
325 ) -> BoxFuture<'a, Result<Vec<StoreItem>>> {
326 Box::pin(async move {
327 match query.mode {
328 SearchMode::Keyword => self.inner.search(namespace, query.text, query.limit).await,
329 SearchMode::Semantic => {
330 self.semantic_search_impl(namespace, query.text, query.limit)
331 .await
332 }
333 SearchMode::Hybrid { vector_weight } => {
334 let semantic_items = self
336 .semantic_search_impl(namespace, query.text, query.limit)
337 .await?;
338 let keyword_items = self
339 .inner
340 .search(namespace, query.text, query.limit)
341 .await?;
342
343 let semantic_ranks: HashMap<&str, usize> = semantic_items
345 .iter()
346 .enumerate()
347 .map(|(i, item)| (item.key.as_str(), i))
348 .collect();
349 let keyword_ranks: HashMap<&str, usize> = keyword_items
350 .iter()
351 .enumerate()
352 .map(|(i, item)| (item.key.as_str(), i))
353 .collect();
354
355 let all_keys: Vec<&str> = semantic_ranks
357 .keys()
358 .chain(keyword_ranks.keys())
359 .copied()
360 .collect::<std::collections::HashSet<_>>()
361 .into_iter()
362 .collect();
363
364 let mut items: Vec<StoreItem> = all_keys
366 .iter()
367 .filter_map(|&key| {
368 let sr = semantic_ranks.get(key).copied().unwrap_or(usize::MAX);
369 let kr = keyword_ranks.get(key).copied().unwrap_or(usize::MAX);
370 let rrf = echo_core::memory::store::rrf_score(sr, kr, vector_weight);
371
372 let mut item = semantic_items
374 .iter()
375 .find(|i| i.key == key)
376 .or_else(|| keyword_items.iter().find(|i| i.key == key))?
377 .clone();
378 item.score = Some(rrf);
379 Some(item)
380 })
381 .collect();
382
383 items.sort_by(|a, b| {
384 b.score
385 .unwrap_or_default()
386 .partial_cmp(&a.score.unwrap_or_default())
387 .unwrap_or(std::cmp::Ordering::Equal)
388 });
389 items.truncate(query.limit);
390 Ok(items)
391 }
392 }
393 })
394 }
395}
396
397fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
401 if a.len() != b.len() || a.is_empty() {
402 return 0.0;
403 }
404 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
405 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
406 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
407 if norm_a == 0.0 || norm_b == 0.0 {
408 0.0
409 } else {
410 dot / (norm_a * norm_b)
411 }
412}
413
414fn value_to_text(value: &Value) -> String {
415 match value {
416 Value::String(s) => s.clone(),
417 Value::Array(arr) => arr.iter().map(value_to_text).collect::<Vec<_>>().join(" "),
418 Value::Object(map) => map
419 .values()
420 .map(value_to_text)
421 .collect::<Vec<_>>()
422 .join(" "),
423 Value::Number(n) => n.to_string(),
424 Value::Bool(b) => b.to_string(),
425 Value::Null => String::new(),
426 }
427}
428
429#[cfg(test)]
432mod tests {
433 use super::*;
434 use crate::memory::MockEmbedder;
435 use crate::memory::store::InMemoryStore;
436 use serde_json::json;
437
438 async fn make_store() -> EmbeddingStore {
439 let inner = Arc::new(InMemoryStore::new());
440 let embedder = Arc::new(MockEmbedder::new(4));
441 EmbeddingStore::new(inner, embedder)
442 }
443
444 #[tokio::test]
445 async fn test_put_and_semantic_search() {
446 let store = make_store().await;
447 let ns = &["test", "ns"];
448
449 store
450 .put(ns, "k1", json!({"content": "Rust programming"}))
451 .await
452 .unwrap();
453 store
454 .put(ns, "k2", json!({"content": "Python machine learning"}))
455 .await
456 .unwrap();
457
458 let results = store
459 .search_with(ns, SearchQuery::semantic("Rust", 5))
460 .await
461 .unwrap();
462 assert!(!results.is_empty());
463 assert!(results[0].score.is_some());
464 }
465
466 #[tokio::test]
467 async fn test_delete_removes_from_index() {
468 let store = make_store().await;
469 let ns = &["test", "del"];
470
471 store
472 .put(ns, "k1", json!({"content": "hello world"}))
473 .await
474 .unwrap();
475 store.delete(ns, "k1").await.unwrap();
476
477 let index = store.index.read().await;
479 let ns_vecs = index.get_namespace("test/del");
480 assert!(ns_vecs.map(|m| m.is_empty()).unwrap_or(true));
481 }
482
483 #[tokio::test]
484 async fn test_cosine_similarity() {
485 let a = vec![1.0f32, 0.0, 0.0];
486 let b = vec![1.0f32, 0.0, 0.0];
487 let sim = cosine_similarity(&a, &b);
488 assert!((sim - 1.0).abs() < 1e-5);
489
490 let c = vec![0.0f32, 1.0, 0.0];
491 let sim2 = cosine_similarity(&a, &c);
492 assert!((sim2 - 0.0).abs() < 1e-5);
493 }
494}