use fastembed::{EmbeddingModel, InitOptions, TextEmbedding};
use parking_lot::Mutex;
use crate::prime::error::{PrimeError, PrimeResult};
pub const DEFAULT_EMBEDDING_DIMENSIONS: usize = 384;
pub struct TextEmbedder {
model: Mutex<TextEmbedding>,
dimensions: usize,
}
impl TextEmbedder {
pub fn new() -> PrimeResult<Self> {
let model = TextEmbedding::try_new(
InitOptions::new(EmbeddingModel::AllMiniLML6V2).with_show_download_progress(false),
)
.map_err(|e| {
PrimeError::CoreError(anyhow::anyhow!("failed to initialize embedder: {e}"))
})?;
Ok(Self {
model: Mutex::new(model),
dimensions: DEFAULT_EMBEDDING_DIMENSIONS,
})
}
pub fn embed(&self, text: &str) -> PrimeResult<Vec<f32>> {
let mut out = self
.model
.lock()
.embed(vec![text], None)
.map_err(|e| PrimeError::CoreError(anyhow::anyhow!("embedding failed: {e}")))?;
out.pop()
.ok_or_else(|| PrimeError::CoreError(anyhow::anyhow!("embedder produced no output")))
}
pub fn dimensions(&self) -> usize {
self.dimensions
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore = "downloads model — run with --ignored"]
fn embed_returns_expected_dimensions() {
let embedder = TextEmbedder::new().expect("embedder init");
let v = embedder.embed("hello world").expect("embed");
assert_eq!(v.len(), DEFAULT_EMBEDDING_DIMENSIONS);
assert_eq!(v.len(), embedder.dimensions());
}
#[test]
#[ignore = "downloads model — run with --ignored"]
fn embed_is_deterministic_for_same_input() {
let embedder = TextEmbedder::new().expect("embedder init");
let a = embedder.embed("test sentence").expect("embed a");
let b = embedder.embed("test sentence").expect("embed b");
assert_eq!(a, b);
}
#[test]
#[ignore = "downloads model — run with --ignored"]
fn embed_differs_for_different_input() {
let embedder = TextEmbedder::new().expect("embedder init");
let a = embedder.embed("project status update").expect("embed a");
let b = embedder.embed("apple pie recipe").expect("embed b");
assert_ne!(a, b);
}
}