#[cfg(feature = "builtin-embeddings")]
#[cfg_attr(docsrs, doc(cfg(feature = "builtin-embeddings")))]
pub mod onnx;
use crate::error::{PulseDBError, Result};
use crate::types::Embedding;
pub trait EmbeddingService: Send + Sync {
fn embed(&self, text: &str) -> Result<Embedding>;
fn embed_batch(&self, texts: &[&str]) -> Result<Vec<Embedding>>;
fn dimension(&self) -> usize;
fn validate_embedding(&self, embedding: &Embedding) -> Result<()> {
let expected = self.dimension();
let actual = embedding.len();
if actual != expected {
return Err(PulseDBError::Validation(
crate::error::ValidationError::dimension_mismatch(expected, actual),
));
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct ExternalEmbedding {
dimension: usize,
}
impl ExternalEmbedding {
pub fn new(dimension: usize) -> Self {
Self { dimension }
}
}
impl EmbeddingService for ExternalEmbedding {
fn embed(&self, _text: &str) -> Result<Embedding> {
Err(PulseDBError::embedding(
"External embedding mode: embeddings must be provided by the caller",
))
}
fn embed_batch(&self, _texts: &[&str]) -> Result<Vec<Embedding>> {
Err(PulseDBError::embedding(
"External embedding mode: embeddings must be provided by the caller",
))
}
fn dimension(&self) -> usize {
self.dimension
}
}
pub fn create_embedding_service(
config: &crate::config::Config,
) -> Result<Box<dyn EmbeddingService>> {
use crate::config::EmbeddingProvider;
match &config.embedding_provider {
EmbeddingProvider::External => {
let dimension = config.embedding_dimension.size();
Ok(Box::new(ExternalEmbedding::new(dimension)))
}
#[cfg(feature = "builtin-embeddings")]
EmbeddingProvider::Builtin { model_path } => {
let dim = config.embedding_dimension.size();
match onnx::OnnxEmbedding::with_dimension(model_path.clone(), dim) {
Ok(service) => Ok(Box::new(service)),
Err(ref e) if e.to_string().contains("Model not found") => {
tracing::info!(
"Builtin embedding model not found, downloading (dimension: {dim})..."
);
let _path = onnx::OnnxEmbedding::download_default_model(dim)?;
let service = onnx::OnnxEmbedding::with_dimension(model_path.clone(), dim)?;
Ok(Box::new(service))
}
Err(e) => Err(e),
}
}
#[cfg(not(feature = "builtin-embeddings"))]
EmbeddingProvider::Builtin { .. } => Err(PulseDBError::embedding(
"Builtin embeddings require the 'builtin-embeddings' feature",
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_external_embedding_dimension() {
let service = ExternalEmbedding::new(384);
assert_eq!(service.dimension(), 384);
}
#[test]
fn test_external_embedding_embed_returns_error() {
let service = ExternalEmbedding::new(384);
let result = service.embed("hello world");
assert!(result.is_err());
}
#[test]
fn test_external_embedding_embed_batch_returns_error() {
let service = ExternalEmbedding::new(384);
let result = service.embed_batch(&["hello", "world"]);
assert!(result.is_err());
}
#[test]
fn test_validate_embedding_correct_dimension() {
let service = ExternalEmbedding::new(3);
let embedding = vec![1.0, 2.0, 3.0];
assert!(service.validate_embedding(&embedding).is_ok());
}
#[test]
fn test_validate_embedding_wrong_dimension() {
let service = ExternalEmbedding::new(3);
let embedding = vec![1.0, 2.0]; let result = service.validate_embedding(&embedding);
assert!(result.is_err());
}
#[test]
fn test_external_embedding_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ExternalEmbedding>();
}
#[test]
fn test_create_embedding_service_external() {
let config = crate::config::Config::default();
let service = create_embedding_service(&config).unwrap();
assert_eq!(service.dimension(), 384);
}
#[test]
#[ignore] fn test_create_embedding_service_builtin_auto_downloads() {
let config = crate::config::Config::with_builtin_embeddings();
let result = create_embedding_service(&config);
assert!(result.is_ok());
}
}