1#[cfg(feature = "search")]
17mod index;
18#[cfg(feature = "search")]
19pub use index::{Index, Neighbor};
20
21#[cfg(not(all(target_arch = "aarch64", target_os = "linux")))]
22mod candle;
23#[cfg(any(all(target_arch = "aarch64", target_os = "linux"), test))]
24mod cpu;
25mod weights;
26#[cfg(not(all(target_arch = "aarch64", target_os = "linux")))]
27use candle::Model;
28#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
29use cpu::Model;
30use serde::{Deserialize, Serialize};
31use std::sync::{Arc, OnceLock};
32use tokenizers::{Tokenizer, TruncationParams};
33
34#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
37pub struct ModelInfo {
38 pub id: String,
40 pub revision: String,
42 pub upstream_model: String,
44 pub upstream_revision: String,
46 pub tokenizer_sha256: String,
48 pub dimensions: usize,
50 pub max_tokens: usize,
52 pub pooling: String,
54}
55
56pub fn model_info() -> ModelInfo {
58 let value: serde_json::Value = serde_json::from_str(include_str!("../assets/provenance.json"))
59 .expect("bundled provenance");
60 ModelInfo {
61 id: "pocket-encoder/minilm-l3-q4-g32-v1".into(),
62 revision: format!("sha256:{}", value["bundled_sha256"].as_str().unwrap()),
63 upstream_model: value["upstream_model"].as_str().unwrap().into(),
64 upstream_revision: value["upstream_revision"].as_str().unwrap().into(),
65 tokenizer_sha256: value["tokenizer_sha256"].as_str().unwrap().into(),
66 dimensions: 384,
67 max_tokens: 128,
68 pooling: "attention-masked mean; L2 normalized; no query prefix; right truncation".into(),
69 }
70}
71
72#[derive(Debug, thiserror::Error)]
74pub enum Error {
75 #[error("{0}")]
77 Input(&'static str),
78 #[error("encoder failed: {0}")]
80 Inference(String),
81}
82
83struct Inner {
84 model: Model,
85 tokenizer: Tokenizer,
86}
87
88#[derive(Clone)]
90pub struct Encoder {
91 inner: Arc<Inner>,
92}
93
94impl Encoder {
95 pub fn new() -> Result<Self, Error> {
97 static MODEL: OnceLock<Result<Arc<Inner>, String>> = OnceLock::new();
98 MODEL
99 .get_or_init(|| load_bundled().map(Arc::new).map_err(|e| e.to_string()))
100 .clone()
101 .map(|inner| Self { inner })
102 .map_err(Error::Inference)
103 }
104
105 pub fn embed(&self, texts: &[impl AsRef<str>]) -> Result<Vec<Vec<f32>>, Error> {
110 if texts.len() > 4096 {
111 return Err(Error::Input("at most 4096 texts per call"));
112 }
113 let mut bytes = 0usize;
114 for text in texts {
115 let text = text.as_ref();
116 if text.trim().is_empty() {
117 return Err(Error::Input("text must not be empty"));
118 }
119 if text.len() > 65536 {
120 return Err(Error::Input("text exceeds 64 KiB"));
121 }
122 bytes = bytes.saturating_add(text.len());
123 }
124 if bytes > 4 * 1024 * 1024 {
125 return Err(Error::Input("texts exceed 4 MiB"));
126 }
127 texts.iter().map(|text| self.one(text.as_ref())).collect()
128 }
129
130 fn one(&self, text: &str) -> Result<Vec<f32>, Error> {
131 let encoded = self.inner.tokenizer.encode(text, true).map_err(inference)?;
132 normalize(self.inner.model.pooled(
133 encoded.get_ids(),
134 encoded.get_type_ids(),
135 encoded.get_attention_mask(),
136 )?)
137 }
138}
139
140fn inference(error: impl std::fmt::Display) -> Error {
141 Error::Inference(error.to_string())
142}
143
144fn normalize(pooled: Vec<f32>) -> Result<Vec<f32>, Error> {
145 let norm = pooled
146 .iter()
147 .map(|v| (*v as f64).powi(2))
148 .sum::<f64>()
149 .sqrt();
150 if !norm.is_finite() || norm <= 0.0 {
151 return Err(inference("non-finite or zero embedding"));
152 }
153 Ok(pooled
154 .into_iter()
155 .map(|v| (v as f64 / norm) as f32)
156 .collect())
157}
158
159fn tokenizer() -> Result<Tokenizer, Error> {
160 let mut tokenizer =
161 Tokenizer::from_bytes(include_bytes!("../assets/tokenizer.json")).map_err(inference)?;
162 tokenizer.with_padding(None);
163 tokenizer
164 .with_truncation(Some(TruncationParams {
165 max_length: 128,
166 ..Default::default()
167 }))
168 .map_err(inference)?;
169 Ok(tokenizer)
170}
171
172fn load_bundled() -> Result<Inner, Error> {
173 Ok(Inner {
174 model: Model::load()?,
175 tokenizer: tokenizer()?,
176 })
177}