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