llm_kernel/embedding/
fastembed.rs1use std::path::PathBuf;
16use std::sync::Mutex;
17
18use crate::embedding::catalog::EmbeddingModel;
19use crate::embedding::types::{EmbeddingProvider, EmbeddingResult};
20use crate::error::{KernelError, Result};
21
22pub struct FastembedProvider {
28 inner: Mutex<fastembed::TextEmbedding>,
29 model: EmbeddingModel,
30}
31
32impl FastembedProvider {
33 pub fn new(model: EmbeddingModel, cache_dir: Option<PathBuf>) -> Result<Self> {
38 let mut options = fastembed::TextInitOptions::new(model.as_fastembed())
39 .with_show_download_progress(false);
40 if let Some(dir) = cache_dir {
41 options = options.with_cache_dir(dir);
42 }
43 let te = fastembed::TextEmbedding::try_new(options).map_err(KernelError::embedding)?;
44 Ok(Self {
45 inner: Mutex::new(te),
46 model,
47 })
48 }
49
50 #[cfg(all(feature = "embedding-fastembed-directml", target_os = "windows"))]
61 pub fn new_with_directml(model: EmbeddingModel, cache_dir: Option<PathBuf>) -> Result<Self> {
62 use ort::execution_providers::DirectMLExecutionProvider;
63 let mut options = fastembed::TextInitOptions::new(model.as_fastembed())
64 .with_show_download_progress(false)
65 .with_execution_providers(vec![DirectMLExecutionProvider::default().build()]);
66 if let Some(dir) = cache_dir {
67 options = options.with_cache_dir(dir);
68 }
69 let te = fastembed::TextEmbedding::try_new(options).map_err(KernelError::embedding)?;
70 Ok(Self {
71 inner: Mutex::new(te),
72 model,
73 })
74 }
75
76 pub fn with_max_length(
78 model: EmbeddingModel,
79 cache_dir: Option<PathBuf>,
80 max_length: usize,
81 ) -> Result<Self> {
82 let mut options = fastembed::TextInitOptions::new(model.as_fastembed())
83 .with_show_download_progress(false)
84 .with_max_length(max_length);
85 if let Some(dir) = cache_dir {
86 options = options.with_cache_dir(dir);
87 }
88 let te = fastembed::TextEmbedding::try_new(options).map_err(KernelError::embedding)?;
89 Ok(Self {
90 inner: Mutex::new(te),
91 model,
92 })
93 }
94}
95
96use super::types::text_preview;
97
98impl EmbeddingProvider for FastembedProvider {
99 fn dim(&self) -> usize {
100 self.model.dimension()
101 }
102
103 fn name(&self) -> &str {
104 self.model.as_str()
105 }
106
107 fn embed(&self, text: &str) -> Result<EmbeddingResult> {
108 let owned = match self.model.query_prefix() {
109 Some(prefix) => format!("{prefix}{text}"),
110 None => text.to_string(),
111 };
112 let mut te = self
113 .inner
114 .lock()
115 .map_err(|e| KernelError::Embedding(format!("lock: {e}")))?;
116 let embeddings = te
117 .embed(vec![owned], None)
118 .map_err(KernelError::embedding)?;
119 let vector = embeddings
120 .into_iter()
121 .next()
122 .ok_or_else(|| KernelError::Embedding("empty embedding output".into()))?;
123
124 Ok(EmbeddingResult {
125 vector,
126 text_preview: text_preview(text),
127 })
128 }
129
130 fn embed_batch(&self, texts: &[&str]) -> Result<Vec<EmbeddingResult>> {
131 if texts.is_empty() {
132 return Ok(vec![]);
133 }
134 let prefix = self.model.query_prefix();
135 let prepared: Vec<String> = texts
136 .iter()
137 .map(|t| match prefix {
138 Some(p) => format!("{p}{t}"),
139 None => t.to_string(),
140 })
141 .collect();
142
143 let mut te = self
144 .inner
145 .lock()
146 .map_err(|e| KernelError::Embedding(format!("lock: {e}")))?;
147 let embeddings = te.embed(prepared, None).map_err(KernelError::embedding)?;
148
149 Ok(embeddings
150 .into_iter()
151 .zip(texts.iter())
152 .map(|(vector, &text)| EmbeddingResult {
153 vector,
154 text_preview: text_preview(text),
155 })
156 .collect())
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn provider_name_matches_model() {
166 for &m in EmbeddingModel::ALL {
169 let fe = m.as_fastembed();
171 assert_eq!(format!("{fe:?}"), m.as_str());
172 }
173 }
174
175 #[test]
176 #[ignore = "requires model download"]
177 fn embed_single_text() {
178 let dir = tempfile::tempdir().unwrap();
179 let provider = FastembedProvider::new(
180 EmbeddingModel::BGESmallENV15,
181 Some(dir.path().to_path_buf()),
182 )
183 .unwrap();
184 let result = provider.embed("hello world").unwrap();
185 assert_eq!(result.vector.len(), 384);
186 assert!(!result.vector.is_empty());
187 }
188
189 #[test]
190 #[ignore = "requires model download"]
191 fn embed_batch_texts() {
192 let dir = tempfile::tempdir().unwrap();
193 let provider = FastembedProvider::new(
194 EmbeddingModel::BGESmallENV15,
195 Some(dir.path().to_path_buf()),
196 )
197 .unwrap();
198 let results = provider
199 .embed_batch(&["hello", "world", "foo bar"])
200 .unwrap();
201 assert_eq!(results.len(), 3);
202 for r in &results {
203 assert_eq!(r.vector.len(), 384);
204 }
205 }
206}