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 #[cfg(all(feature = "embedding-fastembed-coreml", target_os = "macos"))]
86 pub fn new_with_coreml(model: EmbeddingModel, cache_dir: Option<PathBuf>) -> Result<Self> {
87 use ort::execution_providers::CoreMLExecutionProvider;
88 let hf_cache = cache_dir
91 .clone()
92 .unwrap_or_else(|| PathBuf::from(fastembed::get_cache_dir()));
93 let coreml_cache = hf_cache.join("coreml");
94 let _ = std::fs::create_dir_all(&coreml_cache);
95 let coreml_ep = CoreMLExecutionProvider::default()
96 .with_model_cache_dir(coreml_cache.to_string_lossy())
101 .build();
102 let mut options = fastembed::TextInitOptions::new(model.as_fastembed())
103 .with_show_download_progress(false)
104 .with_intra_threads(4)
108 .with_execution_providers(vec![coreml_ep]);
109 options = options.with_cache_dir(hf_cache);
110 let te = fastembed::TextEmbedding::try_new(options).map_err(KernelError::embedding)?;
111 Ok(Self {
112 inner: Mutex::new(te),
113 model,
114 })
115 }
116
117 pub fn with_max_length(
119 model: EmbeddingModel,
120 cache_dir: Option<PathBuf>,
121 max_length: usize,
122 ) -> Result<Self> {
123 let mut options = fastembed::TextInitOptions::new(model.as_fastembed())
124 .with_show_download_progress(false)
125 .with_max_length(max_length);
126 if let Some(dir) = cache_dir {
127 options = options.with_cache_dir(dir);
128 }
129 let te = fastembed::TextEmbedding::try_new(options).map_err(KernelError::embedding)?;
130 Ok(Self {
131 inner: Mutex::new(te),
132 model,
133 })
134 }
135}
136
137use super::types::text_preview;
138
139impl EmbeddingProvider for FastembedProvider {
140 fn dim(&self) -> usize {
141 self.model.dimension()
142 }
143
144 fn name(&self) -> &str {
145 self.model.as_str()
146 }
147
148 fn embed(&self, text: &str) -> Result<EmbeddingResult> {
149 self.embed_with_prefix(text, self.model.query_prefix())
150 }
151
152 fn embed_document(&self, text: &str) -> Result<EmbeddingResult> {
153 self.embed_with_prefix(text, self.model.doc_prefix())
156 }
157
158 fn embed_batch(&self, texts: &[&str]) -> Result<Vec<EmbeddingResult>> {
159 self.embed_batch_with_prefix(texts, self.model.query_prefix())
160 }
161
162 fn embed_documents(&self, texts: &[&str]) -> Result<Vec<EmbeddingResult>> {
163 self.embed_batch_with_prefix(texts, self.model.doc_prefix())
164 }
165}
166
167impl FastembedProvider {
168 fn embed_with_prefix(&self, text: &str, prefix: Option<&str>) -> Result<EmbeddingResult> {
170 let owned = match prefix {
171 Some(p) => format!("{p}{text}"),
172 None => text.to_string(),
173 };
174 let mut te = self
175 .inner
176 .lock()
177 .map_err(|e| KernelError::Embedding(format!("lock: {e}")))?;
178 let embeddings = te
179 .embed(vec![owned], None)
180 .map_err(KernelError::embedding)?;
181 let vector = embeddings
182 .into_iter()
183 .next()
184 .ok_or_else(|| KernelError::Embedding("empty embedding output".into()))?;
185
186 Ok(EmbeddingResult {
187 vector,
188 text_preview: text_preview(text),
189 })
190 }
191
192 fn embed_batch_with_prefix(
194 &self,
195 texts: &[&str],
196 prefix: Option<&str>,
197 ) -> Result<Vec<EmbeddingResult>> {
198 if texts.is_empty() {
199 return Ok(vec![]);
200 }
201 let prepared: Vec<String> = texts
202 .iter()
203 .map(|t| match prefix {
204 Some(p) => format!("{p}{t}"),
205 None => t.to_string(),
206 })
207 .collect();
208
209 let mut te = self
210 .inner
211 .lock()
212 .map_err(|e| KernelError::Embedding(format!("lock: {e}")))?;
213 let embeddings = te.embed(prepared, None).map_err(KernelError::embedding)?;
214
215 Ok(embeddings
216 .into_iter()
217 .zip(texts.iter())
218 .map(|(vector, &text)| EmbeddingResult {
219 vector,
220 text_preview: text_preview(text),
221 })
222 .collect())
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn provider_name_matches_model() {
232 for &m in EmbeddingModel::ALL {
235 let fe = m.as_fastembed();
237 assert_eq!(format!("{fe:?}"), m.as_str());
238 }
239 }
240
241 #[test]
242 #[ignore = "requires model download"]
243 fn embed_single_text() {
244 let dir = tempfile::tempdir().unwrap();
245 let provider = FastembedProvider::new(
246 EmbeddingModel::BGESmallENV15,
247 Some(dir.path().to_path_buf()),
248 )
249 .unwrap();
250 let result = provider.embed("hello world").unwrap();
251 assert_eq!(result.vector.len(), 384);
252 assert!(!result.vector.is_empty());
253 }
254
255 #[test]
256 #[ignore = "requires model download"]
257 fn embed_batch_texts() {
258 let dir = tempfile::tempdir().unwrap();
259 let provider = FastembedProvider::new(
260 EmbeddingModel::BGESmallENV15,
261 Some(dir.path().to_path_buf()),
262 )
263 .unwrap();
264 let results = provider
265 .embed_batch(&["hello", "world", "foo bar"])
266 .unwrap();
267 assert_eq!(results.len(), 3);
268 for r in &results {
269 assert_eq!(r.vector.len(), 384);
270 }
271 }
272
273 #[cfg(all(feature = "embedding-fastembed-coreml", target_os = "macos"))]
277 #[test]
278 #[ignore = "requires model download"]
279 fn coreml_repeated_session_cycles() {
280 let dir = tempfile::tempdir().unwrap();
281 let cache_dir = Some(dir.path().to_path_buf());
282 for _ in 0..10 {
283 let provider = FastembedProvider::new_with_coreml(
284 EmbeddingModel::BGESmallENV15,
285 cache_dir.clone(),
286 )
287 .unwrap();
288 drop(provider);
289 }
290 let cache = dir.path().join("coreml");
292 std::fs::read_dir(&cache).expect("coreml cache dir populated");
293 let provider =
294 FastembedProvider::new_with_coreml(EmbeddingModel::BGESmallENV15, cache_dir).unwrap();
295 let result = provider.embed("hello world").unwrap();
296 assert_eq!(result.vector.len(), 384);
297 }
298}