Skip to main content

fastembed/
common.rs

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
24/// Type alias for the embedding vector
25pub type Embedding = Vec<f32>;
26
27/// Type alias for the error type
28pub type Error = anyhow::Error;
29
30// Tokenizer files for "bring your own" models
31#[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/// The procedure for loading tokenizer files from the hugging face hub is separated
40/// from the main load_tokenizer function (which is expecting bytes, from any source).
41#[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
54/// Function can be called directly from the try_new_from_user_defined function (providing file bytes)
55///
56/// Or indirectly from the try_new function via load_tokenizer_hf_hub (converting HF files to bytes)
57pub 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    // Deserialize each tokenizer file
62    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    //For BGEBaseSmall, the model_max_length value is set to 1000000000000000019884624838656. Which fits in a f64
92    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            // TODO: the user should be able to choose the padding strategy
109            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    // We add the super-small epsilon to avoid dividing by zero
164    v.iter().map(|&val| val / (norm + epsilon)).collect()
165}
166
167/// Pulls a model repo from HuggingFace..
168/// HF_HOME decides the location of the cache folder
169/// HF_ENDPOINT modifies the URL for the HuggingFace location.
170#[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        // Minimal valid tokenizer.json (BPE with a tiny vocab; no ## prefix needed).
237        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}