use std::sync::{Arc, Mutex, OnceLock};
use async_trait::async_trait;
use crate::embedding::{Embedder, EmbedderError};
use fastembed::{TextEmbedding, TextInitOptions};
use ort::environment::Environment;
pub fn onnx_environment() -> Result<Environment, EmbedderError> {
static ENVIRONMENT: OnceLock<Result<Environment, String>> = OnceLock::new();
ENVIRONMENT
.get_or_init(|| ort::init().build().map_err(|e| e.to_string()))
.clone()
.map_err(EmbedderError::FastEmbedError)
}
pub struct FastEmbed {
model: Arc<Mutex<TextEmbedding>>,
batch_size: Option<usize>,
}
impl FastEmbed {
pub fn try_new() -> Result<Self, EmbedderError> {
Ok(Self {
model: Arc::new(Mutex::new(
TextEmbedding::try_new(
TextInitOptions::default().with_environment(onnx_environment()?),
)
.map_err(|e| EmbedderError::FastEmbedError(e.to_string()))?,
)),
batch_size: None,
})
}
pub fn with_batch_size(mut self, batch_size: usize) -> Self {
self.batch_size = Some(batch_size);
self
}
async fn embed(&self, texts: Vec<String>) -> Result<Vec<Vec<f32>>, EmbedderError> {
let model = self.model.clone();
let batch_size = self.batch_size;
crate::blocking::run_blocking(move || {
model
.lock()
.map_err(|e| EmbedderError::FastEmbedError(e.to_string()))?
.embed(texts, batch_size)
.map_err(|e| EmbedderError::FastEmbedError(e.to_string()))
})
.await
}
}
impl From<TextEmbedding> for FastEmbed {
fn from(model: TextEmbedding) -> Self {
Self {
model: Arc::new(Mutex::new(model)),
batch_size: None,
}
}
}
#[async_trait]
impl Embedder for FastEmbed {
async fn embed_documents(&self, documents: &[String]) -> Result<Vec<Vec<f64>>, EmbedderError> {
let embeddings = self.embed(documents.to_vec()).await?;
Ok(embeddings
.into_iter()
.map(|inner_vec| {
inner_vec
.into_iter()
.map(|x| x as f64)
.collect::<Vec<f64>>()
})
.collect::<Vec<Vec<f64>>>())
}
async fn embed_query(&self, text: &str) -> Result<Vec<f64>, EmbedderError> {
let embedding = self.embed(vec![text.to_string()]).await?;
embedding
.into_iter()
.next()
.map(|v| v.into_iter().map(f64::from).collect())
.ok_or_else(|| EmbedderError::FastEmbedError("no embedding returned".into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn new_model() -> FastEmbed {
static CREATE: Mutex<()> = Mutex::new(());
let _guard = CREATE.lock().unwrap_or_else(|e| e.into_inner());
FastEmbed::try_new().unwrap()
}
#[tokio::test]
async fn test_fastembed() {
let fastembed = new_model();
let embeddings = fastembed
.embed_documents(&["hello world".to_string(), "foo bar".to_string()])
.await
.unwrap();
assert_eq!(embeddings.len(), 2);
}
#[tokio::test]
async fn fastembed_can_be_created_twice() {
let first = new_model();
drop(first);
let second = new_model();
let both_alive = new_model();
assert_eq!(second.embed_query("hi").await.unwrap().len(), 384);
assert_eq!(both_alive.embed_query("hi").await.unwrap().len(), 384);
}
#[tokio::test]
async fn fastembed_does_not_block_the_runtime() {
let fastembed = new_model();
let texts: Vec<String> = (0..64)
.map(|i| format!("document number {i} about rust and async runtimes"))
.collect();
let (embeddings, ticks) =
crate::blocking::testing::ticks_while(fastembed.embed_documents(&texts)).await;
assert_eq!(embeddings.unwrap().len(), 64);
assert!(ticks > 0, "embedding blocked the runtime thread");
}
}