use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use fastembed::{EmbeddingModel, TextEmbedding, TextInitOptions};
use crate::{EmbeddingError, Embeddings};
pub struct FastEmbedEmbeddings {
model: Arc<Mutex<TextEmbedding>>,
model_name: String,
dimension: usize,
}
impl std::fmt::Debug for FastEmbedEmbeddings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FastEmbedEmbeddings")
.field("model", &self.model_name)
.field("dimension", &self.dimension)
.finish()
}
}
impl FastEmbedEmbeddings {
pub fn new(options: TextInitOptions) -> Result<Self, EmbeddingError> {
let model_name = Self::friendly_model_name(options.model_name.clone()).to_string();
let dimension = Self::infer_dimension(options.model_name.clone())?;
let model = TextEmbedding::try_new(options).map_err(|e| {
EmbeddingError::ApiError(format!("Failed to initialize FastEmbed model: {}", e))
})?;
Ok(Self {
model: Arc::new(Mutex::new(model)),
model_name,
dimension,
})
}
pub fn default_model() -> Result<Self, EmbeddingError> {
Self::new(
TextInitOptions::new(EmbeddingModel::BGESmallENV15).with_show_download_progress(false),
)
}
pub fn with_model(model: EmbeddingModel) -> Result<Self, EmbeddingError> {
Self::new(TextInitOptions::new(model).with_show_download_progress(false))
}
fn infer_dimension(model: EmbeddingModel) -> Result<usize, EmbeddingError> {
let dim = match model {
EmbeddingModel::BGESmallENV15 | EmbeddingModel::BGESmallENV15Q => 384,
EmbeddingModel::BGEBaseENV15 | EmbeddingModel::BGEBaseENV15Q => 768,
EmbeddingModel::BGELargeENV15 | EmbeddingModel::BGELargeENV15Q => 1024,
EmbeddingModel::AllMiniLML6V2 | EmbeddingModel::AllMiniLML6V2Q => 384,
EmbeddingModel::AllMiniLML12V2 | EmbeddingModel::AllMiniLML12V2Q => 384,
EmbeddingModel::AllMpnetBaseV2 => 768,
EmbeddingModel::NomicEmbedTextV1 => 768,
EmbeddingModel::NomicEmbedTextV15 | EmbeddingModel::NomicEmbedTextV15Q => 768,
EmbeddingModel::MxbaiEmbedLargeV1 | EmbeddingModel::MxbaiEmbedLargeV1Q => 1024,
EmbeddingModel::GTEBaseENV15 | EmbeddingModel::GTEBaseENV15Q => 768,
EmbeddingModel::GTELargeENV15 | EmbeddingModel::GTELargeENV15Q => 1024,
EmbeddingModel::MultilingualE5Small => 384,
EmbeddingModel::MultilingualE5Base => 768,
EmbeddingModel::MultilingualE5Large => 1024,
EmbeddingModel::BGESmallZHV15 => 512,
EmbeddingModel::BGELargeZHV15 => 1024,
EmbeddingModel::BGEM3 => 1024,
EmbeddingModel::ClipVitB32 => 512,
EmbeddingModel::JinaEmbeddingsV2BaseEN => 768,
EmbeddingModel::JinaEmbeddingsV2BaseCode => 768,
EmbeddingModel::ParaphraseMLMiniLML12V2 | EmbeddingModel::ParaphraseMLMiniLML12V2Q => {
384
}
EmbeddingModel::ParaphraseMLMpnetBaseV2 => 768,
EmbeddingModel::ModernBertEmbedLarge => 1024,
EmbeddingModel::EmbeddingGemma300M
| EmbeddingModel::EmbeddingGemma300MQ
| EmbeddingModel::EmbeddingGemma300MQ4 => 768,
EmbeddingModel::SnowflakeArcticEmbedXS | EmbeddingModel::SnowflakeArcticEmbedXSQ => 384,
EmbeddingModel::SnowflakeArcticEmbedS | EmbeddingModel::SnowflakeArcticEmbedSQ => 384,
EmbeddingModel::SnowflakeArcticEmbedM | EmbeddingModel::SnowflakeArcticEmbedMQ => 768,
EmbeddingModel::SnowflakeArcticEmbedMLong
| EmbeddingModel::SnowflakeArcticEmbedMLongQ => 768,
EmbeddingModel::SnowflakeArcticEmbedL | EmbeddingModel::SnowflakeArcticEmbedLQ => 1024,
};
Ok(dim)
}
fn friendly_model_name(model: EmbeddingModel) -> &'static str {
match model {
EmbeddingModel::AllMiniLML6V2 => "all-MiniLM-L6-v2",
EmbeddingModel::AllMiniLML6V2Q => "all-MiniLM-L6-v2 (quantized)",
EmbeddingModel::AllMiniLML12V2 => "all-MiniLM-L12-v2",
EmbeddingModel::AllMiniLML12V2Q => "all-MiniLM-L12-v2 (quantized)",
EmbeddingModel::AllMpnetBaseV2 => "all-mpnet-base-v2",
EmbeddingModel::BGEBaseENV15 => "BGE-base-en-v1.5",
EmbeddingModel::BGEBaseENV15Q => "BGE-base-en-v1.5 (quantized)",
EmbeddingModel::BGELargeENV15 => "BGE-large-en-v1.5",
EmbeddingModel::BGELargeENV15Q => "BGE-large-en-v1.5 (quantized)",
EmbeddingModel::BGESmallENV15 => "BGE-small-en-v1.5",
EmbeddingModel::BGESmallENV15Q => "BGE-small-en-v1.5 (quantized)",
EmbeddingModel::NomicEmbedTextV1 => "nomic-embed-text-v1",
EmbeddingModel::NomicEmbedTextV15 => "nomic-embed-text-v1.5",
EmbeddingModel::NomicEmbedTextV15Q => "nomic-embed-text-v1.5 (quantized)",
EmbeddingModel::ParaphraseMLMiniLML12V2 => "paraphrase-multilingual-MiniLM-L12-v2",
EmbeddingModel::ParaphraseMLMiniLML12V2Q => {
"paraphrase-multilingual-MiniLM-L12-v2 (quantized)"
}
EmbeddingModel::ParaphraseMLMpnetBaseV2 => "paraphrase-multilingual-mpnet-base-v2",
EmbeddingModel::BGESmallZHV15 => "BGE-small-zh-v1.5",
EmbeddingModel::BGELargeZHV15 => "BGE-large-zh-v1.5",
EmbeddingModel::BGEM3 => "BGE-m3",
EmbeddingModel::ModernBertEmbedLarge => "modernbert-embed-large",
EmbeddingModel::MultilingualE5Small => "multilingual-e5-small",
EmbeddingModel::MultilingualE5Base => "multilingual-e5-base",
EmbeddingModel::MultilingualE5Large => "multilingual-e5-large",
EmbeddingModel::MxbaiEmbedLargeV1 => "mxbai-embed-large-v1",
EmbeddingModel::MxbaiEmbedLargeV1Q => "mxbai-embed-large-v1 (quantized)",
EmbeddingModel::GTEBaseENV15 => "gte-base-en-v1.5",
EmbeddingModel::GTEBaseENV15Q => "gte-base-en-v1.5 (quantized)",
EmbeddingModel::GTELargeENV15 => "gte-large-en-v1.5",
EmbeddingModel::GTELargeENV15Q => "gte-large-en-v1.5 (quantized)",
EmbeddingModel::ClipVitB32 => "clip-ViT-B-32-text",
EmbeddingModel::JinaEmbeddingsV2BaseCode => "jina-embeddings-v2-base-code",
EmbeddingModel::JinaEmbeddingsV2BaseEN => "jina-embeddings-v2-base-en",
EmbeddingModel::EmbeddingGemma300M => "embeddinggemma-300m",
EmbeddingModel::EmbeddingGemma300MQ4 => "embeddinggemma-300m (q4)",
EmbeddingModel::EmbeddingGemma300MQ => "embeddinggemma-300m (quantized)",
EmbeddingModel::SnowflakeArcticEmbedXS => "snowflake-arctic-embed-xs",
EmbeddingModel::SnowflakeArcticEmbedXSQ => "snowflake-arctic-embed-xs (quantized)",
EmbeddingModel::SnowflakeArcticEmbedS => "snowflake-arctic-embed-s",
EmbeddingModel::SnowflakeArcticEmbedSQ => "snowflake-arctic-embed-s (quantized)",
EmbeddingModel::SnowflakeArcticEmbedM => "snowflake-arctic-embed-m",
EmbeddingModel::SnowflakeArcticEmbedMQ => "snowflake-arctic-embed-m (quantized)",
EmbeddingModel::SnowflakeArcticEmbedMLong => "snowflake-arctic-embed-m-long",
EmbeddingModel::SnowflakeArcticEmbedMLongQ => {
"snowflake-arctic-embed-m-long (quantized)"
}
EmbeddingModel::SnowflakeArcticEmbedL => "snowflake-arctic-embed-l",
EmbeddingModel::SnowflakeArcticEmbedLQ => "snowflake-arctic-embed-l (quantized)",
}
}
}
#[async_trait]
impl Embeddings for FastEmbedEmbeddings {
async fn embed_query(&self, text: &str) -> Result<Vec<f32>, EmbeddingError> {
if text.trim().is_empty() {
return Err(EmbeddingError::EmptyInput);
}
let text = text.to_string();
let model = Arc::clone(&self.model);
tokio::task::spawn_blocking(move || {
let mut model = model.lock().map_err(|e| {
EmbeddingError::ApiError(format!("FastEmbed model lock poisoned: {}", e))
})?;
let result = model.embed(vec![text.as_str()], None).map_err(|e| {
EmbeddingError::ApiError(format!("FastEmbed inference failed: {}", e))
})?;
result
.first()
.map(|v| v.to_vec())
.ok_or_else(|| EmbeddingError::ApiError("No embedding returned".to_string()))
})
.await
.map_err(|e| EmbeddingError::ApiError(format!("Task execution failed: {}", e)))?
}
async fn embed_documents(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, EmbeddingError> {
if texts.is_empty() {
return Ok(Vec::new());
}
if texts.iter().any(|t| t.trim().is_empty()) {
return Err(EmbeddingError::EmptyInput);
}
let text_vec: Vec<String> = texts.iter().map(|s| s.to_string()).collect();
let model = Arc::clone(&self.model);
tokio::task::spawn_blocking(move || {
let mut model = model.lock().map_err(|e| {
EmbeddingError::ApiError(format!("FastEmbed model lock poisoned: {}", e))
})?;
let str_vec: Vec<&str> = text_vec.iter().map(|s| s.as_str()).collect();
let result = model.embed(str_vec, None).map_err(|e| {
EmbeddingError::ApiError(format!("FastEmbed batch inference failed: {}", e))
})?;
Ok(result.into_iter().map(|v| v.to_vec()).collect())
})
.await
.map_err(|e| EmbeddingError::ApiError(format!("Task execution failed: {}", e)))?
}
fn dimension(&self) -> usize {
self.dimension
}
fn model_name(&self) -> &str {
&self.model_name
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dim(model: EmbeddingModel) -> usize {
FastEmbedEmbeddings::infer_dimension(model).unwrap()
}
#[test]
fn test_infer_dimension_bge_small() {
assert_eq!(dim(EmbeddingModel::BGESmallENV15), 384);
}
#[test]
fn test_infer_dimension_bge_base() {
assert_eq!(dim(EmbeddingModel::BGEBaseENV15), 768);
}
#[test]
fn test_infer_dimension_bge_large() {
assert_eq!(dim(EmbeddingModel::BGELargeENV15), 1024);
}
#[test]
fn test_infer_dimension_mini_lm() {
assert_eq!(dim(EmbeddingModel::AllMiniLML6V2), 384);
}
#[test]
fn test_infer_dimension_nomic() {
assert_eq!(dim(EmbeddingModel::NomicEmbedTextV15), 768);
}
#[test]
fn test_infer_dimension_mxbai() {
assert_eq!(dim(EmbeddingModel::MxbaiEmbedLargeV1), 1024);
}
#[test]
fn test_infer_dimension_gte_base() {
assert_eq!(dim(EmbeddingModel::GTEBaseENV15), 768);
}
#[test]
fn test_infer_dimension_multilingual_e5() {
assert_eq!(dim(EmbeddingModel::MultilingualE5Base), 768);
}
#[test]
fn test_infer_dimension_snowflake_xs() {
assert_eq!(dim(EmbeddingModel::SnowflakeArcticEmbedXS), 384);
}
#[test]
fn test_infer_dimension_modern_bert() {
assert_eq!(dim(EmbeddingModel::ModernBertEmbedLarge), 1024);
}
#[test]
fn test_friendly_model_name() {
assert_eq!(
FastEmbedEmbeddings::friendly_model_name(EmbeddingModel::BGESmallENV15),
"BGE-small-en-v1.5"
);
assert_eq!(
FastEmbedEmbeddings::friendly_model_name(EmbeddingModel::SnowflakeArcticEmbedMLong),
"snowflake-arctic-embed-m-long"
);
}
}