1use std::sync::LazyLock;
34
35use murmur3_32::Murmur3;
36use phf::phf_set;
37use qql_core::error::QqlError;
38use rust_stemmers::{Algorithm, Stemmer};
39
40#[derive(Debug, Clone, PartialEq, Default)]
42pub struct SparseVector {
43 pub indices: Vec<u32>,
45 pub values: Vec<f32>,
47}
48
49pub const DEFAULT_K1: f64 = 1.2;
51pub const DEFAULT_B: f64 = 0.75;
53pub const DEFAULT_AVGDL: f64 = 256.0;
56
57#[derive(Debug, Clone, Copy, PartialEq)]
69pub struct Bm25Params {
70 k1: f64,
71 b: f64,
72 avg_len: f64,
73}
74
75impl Default for Bm25Params {
76 fn default() -> Self {
79 Self {
80 k1: DEFAULT_K1,
81 b: DEFAULT_B,
82 avg_len: DEFAULT_AVGDL,
83 }
84 }
85}
86
87impl Bm25Params {
88 pub fn new(k1: f64, b: f64, avg_len: f64) -> Result<Self, QqlError> {
93 if !k1.is_finite() || k1 <= 0.0 {
94 return Err(config_error(
95 "bm25 k1 must be a finite number greater than zero".to_string(),
96 ));
97 }
98 if !b.is_finite() || !(0.0..=1.0).contains(&b) {
99 return Err(config_error(
100 "bm25 b must be a finite number in [0, 1]".to_string(),
101 ));
102 }
103 if !avg_len.is_finite() || avg_len <= 0.0 {
104 return Err(config_error(
105 "bm25 avg_len must be a finite number greater than zero".to_string(),
106 ));
107 }
108 Ok(Self { k1, b, avg_len })
109 }
110
111 pub fn resolve(
114 k1: Option<f64>,
115 b: Option<f64>,
116 avg_len: Option<f64>,
117 ) -> Result<Self, QqlError> {
118 let defaults = Self::default();
119 Self::new(
120 k1.unwrap_or(defaults.k1),
121 b.unwrap_or(defaults.b),
122 avg_len.unwrap_or(defaults.avg_len),
123 )
124 }
125
126 pub fn k1(&self) -> f64 {
128 self.k1
129 }
130
131 pub fn b(&self) -> f64 {
133 self.b
134 }
135
136 pub fn avg_len(&self) -> f64 {
138 self.avg_len
139 }
140}
141
142fn config_error(message: String) -> QqlError {
143 QqlError::validation("QQL-VALIDATION-CONFIG", message, None)
144}
145
146pub fn token_id(token: &str) -> u32 {
154 (Murmur3::hash(0, token.as_bytes()) as i32).unsigned_abs()
155}
156
157static STOPWORDS: phf::Set<&'static str> = phf_set! {
160 "i",
161 "me",
162 "my",
163 "myself",
164 "we",
165 "our",
166 "ours",
167 "ourselves",
168 "you",
169 "you're",
170 "you've",
171 "you'll",
172 "you'd",
173 "your",
174 "yours",
175 "yourself",
176 "yourselves",
177 "he",
178 "him",
179 "his",
180 "himself",
181 "she",
182 "she's",
183 "her",
184 "hers",
185 "herself",
186 "it",
187 "it's",
188 "its",
189 "itself",
190 "they",
191 "them",
192 "their",
193 "theirs",
194 "themselves",
195 "what",
196 "which",
197 "who",
198 "whom",
199 "this",
200 "that",
201 "that'll",
202 "these",
203 "those",
204 "am",
205 "is",
206 "are",
207 "was",
208 "were",
209 "be",
210 "been",
211 "being",
212 "have",
213 "has",
214 "had",
215 "having",
216 "do",
217 "does",
218 "did",
219 "doing",
220 "a",
221 "an",
222 "the",
223 "and",
224 "but",
225 "if",
226 "or",
227 "because",
228 "as",
229 "until",
230 "while",
231 "of",
232 "at",
233 "by",
234 "for",
235 "with",
236 "about",
237 "against",
238 "between",
239 "into",
240 "through",
241 "during",
242 "before",
243 "after",
244 "above",
245 "below",
246 "to",
247 "from",
248 "up",
249 "down",
250 "in",
251 "out",
252 "on",
253 "off",
254 "over",
255 "under",
256 "again",
257 "further",
258 "then",
259 "once",
260 "here",
261 "there",
262 "when",
263 "where",
264 "why",
265 "how",
266 "all",
267 "any",
268 "both",
269 "each",
270 "few",
271 "more",
272 "most",
273 "other",
274 "some",
275 "such",
276 "no",
277 "nor",
278 "not",
279 "only",
280 "own",
281 "same",
282 "so",
283 "than",
284 "too",
285 "very",
286 "s",
287 "t",
288 "can",
289 "will",
290 "just",
291 "don",
292 "don't",
293 "should",
294 "should've",
295 "now",
296 "d",
297 "ll",
298 "m",
299 "o",
300 "re",
301 "ve",
302 "y",
303 "ain",
304 "aren",
305 "aren't",
306 "couldn",
307 "couldn't",
308 "didn",
309 "didn't",
310 "doesn",
311 "doesn't",
312 "hadn",
313 "hadn't",
314 "hasn",
315 "hasn't",
316 "haven",
317 "haven't",
318 "isn",
319 "isn't",
320 "ma",
321 "mightn",
322 "mightn't",
323 "mustn",
324 "mustn't",
325 "needn",
326 "needn't",
327 "shan",
328 "shan't",
329 "shouldn",
330 "shouldn't",
331 "wasn",
332 "wasn't",
333 "weren",
334 "weren't",
335 "won",
336 "won't",
337 "wouldn",
338 "wouldn't",
339};
340
341static STEMMER: LazyLock<Stemmer> = LazyLock::new(|| Stemmer::create(Algorithm::English));
342
343#[inline]
344fn process_token<F>(raw: &str, buf: &mut [u8; 64], f: &mut F)
345where
346 F: FnMut(&str),
347{
348 let bytes = raw.as_bytes();
349 let len = bytes.len();
350 if len <= buf.len() && raw.is_ascii() {
351 for (j, &b) in bytes.iter().enumerate() {
352 buf[j] = b.to_ascii_lowercase();
353 }
354 let lower =
359 std::str::from_utf8(&buf[..len]).expect("ascii lowercasing preserves valid UTF-8");
360 if !STOPWORDS.contains(lower) {
361 let stemmed = STEMMER.stem(lower);
362 f(&stemmed);
363 }
364 } else {
365 let lower = raw.to_lowercase();
366 if !STOPWORDS.contains(lower.as_str()) {
367 let stemmed = STEMMER.stem(&lower);
368 f(&stemmed);
369 }
370 }
371}
372
373#[inline]
375pub fn for_each_token<F>(text: &str, mut f: F)
376where
377 F: FnMut(&str),
378{
379 let mut buf = [0u8; 64];
380
381 if text.is_ascii() {
382 let bytes = text.as_bytes();
383 let mut start = None;
384 for (i, &b) in bytes.iter().enumerate() {
385 if b.is_ascii_alphanumeric() {
386 if start.is_none() {
387 start = Some(i);
388 }
389 } else if let Some(s) = start {
390 process_token(&text[s..i], &mut buf, &mut f);
391 start = None;
392 }
393 }
394 if let Some(s) = start {
395 process_token(&text[s..], &mut buf, &mut f);
396 }
397 } else {
398 let mut start = None;
399 for (i, c) in text.char_indices() {
400 if c.is_alphanumeric() {
401 if start.is_none() {
402 start = Some(i);
403 }
404 } else if let Some(s) = start {
405 process_token(&text[s..i], &mut buf, &mut f);
406 start = None;
407 }
408 }
409 if let Some(s) = start {
410 process_token(&text[s..], &mut buf, &mut f);
411 }
412 }
413}
414
415#[inline]
417pub fn for_each_token_id<F>(text: &str, mut f: F)
418where
419 F: FnMut(u32),
420{
421 for_each_token(text, |token| {
422 f(token_id(token));
423 });
424}
425
426pub fn tokenize(text: &str) -> Vec<String> {
432 let mut tokens = Vec::new();
433 for_each_token(text, |token| {
434 tokens.push(token.to_string());
435 });
436 tokens
437}
438
439pub fn embed_query(text: &str) -> SparseVector {
442 let mut indices: Vec<u32> = Vec::with_capacity(text.len() / 6 + 1);
443 for_each_token_id(text, |id| {
444 indices.push(id);
445 });
446
447 if indices.is_empty() {
448 return SparseVector::default();
449 }
450
451 indices.sort_unstable();
452 indices.dedup();
453
454 let values = vec![1.0; indices.len()];
455 SparseVector { indices, values }
456}
457
458pub fn embed_document(text: &str) -> SparseVector {
461 embed_document_with(text, DEFAULT_K1, DEFAULT_B, DEFAULT_AVGDL)
462}
463
464pub fn embed_document_with_params(text: &str, params: &Bm25Params) -> SparseVector {
469 embed_document_impl(text, params.k1, params.b, params.avg_len)
470}
471
472pub fn embed_document_with(text: &str, k1: f64, b: f64, avgdl: f64) -> SparseVector {
482 let safe_avgdl = if avgdl.is_finite() && avgdl > 0.0 {
483 avgdl
484 } else {
485 DEFAULT_AVGDL
486 };
487 embed_document_impl(text, k1, b, safe_avgdl)
488}
489
490fn embed_document_impl(text: &str, k1: f64, b: f64, avgdl: f64) -> SparseVector {
491 let mut token_ids: Vec<u32> = Vec::with_capacity(text.len() / 6 + 1);
492 for_each_token_id(text, |id| {
493 token_ids.push(id);
494 });
495
496 if token_ids.is_empty() {
497 return SparseVector::default();
498 }
499
500 let doc_len = token_ids.len() as f64;
501 let denom_scale = k1 * (1.0 - b + b * doc_len / avgdl);
502 let k1p1 = k1 + 1.0;
503
504 token_ids.sort_unstable();
505
506 let mut indices = Vec::with_capacity(token_ids.len());
507 let mut values = Vec::with_capacity(token_ids.len());
508
509 let mut i = 0;
510 while i < token_ids.len() {
511 let id = token_ids[i];
512 let mut count = 1u32;
513 while i + 1 < token_ids.len() && token_ids[i + 1] == id {
514 count += 1;
515 i += 1;
516 }
517 indices.push(id);
518 let n = count as f64;
519 values.push((n * k1p1 / (denom_scale + n)) as f32);
520 i += 1;
521 }
522
523 SparseVector { indices, values }
524}