use std::sync::{Arc, Mutex};
use rusqlite::Connection;
use uuid::Uuid;
#[derive(Clone)]
pub struct EmbeddingStore {
conn: Arc<Mutex<Connection>>,
}
impl EmbeddingStore {
pub fn new(conn: Arc<Mutex<Connection>>) -> Self {
Self { conn }
}
pub fn save(
&self,
memory_id: Uuid,
embedding: &[f32],
model_name: &str,
) -> crate::error::Result<()> {
let conn = self
.conn
.lock()
.map_err(|_| crate::error::MnemeError::Config("mutex poisoned".into()))?;
let bytes = Self::serialize(embedding);
let now = chrono::Utc::now().to_rfc3339();
conn.execute(
"INSERT OR REPLACE INTO memory_embeddings (memory_id, embedding, model_name, dimensions, created_at)
VALUES (?1, ?2, ?3, ?4, ?5)",
rusqlite::params![
memory_id.to_string(),
bytes,
model_name,
embedding.len() as i32,
now
],
)?;
Ok(())
}
pub fn load(&self, memory_id: Uuid) -> crate::error::Result<Option<Vec<f32>>> {
let conn = self
.conn
.lock()
.map_err(|_| crate::error::MnemeError::Config("mutex poisoned".into()))?;
let result: Result<Vec<u8>, _> = conn.query_row(
"SELECT embedding FROM memory_embeddings WHERE memory_id = ?1",
rusqlite::params![memory_id.to_string()],
|row| row.get(0),
);
match result {
Ok(bytes) => Ok(Some(Self::deserialize(&bytes))),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
pub fn load_all_for_project(
&self,
project: &str,
) -> crate::error::Result<Vec<(Uuid, Vec<f32>)>> {
let conn = self
.conn
.lock()
.map_err(|_| crate::error::MnemeError::Config("mutex poisoned".into()))?;
let mut stmt = conn.prepare(
"SELECT e.memory_id, e.embedding
FROM memory_embeddings e
JOIN memories m ON e.memory_id = m.id
WHERE m.project = ?1 AND m.deleted_at IS NULL",
)?;
let rows = stmt.query_map(rusqlite::params![project], |row| {
let id_str: String = row.get(0)?;
let bytes: Vec<u8> = row.get(1)?;
let id = Uuid::parse_str(&id_str).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
0,
rusqlite::types::Type::Text,
Box::new(e),
)
})?;
Ok((id, Self::deserialize(&bytes)))
})?;
let mut results = Vec::new();
for row in rows {
results.push(row?);
}
Ok(results)
}
pub fn delete(&self, memory_id: Uuid) -> crate::error::Result<()> {
let conn = self
.conn
.lock()
.map_err(|_| crate::error::MnemeError::Config("mutex poisoned".into()))?;
conn.execute(
"DELETE FROM memory_embeddings WHERE memory_id = ?1",
rusqlite::params![memory_id.to_string()],
)?;
Ok(())
}
pub fn find_unindexed(&self, project: &str) -> crate::error::Result<Vec<Uuid>> {
let conn = self
.conn
.lock()
.map_err(|_| crate::error::MnemeError::Config("mutex poisoned".into()))?;
let mut stmt = conn.prepare(
"SELECT m.id FROM memories m
LEFT JOIN memory_embeddings e ON m.id = e.memory_id
WHERE m.project = ?1 AND m.deleted_at IS NULL AND e.memory_id IS NULL",
)?;
let rows = stmt.query_map(rusqlite::params![project], |row| {
let id_str: String = row.get(0)?;
Uuid::parse_str(&id_str).map_err(|e| {
rusqlite::Error::FromSqlConversionFailure(
0,
rusqlite::types::Type::Text,
Box::new(e),
)
})
})?;
let mut results = Vec::new();
for row in rows {
results.push(row?);
}
Ok(results)
}
pub fn serialize(v: &[f32]) -> Vec<u8> {
v.iter().flat_map(|f| f.to_le_bytes()).collect()
}
pub fn deserialize(bytes: &[u8]) -> Vec<f32> {
bytes
.chunks_exact(4)
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.collect()
}
}