use crate::error::VectorError;
use crate::extensions::DimensionValidator;
use crate::{validate_top_k, PgVectorStore, SearchResult, VectorMetric, VectorRecord};
pub struct EmbeddingStore {
backend: Box<dyn PgVectorStore>,
metric: VectorMetric,
}
impl EmbeddingStore {
pub fn new(backend: Box<dyn PgVectorStore>, metric: VectorMetric) -> Self {
Self { backend, metric }
}
pub async fn write(
&self,
collection: &str,
records: Vec<VectorRecord>,
) -> Result<(), VectorError> {
if records.is_empty() {
return Ok(());
}
let expected_dim = records[0].vector.len();
DimensionValidator::validate_dimension(expected_dim)?;
for r in &records {
DimensionValidator::validate_vector(&r.vector, expected_dim)?;
}
self.backend
.insert(collection, records)
.await
.map_err(Self::map_backend_error)
}
pub async fn search(
&self,
collection: &str,
query: &[f32],
top_k: usize,
) -> Result<Vec<SearchResult>, VectorError> {
let valid_top_k = validate_top_k(top_k)?;
DimensionValidator::validate_query(query, query.len())?;
self.backend
.search(collection, query, valid_top_k)
.await
.map_err(Self::map_backend_error)
}
pub async fn get(
&self,
collection: &str,
id: &str,
) -> Result<Option<VectorRecord>, VectorError> {
self.backend
.get(collection, id)
.await
.map_err(Self::map_backend_error)
}
pub async fn create_collection(&self, name: &str, dimension: usize) -> Result<(), VectorError> {
DimensionValidator::validate_dimension(dimension)?;
self.backend
.create_collection(name, dimension, Some(self.metric))
.await
.map_err(Self::map_backend_error)
}
pub fn metric(&self) -> &VectorMetric {
&self.metric
}
fn map_backend_error(err: VectorError) -> VectorError {
match err {
VectorError::Connection(msg) => VectorError::BackendUnreachable { reason: msg },
VectorError::Unsupported(ref msg) if msg.contains("connection") => {
VectorError::BackendUnreachable {
reason: msg.clone(),
}
}
other => other,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::InMemoryVectorStore;
fn make_store(_dim: usize) -> EmbeddingStore {
let backend = InMemoryVectorStore::new();
EmbeddingStore::new(Box::new(backend), VectorMetric::Cosine)
}
#[tokio::test]
async fn write_and_get_roundtrip() {
let store = make_store(3);
store.create_collection("test", 3).await.unwrap();
let record = VectorRecord::new("r1", vec![1.0, 2.0, 3.0]);
store.write("test", vec![record]).await.unwrap();
let got = store.get("test", "r1").await.unwrap();
assert!(got.is_some());
assert_eq!(got.unwrap().vector, vec![1.0, 2.0, 3.0]);
}
#[tokio::test]
async fn write_dimension_mismatch_detected() {
let store = make_store(3);
store.create_collection("test", 3).await.unwrap();
let records = vec![
VectorRecord::new("r1", vec![1.0, 2.0, 3.0]),
VectorRecord::new("r2", vec![1.0, 2.0]),
];
let result = store.write("test", records).await;
assert!(result.is_err());
}
#[tokio::test]
async fn search_returns_results() {
let store = make_store(2);
store.create_collection("test", 2).await.unwrap();
store
.write(
"test",
vec![
VectorRecord::new("a", vec![1.0, 0.0]),
VectorRecord::new("b", vec![0.0, 1.0]),
],
)
.await
.unwrap();
let results = store.search("test", &[1.0, 0.0], 2).await.unwrap();
assert_eq!(results.len(), 2);
}
#[tokio::test]
async fn search_top_k_validation() {
let store = make_store(2);
store.create_collection("test", 2).await.unwrap();
let result = store.search("test", &[1.0, 0.0], 0).await;
assert!(result.is_err());
}
#[tokio::test]
async fn write_empty_records_ok() {
let store = make_store(2);
store.create_collection("test", 2).await.unwrap();
assert!(store.write("test", vec![]).await.is_ok());
}
#[tokio::test]
async fn get_nonexistent_returns_none() {
let store = make_store(2);
store.create_collection("test", 2).await.unwrap();
let result = store.get("test", "nonexistent").await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn metric_accessor() {
let store = make_store(2);
assert_eq!(store.metric(), &VectorMetric::Cosine);
}
#[tokio::test]
async fn backend_unreachable_from_stub() {
use crate::stub::StubVectorStore;
let store = EmbeddingStore::new(Box::new(StubVectorStore), VectorMetric::Cosine);
let result = store.search("any", &[1.0, 2.0, 3.0], 5).await;
assert!(result.is_err());
}
#[tokio::test]
async fn write_768_dim_vector() {
let store = make_store(768);
store.create_collection("embeddings", 768).await.unwrap();
let vec = vec![0.1; 768];
let record = VectorRecord::new("doc1", vec);
store.write("embeddings", vec![record]).await.unwrap();
let got = store.get("embeddings", "doc1").await.unwrap();
assert!(got.is_some());
assert_eq!(got.unwrap().vector.len(), 768);
}
#[tokio::test]
async fn create_collection_invalid_dimension() {
let store = make_store(0);
let result = store.create_collection("bad", 0).await;
assert!(result.is_err());
}
#[tokio::test]
async fn search_with_different_metrics() {
let euclidean_store = {
let backend = InMemoryVectorStore::new();
EmbeddingStore::new(Box::new(backend), VectorMetric::Euclidean)
};
euclidean_store.create_collection("test", 2).await.unwrap();
euclidean_store
.write("test", vec![VectorRecord::new("a", vec![1.0, 0.0])])
.await
.unwrap();
let results = euclidean_store
.search("test", &[1.0, 0.0], 1)
.await
.unwrap();
assert_eq!(results.len(), 1);
}
}