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"))]
82 pub fn new_with_coreml(model: EmbeddingModel, cache_dir: Option<PathBuf>) -> Result<Self> {
83 use ort::execution_providers::CoreMLExecutionProvider;
84 let mut options = fastembed::TextInitOptions::new(model.as_fastembed())
85 .with_show_download_progress(false)
86 .with_execution_providers(vec![CoreMLExecutionProvider::default().build()]);
87 if let Some(dir) = cache_dir {
88 options = options.with_cache_dir(dir);
89 }
90 let te = fastembed::TextEmbedding::try_new(options).map_err(KernelError::embedding)?;
91 Ok(Self {
92 inner: Mutex::new(te),
93 model,
94 })
95 }
96
97 pub fn with_max_length(
99 model: EmbeddingModel,
100 cache_dir: Option<PathBuf>,
101 max_length: usize,
102 ) -> Result<Self> {
103 let mut options = fastembed::TextInitOptions::new(model.as_fastembed())
104 .with_show_download_progress(false)
105 .with_max_length(max_length);
106 if let Some(dir) = cache_dir {
107 options = options.with_cache_dir(dir);
108 }
109 let te = fastembed::TextEmbedding::try_new(options).map_err(KernelError::embedding)?;
110 Ok(Self {
111 inner: Mutex::new(te),
112 model,
113 })
114 }
115}
116
117use super::types::text_preview;
118
119impl EmbeddingProvider for FastembedProvider {
120 fn dim(&self) -> usize {
121 self.model.dimension()
122 }
123
124 fn name(&self) -> &str {
125 self.model.as_str()
126 }
127
128 fn embed(&self, text: &str) -> Result<EmbeddingResult> {
129 let owned = match self.model.query_prefix() {
130 Some(prefix) => format!("{prefix}{text}"),
131 None => text.to_string(),
132 };
133 let mut te = self
134 .inner
135 .lock()
136 .map_err(|e| KernelError::Embedding(format!("lock: {e}")))?;
137 let embeddings = te
138 .embed(vec![owned], None)
139 .map_err(KernelError::embedding)?;
140 let vector = embeddings
141 .into_iter()
142 .next()
143 .ok_or_else(|| KernelError::Embedding("empty embedding output".into()))?;
144
145 Ok(EmbeddingResult {
146 vector,
147 text_preview: text_preview(text),
148 })
149 }
150
151 fn embed_batch(&self, texts: &[&str]) -> Result<Vec<EmbeddingResult>> {
152 if texts.is_empty() {
153 return Ok(vec![]);
154 }
155 let prefix = self.model.query_prefix();
156 let prepared: Vec<String> = texts
157 .iter()
158 .map(|t| match prefix {
159 Some(p) => format!("{p}{t}"),
160 None => t.to_string(),
161 })
162 .collect();
163
164 let mut te = self
165 .inner
166 .lock()
167 .map_err(|e| KernelError::Embedding(format!("lock: {e}")))?;
168 let embeddings = te.embed(prepared, None).map_err(KernelError::embedding)?;
169
170 Ok(embeddings
171 .into_iter()
172 .zip(texts.iter())
173 .map(|(vector, &text)| EmbeddingResult {
174 vector,
175 text_preview: text_preview(text),
176 })
177 .collect())
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn provider_name_matches_model() {
187 for &m in EmbeddingModel::ALL {
190 let fe = m.as_fastembed();
192 assert_eq!(format!("{fe:?}"), m.as_str());
193 }
194 }
195
196 #[test]
197 #[ignore = "requires model download"]
198 fn embed_single_text() {
199 let dir = tempfile::tempdir().unwrap();
200 let provider = FastembedProvider::new(
201 EmbeddingModel::BGESmallENV15,
202 Some(dir.path().to_path_buf()),
203 )
204 .unwrap();
205 let result = provider.embed("hello world").unwrap();
206 assert_eq!(result.vector.len(), 384);
207 assert!(!result.vector.is_empty());
208 }
209
210 #[test]
211 #[ignore = "requires model download"]
212 fn embed_batch_texts() {
213 let dir = tempfile::tempdir().unwrap();
214 let provider = FastembedProvider::new(
215 EmbeddingModel::BGESmallENV15,
216 Some(dir.path().to_path_buf()),
217 )
218 .unwrap();
219 let results = provider
220 .embed_batch(&["hello", "world", "foo bar"])
221 .unwrap();
222 assert_eq!(results.len(), 3);
223 for r in &results {
224 assert_eq!(r.vector.len(), 384);
225 }
226 }
227}