use crate::embedding::{EmbeddingError, EmbeddingProvider, Result};
const DEFAULT_MODEL: &str = "BAAI/bge-small-en-v1.5";
const DEFAULT_DIM: usize = 384;
pub struct FastembedProvider {
model: std::sync::Mutex<fastembed::TextEmbedding>,
dim: usize,
}
impl FastembedProvider {
pub fn new() -> std::result::Result<Self, EmbeddingError> {
Self::with_model(DEFAULT_MODEL, DEFAULT_DIM)
}
pub fn with_model(model_name: &str, dim: usize) -> std::result::Result<Self, EmbeddingError> {
let model_info = fastembed::TextEmbedding::list_supported_models()
.into_iter()
.find(|m| m.model_code == model_name)
.ok_or_else(|| {
EmbeddingError::Provider(format!(
"Model '{}' not supported by fastembed. Use TextEmbedding::list_supported_models() to see available models.",
model_name
))
})?;
let options =
fastembed::InitOptions::new(model_info.model).with_show_download_progress(true);
let model = fastembed::TextEmbedding::try_new(options).map_err(|e| {
EmbeddingError::Provider(format!("Failed to initialize fastembed model: {e}"))
})?;
Ok(Self {
model: std::sync::Mutex::new(model),
dim,
})
}
pub fn with_cache_dir(
model_name: &str,
dim: usize,
cache_dir: impl Into<std::path::PathBuf>,
) -> std::result::Result<Self, EmbeddingError> {
let model_info = fastembed::TextEmbedding::list_supported_models()
.into_iter()
.find(|m| m.model_code == model_name)
.ok_or_else(|| {
EmbeddingError::Provider(format!("Model '{}' not supported", model_name))
})?;
let options = fastembed::InitOptions::new(model_info.model)
.with_show_download_progress(true)
.with_cache_dir(cache_dir.into());
let model = fastembed::TextEmbedding::try_new(options).map_err(|e| {
EmbeddingError::Provider(format!("Failed to initialize fastembed model: {e}"))
})?;
Ok(Self {
model: std::sync::Mutex::new(model),
dim,
})
}
}
impl EmbeddingProvider for FastembedProvider {
fn embed_batch(&self, texts: &[String]) -> Result<Vec<Vec<f32>>> {
if texts.is_empty() {
return Ok(Vec::new());
}
let mut model = self
.model
.lock()
.map_err(|e| EmbeddingError::Provider(format!("fastembed lock poisoned: {e}")))?;
let results = model
.embed(texts.to_vec(), None)
.map_err(|e| EmbeddingError::Provider(format!("fastembed embed failed: {e}")))?;
for (i, vec) in results.iter().enumerate() {
if vec.len() != self.dim {
return Err(EmbeddingError::DimensionMismatch {
expected: self.dim,
actual: vec.len(),
});
}
let _ = i; }
Ok(results)
}
fn dim(&self) -> usize {
self.dim
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore] fn test_fastembed_single() {
let provider = FastembedProvider::new().expect("init should succeed");
let vec = provider.embed("Hello world").expect("embed should succeed");
assert_eq!(vec.len(), DEFAULT_DIM);
let norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
assert!(
(norm - 1.0).abs() < 0.1,
"expected ~unit vector, got norm={norm}"
);
}
#[test]
#[ignore] fn test_fastembed_batch() {
let provider = FastembedProvider::new().expect("init should succeed");
let texts = vec![
"The cat sat on the mat".to_string(),
"Dogs are loyal animals".to_string(),
"Mathematics is beautiful".to_string(),
];
let results = provider
.embed_batch(&texts)
.expect("batch embed should succeed");
assert_eq!(results.len(), 3);
for vec in &results {
assert_eq!(vec.len(), DEFAULT_DIM);
}
}
#[test]
#[ignore] fn test_fastembed_similarity() {
use crate::embedding::cosine_similarity;
let provider = FastembedProvider::new().expect("init should succeed");
let cat = provider.embed("cat").expect("embed");
let dog = provider.embed("dog").expect("embed");
let math = provider.embed("integral calculus").expect("embed");
let cat_dog = cosine_similarity(&cat, &dog);
let cat_math = cosine_similarity(&cat, &math);
assert!(
cat_dog > cat_math,
"cat-dog ({cat_dog}) should be more similar than cat-math ({cat_math})"
);
}
#[test]
#[ignore] fn test_fastembed_empty_batch() {
let provider = FastembedProvider::new().expect("init should succeed");
let results = provider
.embed_batch(&[])
.expect("empty batch should succeed");
assert!(results.is_empty());
}
}