use crate::enrichment::EnrichmentJob;
use crate::store::MemoryStore;
use sochdb_query::EmbeddingProvider;
use std::sync::Arc;
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
let mut dot = 0.0f32;
for (x, y) in a.iter().zip(b.iter()) {
dot += x * y;
}
dot
}
impl MemoryStore {
pub fn embedder(&self) -> &Arc<dyn EmbeddingProvider> {
&self.embedder
}
pub fn enrich_episode(&self, job: &EnrichmentJob) -> Result<(), String> {
let mut embedding = self.embedder.embed(&job.text).map_err(|e| e.to_string())?;
self.embedder.normalize(&mut embedding);
let mut namespaces = self.namespaces.write();
let ns = namespaces
.get_mut(&job.namespace)
.ok_or_else(|| format!("namespace not found: {}", job.namespace))?;
ns.vectors.insert(job.episode_id, embedding);
if let Some(episode) = ns.episodes.get_mut(&job.episode_id) {
episode.enriched = true;
}
Ok(())
}
pub fn drain_enrichment_queue(&self) -> usize {
let mut processed = 0usize;
while let Some(job) = self.enrichment.pop() {
if self.enrich_episode(&job).is_ok() {
processed += 1;
}
self.enrichment.mark_processed();
}
processed
}
pub fn search_vector(&self, namespace: &str, query: &str, k: usize) -> Vec<(u64, f32)> {
let mut query_emb = match self.embedder.embed(query) {
Ok(v) => v,
Err(_) => return Vec::new(),
};
self.embedder.normalize(&mut query_emb);
let namespaces = self.namespaces.read();
let Some(ns) = namespaces.get(namespace) else {
return Vec::new();
};
if ns.vectors.is_empty() {
return Vec::new();
}
let mut ranked: Vec<(u64, f32)> = ns
.vectors
.iter()
.map(|(id, vec)| (*id, cosine_similarity(&query_emb, vec)))
.collect();
ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
ranked.truncate(k);
ranked
}
pub fn enriched_episode_count(&self, namespace: &str) -> usize {
self.namespaces
.read()
.get(namespace)
.map(|ns| ns.vectors.len())
.unwrap_or(0)
}
}