1use anyhow::Result;
2#[cfg(feature = "hf-hub")]
3use hf_hub::api::sync::{ApiBuilder, ApiRepo};
4use ort::{
5 execution_providers::ExecutionProviderDispatch,
6 session::builder::{GraphOptimizationLevel, SessionBuilder},
7};
8#[cfg(feature = "hf-hub")]
9use std::path::PathBuf;
10use tokenizers::{AddedToken, PaddingParams, PaddingStrategy, Tokenizer, TruncationParams};
11
12const DEFAULT_CACHE_DIR: &str = ".fastembed_cache";
13
14pub fn get_cache_dir() -> String {
15 std::env::var("FASTEMBED_CACHE_DIR").unwrap_or(DEFAULT_CACHE_DIR.into())
16}
17
18#[derive(Debug, Clone, PartialEq)]
19pub struct SparseEmbedding {
20 pub indices: Vec<usize>,
21 pub values: Vec<f32>,
22}
23
24pub type Embedding = Vec<f32>;
26
27pub type Error = anyhow::Error;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct TokenizerFiles {
33 pub tokenizer_file: Vec<u8>,
34 pub config_file: Vec<u8>,
35 pub special_tokens_map_file: Vec<u8>,
36 pub tokenizer_config_file: Vec<u8>,
37}
38
39#[cfg(feature = "hf-hub")]
42pub fn load_tokenizer_hf_hub(model_repo: ApiRepo, max_length: usize) -> Result<Tokenizer> {
43 let tokenizer_files: TokenizerFiles = TokenizerFiles {
44 tokenizer_file: std::fs::read(model_repo.get("tokenizer.json")?)?,
45 config_file: std::fs::read(&model_repo.get("config.json")?)?,
46 special_tokens_map_file: std::fs::read(&model_repo.get("special_tokens_map.json")?)?,
47
48 tokenizer_config_file: std::fs::read(&model_repo.get("tokenizer_config.json")?)?,
49 };
50
51 load_tokenizer(tokenizer_files, max_length)
52}
53
54pub fn load_tokenizer(tokenizer_files: TokenizerFiles, max_length: usize) -> Result<Tokenizer> {
58 let base_error_message =
59 "Error building TokenizerFiles for UserDefinedEmbeddingModel. Could not read {} file.";
60
61 let config: serde_json::Value =
63 serde_json::from_slice(&tokenizer_files.config_file).map_err(|_| {
64 std::io::Error::new(
65 std::io::ErrorKind::InvalidData,
66 base_error_message.replace("{}", "config.json"),
67 )
68 })?;
69 let special_tokens_map: serde_json::Value =
70 serde_json::from_slice(&tokenizer_files.special_tokens_map_file).map_err(|_| {
71 std::io::Error::new(
72 std::io::ErrorKind::InvalidData,
73 base_error_message.replace("{}", "special_tokens_map.json"),
74 )
75 })?;
76 let tokenizer_config: serde_json::Value =
77 serde_json::from_slice(&tokenizer_files.tokenizer_config_file).map_err(|_| {
78 std::io::Error::new(
79 std::io::ErrorKind::InvalidData,
80 base_error_message.replace("{}", "tokenizer_config.json"),
81 )
82 })?;
83 let mut tokenizer: tokenizers::Tokenizer =
84 tokenizers::Tokenizer::from_bytes(tokenizer_files.tokenizer_file).map_err(|_| {
85 std::io::Error::new(
86 std::io::ErrorKind::InvalidData,
87 base_error_message.replace("{}", "tokenizer.json"),
88 )
89 })?;
90
91 let model_max_length = tokenizer_config["model_max_length"]
93 .as_f64()
94 .ok_or_else(|| {
95 anyhow::anyhow!("tokenizer_config.json is missing a numeric `model_max_length` field")
96 })? as f32;
97 let max_length = max_length.min(model_max_length as usize);
98 let pad_id = config["pad_token_id"].as_u64().unwrap_or(0) as u32;
99 let pad_token: String = tokenizer_config["pad_token"]
100 .as_str()
101 .ok_or_else(|| {
102 anyhow::anyhow!("tokenizer_config.json is missing a string `pad_token` field")
103 })?
104 .into();
105
106 let mut tokenizer = tokenizer
107 .with_padding(Some(PaddingParams {
108 strategy: PaddingStrategy::BatchLongest,
110 pad_token,
111 pad_id,
112 ..Default::default()
113 }))
114 .with_truncation(Some(TruncationParams {
115 max_length,
116 ..Default::default()
117 }))
118 .map_err(anyhow::Error::msg)?
119 .clone();
120 if let serde_json::Value::Object(root_object) = special_tokens_map {
121 for (_, value) in root_object.iter() {
122 if value.is_string() {
123 if let Some(content) = value.as_str() {
124 tokenizer.add_special_tokens(&[AddedToken {
125 content: content.into(),
126 special: true,
127 ..Default::default()
128 }]);
129 }
130 } else if value.is_object() {
131 if let (
132 Some(content),
133 Some(single_word),
134 Some(lstrip),
135 Some(rstrip),
136 Some(normalized),
137 ) = (
138 value["content"].as_str(),
139 value["single_word"].as_bool(),
140 value["lstrip"].as_bool(),
141 value["rstrip"].as_bool(),
142 value["normalized"].as_bool(),
143 ) {
144 tokenizer.add_special_tokens(&[AddedToken {
145 content: content.into(),
146 special: true,
147 single_word,
148 lstrip,
149 rstrip,
150 normalized,
151 }]);
152 }
153 }
154 }
155 }
156 Ok(tokenizer.into())
157}
158
159pub fn normalize(v: &[f32]) -> Vec<f32> {
160 let norm = (v.iter().map(|val| val * val).sum::<f32>()).sqrt();
161 let epsilon = 1e-12;
162
163 v.iter().map(|&val| val / (norm + epsilon)).collect()
165}
166
167#[cfg(feature = "hf-hub")]
171pub fn pull_from_hf(
172 model_name: String,
173 default_cache_dir: PathBuf,
174 show_download_progress: bool,
175) -> anyhow::Result<ApiRepo> {
176 use std::env;
177
178 let cache_dir = env::var("HF_HOME")
179 .map(PathBuf::from)
180 .unwrap_or(default_cache_dir);
181
182 let endpoint = env::var("HF_ENDPOINT").unwrap_or_else(|_| "https://huggingface.co".to_string());
183
184 let api = ApiBuilder::new()
185 .with_cache_dir(cache_dir)
186 .with_endpoint(endpoint)
187 .with_progress(show_download_progress)
188 .build()?;
189
190 let repo = api.model(model_name);
191 Ok(repo)
192}
193
194pub(crate) fn init_session_builder(
195 execution_providers: Vec<ExecutionProviderDispatch>,
196 intra_threads: Option<usize>,
197) -> anyhow::Result<SessionBuilder> {
198 let threads = match intra_threads {
199 Some(n) => n,
200 None => std::thread::available_parallelism()?.get(),
201 };
202
203 #[cfg(feature = "directml")]
204 let has_directml = execution_providers
205 .iter()
206 .any(|ep| ep.downcast_ref::<ort::ep::DirectML>().is_some());
207 #[cfg(not(feature = "directml"))]
208 let has_directml = false;
209
210 let builder_error = |err: ort::Error<SessionBuilder>| anyhow::Error::msg(err.to_string());
211
212 let mut builder = ort::session::Session::builder()?
213 .with_execution_providers(execution_providers)
214 .map_err(builder_error)?
215 .with_optimization_level(GraphOptimizationLevel::Level3)
216 .map_err(builder_error)?
217 .with_intra_threads(threads)
218 .map_err(builder_error)?;
219
220 if has_directml {
221 builder = builder
222 .with_memory_pattern(false)
223 .map_err(builder_error)?
224 .with_parallel_execution(false)
225 .map_err(builder_error)?;
226 }
227
228 Ok(builder)
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn minimal_tokenizer_bytes() -> Vec<u8> {
236 br#"{
238 "version": "1.0",
239 "truncation": null,
240 "padding": null,
241 "added_tokens": [],
242 "normalizer": null,
243 "pre_tokenizer": null,
244 "post_processor": null,
245 "decoder": null,
246 "model": {
247 "type": "BPE",
248 "dropout": null,
249 "unk_token": "[UNK]",
250 "fuse_unk": false,
251 "byte_fallback": false,
252 "vocab": {"[UNK]": 0, "[PAD]": 1, "hello": 2},
253 "merges": []
254 }
255 }"#
256 .to_vec()
257 }
258
259 fn tokenizer_files(tokenizer_config: &str) -> TokenizerFiles {
260 TokenizerFiles {
261 tokenizer_file: minimal_tokenizer_bytes(),
262 config_file: br#"{"pad_token_id": 0}"#.to_vec(),
263 special_tokens_map_file: b"{}".to_vec(),
264 tokenizer_config_file: tokenizer_config.as_bytes().to_vec(),
265 }
266 }
267
268 #[test]
269 fn load_tokenizer_ok_with_complete_config() {
270 let files = tokenizer_files(r#"{"model_max_length": 512, "pad_token": "[PAD]"}"#);
271 assert!(load_tokenizer(files, 512).is_ok());
272 }
273
274 #[test]
275 fn load_tokenizer_errors_on_missing_pad_token() {
276 let files = tokenizer_files(r#"{"model_max_length": 512}"#);
277 let err = load_tokenizer(files, 512).unwrap_err();
278 assert!(
279 err.to_string().contains("pad_token"),
280 "error message was: {err}"
281 );
282 }
283
284 #[test]
285 fn load_tokenizer_errors_on_missing_model_max_length() {
286 let files = tokenizer_files(r#"{"pad_token": "[PAD]"}"#);
287 let err = load_tokenizer(files, 512).unwrap_err();
288 assert!(
289 err.to_string().contains("model_max_length"),
290 "error message was: {err}"
291 );
292 }
293}