1#[cfg(any(feature = "native", feature = "wasm"))]
4mod hf_tokenizer;
5
6#[cfg(feature = "native")]
7mod idf_weights;
8
9#[cfg(any(feature = "native", feature = "wasm"))]
10pub use hf_tokenizer::{HfTokenizer, TokenizerSource};
11
12#[cfg(feature = "native")]
13pub use hf_tokenizer::{TokenizerCache, tokenizer_cache};
14
15#[cfg(feature = "native")]
16pub use idf_weights::{IdfWeights, IdfWeightsCache, idf_weights_cache};
17
18use std::collections::HashMap;
19use std::sync::Arc;
20
21use parking_lot::RwLock;
22use rust_stemmers::Algorithm;
23use serde::{Deserialize, Serialize};
24use stop_words::LANGUAGE;
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Token {
29 pub text: String,
31 pub position: u32,
33 pub offset_from: usize,
35 pub offset_to: usize,
37}
38
39impl Token {
40 pub fn new(text: String, position: u32, offset_from: usize, offset_to: usize) -> Self {
41 Self {
42 text,
43 position,
44 offset_from,
45 offset_to,
46 }
47 }
48}
49
50pub trait Tokenizer: Send + Sync + Clone + 'static {
52 fn tokenize(&self, text: &str) -> Vec<Token>;
54
55 fn tokenize_hinted(&self, text: &str, hint: Option<&str>) -> Vec<Token> {
62 let _ = hint;
63 self.tokenize(text)
64 }
65}
66
67#[derive(Debug, Clone, Default)]
71pub struct SimpleTokenizer;
72
73impl Tokenizer for SimpleTokenizer {
74 fn tokenize(&self, text: &str) -> Vec<Token> {
75 tokenize_and_clean(text, std::convert::identity)
76 }
77}
78
79#[derive(Debug, Clone, Default)]
83pub struct RawTokenizer;
84
85impl Tokenizer for RawTokenizer {
86 fn tokenize(&self, text: &str) -> Vec<Token> {
87 let trimmed = text.trim();
88 if trimmed.is_empty() {
89 return Vec::new();
90 }
91 let offset = text.as_ptr() as usize;
92 let trimmed_offset = trimmed.as_ptr() as usize - offset;
93 vec![Token::new(
94 trimmed.to_string(),
95 0,
96 trimmed_offset,
97 trimmed_offset + trimmed.len(),
98 )]
99 }
100}
101
102#[derive(Debug, Clone, Default)]
106pub struct RawCiTokenizer;
107
108impl Tokenizer for RawCiTokenizer {
109 fn tokenize(&self, text: &str) -> Vec<Token> {
110 let trimmed = text.trim();
111 if trimmed.is_empty() {
112 return Vec::new();
113 }
114 let offset = text.as_ptr() as usize;
115 let trimmed_offset = trimmed.as_ptr() as usize - offset;
116 vec![Token::new(
117 lowercase_word(trimmed),
118 0,
119 trimmed_offset,
120 trimmed_offset + trimmed.len(),
121 )]
122 }
123}
124
125#[inline]
129fn lowercase_word(word: &str) -> String {
130 if word.is_ascii() {
131 if word.bytes().all(|b| !b.is_ascii_uppercase()) {
132 return word.to_string();
133 }
134 let mut s = word.to_string();
135 s.make_ascii_lowercase();
136 s
137 } else {
138 word.chars().flat_map(|c| c.to_lowercase()).collect()
139 }
140}
141
142#[inline]
147fn clean_word(word: &str) -> String {
148 if word.is_ascii() {
149 let bytes = word.as_bytes();
150 if bytes
152 .iter()
153 .all(|&b| b.is_ascii_lowercase() || b.is_ascii_digit())
154 {
155 return word.to_string();
156 }
157 let mut result = String::with_capacity(bytes.len());
159 for &b in bytes {
160 if b.is_ascii_alphanumeric() {
161 result.push(b.to_ascii_lowercase() as char);
162 }
163 }
164 result
165 } else {
166 word.chars()
168 .filter(|c| c.is_alphanumeric())
169 .flat_map(|c| c.to_lowercase())
170 .collect()
171 }
172}
173
174fn tokenize_and_clean(text: &str, transform: impl Fn(String) -> String) -> Vec<Token> {
180 let mut tokens = Vec::with_capacity(text.len() / 5);
181 let mut position = 0u32;
182 for (offset, word) in split_whitespace_with_offsets(text) {
183 if !word.is_empty() {
184 let cleaned = clean_word(word);
185 if !cleaned.is_empty() {
186 tokens.push(Token::new(
187 transform(cleaned),
188 position,
189 offset,
190 offset + word.len(),
191 ));
192 position += 1;
193 }
194 }
195 }
196 tokens
197}
198
199fn split_whitespace_with_offsets(text: &str) -> impl Iterator<Item = (usize, &str)> {
204 let base = text.as_ptr() as usize;
205 text.split_whitespace()
206 .map(move |word| (word.as_ptr() as usize - base, word))
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
211#[allow(missing_docs)]
212#[derive(Default)]
213pub enum Language {
214 Arabic,
215 Danish,
216 Dutch,
217 #[default]
218 English,
219 Finnish,
220 French,
221 German,
222 Greek,
223 Hungarian,
224 Italian,
225 Norwegian,
226 Portuguese,
227 Romanian,
228 Russian,
229 Spanish,
230 Swedish,
231 Tamil,
232 Turkish,
233}
234
235impl Language {
236 fn to_algorithm(self) -> Algorithm {
237 match self {
238 Language::Arabic => Algorithm::Arabic,
239 Language::Danish => Algorithm::Danish,
240 Language::Dutch => Algorithm::Dutch,
241 Language::English => Algorithm::English,
242 Language::Finnish => Algorithm::Finnish,
243 Language::French => Algorithm::French,
244 Language::German => Algorithm::German,
245 Language::Greek => Algorithm::Greek,
246 Language::Hungarian => Algorithm::Hungarian,
247 Language::Italian => Algorithm::Italian,
248 Language::Norwegian => Algorithm::Norwegian,
249 Language::Portuguese => Algorithm::Portuguese,
250 Language::Romanian => Algorithm::Romanian,
251 Language::Russian => Algorithm::Russian,
252 Language::Spanish => Algorithm::Spanish,
253 Language::Swedish => Algorithm::Swedish,
254 Language::Tamil => Algorithm::Tamil,
255 Language::Turkish => Algorithm::Turkish,
256 }
257 }
258
259 fn to_stop_words_language(self) -> LANGUAGE {
260 match self {
261 Language::Arabic => LANGUAGE::Arabic,
262 Language::Danish => LANGUAGE::Danish,
263 Language::Dutch => LANGUAGE::Dutch,
264 Language::English => LANGUAGE::English,
265 Language::Finnish => LANGUAGE::Finnish,
266 Language::French => LANGUAGE::French,
267 Language::German => LANGUAGE::German,
268 Language::Greek => LANGUAGE::Greek,
269 Language::Hungarian => LANGUAGE::Hungarian,
270 Language::Italian => LANGUAGE::Italian,
271 Language::Norwegian => LANGUAGE::Norwegian,
272 Language::Portuguese => LANGUAGE::Portuguese,
273 Language::Romanian => LANGUAGE::Romanian,
274 Language::Russian => LANGUAGE::Russian,
275 Language::Spanish => LANGUAGE::Spanish,
276 Language::Swedish => LANGUAGE::Swedish,
277 Language::Tamil => LANGUAGE::English, Language::Turkish => LANGUAGE::Turkish,
279 }
280 }
281}
282
283#[derive(Debug, Clone)]
287pub struct StopWordTokenizer<T: Tokenizer> {
288 inner: T,
289 stop_words: HashSet<String>,
290}
291
292use std::collections::HashSet;
293
294impl<T: Tokenizer> StopWordTokenizer<T> {
295 pub fn new(inner: T, language: Language) -> Self {
297 let stop_words: HashSet<String> = stop_words::get(language.to_stop_words_language())
298 .iter()
299 .map(|s| s.to_string())
300 .collect();
301 Self { inner, stop_words }
302 }
303
304 pub fn english(inner: T) -> Self {
306 Self::new(inner, Language::English)
307 }
308
309 pub fn with_custom_stop_words(inner: T, stop_words: HashSet<String>) -> Self {
311 Self { inner, stop_words }
312 }
313
314 pub fn is_stop_word(&self, word: &str) -> bool {
316 self.stop_words.contains(word)
317 }
318}
319
320impl<T: Tokenizer> Tokenizer for StopWordTokenizer<T> {
321 fn tokenize(&self, text: &str) -> Vec<Token> {
322 self.inner
323 .tokenize(text)
324 .into_iter()
325 .filter(|token| !self.stop_words.contains(token.text.as_str()))
326 .collect()
327 }
328}
329
330#[derive(Debug, Clone)]
335pub struct StemmerTokenizer {
336 language: Language,
337}
338
339impl StemmerTokenizer {
340 pub fn new(language: Language) -> Self {
342 Self { language }
343 }
344
345 pub fn english() -> Self {
347 Self::new(Language::English)
348 }
349}
350
351impl Default for StemmerTokenizer {
352 fn default() -> Self {
353 Self::english()
354 }
355}
356
357impl Tokenizer for StemmerTokenizer {
358 fn tokenize(&self, text: &str) -> Vec<Token> {
359 let stemmer = rust_stemmers::Stemmer::create(self.language.to_algorithm());
360 tokenize_and_clean(text, |s| stemmer.stem(&s).into_owned())
361 }
362}
363
364#[derive(Debug, Clone)]
369pub struct MultiLanguageStemmer {
370 default_language: Language,
371}
372
373impl MultiLanguageStemmer {
374 pub fn new(default_language: Language) -> Self {
376 Self { default_language }
377 }
378
379 pub fn tokenize_with_language(&self, text: &str, language: Language) -> Vec<Token> {
381 let stemmer = rust_stemmers::Stemmer::create(language.to_algorithm());
382 tokenize_and_clean(text, |s| stemmer.stem(&s).into_owned())
383 }
384
385 pub fn default_language(&self) -> Language {
387 self.default_language
388 }
389}
390
391impl Default for MultiLanguageStemmer {
392 fn default() -> Self {
393 Self::new(Language::English)
394 }
395}
396
397impl Tokenizer for MultiLanguageStemmer {
398 fn tokenize(&self, text: &str) -> Vec<Token> {
399 self.tokenize_with_language(text, self.default_language)
400 }
401}
402
403#[derive(Clone)]
408pub struct LanguageAwareTokenizer<F>
409where
410 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
411{
412 language_selector: F,
413 stemmer: MultiLanguageStemmer,
414}
415
416impl<F> LanguageAwareTokenizer<F>
417where
418 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
419{
420 pub fn new(language_selector: F) -> Self {
437 Self {
438 language_selector,
439 stemmer: MultiLanguageStemmer::default(),
440 }
441 }
442
443 pub fn tokenize_with_hint(&self, text: &str, language_hint: &str) -> Vec<Token> {
447 let language = (self.language_selector)(language_hint);
448 self.stemmer.tokenize_with_language(text, language)
449 }
450}
451
452impl<F> Tokenizer for LanguageAwareTokenizer<F>
453where
454 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
455{
456 fn tokenize(&self, text: &str) -> Vec<Token> {
457 self.stemmer.tokenize_with_language(text, Language::English)
459 }
460
461 fn tokenize_hinted(&self, text: &str, hint: Option<&str>) -> Vec<Token> {
462 match hint {
463 Some(hint) => self.tokenize_with_hint(text, hint),
464 None => Tokenizer::tokenize(self, text),
465 }
466 }
467}
468
469pub fn parse_language(s: &str) -> Language {
473 match s.to_lowercase().as_str() {
474 "ar" | "arabic" => Language::Arabic,
475 "da" | "danish" => Language::Danish,
476 "nl" | "dutch" => Language::Dutch,
477 "en" | "english" => Language::English,
478 "fi" | "finnish" => Language::Finnish,
479 "fr" | "french" => Language::French,
480 "de" | "german" => Language::German,
481 "el" | "greek" => Language::Greek,
482 "hu" | "hungarian" => Language::Hungarian,
483 "it" | "italian" => Language::Italian,
484 "no" | "norwegian" => Language::Norwegian,
485 "pt" | "portuguese" => Language::Portuguese,
486 "ro" | "romanian" => Language::Romanian,
487 "ru" | "russian" => Language::Russian,
488 "es" | "spanish" => Language::Spanish,
489 "sv" | "swedish" => Language::Swedish,
490 "ta" | "tamil" => Language::Tamil,
491 "tr" | "turkish" => Language::Turkish,
492 _ => Language::English, }
494}
495
496pub fn parse_language_opt(s: &str) -> Option<Language> {
501 Some(match s.trim().to_lowercase().as_str() {
502 "ar" | "arabic" => Language::Arabic,
503 "da" | "danish" => Language::Danish,
504 "nl" | "dutch" => Language::Dutch,
505 "en" | "english" => Language::English,
506 "fi" | "finnish" => Language::Finnish,
507 "fr" | "french" => Language::French,
508 "de" | "german" => Language::German,
509 "el" | "greek" => Language::Greek,
510 "hu" | "hungarian" => Language::Hungarian,
511 "it" | "italian" => Language::Italian,
512 "no" | "norwegian" => Language::Norwegian,
513 "pt" | "portuguese" => Language::Portuguese,
514 "ro" | "romanian" => Language::Romanian,
515 "ru" | "russian" => Language::Russian,
516 "es" | "spanish" => Language::Spanish,
517 "sv" | "swedish" => Language::Swedish,
518 "ta" | "tamil" => Language::Tamil,
519 "tr" | "turkish" => Language::Turkish,
520 _ => return None,
521 })
522}
523
524#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
526pub enum Script {
527 Latin,
528 Cyrillic,
529 Greek,
530 Arabic,
531 Tamil,
532 Other,
534}
535
536impl Script {
537 pub fn of_token(token: &str) -> Script {
539 let Some(c) = token.chars().find(|c| c.is_alphabetic()) else {
540 return Script::Other;
541 };
542 Script::of_char(c)
543 }
544
545 fn of_char(c: char) -> Script {
546 match c as u32 {
547 0x0041..=0x024F | 0x1E00..=0x1EFF => Script::Latin,
549 0x0370..=0x03FF | 0x1F00..=0x1FFF => Script::Greek,
550 0x0400..=0x052F => Script::Cyrillic,
551 0x0600..=0x06FF | 0x0750..=0x077F | 0x08A0..=0x08FF => Script::Arabic,
552 0x0B80..=0x0BFF => Script::Tamil,
553 _ => Script::Other,
554 }
555 }
556}
557
558impl Language {
559 pub fn script(self) -> Script {
561 match self {
562 Language::Russian => Script::Cyrillic,
563 Language::Greek => Script::Greek,
564 Language::Arabic => Script::Arabic,
565 Language::Tamil => Script::Tamil,
566 _ => Script::Latin,
567 }
568 }
569}
570
571thread_local! {
572 static STEMMER_CACHE: std::cell::RefCell<HashMap<Language, rust_stemmers::Stemmer>> =
573 std::cell::RefCell::new(HashMap::new());
574}
575
576fn stem_cached(language: Language, word: &str) -> String {
578 STEMMER_CACHE.with(|cache| {
579 let mut cache = cache.borrow_mut();
580 let stemmer = cache
581 .entry(language)
582 .or_insert_with(|| rust_stemmers::Stemmer::create(language.to_algorithm()));
583 stemmer.stem(word).into_owned()
584 })
585}
586
587#[derive(Debug, Clone, Default)]
601pub struct DynamicStemmer {
602 default: Option<Language>,
603}
604
605impl DynamicStemmer {
606 pub fn new(default: Option<Language>) -> Self {
608 Self { default }
609 }
610
611 pub fn default_language(&self) -> Option<Language> {
613 self.default
614 }
615
616 pub fn parse_hint(hint: &str) -> Vec<Language> {
618 let mut languages = Vec::new();
619 for part in hint.split(',') {
620 if let Some(language) = parse_language_opt(part)
621 && !languages.contains(&language)
622 {
623 languages.push(language);
624 }
625 }
626 languages
627 }
628
629 fn tokenize_with_languages(&self, text: &str, languages: &[Language]) -> Vec<Token> {
630 match languages {
631 [] => tokenize_and_clean(text, |s| s),
632 [single] if single.script() == Script::Latin => {
633 let language = *single;
634 tokenize_and_clean(text, |s| {
635 if Script::of_token(&s) == Script::Latin {
636 stem_cached(language, &s)
637 } else {
638 s
639 }
640 })
641 }
642 many => tokenize_and_clean(text, |s| {
643 let script = Script::of_token(&s);
644 match many.iter().find(|language| language.script() == script) {
645 Some(language) => stem_cached(*language, &s),
646 None => s,
647 }
648 }),
649 }
650 }
651}
652
653impl Tokenizer for DynamicStemmer {
654 fn tokenize(&self, text: &str) -> Vec<Token> {
655 match self.default {
656 Some(language) => self.tokenize_with_languages(text, &[language]),
657 None => tokenize_and_clean(text, |s| s),
658 }
659 }
660
661 fn tokenize_hinted(&self, text: &str, hint: Option<&str>) -> Vec<Token> {
662 match hint.map(str::trim).filter(|hint| !hint.is_empty()) {
663 Some(hint) => {
664 let languages = Self::parse_hint(hint);
665 if languages.is_empty() {
666 Tokenizer::tokenize(self, text)
668 } else {
669 self.tokenize_with_languages(text, &languages)
670 }
671 }
672 None => Tokenizer::tokenize(self, text),
673 }
674 }
675}
676
677#[derive(Debug, Clone, PartialEq, Eq)]
685pub enum TokenizerSpec {
686 Named(String),
688 DynamicStem {
690 by: String,
692 default: Option<Language>,
694 },
695}
696
697impl TokenizerSpec {
698 pub fn parse(spec: &str) -> Result<TokenizerSpec, String> {
700 let spec = spec.trim();
701 let Some(rest) = spec.strip_prefix("stem(") else {
702 if spec.is_empty() || spec.contains(['(', ')', ':', ',']) {
703 return Err(format!("invalid tokenizer spec '{spec}'"));
704 }
705 return Ok(TokenizerSpec::Named(spec.to_string()));
706 };
707 let Some(params) = rest.strip_suffix(')') else {
708 return Err(format!("tokenizer spec '{spec}' is missing ')'"));
709 };
710 let mut by = None;
711 let mut default = None;
712 for param in params.split(',') {
713 let param = param.trim();
714 if param.is_empty() {
715 continue;
716 }
717 let Some((key, value)) = param.split_once(':') else {
718 return Err(format!(
719 "tokenizer spec '{spec}': parameter '{param}' must be 'key: value'"
720 ));
721 };
722 let (key, value) = (key.trim(), value.trim());
723 match key {
724 "by" if !value.is_empty() => by = Some(value.to_string()),
725 "by" => return Err(format!("tokenizer spec '{spec}': 'by' needs a field name")),
726 "default" => {
727 default = match value {
728 "simple" | "none" => None,
729 other => Some(parse_language_opt(other).ok_or_else(|| {
730 format!("tokenizer spec '{spec}': unknown default language '{other}'")
731 })?),
732 };
733 }
734 other => {
735 return Err(format!(
736 "tokenizer spec '{spec}': unknown parameter '{other}'"
737 ));
738 }
739 }
740 }
741 let by = by.ok_or_else(|| format!("tokenizer spec '{spec}' requires 'by: <field>'"))?;
742 Ok(TokenizerSpec::DynamicStem { by, default })
743 }
744
745 pub fn hint_field(&self) -> Option<&str> {
747 match self {
748 TokenizerSpec::Named(_) => None,
749 TokenizerSpec::DynamicStem { by, .. } => Some(by),
750 }
751 }
752
753 pub fn dynamic_tokenizer(&self) -> Option<BoxedTokenizer> {
755 match self {
756 TokenizerSpec::Named(_) => None,
757 TokenizerSpec::DynamicStem { default, .. } => {
758 Some(Box::new(DynamicStemmer::new(*default)))
759 }
760 }
761 }
762}
763
764impl std::fmt::Display for TokenizerSpec {
765 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
766 match self {
767 TokenizerSpec::Named(name) => f.write_str(name),
768 TokenizerSpec::DynamicStem { by, default } => {
769 let default = match default {
770 None => "simple".to_string(),
771 Some(language) => language_code(*language).to_string(),
772 };
773 write!(f, "stem(by: {by}, default: {default})")
774 }
775 }
776 }
777}
778
779pub fn language_code(language: Language) -> &'static str {
781 match language {
782 Language::Arabic => "ar",
783 Language::Danish => "da",
784 Language::Dutch => "nl",
785 Language::English => "en",
786 Language::Finnish => "fi",
787 Language::French => "fr",
788 Language::German => "de",
789 Language::Greek => "el",
790 Language::Hungarian => "hu",
791 Language::Italian => "it",
792 Language::Norwegian => "no",
793 Language::Portuguese => "pt",
794 Language::Romanian => "ro",
795 Language::Russian => "ru",
796 Language::Spanish => "es",
797 Language::Swedish => "sv",
798 Language::Tamil => "ta",
799 Language::Turkish => "tr",
800 }
801}
802
803pub type BoxedTokenizer = Box<dyn TokenizerClone>;
805
806pub trait TokenizerClone: Send + Sync {
807 fn tokenize(&self, text: &str) -> Vec<Token>;
808 fn tokenize_hinted(&self, text: &str, hint: Option<&str>) -> Vec<Token>;
810 fn clone_box(&self) -> BoxedTokenizer;
811}
812
813impl<T: Tokenizer> TokenizerClone for T {
814 fn tokenize(&self, text: &str) -> Vec<Token> {
815 Tokenizer::tokenize(self, text)
816 }
817
818 fn tokenize_hinted(&self, text: &str, hint: Option<&str>) -> Vec<Token> {
819 Tokenizer::tokenize_hinted(self, text, hint)
820 }
821
822 fn clone_box(&self) -> BoxedTokenizer {
823 Box::new(self.clone())
824 }
825}
826
827impl Clone for BoxedTokenizer {
828 fn clone(&self) -> Self {
829 self.clone_box()
830 }
831}
832
833#[derive(Clone)]
838pub struct TokenizerRegistry {
839 tokenizers: Arc<RwLock<HashMap<String, BoxedTokenizer>>>,
840}
841
842impl TokenizerRegistry {
843 pub fn new() -> Self {
845 let registry = Self {
846 tokenizers: Arc::new(RwLock::new(HashMap::new())),
847 };
848 registry.register_defaults();
849 registry
850 }
851
852 fn register_defaults(&self) {
854 self.register("simple", SimpleTokenizer);
856 self.register("default", SimpleTokenizer);
857 self.register("raw", RawTokenizer);
858 self.register("raw_ci", RawCiTokenizer);
859
860 self.register("en_stem", StemmerTokenizer::new(Language::English));
862 self.register("english", StemmerTokenizer::new(Language::English));
863
864 self.register("ar_stem", StemmerTokenizer::new(Language::Arabic));
866 self.register("arabic", StemmerTokenizer::new(Language::Arabic));
867 self.register("da_stem", StemmerTokenizer::new(Language::Danish));
868 self.register("danish", StemmerTokenizer::new(Language::Danish));
869 self.register("nl_stem", StemmerTokenizer::new(Language::Dutch));
870 self.register("dutch", StemmerTokenizer::new(Language::Dutch));
871 self.register("fi_stem", StemmerTokenizer::new(Language::Finnish));
872 self.register("finnish", StemmerTokenizer::new(Language::Finnish));
873 self.register("fr_stem", StemmerTokenizer::new(Language::French));
874 self.register("french", StemmerTokenizer::new(Language::French));
875 self.register("de_stem", StemmerTokenizer::new(Language::German));
876 self.register("german", StemmerTokenizer::new(Language::German));
877 self.register("el_stem", StemmerTokenizer::new(Language::Greek));
878 self.register("greek", StemmerTokenizer::new(Language::Greek));
879 self.register("hu_stem", StemmerTokenizer::new(Language::Hungarian));
880 self.register("hungarian", StemmerTokenizer::new(Language::Hungarian));
881 self.register("it_stem", StemmerTokenizer::new(Language::Italian));
882 self.register("italian", StemmerTokenizer::new(Language::Italian));
883 self.register("no_stem", StemmerTokenizer::new(Language::Norwegian));
884 self.register("norwegian", StemmerTokenizer::new(Language::Norwegian));
885 self.register("pt_stem", StemmerTokenizer::new(Language::Portuguese));
886 self.register("portuguese", StemmerTokenizer::new(Language::Portuguese));
887 self.register("ro_stem", StemmerTokenizer::new(Language::Romanian));
888 self.register("romanian", StemmerTokenizer::new(Language::Romanian));
889 self.register("ru_stem", StemmerTokenizer::new(Language::Russian));
890 self.register("russian", StemmerTokenizer::new(Language::Russian));
891 self.register("es_stem", StemmerTokenizer::new(Language::Spanish));
892 self.register("spanish", StemmerTokenizer::new(Language::Spanish));
893 self.register("sv_stem", StemmerTokenizer::new(Language::Swedish));
894 self.register("swedish", StemmerTokenizer::new(Language::Swedish));
895 self.register("ta_stem", StemmerTokenizer::new(Language::Tamil));
896 self.register("tamil", StemmerTokenizer::new(Language::Tamil));
897 self.register("tr_stem", StemmerTokenizer::new(Language::Turkish));
898 self.register("turkish", StemmerTokenizer::new(Language::Turkish));
899
900 self.register(
902 "en_stop",
903 StopWordTokenizer::new(SimpleTokenizer, Language::English),
904 );
905 self.register(
906 "de_stop",
907 StopWordTokenizer::new(SimpleTokenizer, Language::German),
908 );
909 self.register(
910 "fr_stop",
911 StopWordTokenizer::new(SimpleTokenizer, Language::French),
912 );
913 self.register(
914 "ru_stop",
915 StopWordTokenizer::new(SimpleTokenizer, Language::Russian),
916 );
917 self.register(
918 "es_stop",
919 StopWordTokenizer::new(SimpleTokenizer, Language::Spanish),
920 );
921
922 self.register(
924 "en_stem_stop",
925 StopWordTokenizer::new(StemmerTokenizer::new(Language::English), Language::English),
926 );
927 self.register(
928 "de_stem_stop",
929 StopWordTokenizer::new(StemmerTokenizer::new(Language::German), Language::German),
930 );
931 self.register(
932 "fr_stem_stop",
933 StopWordTokenizer::new(StemmerTokenizer::new(Language::French), Language::French),
934 );
935 self.register(
936 "ru_stem_stop",
937 StopWordTokenizer::new(StemmerTokenizer::new(Language::Russian), Language::Russian),
938 );
939 self.register(
940 "es_stem_stop",
941 StopWordTokenizer::new(StemmerTokenizer::new(Language::Spanish), Language::Spanish),
942 );
943 }
944
945 pub fn register<T: Tokenizer>(&self, name: &str, tokenizer: T) {
947 let mut tokenizers = self.tokenizers.write();
948 tokenizers.insert(name.to_string(), Box::new(tokenizer));
949 }
950
951 pub fn get(&self, name: &str) -> Option<BoxedTokenizer> {
956 if name.starts_with("stem(") {
957 return TokenizerSpec::parse(name)
958 .ok()
959 .and_then(|spec| spec.dynamic_tokenizer());
960 }
961 let tokenizers = self.tokenizers.read();
962 tokenizers.get(name).cloned()
963 }
964
965 pub fn contains(&self, name: &str) -> bool {
967 let tokenizers = self.tokenizers.read();
968 tokenizers.contains_key(name)
969 }
970
971 pub fn names(&self) -> Vec<String> {
973 let tokenizers = self.tokenizers.read();
974 tokenizers.keys().cloned().collect()
975 }
976}
977
978impl Default for TokenizerRegistry {
979 fn default() -> Self {
980 Self::new()
981 }
982}
983
984#[cfg(test)]
985mod tests {
986 use super::*;
987
988 #[test]
989 fn test_simple_tokenizer() {
990 let tokenizer = SimpleTokenizer;
991 let tokens = Tokenizer::tokenize(&tokenizer, "Hello World");
992
993 assert_eq!(tokens.len(), 2);
994 assert_eq!(tokens[0].text, "hello");
995 assert_eq!(tokens[0].position, 0);
996 assert_eq!(tokens[1].text, "world");
997 assert_eq!(tokens[1].position, 1);
998 }
999
1000 #[test]
1001 fn test_raw_tokenizer() {
1002 let tokenizer = RawTokenizer;
1003 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
1005 assert_eq!(tokens.len(), 1);
1006 assert_eq!(tokens[0].text, "Hello, World!");
1007 assert_eq!(tokens[0].position, 0);
1008 }
1009
1010 #[test]
1011 fn test_raw_tokenizer_trims() {
1012 let tokenizer = RawTokenizer;
1013 let tokens = Tokenizer::tokenize(&tokenizer, " spaced ");
1014 assert_eq!(tokens.len(), 1);
1015 assert_eq!(tokens[0].text, "spaced");
1016 assert_eq!(tokens[0].offset_from, 2);
1017 }
1018
1019 #[test]
1020 fn test_raw_tokenizer_empty() {
1021 let tokenizer = RawTokenizer;
1022 assert!(Tokenizer::tokenize(&tokenizer, "").is_empty());
1023 assert!(Tokenizer::tokenize(&tokenizer, " ").is_empty());
1024 }
1025
1026 #[test]
1027 fn test_raw_ci_tokenizer() {
1028 let tokenizer = RawCiTokenizer;
1029 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
1031 assert_eq!(tokens.len(), 1);
1032 assert_eq!(tokens[0].text, "hello, world!");
1033 assert_eq!(tokens[0].position, 0);
1034 }
1035
1036 #[test]
1037 fn test_raw_ci_tokenizer_preserves_structure() {
1038 let tokenizer = RawCiTokenizer;
1039 let tokens = Tokenizer::tokenize(&tokenizer, "HTTPS://Example.COM/Page");
1040 assert_eq!(tokens.len(), 1);
1041 assert_eq!(tokens[0].text, "https://example.com/page");
1042 }
1043
1044 #[test]
1045 fn test_simple_tokenizer_strips_punctuation() {
1046 let tokenizer = SimpleTokenizer;
1047 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
1048
1049 assert_eq!(tokens.len(), 2);
1050 assert_eq!(tokens[0].text, "hello");
1051 assert_eq!(tokens[1].text, "world");
1052 }
1053
1054 #[test]
1055 fn test_empty_text() {
1056 let tokenizer = SimpleTokenizer;
1057 let tokens = Tokenizer::tokenize(&tokenizer, "");
1058 assert!(tokens.is_empty());
1059 }
1060
1061 #[test]
1062 fn test_stemmer_tokenizer_english() {
1063 let tokenizer = StemmerTokenizer::english();
1064 let tokens = Tokenizer::tokenize(&tokenizer, "Dogs are running quickly");
1065
1066 assert_eq!(tokens.len(), 4);
1067 assert_eq!(tokens[0].text, "dog"); assert_eq!(tokens[1].text, "are"); assert_eq!(tokens[2].text, "run"); assert_eq!(tokens[3].text, "quick"); }
1072
1073 #[test]
1074 fn test_stemmer_tokenizer_preserves_offsets() {
1075 let tokenizer = StemmerTokenizer::english();
1076 let tokens = Tokenizer::tokenize(&tokenizer, "Running dogs");
1077
1078 assert_eq!(tokens.len(), 2);
1079 assert_eq!(tokens[0].text, "run");
1080 assert_eq!(tokens[0].offset_from, 0);
1081 assert_eq!(tokens[0].offset_to, 7); assert_eq!(tokens[1].text, "dog");
1083 assert_eq!(tokens[1].offset_from, 8);
1084 assert_eq!(tokens[1].offset_to, 12); }
1086
1087 #[test]
1088 fn test_stemmer_tokenizer_german() {
1089 let tokenizer = StemmerTokenizer::new(Language::German);
1090 let tokens = Tokenizer::tokenize(&tokenizer, "Häuser Bücher");
1091
1092 assert_eq!(tokens.len(), 2);
1093 assert_eq!(tokens[0].text, "haus"); assert_eq!(tokens[1].text, "buch"); }
1097
1098 #[test]
1099 fn test_stemmer_tokenizer_russian() {
1100 let tokenizer = StemmerTokenizer::new(Language::Russian);
1101 let tokens = Tokenizer::tokenize(&tokenizer, "бегущие собаки");
1102
1103 assert_eq!(tokens.len(), 2);
1104 assert_eq!(tokens[0].text, "бегущ"); assert_eq!(tokens[1].text, "собак"); }
1108
1109 #[test]
1110 fn test_multi_language_stemmer() {
1111 let stemmer = MultiLanguageStemmer::new(Language::English);
1112
1113 let tokens = stemmer.tokenize_with_language("running dogs", Language::English);
1115 assert_eq!(tokens[0].text, "run");
1116 assert_eq!(tokens[1].text, "dog");
1117
1118 let tokens = stemmer.tokenize_with_language("Häuser Bücher", Language::German);
1120 assert_eq!(tokens[0].text, "haus");
1121 assert_eq!(tokens[1].text, "buch");
1122
1123 let tokens = stemmer.tokenize_with_language("бегущие собаки", Language::Russian);
1125 assert_eq!(tokens[0].text, "бегущ");
1126 assert_eq!(tokens[1].text, "собак");
1127 }
1128
1129 #[test]
1130 fn test_language_aware_tokenizer() {
1131 let tokenizer = LanguageAwareTokenizer::new(parse_language);
1132
1133 let tokens = tokenizer.tokenize_with_hint("running dogs", "en");
1135 assert_eq!(tokens[0].text, "run");
1136 assert_eq!(tokens[1].text, "dog");
1137
1138 let tokens = tokenizer.tokenize_with_hint("Häuser Bücher", "de");
1140 assert_eq!(tokens[0].text, "haus");
1141 assert_eq!(tokens[1].text, "buch");
1142
1143 let tokens = tokenizer.tokenize_with_hint("бегущие собаки", "russian");
1145 assert_eq!(tokens[0].text, "бегущ");
1146 assert_eq!(tokens[1].text, "собак");
1147 }
1148
1149 #[test]
1150 fn test_parse_language() {
1151 assert_eq!(parse_language("en"), Language::English);
1152 assert_eq!(parse_language("english"), Language::English);
1153 assert_eq!(parse_language("English"), Language::English);
1154 assert_eq!(parse_language("de"), Language::German);
1155 assert_eq!(parse_language("german"), Language::German);
1156 assert_eq!(parse_language("ru"), Language::Russian);
1157 assert_eq!(parse_language("russian"), Language::Russian);
1158 assert_eq!(parse_language("unknown"), Language::English); }
1160
1161 #[test]
1162 fn test_tokenizer_registry_defaults() {
1163 let registry = TokenizerRegistry::new();
1164
1165 assert!(registry.contains("simple"));
1167 assert!(registry.contains("raw"));
1168 assert!(registry.contains("raw_ci"));
1169 assert!(registry.contains("raw"));
1170 assert!(registry.contains("raw_ci"));
1171 assert!(registry.contains("en_stem"));
1172 assert!(registry.contains("german"));
1173 assert!(registry.contains("russian"));
1174 }
1175
1176 #[test]
1177 fn test_tokenizer_registry_get() {
1178 let registry = TokenizerRegistry::new();
1179
1180 let tokenizer = registry.get("en_stem").unwrap();
1182 let tokens = tokenizer.tokenize("running dogs");
1183 assert_eq!(tokens[0].text, "run");
1184 assert_eq!(tokens[1].text, "dog");
1185
1186 let tokenizer = registry.get("german").unwrap();
1188 let tokens = tokenizer.tokenize("Häuser Bücher");
1189 assert_eq!(tokens[0].text, "haus");
1190 assert_eq!(tokens[1].text, "buch");
1191 }
1192
1193 #[test]
1194 fn test_tokenizer_registry_custom() {
1195 let registry = TokenizerRegistry::new();
1196
1197 registry.register("my_tokenizer", SimpleTokenizer);
1199
1200 assert!(registry.contains("my_tokenizer"));
1201 let tokenizer = registry.get("my_tokenizer").unwrap();
1202 let tokens = tokenizer.tokenize("Hello World");
1203 assert_eq!(tokens[0].text, "hello");
1204 assert_eq!(tokens[1].text, "world");
1205 }
1206
1207 #[test]
1208 fn test_tokenizer_registry_nonexistent() {
1209 let registry = TokenizerRegistry::new();
1210 assert!(registry.get("nonexistent").is_none());
1211 }
1212
1213 #[test]
1214 fn test_stop_word_tokenizer_english() {
1215 let tokenizer = StopWordTokenizer::english(SimpleTokenizer);
1216 let tokens = Tokenizer::tokenize(&tokenizer, "The quick brown fox jumps over the lazy dog");
1217
1218 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1220 assert!(!texts.contains(&"the"));
1221 assert!(!texts.contains(&"over"));
1222 assert!(texts.contains(&"quick"));
1223 assert!(texts.contains(&"brown"));
1224 assert!(texts.contains(&"fox"));
1225 assert!(texts.contains(&"jumps"));
1226 assert!(texts.contains(&"lazy"));
1227 assert!(texts.contains(&"dog"));
1228 }
1229
1230 #[test]
1231 fn test_stop_word_tokenizer_with_stemmer() {
1232 let tokenizer = StopWordTokenizer::new(StemmerTokenizer::english(), Language::English);
1236 let tokens = Tokenizer::tokenize(&tokenizer, "elephants galaxies quantum");
1237
1238 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1239 assert!(texts.contains(&"eleph")); assert!(texts.contains(&"galaxi")); assert!(texts.contains(&"quantum")); }
1244
1245 #[test]
1246 fn test_stop_word_tokenizer_german() {
1247 let tokenizer = StopWordTokenizer::new(SimpleTokenizer, Language::German);
1248 let tokens = Tokenizer::tokenize(&tokenizer, "Der Hund und die Katze");
1249
1250 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1252 assert!(!texts.contains(&"der"));
1253 assert!(!texts.contains(&"und"));
1254 assert!(!texts.contains(&"die"));
1255 assert!(texts.contains(&"hund"));
1256 assert!(texts.contains(&"katze"));
1257 }
1258
1259 #[test]
1260 fn test_stop_word_tokenizer_custom() {
1261 let custom_stops: HashSet<String> = ["foo", "bar"].iter().map(|s| s.to_string()).collect();
1262 let tokenizer = StopWordTokenizer::with_custom_stop_words(SimpleTokenizer, custom_stops);
1263 let tokens = Tokenizer::tokenize(&tokenizer, "foo baz bar qux");
1264
1265 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1266 assert!(!texts.contains(&"foo"));
1267 assert!(!texts.contains(&"bar"));
1268 assert!(texts.contains(&"baz"));
1269 assert!(texts.contains(&"qux"));
1270 }
1271
1272 #[test]
1273 fn test_stop_word_tokenizer_is_stop_word() {
1274 let tokenizer = StopWordTokenizer::english(SimpleTokenizer);
1275 assert!(tokenizer.is_stop_word("the"));
1276 assert!(tokenizer.is_stop_word("and"));
1277 assert!(tokenizer.is_stop_word("is"));
1278 assert!(!tokenizer.is_stop_word("elephant"));
1280 assert!(!tokenizer.is_stop_word("quantum"));
1281 }
1282
1283 #[test]
1284 fn test_tokenizer_registry_stop_word_tokenizers() {
1285 let registry = TokenizerRegistry::new();
1286
1287 assert!(registry.contains("en_stop"));
1289 assert!(registry.contains("en_stem_stop"));
1290 assert!(registry.contains("de_stop"));
1291 assert!(registry.contains("ru_stop"));
1292
1293 let tokenizer = registry.get("en_stop").unwrap();
1295 let tokens = tokenizer.tokenize("The quick fox");
1296 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1297 assert!(!texts.contains(&"the"));
1298 assert!(texts.contains(&"quick"));
1299 assert!(texts.contains(&"fox"));
1300
1301 let tokenizer = registry.get("en_stem_stop").unwrap();
1303 let tokens = tokenizer.tokenize("elephants galaxies");
1304 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1305 assert!(texts.contains(&"eleph")); assert!(texts.contains(&"galaxi")); }
1308
1309 fn hinted<T: Tokenizer>(tokenizer: &T, text: &str, hint: Option<&str>) -> Vec<Token> {
1310 Tokenizer::tokenize_hinted(tokenizer, text, hint)
1311 }
1312
1313 fn texts(tokens: &[Token]) -> Vec<&str> {
1314 tokens.iter().map(|t| t.text.as_str()).collect()
1315 }
1316
1317 #[test]
1318 fn dynamic_stemmer_selects_language_from_hint() {
1319 let stemmer = DynamicStemmer::new(None);
1320 assert_eq!(
1321 texts(&hinted(&stemmer, "Running Foxes", Some("en"))),
1322 vec!["run", "fox"]
1323 );
1324 assert_eq!(
1325 texts(&hinted(&stemmer, "бегущие собаки", Some("ru"))),
1326 vec!["бегущ", "собак"]
1327 );
1328 assert_eq!(
1330 texts(&hinted(&stemmer, "Running Foxes", Some("xx"))),
1331 vec!["running", "foxes"]
1332 );
1333 assert_eq!(
1334 texts(&hinted(&stemmer, "Running Foxes", None)),
1335 vec!["running", "foxes"]
1336 );
1337 assert_eq!(
1338 texts(&Tokenizer::tokenize(&stemmer, "Running, Foxes!")),
1339 vec!["running", "foxes"]
1340 );
1341 let english = DynamicStemmer::new(Some(Language::English));
1343 assert_eq!(
1344 texts(&hinted(&english, "Running Foxes", None)),
1345 vec!["run", "fox"]
1346 );
1347 }
1348
1349 #[test]
1350 fn dynamic_stemmer_routes_tokens_by_script() {
1351 let stemmer = DynamicStemmer::new(None);
1352 assert_eq!(
1354 texts(&hinted(&stemmer, "бегущие foxes", Some("ru,en"))),
1355 vec!["бегущ", "fox"]
1356 );
1357 assert_eq!(
1358 texts(&hinted(&stemmer, "бегущие foxes", Some("en, ru"))),
1359 vec!["бегущ", "fox"]
1360 );
1361 assert_eq!(
1363 texts(&hinted(&stemmer, "бегущие foxes", Some("ru"))),
1364 vec!["бегущ", "foxes"]
1365 );
1366 assert_eq!(
1367 texts(&hinted(&stemmer, "бегущие foxes", Some("en"))),
1368 vec!["бегущие", "fox"]
1369 );
1370 assert_eq!(
1372 texts(&hinted(&stemmer, "running", Some("de,en"))),
1373 vec!["running"]
1374 );
1375 assert_eq!(
1376 texts(&hinted(&stemmer, "running", Some("en,de"))),
1377 vec!["run"]
1378 );
1379 let tokens = hinted(&stemmer, "бегущие foxes run", Some("ru,en"));
1381 assert_eq!(
1382 tokens.iter().map(|t| t.position).collect::<Vec<_>>(),
1383 vec![0, 1, 2]
1384 );
1385 }
1386
1387 #[test]
1388 fn script_detection_covers_supported_stemmer_scripts() {
1389 assert_eq!(Script::of_token("hello"), Script::Latin);
1390 assert_eq!(Script::of_token("straße"), Script::Latin);
1391 assert_eq!(Script::of_token("собака"), Script::Cyrillic);
1392 assert_eq!(Script::of_token("γεια"), Script::Greek);
1393 assert_eq!(Script::of_token("مرحبا"), Script::Arabic);
1394 assert_eq!(Script::of_token("தமிழ்"), Script::Tamil);
1395 assert_eq!(Script::of_token("日本語"), Script::Other);
1396 assert_eq!(Script::of_token("2024"), Script::Other);
1397 assert_eq!(Language::Russian.script(), Script::Cyrillic);
1398 assert_eq!(Language::Turkish.script(), Script::Latin);
1399 }
1400
1401 #[test]
1402 fn tokenizer_spec_parses_and_renders_canonically() {
1403 assert_eq!(
1404 TokenizerSpec::parse("en_stem").unwrap(),
1405 TokenizerSpec::Named("en_stem".to_string())
1406 );
1407 let spec = TokenizerSpec::parse("stem(by:languages,default:simple)").unwrap();
1408 assert_eq!(
1409 spec,
1410 TokenizerSpec::DynamicStem {
1411 by: "languages".to_string(),
1412 default: None
1413 }
1414 );
1415 assert_eq!(spec.to_string(), "stem(by: languages, default: simple)");
1416 assert_eq!(spec.hint_field(), Some("languages"));
1417
1418 let spec = TokenizerSpec::parse("stem(by: lang, default: english)").unwrap();
1419 assert_eq!(spec.to_string(), "stem(by: lang, default: en)");
1420 assert_eq!(
1421 TokenizerSpec::parse("stem(by: lang)").unwrap(),
1422 TokenizerSpec::DynamicStem {
1423 by: "lang".to_string(),
1424 default: None
1425 }
1426 );
1427
1428 assert!(TokenizerSpec::parse("stem(default: en)").is_err());
1429 assert!(TokenizerSpec::parse("stem(by: lang, default: klingon)").is_err());
1430 assert!(TokenizerSpec::parse("stem(by: lang").is_err());
1431 assert!(TokenizerSpec::parse("stem(by: lang, color: red)").is_err());
1432 assert!(TokenizerSpec::parse("en_stem(foo)").is_err());
1433 assert!(TokenizerSpec::parse("").is_err());
1434 }
1435
1436 #[test]
1437 fn registry_builds_dynamic_stemmer_from_spec() {
1438 let registry = TokenizerRegistry::new();
1439 let tokenizer = registry
1440 .get("stem(by: languages, default: simple)")
1441 .expect("dynamic spec resolves without registration");
1442 assert_eq!(
1443 texts(&tokenizer.tokenize_hinted("Running Foxes", Some("en"))),
1444 vec!["run", "fox"]
1445 );
1446 assert_eq!(
1447 texts(&tokenizer.tokenize("Running Foxes")),
1448 vec!["running", "foxes"]
1449 );
1450 assert!(
1451 registry
1452 .get("stem(by: languages, default: klingon)")
1453 .is_none()
1454 );
1455 let simple = registry.get("en_stem").unwrap();
1457 assert_eq!(
1458 texts(&simple.tokenize_hinted("Running Foxes", Some("ru"))),
1459 vec!["run", "fox"]
1460 );
1461 }
1462
1463 #[test]
1464 fn parse_language_opt_rejects_unknown_codes() {
1465 assert_eq!(parse_language_opt(" RU "), Some(Language::Russian));
1466 assert_eq!(parse_language_opt("german"), Some(Language::German));
1467 assert_eq!(parse_language_opt("xx"), None);
1468 assert_eq!(parse_language_opt(""), None);
1469 for language in [Language::English, Language::Russian, Language::Tamil] {
1470 assert_eq!(parse_language_opt(language_code(language)), Some(language));
1471 }
1472 }
1473}