use async_trait::async_trait;
use super::entities::{Embedding, EmbeddingId, EmbeddingModel, SegmentId, StorageTier};
use crate::EmbeddingError;
#[async_trait]
pub trait EmbeddingRepository: Send + Sync {
async fn save(&self, embedding: &Embedding) -> Result<(), EmbeddingError>;
async fn find_by_id(&self, id: &EmbeddingId) -> Result<Option<Embedding>, EmbeddingError>;
async fn find_by_segment(&self, segment_id: &SegmentId) -> Result<Option<Embedding>, EmbeddingError>;
async fn batch_save(&self, embeddings: &[Embedding]) -> Result<(), EmbeddingError>;
async fn delete(&self, id: &EmbeddingId) -> Result<bool, EmbeddingError>;
async fn delete_by_segment(&self, segment_id: &SegmentId) -> Result<usize, EmbeddingError>;
async fn count(&self) -> Result<u64, EmbeddingError>;
async fn count_by_tier(&self, tier: StorageTier) -> Result<u64, EmbeddingError>;
async fn find_by_model_version(
&self,
model_version: &str,
limit: usize,
offset: usize,
) -> Result<Vec<Embedding>, EmbeddingError>;
async fn update_tier(
&self,
id: &EmbeddingId,
tier: StorageTier,
) -> Result<bool, EmbeddingError>;
async fn exists(&self, id: &EmbeddingId) -> Result<bool, EmbeddingError> {
Ok(self.find_by_id(id).await?.is_some())
}
async fn exists_for_segment(&self, segment_id: &SegmentId) -> Result<bool, EmbeddingError> {
Ok(self.find_by_segment(segment_id).await?.is_some())
}
}
#[async_trait]
pub trait ModelRepository: Send + Sync {
async fn save_model(&self, model: &EmbeddingModel) -> Result<(), EmbeddingError>;
async fn find_model(
&self,
name: &str,
version: &str,
) -> Result<Option<EmbeddingModel>, EmbeddingError>;
async fn get_active_model(&self, name: &str) -> Result<Option<EmbeddingModel>, EmbeddingError>;
async fn list_models(&self) -> Result<Vec<EmbeddingModel>, EmbeddingError>;
async fn delete_model(&self, name: &str, version: &str) -> Result<bool, EmbeddingError>;
}
#[derive(Debug, Clone, Default)]
pub struct EmbeddingQuery {
pub segment_ids: Option<Vec<SegmentId>>,
pub model_version: Option<String>,
pub tier: Option<StorageTier>,
pub created_after: Option<chrono::DateTime<chrono::Utc>>,
pub created_before: Option<chrono::DateTime<chrono::Utc>>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
impl EmbeddingQuery {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_segment_ids(mut self, ids: Vec<SegmentId>) -> Self {
self.segment_ids = Some(ids);
self
}
#[must_use]
pub fn with_model_version(mut self, version: impl Into<String>) -> Self {
self.model_version = Some(version.into());
self
}
#[must_use]
pub const fn with_tier(mut self, tier: StorageTier) -> Self {
self.tier = Some(tier);
self
}
#[must_use]
pub const fn with_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub const fn with_offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
}
#[async_trait]
pub trait QueryableEmbeddingRepository: EmbeddingRepository {
async fn query(&self, query: &EmbeddingQuery) -> Result<Vec<Embedding>, EmbeddingError>;
async fn find_nearest(
&self,
query_vector: &[f32],
k: usize,
ef_search: Option<usize>,
) -> Result<Vec<(Embedding, f32)>, EmbeddingError>;
async fn find_within_distance(
&self,
query_vector: &[f32],
max_distance: f32,
) -> Result<Vec<(Embedding, f32)>, EmbeddingError>;
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
struct InMemoryEmbeddingRepository {
embeddings: Arc<RwLock<HashMap<EmbeddingId, Embedding>>>,
}
impl InMemoryEmbeddingRepository {
fn new() -> Self {
Self {
embeddings: Arc::new(RwLock::new(HashMap::new())),
}
}
}
#[async_trait]
impl EmbeddingRepository for InMemoryEmbeddingRepository {
async fn save(&self, embedding: &Embedding) -> Result<(), EmbeddingError> {
self.embeddings.write().await.insert(embedding.id, embedding.clone());
Ok(())
}
async fn find_by_id(&self, id: &EmbeddingId) -> Result<Option<Embedding>, EmbeddingError> {
Ok(self.embeddings.read().await.get(id).cloned())
}
async fn find_by_segment(&self, segment_id: &SegmentId) -> Result<Option<Embedding>, EmbeddingError> {
Ok(self.embeddings.read().await.values()
.find(|e| e.segment_id == *segment_id)
.cloned())
}
async fn batch_save(&self, embeddings: &[Embedding]) -> Result<(), EmbeddingError> {
let mut store = self.embeddings.write().await;
for embedding in embeddings {
store.insert(embedding.id, embedding.clone());
}
Ok(())
}
async fn delete(&self, id: &EmbeddingId) -> Result<bool, EmbeddingError> {
Ok(self.embeddings.write().await.remove(id).is_some())
}
async fn delete_by_segment(&self, segment_id: &SegmentId) -> Result<usize, EmbeddingError> {
let mut store = self.embeddings.write().await;
let to_remove: Vec<_> = store.iter()
.filter(|(_, e)| e.segment_id == *segment_id)
.map(|(id, _)| *id)
.collect();
let count = to_remove.len();
for id in to_remove {
store.remove(&id);
}
Ok(count)
}
async fn count(&self) -> Result<u64, EmbeddingError> {
Ok(self.embeddings.read().await.len() as u64)
}
async fn count_by_tier(&self, tier: StorageTier) -> Result<u64, EmbeddingError> {
Ok(self.embeddings.read().await.values()
.filter(|e| e.tier == tier)
.count() as u64)
}
async fn find_by_model_version(
&self,
model_version: &str,
limit: usize,
offset: usize,
) -> Result<Vec<Embedding>, EmbeddingError> {
Ok(self.embeddings.read().await.values()
.filter(|e| e.model_version == model_version)
.skip(offset)
.take(limit)
.cloned()
.collect())
}
async fn update_tier(
&self,
id: &EmbeddingId,
tier: StorageTier,
) -> Result<bool, EmbeddingError> {
let mut store = self.embeddings.write().await;
if let Some(embedding) = store.get_mut(id) {
embedding.tier = tier;
Ok(true)
} else {
Ok(false)
}
}
}
#[tokio::test]
async fn test_in_memory_repository() {
let repo = InMemoryEmbeddingRepository::new();
let segment_id = SegmentId::new();
let vector = vec![0.0; crate::EMBEDDING_DIM];
let embedding = Embedding::new(segment_id, vector, "test".to_string()).unwrap();
repo.save(&embedding).await.unwrap();
let found = repo.find_by_id(&embedding.id).await.unwrap();
assert!(found.is_some());
let found = repo.find_by_segment(&segment_id).await.unwrap();
assert!(found.is_some());
assert_eq!(repo.count().await.unwrap(), 1);
assert!(repo.delete(&embedding.id).await.unwrap());
assert_eq!(repo.count().await.unwrap(), 0);
}
#[test]
fn test_embedding_query_builder() {
let query = EmbeddingQuery::new()
.with_model_version("perch-v2-2.0.0-base")
.with_tier(StorageTier::Hot)
.with_limit(100)
.with_offset(0);
assert_eq!(query.model_version.as_deref(), Some("perch-v2-2.0.0-base"));
assert_eq!(query.tier, Some(StorageTier::Hot));
assert_eq!(query.limit, Some(100));
assert_eq!(query.offset, Some(0));
}
}