1#[cfg(any(feature = "native", feature = "wasm"))]
4mod hf_tokenizer;
5
6mod cjk_morph;
7mod han_t2s;
8#[cfg(feature = "native")]
9mod idf_weights;
10mod lex;
11pub mod light_stem;
12
13pub use cjk_morph::{available as cjk_dictionaries_available, warm_up as warm_up_cjk_dictionaries};
14pub use lex::{
15 CjkMode, DEFAULT_MAX_TOKEN_LENGTH, HanForm, LexOptions, LexTokenizer, Segmenter, StemMode,
16 TokenizerSpec,
17};
18
19#[cfg(any(feature = "native", feature = "wasm"))]
20pub use hf_tokenizer::{HfTokenizer, TokenizerSource};
21
22#[cfg(feature = "native")]
23pub use hf_tokenizer::{TokenizerCache, tokenizer_cache};
24
25#[cfg(feature = "native")]
26pub use idf_weights::{IdfWeights, IdfWeightsCache, idf_weights_cache};
27
28use std::collections::HashMap;
29use std::sync::Arc;
30
31use parking_lot::RwLock;
32use rust_stemmers::Algorithm;
33use serde::{Deserialize, Serialize};
34use stop_words::Language as StopWordLanguage;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Purpose {
39 Index,
42 Match,
45 Exact,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub struct Token {
52 pub text: String,
54 pub position: u32,
56 pub offset_from: usize,
58 pub offset_to: usize,
60 pub variant: bool,
64}
65
66impl Token {
67 pub fn variant_of(text: String, position: u32, offset_from: usize, offset_to: usize) -> Self {
69 Self {
70 text,
71 position,
72 offset_from,
73 offset_to,
74 variant: true,
75 }
76 }
77
78 pub fn new(text: String, position: u32, offset_from: usize, offset_to: usize) -> Self {
79 Self {
80 text,
81 position,
82 offset_from,
83 offset_to,
84 variant: false,
85 }
86 }
87}
88
89pub trait Tokenizer: Send + Sync + Clone + 'static {
91 fn tokenize(&self, text: &str) -> Vec<Token>;
93
94 fn tokenize_with(&self, text: &str, hint: Option<&str>, purpose: Purpose) -> Vec<Token> {
102 let _ = (hint, purpose);
103 self.tokenize(text)
104 }
105}
106
107#[derive(Debug, Clone, Default)]
111pub struct SimpleTokenizer;
112
113impl Tokenizer for SimpleTokenizer {
114 fn tokenize(&self, text: &str) -> Vec<Token> {
115 tokenize_and_clean(text, std::convert::identity)
116 }
117}
118
119#[derive(Debug, Clone, Default)]
122pub struct UnicodeWordTokenizer;
123
124impl Tokenizer for UnicodeWordTokenizer {
125 fn tokenize(&self, text: &str) -> Vec<Token> {
126 use unicode_segmentation::UnicodeSegmentation;
127 text.unicode_word_indices()
128 .enumerate()
129 .filter(|(_, (_, word))| word.chars().count() <= 255)
130 .map(|(position, (offset, word))| {
131 Token::new(
132 word.to_lowercase(),
133 position as u32,
134 offset,
135 offset + word.len(),
136 )
137 })
138 .collect()
139 }
140}
141
142#[derive(Debug, Clone, Default)]
146pub struct RawTokenizer;
147
148impl Tokenizer for RawTokenizer {
149 fn tokenize(&self, text: &str) -> Vec<Token> {
150 let trimmed = text.trim();
151 if trimmed.is_empty() {
152 return Vec::new();
153 }
154 let offset = text.as_ptr() as usize;
155 let trimmed_offset = trimmed.as_ptr() as usize - offset;
156 vec![Token::new(
157 trimmed.to_string(),
158 0,
159 trimmed_offset,
160 trimmed_offset + trimmed.len(),
161 )]
162 }
163}
164
165#[derive(Debug, Clone, Default)]
169pub struct RawCiTokenizer;
170
171impl Tokenizer for RawCiTokenizer {
172 fn tokenize(&self, text: &str) -> Vec<Token> {
173 let trimmed = text.trim();
174 if trimmed.is_empty() {
175 return Vec::new();
176 }
177 let offset = text.as_ptr() as usize;
178 let trimmed_offset = trimmed.as_ptr() as usize - offset;
179 vec![Token::new(
180 lowercase_word(trimmed),
181 0,
182 trimmed_offset,
183 trimmed_offset + trimmed.len(),
184 )]
185 }
186}
187
188#[inline]
192fn lowercase_word(word: &str) -> String {
193 if word.is_ascii() {
194 if word.bytes().all(|b| !b.is_ascii_uppercase()) {
195 return word.to_string();
196 }
197 let mut s = word.to_string();
198 s.make_ascii_lowercase();
199 s
200 } else {
201 word.chars().flat_map(|c| c.to_lowercase()).collect()
202 }
203}
204
205#[inline]
210pub(super) fn clean_word(word: &str) -> String {
211 if word.is_ascii() {
212 let bytes = word.as_bytes();
213 if bytes
215 .iter()
216 .all(|&b| b.is_ascii_lowercase() || b.is_ascii_digit())
217 {
218 return word.to_string();
219 }
220 let mut result = String::with_capacity(bytes.len());
222 for &b in bytes {
223 if b.is_ascii_alphanumeric() {
224 result.push(b.to_ascii_lowercase() as char);
225 }
226 }
227 result
228 } else {
229 word.chars()
231 .filter(|c| c.is_alphanumeric())
232 .flat_map(|c| c.to_lowercase())
233 .collect()
234 }
235}
236
237fn tokenize_and_clean(text: &str, transform: impl Fn(String) -> String) -> Vec<Token> {
243 tokenize_and_clean_filtered(text, |word| Some(transform(word)))
244}
245
246fn tokenize_and_clean_filtered(
251 text: &str,
252 transform: impl Fn(String) -> Option<String>,
253) -> Vec<Token> {
254 let mut tokens = Vec::with_capacity(text.len() / 5);
255 let mut position = 0u32;
256 for (offset, word) in split_whitespace_with_offsets(text) {
257 if !word.is_empty() {
258 let cleaned = clean_word(word);
259 if !cleaned.is_empty() {
260 if let Some(text) = transform(cleaned) {
261 tokens.push(Token::new(text, position, offset, offset + word.len()));
262 }
263 position += 1;
264 }
265 }
266 }
267 tokens
268}
269
270pub(super) fn split_whitespace_with_offsets(text: &str) -> impl Iterator<Item = (usize, &str)> {
275 let base = text.as_ptr() as usize;
276 text.split_whitespace()
277 .map(move |word| (word.as_ptr() as usize - base, word))
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
282#[allow(missing_docs)]
283#[derive(Default)]
284pub enum Language {
285 Arabic,
286 Danish,
287 Dutch,
288 #[default]
289 English,
290 Finnish,
291 French,
292 German,
293 Greek,
294 Hungarian,
295 Italian,
296 Norwegian,
297 Portuguese,
298 Romanian,
299 Russian,
300 Spanish,
301 Swedish,
302 Tamil,
303 Turkish,
304}
305
306impl Language {
307 fn to_algorithm(self) -> Algorithm {
308 match self {
309 Language::Arabic => Algorithm::Arabic,
310 Language::Danish => Algorithm::Danish,
311 Language::Dutch => Algorithm::Dutch,
312 Language::English => Algorithm::English,
313 Language::Finnish => Algorithm::Finnish,
314 Language::French => Algorithm::French,
315 Language::German => Algorithm::German,
316 Language::Greek => Algorithm::Greek,
317 Language::Hungarian => Algorithm::Hungarian,
318 Language::Italian => Algorithm::Italian,
319 Language::Norwegian => Algorithm::Norwegian,
320 Language::Portuguese => Algorithm::Portuguese,
321 Language::Romanian => Algorithm::Romanian,
322 Language::Russian => Algorithm::Russian,
323 Language::Spanish => Algorithm::Spanish,
324 Language::Swedish => Algorithm::Swedish,
325 Language::Tamil => Algorithm::Tamil,
326 Language::Turkish => Algorithm::Turkish,
327 }
328 }
329
330 pub(super) fn to_stop_words_language(self) -> StopWordLanguage {
331 match self {
332 Language::Arabic => StopWordLanguage::Arabic,
333 Language::Danish => StopWordLanguage::Danish,
334 Language::Dutch => StopWordLanguage::Dutch,
335 Language::English => StopWordLanguage::English,
336 Language::Finnish => StopWordLanguage::Finnish,
337 Language::French => StopWordLanguage::French,
338 Language::German => StopWordLanguage::German,
339 Language::Greek => StopWordLanguage::Greek,
340 Language::Hungarian => StopWordLanguage::Hungarian,
341 Language::Italian => StopWordLanguage::Italian,
342 Language::Norwegian => StopWordLanguage::Norwegian,
343 Language::Portuguese => StopWordLanguage::Portuguese,
344 Language::Romanian => StopWordLanguage::Romanian,
345 Language::Russian => StopWordLanguage::Russian,
346 Language::Spanish => StopWordLanguage::Spanish,
347 Language::Swedish => StopWordLanguage::Swedish,
348 Language::Tamil => StopWordLanguage::Tamil,
349 Language::Turkish => StopWordLanguage::Turkish,
350 }
351 }
352}
353
354#[derive(Debug, Clone)]
358pub struct StopWordTokenizer<T: Tokenizer> {
359 inner: T,
360 stop_words: HashSet<String>,
361}
362
363use std::collections::HashSet;
364
365impl<T: Tokenizer> StopWordTokenizer<T> {
366 pub fn new(inner: T, language: Language) -> Self {
368 let stop_words: HashSet<String> = stop_words::get(language.to_stop_words_language())
369 .iter()
370 .map(|s| s.to_string())
371 .collect();
372 Self { inner, stop_words }
373 }
374
375 pub fn english(inner: T) -> Self {
377 Self::new(inner, Language::English)
378 }
379
380 pub fn with_custom_stop_words(inner: T, stop_words: HashSet<String>) -> Self {
382 Self { inner, stop_words }
383 }
384
385 pub fn is_stop_word(&self, word: &str) -> bool {
387 self.stop_words.contains(word)
388 }
389}
390
391impl<T: Tokenizer> Tokenizer for StopWordTokenizer<T> {
392 fn tokenize(&self, text: &str) -> Vec<Token> {
393 self.inner
394 .tokenize(text)
395 .into_iter()
396 .filter(|token| !self.stop_words.contains(token.text.as_str()))
397 .collect()
398 }
399}
400
401#[derive(Debug, Clone)]
406pub struct StemmerTokenizer {
407 language: Language,
408}
409
410impl StemmerTokenizer {
411 pub fn new(language: Language) -> Self {
413 Self { language }
414 }
415
416 pub fn english() -> Self {
418 Self::new(Language::English)
419 }
420}
421
422impl Default for StemmerTokenizer {
423 fn default() -> Self {
424 Self::english()
425 }
426}
427
428impl Tokenizer for StemmerTokenizer {
429 fn tokenize(&self, text: &str) -> Vec<Token> {
430 let stemmer = rust_stemmers::Stemmer::create(self.language.to_algorithm());
431 tokenize_and_clean(text, |s| stemmer.stem(&s).into_owned())
432 }
433}
434
435#[derive(Debug, Clone)]
440pub struct MultiLanguageStemmer {
441 default_language: Language,
442}
443
444impl MultiLanguageStemmer {
445 pub fn new(default_language: Language) -> Self {
447 Self { default_language }
448 }
449
450 pub fn tokenize_with_language(&self, text: &str, language: Language) -> Vec<Token> {
452 let stemmer = rust_stemmers::Stemmer::create(language.to_algorithm());
453 tokenize_and_clean(text, |s| stemmer.stem(&s).into_owned())
454 }
455
456 pub fn default_language(&self) -> Language {
458 self.default_language
459 }
460}
461
462impl Default for MultiLanguageStemmer {
463 fn default() -> Self {
464 Self::new(Language::English)
465 }
466}
467
468impl Tokenizer for MultiLanguageStemmer {
469 fn tokenize(&self, text: &str) -> Vec<Token> {
470 self.tokenize_with_language(text, self.default_language)
471 }
472}
473
474#[derive(Clone)]
479pub struct LanguageAwareTokenizer<F>
480where
481 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
482{
483 language_selector: F,
484 stemmer: MultiLanguageStemmer,
485}
486
487impl<F> LanguageAwareTokenizer<F>
488where
489 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
490{
491 pub fn new(language_selector: F) -> Self {
508 Self {
509 language_selector,
510 stemmer: MultiLanguageStemmer::default(),
511 }
512 }
513
514 pub fn tokenize_with_hint(&self, text: &str, language_hint: &str) -> Vec<Token> {
518 let language = (self.language_selector)(language_hint);
519 self.stemmer.tokenize_with_language(text, language)
520 }
521}
522
523impl<F> Tokenizer for LanguageAwareTokenizer<F>
524where
525 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
526{
527 fn tokenize(&self, text: &str) -> Vec<Token> {
528 self.stemmer.tokenize_with_language(text, Language::English)
530 }
531
532 fn tokenize_with(&self, text: &str, hint: Option<&str>, _purpose: Purpose) -> Vec<Token> {
533 match hint {
534 Some(hint) => self.tokenize_with_hint(text, hint),
535 None => Tokenizer::tokenize(self, text),
536 }
537 }
538}
539
540pub fn parse_language(s: &str) -> Language {
544 match s.to_lowercase().as_str() {
545 "ar" | "arabic" => Language::Arabic,
546 "da" | "danish" => Language::Danish,
547 "nl" | "dutch" => Language::Dutch,
548 "en" | "english" => Language::English,
549 "fi" | "finnish" => Language::Finnish,
550 "fr" | "french" => Language::French,
551 "de" | "german" => Language::German,
552 "el" | "greek" => Language::Greek,
553 "hu" | "hungarian" => Language::Hungarian,
554 "it" | "italian" => Language::Italian,
555 "no" | "norwegian" => Language::Norwegian,
556 "pt" | "portuguese" => Language::Portuguese,
557 "ro" | "romanian" => Language::Romanian,
558 "ru" | "russian" => Language::Russian,
559 "es" | "spanish" => Language::Spanish,
560 "sv" | "swedish" => Language::Swedish,
561 "ta" | "tamil" => Language::Tamil,
562 "tr" | "turkish" => Language::Turkish,
563 _ => Language::English, }
565}
566
567pub fn parse_language_opt(s: &str) -> Option<Language> {
572 Some(match s.trim().to_lowercase().as_str() {
573 "ar" | "arabic" => Language::Arabic,
574 "da" | "danish" => Language::Danish,
575 "nl" | "dutch" => Language::Dutch,
576 "en" | "english" => Language::English,
577 "fi" | "finnish" => Language::Finnish,
578 "fr" | "french" => Language::French,
579 "de" | "german" => Language::German,
580 "el" | "greek" => Language::Greek,
581 "hu" | "hungarian" => Language::Hungarian,
582 "it" | "italian" => Language::Italian,
583 "no" | "norwegian" => Language::Norwegian,
584 "pt" | "portuguese" => Language::Portuguese,
585 "ro" | "romanian" => Language::Romanian,
586 "ru" | "russian" => Language::Russian,
587 "es" | "spanish" => Language::Spanish,
588 "sv" | "swedish" => Language::Swedish,
589 "ta" | "tamil" => Language::Tamil,
590 "tr" | "turkish" => Language::Turkish,
591 _ => return None,
592 })
593}
594
595#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
597pub enum Script {
598 Latin,
599 Cyrillic,
600 Greek,
601 Arabic,
602 Tamil,
603 Other,
605}
606
607impl Script {
608 pub fn of_token(token: &str) -> Script {
610 let Some(c) = token.chars().find(|c| c.is_alphabetic()) else {
611 return Script::Other;
612 };
613 Script::of_char(c)
614 }
615
616 fn of_char(c: char) -> Script {
617 match c as u32 {
618 0x0041..=0x024F | 0x1E00..=0x1EFF => Script::Latin,
620 0x0370..=0x03FF | 0x1F00..=0x1FFF => Script::Greek,
621 0x0400..=0x052F => Script::Cyrillic,
622 0x0600..=0x06FF | 0x0750..=0x077F | 0x08A0..=0x08FF => Script::Arabic,
623 0x0B80..=0x0BFF => Script::Tamil,
624 _ => Script::Other,
625 }
626 }
627}
628
629impl Language {
630 pub fn script(self) -> Script {
632 match self {
633 Language::Russian => Script::Cyrillic,
634 Language::Greek => Script::Greek,
635 Language::Arabic => Script::Arabic,
636 Language::Tamil => Script::Tamil,
637 _ => Script::Latin,
638 }
639 }
640}
641
642thread_local! {
643 static STEMMER_CACHE: std::cell::RefCell<HashMap<Language, rust_stemmers::Stemmer>> =
644 std::cell::RefCell::new(HashMap::new());
645}
646
647pub(super) fn with_stemmers<R>(
652 languages: &[Language],
653 f: impl FnOnce(&[&rust_stemmers::Stemmer]) -> R,
654) -> R {
655 STEMMER_CACHE.with(|cache| {
656 let mut cache = cache.borrow_mut();
657 for language in languages {
658 cache
659 .entry(*language)
660 .or_insert_with(|| rust_stemmers::Stemmer::create(language.to_algorithm()));
661 }
662 let stemmers: Vec<&rust_stemmers::Stemmer> =
663 languages.iter().map(|language| &cache[language]).collect();
664 f(&stemmers)
665 })
666}
667
668pub fn language_code(language: Language) -> &'static str {
670 match language {
671 Language::Arabic => "ar",
672 Language::Danish => "da",
673 Language::Dutch => "nl",
674 Language::English => "en",
675 Language::Finnish => "fi",
676 Language::French => "fr",
677 Language::German => "de",
678 Language::Greek => "el",
679 Language::Hungarian => "hu",
680 Language::Italian => "it",
681 Language::Norwegian => "no",
682 Language::Portuguese => "pt",
683 Language::Romanian => "ro",
684 Language::Russian => "ru",
685 Language::Spanish => "es",
686 Language::Swedish => "sv",
687 Language::Tamil => "ta",
688 Language::Turkish => "tr",
689 }
690}
691
692pub type BoxedTokenizer = Box<dyn TokenizerClone>;
694
695pub trait TokenizerClone: Send + Sync {
696 fn tokenize(&self, text: &str) -> Vec<Token>;
697 fn tokenize_with(&self, text: &str, hint: Option<&str>, purpose: Purpose) -> Vec<Token>;
699 fn clone_box(&self) -> BoxedTokenizer;
700}
701
702impl<T: Tokenizer> TokenizerClone for T {
703 fn tokenize(&self, text: &str) -> Vec<Token> {
704 Tokenizer::tokenize(self, text)
705 }
706
707 fn tokenize_with(&self, text: &str, hint: Option<&str>, purpose: Purpose) -> Vec<Token> {
708 Tokenizer::tokenize_with(self, text, hint, purpose)
709 }
710
711 fn clone_box(&self) -> BoxedTokenizer {
712 Box::new(self.clone())
713 }
714}
715
716impl Clone for BoxedTokenizer {
717 fn clone(&self) -> Self {
718 self.clone_box()
719 }
720}
721
722#[derive(Clone)]
727pub struct TokenizerRegistry {
728 tokenizers: Arc<RwLock<HashMap<String, BoxedTokenizer>>>,
729 dynamic: Arc<RwLock<HashMap<String, BoxedTokenizer>>>,
733}
734
735impl TokenizerRegistry {
736 pub fn new() -> Self {
738 let registry = Self {
739 tokenizers: Arc::new(RwLock::new(HashMap::new())),
740 dynamic: Arc::new(RwLock::new(HashMap::new())),
741 };
742 registry.register_defaults();
743 registry
744 }
745
746 fn register_defaults(&self) {
748 self.register("simple", SimpleTokenizer);
750 self.register("default", SimpleTokenizer);
751 self.register("unicode_word", UnicodeWordTokenizer);
752 self.register("raw", RawTokenizer);
753 self.register("raw_ci", RawCiTokenizer);
754
755 self.register("en_stem", StemmerTokenizer::new(Language::English));
757 self.register("english", StemmerTokenizer::new(Language::English));
758
759 self.register("ar_stem", StemmerTokenizer::new(Language::Arabic));
761 self.register("arabic", StemmerTokenizer::new(Language::Arabic));
762 self.register("da_stem", StemmerTokenizer::new(Language::Danish));
763 self.register("danish", StemmerTokenizer::new(Language::Danish));
764 self.register("nl_stem", StemmerTokenizer::new(Language::Dutch));
765 self.register("dutch", StemmerTokenizer::new(Language::Dutch));
766 self.register("fi_stem", StemmerTokenizer::new(Language::Finnish));
767 self.register("finnish", StemmerTokenizer::new(Language::Finnish));
768 self.register("fr_stem", StemmerTokenizer::new(Language::French));
769 self.register("french", StemmerTokenizer::new(Language::French));
770 self.register("de_stem", StemmerTokenizer::new(Language::German));
771 self.register("german", StemmerTokenizer::new(Language::German));
772 self.register("el_stem", StemmerTokenizer::new(Language::Greek));
773 self.register("greek", StemmerTokenizer::new(Language::Greek));
774 self.register("hu_stem", StemmerTokenizer::new(Language::Hungarian));
775 self.register("hungarian", StemmerTokenizer::new(Language::Hungarian));
776 self.register("it_stem", StemmerTokenizer::new(Language::Italian));
777 self.register("italian", StemmerTokenizer::new(Language::Italian));
778 self.register("no_stem", StemmerTokenizer::new(Language::Norwegian));
779 self.register("norwegian", StemmerTokenizer::new(Language::Norwegian));
780 self.register("pt_stem", StemmerTokenizer::new(Language::Portuguese));
781 self.register("portuguese", StemmerTokenizer::new(Language::Portuguese));
782 self.register("ro_stem", StemmerTokenizer::new(Language::Romanian));
783 self.register("romanian", StemmerTokenizer::new(Language::Romanian));
784 self.register("ru_stem", StemmerTokenizer::new(Language::Russian));
785 self.register("russian", StemmerTokenizer::new(Language::Russian));
786 self.register("es_stem", StemmerTokenizer::new(Language::Spanish));
787 self.register("spanish", StemmerTokenizer::new(Language::Spanish));
788 self.register("sv_stem", StemmerTokenizer::new(Language::Swedish));
789 self.register("swedish", StemmerTokenizer::new(Language::Swedish));
790 self.register("ta_stem", StemmerTokenizer::new(Language::Tamil));
791 self.register("tamil", StemmerTokenizer::new(Language::Tamil));
792 self.register("tr_stem", StemmerTokenizer::new(Language::Turkish));
793 self.register("turkish", StemmerTokenizer::new(Language::Turkish));
794
795 self.register(
797 "en_stop",
798 StopWordTokenizer::new(SimpleTokenizer, Language::English),
799 );
800 self.register(
801 "de_stop",
802 StopWordTokenizer::new(SimpleTokenizer, Language::German),
803 );
804 self.register(
805 "fr_stop",
806 StopWordTokenizer::new(SimpleTokenizer, Language::French),
807 );
808 self.register(
809 "ru_stop",
810 StopWordTokenizer::new(SimpleTokenizer, Language::Russian),
811 );
812 self.register(
813 "es_stop",
814 StopWordTokenizer::new(SimpleTokenizer, Language::Spanish),
815 );
816
817 self.register(
819 "en_stem_stop",
820 StopWordTokenizer::new(StemmerTokenizer::new(Language::English), Language::English),
821 );
822 self.register(
823 "de_stem_stop",
824 StopWordTokenizer::new(StemmerTokenizer::new(Language::German), Language::German),
825 );
826 self.register(
827 "fr_stem_stop",
828 StopWordTokenizer::new(StemmerTokenizer::new(Language::French), Language::French),
829 );
830 self.register(
831 "ru_stem_stop",
832 StopWordTokenizer::new(StemmerTokenizer::new(Language::Russian), Language::Russian),
833 );
834 self.register(
835 "es_stem_stop",
836 StopWordTokenizer::new(StemmerTokenizer::new(Language::Spanish), Language::Spanish),
837 );
838 }
839
840 pub fn register<T: Tokenizer>(&self, name: &str, tokenizer: T) {
842 let mut tokenizers = self.tokenizers.write();
843 tokenizers.insert(name.to_string(), Box::new(tokenizer));
844 }
845
846 pub fn get(&self, name: &str) -> Option<BoxedTokenizer> {
851 if name.starts_with("lex(") {
852 if let Some(tokenizer) = self.dynamic.read().get(name) {
853 return Some(tokenizer.clone());
854 }
855 let tokenizer = TokenizerSpec::parse(name)
856 .ok()
857 .and_then(|spec| spec.dynamic_tokenizer())?;
858 self.dynamic
859 .write()
860 .entry(name.to_string())
861 .or_insert_with(|| tokenizer.clone());
862 return Some(tokenizer);
863 }
864 let tokenizers = self.tokenizers.read();
865 tokenizers.get(name).cloned()
866 }
867
868 pub fn contains(&self, name: &str) -> bool {
870 let tokenizers = self.tokenizers.read();
871 tokenizers.contains_key(name)
872 }
873
874 pub fn names(&self) -> Vec<String> {
876 let tokenizers = self.tokenizers.read();
877 tokenizers.keys().cloned().collect()
878 }
879}
880
881impl Default for TokenizerRegistry {
882 fn default() -> Self {
883 Self::new()
884 }
885}
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890
891 #[test]
892 fn test_simple_tokenizer() {
893 let tokenizer = SimpleTokenizer;
894 let tokens = Tokenizer::tokenize(&tokenizer, "Hello World");
895
896 assert_eq!(tokens.len(), 2);
897 assert_eq!(tokens[0].text, "hello");
898 assert_eq!(tokens[0].position, 0);
899 assert_eq!(tokens[1].text, "world");
900 assert_eq!(tokens[1].position, 1);
901 }
902
903 #[test]
904 fn test_raw_tokenizer() {
905 let tokenizer = RawTokenizer;
906 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
908 assert_eq!(tokens.len(), 1);
909 assert_eq!(tokens[0].text, "Hello, World!");
910 assert_eq!(tokens[0].position, 0);
911 }
912
913 #[test]
914 fn test_raw_tokenizer_trims() {
915 let tokenizer = RawTokenizer;
916 let tokens = Tokenizer::tokenize(&tokenizer, " spaced ");
917 assert_eq!(tokens.len(), 1);
918 assert_eq!(tokens[0].text, "spaced");
919 assert_eq!(tokens[0].offset_from, 2);
920 }
921
922 #[test]
923 fn test_raw_tokenizer_empty() {
924 let tokenizer = RawTokenizer;
925 assert!(Tokenizer::tokenize(&tokenizer, "").is_empty());
926 assert!(Tokenizer::tokenize(&tokenizer, " ").is_empty());
927 }
928
929 #[test]
930 fn test_raw_ci_tokenizer() {
931 let tokenizer = RawCiTokenizer;
932 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
934 assert_eq!(tokens.len(), 1);
935 assert_eq!(tokens[0].text, "hello, world!");
936 assert_eq!(tokens[0].position, 0);
937 }
938
939 #[test]
940 fn test_raw_ci_tokenizer_preserves_structure() {
941 let tokenizer = RawCiTokenizer;
942 let tokens = Tokenizer::tokenize(&tokenizer, "HTTPS://Example.COM/Page");
943 assert_eq!(tokens.len(), 1);
944 assert_eq!(tokens[0].text, "https://example.com/page");
945 }
946
947 #[test]
948 fn test_simple_tokenizer_strips_punctuation() {
949 let tokenizer = SimpleTokenizer;
950 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
951
952 assert_eq!(tokens.len(), 2);
953 assert_eq!(tokens[0].text, "hello");
954 assert_eq!(tokens[1].text, "world");
955 }
956
957 #[test]
958 fn unicode_words_preserve_internal_punctuation_and_original_offsets() {
959 let tokenizer = TokenizerRegistry::new().get("unicode_word").unwrap();
960 let input = "TERM_start5 User:Robert books.google.com John's CAFÉ e\u{301}";
961 let tokens = tokenizer.tokenize(input);
962 let expected = [
963 "term_start5",
964 "user:robert",
965 "books.google.com",
966 "john's",
967 "café",
968 "e\u{301}",
969 ];
970 assert_eq!(tokens.len(), expected.len());
971 for (i, (token, expected)) in tokens.iter().zip(expected).enumerate() {
972 assert_eq!(token.text, expected);
973 assert_eq!(token.position, i as u32);
974 assert_eq!(
975 input[token.offset_from..token.offset_to].to_lowercase(),
976 expected
977 );
978 assert!(!token.variant);
979 }
980 let input = format!("left {} right", "x".repeat(256));
981 let tokens = tokenizer.tokenize(&input);
982 assert_eq!(
983 tokens
984 .iter()
985 .map(|t| (t.text.as_str(), t.position))
986 .collect::<Vec<_>>(),
987 vec![("left", 0), ("right", 2)]
988 );
989 }
990
991 #[test]
992 fn test_empty_text() {
993 let tokenizer = SimpleTokenizer;
994 let tokens = Tokenizer::tokenize(&tokenizer, "");
995 assert!(tokens.is_empty());
996 }
997
998 #[test]
999 fn test_stemmer_tokenizer_english() {
1000 let tokenizer = StemmerTokenizer::english();
1001 let tokens = Tokenizer::tokenize(&tokenizer, "Dogs are running quickly");
1002
1003 assert_eq!(tokens.len(), 4);
1004 assert_eq!(tokens[0].text, "dog"); assert_eq!(tokens[1].text, "are"); assert_eq!(tokens[2].text, "run"); assert_eq!(tokens[3].text, "quick"); }
1009
1010 #[test]
1011 fn test_stemmer_tokenizer_preserves_offsets() {
1012 let tokenizer = StemmerTokenizer::english();
1013 let tokens = Tokenizer::tokenize(&tokenizer, "Running dogs");
1014
1015 assert_eq!(tokens.len(), 2);
1016 assert_eq!(tokens[0].text, "run");
1017 assert_eq!(tokens[0].offset_from, 0);
1018 assert_eq!(tokens[0].offset_to, 7); assert_eq!(tokens[1].text, "dog");
1020 assert_eq!(tokens[1].offset_from, 8);
1021 assert_eq!(tokens[1].offset_to, 12); }
1023
1024 #[test]
1025 fn test_stemmer_tokenizer_german() {
1026 let tokenizer = StemmerTokenizer::new(Language::German);
1027 let tokens = Tokenizer::tokenize(&tokenizer, "Häuser Bücher");
1028
1029 assert_eq!(tokens.len(), 2);
1030 assert_eq!(tokens[0].text, "haus"); assert_eq!(tokens[1].text, "buch"); }
1034
1035 #[test]
1036 fn test_stemmer_tokenizer_russian() {
1037 let tokenizer = StemmerTokenizer::new(Language::Russian);
1038 let tokens = Tokenizer::tokenize(&tokenizer, "бегущие собаки");
1039
1040 assert_eq!(tokens.len(), 2);
1041 assert_eq!(tokens[0].text, "бегущ"); assert_eq!(tokens[1].text, "собак"); }
1045
1046 #[test]
1047 fn test_multi_language_stemmer() {
1048 let stemmer = MultiLanguageStemmer::new(Language::English);
1049
1050 let tokens = stemmer.tokenize_with_language("running dogs", Language::English);
1052 assert_eq!(tokens[0].text, "run");
1053 assert_eq!(tokens[1].text, "dog");
1054
1055 let tokens = stemmer.tokenize_with_language("Häuser Bücher", Language::German);
1057 assert_eq!(tokens[0].text, "haus");
1058 assert_eq!(tokens[1].text, "buch");
1059
1060 let tokens = stemmer.tokenize_with_language("бегущие собаки", Language::Russian);
1062 assert_eq!(tokens[0].text, "бегущ");
1063 assert_eq!(tokens[1].text, "собак");
1064 }
1065
1066 #[test]
1067 fn test_language_aware_tokenizer() {
1068 let tokenizer = LanguageAwareTokenizer::new(parse_language);
1069
1070 let tokens = tokenizer.tokenize_with_hint("running dogs", "en");
1072 assert_eq!(tokens[0].text, "run");
1073 assert_eq!(tokens[1].text, "dog");
1074
1075 let tokens = tokenizer.tokenize_with_hint("Häuser Bücher", "de");
1077 assert_eq!(tokens[0].text, "haus");
1078 assert_eq!(tokens[1].text, "buch");
1079
1080 let tokens = tokenizer.tokenize_with_hint("бегущие собаки", "russian");
1082 assert_eq!(tokens[0].text, "бегущ");
1083 assert_eq!(tokens[1].text, "собак");
1084 }
1085
1086 #[test]
1087 fn test_parse_language() {
1088 assert_eq!(parse_language("en"), Language::English);
1089 assert_eq!(parse_language("english"), Language::English);
1090 assert_eq!(parse_language("English"), Language::English);
1091 assert_eq!(parse_language("de"), Language::German);
1092 assert_eq!(parse_language("german"), Language::German);
1093 assert_eq!(parse_language("ru"), Language::Russian);
1094 assert_eq!(parse_language("russian"), Language::Russian);
1095 assert_eq!(parse_language("unknown"), Language::English); }
1097
1098 #[test]
1099 fn test_tokenizer_registry_defaults() {
1100 let registry = TokenizerRegistry::new();
1101
1102 assert!(registry.contains("simple"));
1104 assert!(registry.contains("raw"));
1105 assert!(registry.contains("raw_ci"));
1106 assert!(registry.contains("raw"));
1107 assert!(registry.contains("raw_ci"));
1108 assert!(registry.contains("en_stem"));
1109 assert!(registry.contains("german"));
1110 assert!(registry.contains("russian"));
1111 }
1112
1113 #[test]
1114 fn test_tokenizer_registry_get() {
1115 let registry = TokenizerRegistry::new();
1116
1117 let tokenizer = registry.get("en_stem").unwrap();
1119 let tokens = tokenizer.tokenize("running dogs");
1120 assert_eq!(tokens[0].text, "run");
1121 assert_eq!(tokens[1].text, "dog");
1122
1123 let tokenizer = registry.get("german").unwrap();
1125 let tokens = tokenizer.tokenize("Häuser Bücher");
1126 assert_eq!(tokens[0].text, "haus");
1127 assert_eq!(tokens[1].text, "buch");
1128 }
1129
1130 #[test]
1131 fn test_tokenizer_registry_custom() {
1132 let registry = TokenizerRegistry::new();
1133
1134 registry.register("my_tokenizer", SimpleTokenizer);
1136
1137 assert!(registry.contains("my_tokenizer"));
1138 let tokenizer = registry.get("my_tokenizer").unwrap();
1139 let tokens = tokenizer.tokenize("Hello World");
1140 assert_eq!(tokens[0].text, "hello");
1141 assert_eq!(tokens[1].text, "world");
1142 }
1143
1144 #[test]
1145 fn test_tokenizer_registry_nonexistent() {
1146 let registry = TokenizerRegistry::new();
1147 assert!(registry.get("nonexistent").is_none());
1148 }
1149
1150 #[test]
1151 fn test_stop_word_tokenizer_english() {
1152 let tokenizer = StopWordTokenizer::english(SimpleTokenizer);
1153 let tokens = Tokenizer::tokenize(&tokenizer, "The quick brown fox jumps over the lazy dog");
1154
1155 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1157 assert!(!texts.contains(&"the"));
1158 assert!(!texts.contains(&"over"));
1159 assert!(texts.contains(&"quick"));
1160 assert!(texts.contains(&"brown"));
1161 assert!(texts.contains(&"fox"));
1162 assert!(texts.contains(&"jumps"));
1163 assert!(texts.contains(&"lazy"));
1164 assert!(texts.contains(&"dog"));
1165 }
1166
1167 #[test]
1168 fn test_stop_word_tokenizer_with_stemmer() {
1169 let tokenizer = StopWordTokenizer::new(StemmerTokenizer::english(), Language::English);
1173 let tokens = Tokenizer::tokenize(&tokenizer, "elephants galaxies quantum");
1174
1175 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1176 assert!(texts.contains(&"eleph")); assert!(texts.contains(&"galaxi")); assert!(texts.contains(&"quantum")); }
1181
1182 #[test]
1183 fn test_stop_word_tokenizer_german() {
1184 let tokenizer = StopWordTokenizer::new(SimpleTokenizer, Language::German);
1185 let tokens = Tokenizer::tokenize(&tokenizer, "Der Hund und die Katze");
1186
1187 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1189 assert!(!texts.contains(&"der"));
1190 assert!(!texts.contains(&"und"));
1191 assert!(!texts.contains(&"die"));
1192 assert!(texts.contains(&"hund"));
1193 assert!(texts.contains(&"katze"));
1194 }
1195
1196 #[test]
1197 fn test_stop_word_tokenizer_custom() {
1198 let custom_stops: HashSet<String> = ["foo", "bar"].iter().map(|s| s.to_string()).collect();
1199 let tokenizer = StopWordTokenizer::with_custom_stop_words(SimpleTokenizer, custom_stops);
1200 let tokens = Tokenizer::tokenize(&tokenizer, "foo baz bar qux");
1201
1202 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1203 assert!(!texts.contains(&"foo"));
1204 assert!(!texts.contains(&"bar"));
1205 assert!(texts.contains(&"baz"));
1206 assert!(texts.contains(&"qux"));
1207 }
1208
1209 #[test]
1210 fn test_stop_word_tokenizer_is_stop_word() {
1211 let tokenizer = StopWordTokenizer::english(SimpleTokenizer);
1212 assert!(tokenizer.is_stop_word("the"));
1213 assert!(tokenizer.is_stop_word("and"));
1214 assert!(tokenizer.is_stop_word("is"));
1215 assert!(!tokenizer.is_stop_word("elephant"));
1217 assert!(!tokenizer.is_stop_word("quantum"));
1218 }
1219
1220 #[test]
1221 fn test_tokenizer_registry_stop_word_tokenizers() {
1222 let registry = TokenizerRegistry::new();
1223
1224 assert!(registry.contains("en_stop"));
1226 assert!(registry.contains("en_stem_stop"));
1227 assert!(registry.contains("de_stop"));
1228 assert!(registry.contains("ru_stop"));
1229
1230 let tokenizer = registry.get("en_stop").unwrap();
1232 let tokens = tokenizer.tokenize("The quick fox");
1233 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1234 assert!(!texts.contains(&"the"));
1235 assert!(texts.contains(&"quick"));
1236 assert!(texts.contains(&"fox"));
1237
1238 let tokenizer = registry.get("en_stem_stop").unwrap();
1240 let tokens = tokenizer.tokenize("elephants galaxies");
1241 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1242 assert!(texts.contains(&"eleph")); assert!(texts.contains(&"galaxi")); }
1245
1246 fn hinted<T: Tokenizer>(tokenizer: &T, text: &str, hint: Option<&str>) -> Vec<Token> {
1247 Tokenizer::tokenize_with(tokenizer, text, hint, Purpose::Index)
1248 }
1249
1250 fn lex(spec: &str) -> LexTokenizer {
1251 LexTokenizer::new(LexOptions::parse(spec).unwrap())
1252 }
1253
1254 fn texts(tokens: &[Token]) -> Vec<&str> {
1255 tokens.iter().map(|t| t.text.as_str()).collect()
1256 }
1257
1258 fn positions(tokens: &[Token]) -> Vec<u32> {
1259 tokens.iter().map(|t| t.position).collect()
1260 }
1261
1262 #[test]
1263 fn unicode_segmenter_splits_on_word_boundaries_and_folds() {
1264 let plain = lex("by: languages, segmenter: unicode, stem: snowball, variants: false");
1265 let tokens = hinted(
1266 &plain,
1267 "Float-zero determinants: p53/CO2, 10.1007/s1 résumé",
1268 None,
1269 );
1270 assert_eq!(
1271 texts(&tokens),
1272 vec![
1273 "float",
1274 "zero",
1275 "determinants",
1276 "p53",
1277 "co2",
1278 "10.1007",
1279 "s1",
1280 "resume"
1281 ]
1282 );
1283 assert_eq!(positions(&tokens), (0..8).collect::<Vec<u32>>());
1284 assert_eq!(
1286 &"Float-zero determinants: p53/CO2, 10.1007/s1 résumé"
1287 [tokens[7].offset_from..tokens[7].offset_to],
1288 "résumé"
1289 );
1290
1291 let english = lex("by: languages, segmenter: unicode, stem: snowball, variants: false");
1294 assert_eq!(
1295 texts(&hinted(&english, "Running foxes' café", Some("en"))),
1296 vec!["run", "fox", "cafe"]
1297 );
1298 assert_eq!(
1299 texts(&hinted(
1300 &lex("by: languages, segmenter: simple, stem: snowball, variants: false"),
1301 "Float-zero café",
1302 Some("en")
1303 )),
1304 vec!["float", "zero", "cafe"]
1305 );
1306 assert_eq!(texts(&hinted(&plain, "ёлка", None)), vec!["елка"]);
1308 let stopping = lex(
1310 "by: languages, stop_words: true, segmenter: unicode, stem: snowball, variants: false",
1311 );
1312 let tokens = hinted(&stopping, "state-of-the-art résumé", Some("en"));
1313 assert_eq!(tokens.len(), 3, "{:?}", texts(&tokens));
1314 assert_eq!(&texts(&tokens)[..2], ["state", "art"]);
1315 assert!(tokens[2].text.starts_with("resum"), "{:?}", tokens[2].text);
1316 assert!(tokens[2].text.is_ascii(), "folded after stemming");
1317 assert_eq!(positions(&tokens), vec![0, 3, 4]);
1318 }
1319
1320 #[test]
1321 fn unicode_segmenter_bigrams_cjk_runs() {
1322 let t = lex("by: languages, segmenter: unicode, stem: snowball, variants: false");
1323 let tokens = hinted(&t, "東京都 tower", Some("en"));
1324 assert_eq!(texts(&tokens), vec!["東京", "京都", "tower"]);
1325 assert_eq!(positions(&tokens), vec![0, 1, 2]);
1326 assert_eq!(
1327 &"東京都 tower"[tokens[1].offset_from..tokens[1].offset_to],
1328 "京都"
1329 );
1330 assert_eq!(
1332 texts(&hinted(&t, "東 tower 京都", None)),
1333 vec!["東", "tower", "京都"]
1334 );
1335 assert_eq!(
1338 texts(&hinted(&t, "トウキョウ タワー", None)),
1339 vec!["トウ", "ウキ", "キョ", "ョウ", "タワ", "ワー"]
1340 );
1341 assert_eq!(texts(&hinted(&t, "東京tower", None)), vec!["東京", "tower"]);
1344 let query = hinted(&t, "東京都", None);
1346 assert_eq!(positions(&query), vec![0, 1]);
1347 }
1348
1349 #[test]
1350 fn dynamic_stemmer_drops_stop_words_but_keeps_positions() {
1351 let stemmer = lex(
1352 "by: languages, stop_words: true, segmenter: simple, stem: snowball, variants: false",
1353 );
1354 let tokens = hinted(&stemmer, "Quantum of the Art", Some("en"));
1355 assert_eq!(texts(&tokens), vec!["quantum", "art"]);
1356 assert_eq!(positions(&tokens), vec![0, 3]);
1357 assert_eq!(tokens[1].offset_from, "Quantum of the ".len());
1359
1360 let tokens = hinted(&stemmer, "бегущие и собаки the foxes", Some("ru,en"));
1362 assert_eq!(texts(&tokens), vec!["бегущ", "собак", "fox"]);
1363 assert_eq!(positions(&tokens), vec![0, 2, 4]);
1364
1365 assert_eq!(
1367 texts(&hinted(&stemmer, "the 日本語 fox", Some("en"))),
1368 vec!["日本語", "fox"]
1369 );
1370
1371 assert_eq!(
1373 texts(&hinted(&stemmer, "the fox", None)),
1374 vec!["the", "fox"]
1375 );
1376 let english = lex(
1378 "by: languages, default: en, stop_words: true, segmenter: simple, stem: snowball, variants: false",
1379 );
1380 assert_eq!(texts(&hinted(&english, "the fox", None)), vec!["fox"]);
1381 assert_eq!(
1383 texts(&hinted(
1384 &lex("by: languages, segmenter: simple, stem: snowball, variants: false"),
1385 "the fox",
1386 Some("en")
1387 )),
1388 vec!["the", "fox"]
1389 );
1390 assert!(hinted(&stemmer, "to be or not to be", Some("en")).is_empty());
1392 }
1393
1394 #[test]
1395 fn dynamic_stemmer_selects_language_from_hint() {
1396 let stemmer = lex("by: languages, segmenter: simple, stem: snowball, variants: false");
1397 assert_eq!(
1398 texts(&hinted(&stemmer, "Running Foxes", Some("en"))),
1399 vec!["run", "fox"]
1400 );
1401 assert_eq!(
1402 texts(&hinted(&stemmer, "бегущие собаки", Some("ru"))),
1403 vec!["бегущ", "собак"]
1404 );
1405 assert_eq!(
1407 texts(&hinted(&stemmer, "Running Foxes", Some("xx"))),
1408 vec!["running", "foxes"]
1409 );
1410 assert_eq!(
1411 texts(&hinted(&stemmer, "Running Foxes", None)),
1412 vec!["running", "foxes"]
1413 );
1414 assert_eq!(
1415 texts(&Tokenizer::tokenize(&stemmer, "Running, Foxes!")),
1416 vec!["running", "foxes"]
1417 );
1418 let english =
1420 lex("by: languages, default: en, segmenter: simple, stem: snowball, variants: false");
1421 assert_eq!(
1422 texts(&hinted(&english, "Running Foxes", None)),
1423 vec!["run", "fox"]
1424 );
1425 }
1426
1427 #[test]
1428 fn dynamic_stemmer_routes_tokens_by_script() {
1429 let stemmer = lex("by: languages, segmenter: simple, stem: snowball, variants: false");
1430 assert_eq!(
1432 texts(&hinted(&stemmer, "бегущие foxes", Some("ru,en"))),
1433 vec!["бегущ", "fox"]
1434 );
1435 assert_eq!(
1436 texts(&hinted(&stemmer, "бегущие foxes", Some("en, ru"))),
1437 vec!["бегущ", "fox"]
1438 );
1439 assert_eq!(
1441 texts(&hinted(&stemmer, "бегущие foxes", Some("ru"))),
1442 vec!["бегущ", "foxes"]
1443 );
1444 assert_eq!(
1445 texts(&hinted(&stemmer, "бегущие foxes", Some("en"))),
1446 vec!["бегущие", "fox"]
1447 );
1448 assert_eq!(
1450 texts(&hinted(&stemmer, "running", Some("de,en"))),
1451 vec!["running"]
1452 );
1453 assert_eq!(
1454 texts(&hinted(&stemmer, "running", Some("en,de"))),
1455 vec!["run"]
1456 );
1457 let tokens = hinted(&stemmer, "бегущие foxes run", Some("ru,en"));
1459 assert_eq!(
1460 tokens.iter().map(|t| t.position).collect::<Vec<_>>(),
1461 vec![0, 1, 2]
1462 );
1463 }
1464
1465 #[test]
1466 fn script_detection_covers_supported_stemmer_scripts() {
1467 assert_eq!(Script::of_token("hello"), Script::Latin);
1468 assert_eq!(Script::of_token("straße"), Script::Latin);
1469 assert_eq!(Script::of_token("собака"), Script::Cyrillic);
1470 assert_eq!(Script::of_token("γεια"), Script::Greek);
1471 assert_eq!(Script::of_token("مرحبا"), Script::Arabic);
1472 assert_eq!(Script::of_token("தமிழ்"), Script::Tamil);
1473 assert_eq!(Script::of_token("日本語"), Script::Other);
1474 assert_eq!(Script::of_token("2024"), Script::Other);
1475 assert_eq!(Language::Russian.script(), Script::Cyrillic);
1476 assert_eq!(Language::Turkish.script(), Script::Latin);
1477 }
1478
1479 #[test]
1480 fn registry_builds_lex_tokenizers_from_specs() {
1481 let registry = TokenizerRegistry::new();
1482 let tokenizer = registry
1483 .get("lex(by: languages, segmenter: simple, stem: snowball, variants: false)")
1484 .expect("lex spec resolves without registration");
1485 assert_eq!(
1486 texts(&tokenizer.tokenize_with("Running Foxes", Some("en"), Purpose::Index)),
1487 vec!["run", "fox"]
1488 );
1489 assert_eq!(
1490 texts(&tokenizer.tokenize("Running Foxes")),
1491 vec!["running", "foxes"]
1492 );
1493 assert!(
1494 registry
1495 .get("lex(by: languages, default: klingon)")
1496 .is_none()
1497 );
1498 let stopping = registry
1499 .get("lex(by: languages, stop_words: true, segmenter: simple, stem: snowball, variants: false)")
1500 .expect("stop-word spec resolves");
1501 let tokens = stopping.tokenize_with("the running foxes", Some("en"), Purpose::Index);
1502 assert_eq!(texts(&tokens), vec!["run", "fox"]);
1503 assert_eq!(positions(&tokens), vec![1, 2]);
1504 let simple = registry.get("en_stem").unwrap();
1506 assert_eq!(
1507 texts(&simple.tokenize_with("Running Foxes", Some("ru"), Purpose::Exact)),
1508 vec!["run", "fox"]
1509 );
1510 }
1511
1512 #[test]
1513 fn spec_without_by_is_a_fixed_tokenizer_that_ignores_hints() {
1514 let spec = TokenizerSpec::parse("lex(stop_words: true, stem: none)").unwrap();
1515 assert_eq!(spec.hint_field(), None);
1516 let tokenizer = spec.dynamic_tokenizer().unwrap();
1517 let plain_tokens = tokenizer.tokenize("The running cells");
1518 let plain = texts(&plain_tokens);
1519 assert_eq!(plain, vec!["the", "running", "cells"]);
1521 let hinted_tokens =
1522 tokenizer.tokenize_with("The running cells", Some("en"), Purpose::Index);
1523 assert_eq!(
1524 texts(&hinted_tokens),
1525 plain,
1526 "hints are ignored without `by`"
1527 );
1528
1529 let english = TokenizerSpec::parse(
1531 "lex(default: en, stop_words: true, stem: snowball, variants: false)",
1532 )
1533 .unwrap();
1534 let tokenizer = english.dynamic_tokenizer().unwrap();
1535 assert_eq!(
1536 texts(&tokenizer.tokenize_with("The running cells", Some("ru"), Purpose::Index)),
1537 vec!["run", "cell"]
1538 );
1539 }
1540
1541 #[test]
1542 fn parse_language_opt_rejects_unknown_codes() {
1543 assert_eq!(parse_language_opt(" RU "), Some(Language::Russian));
1544 assert_eq!(parse_language_opt("german"), Some(Language::German));
1545 assert_eq!(parse_language_opt("xx"), None);
1546 assert_eq!(parse_language_opt(""), None);
1547 for language in [Language::English, Language::Russian, Language::Tamil] {
1548 assert_eq!(parse_language_opt(language_code(language)), Some(language));
1549 }
1550 }
1551}