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;
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)]
123pub struct RawTokenizer;
124
125impl Tokenizer for RawTokenizer {
126 fn tokenize(&self, text: &str) -> Vec<Token> {
127 let trimmed = text.trim();
128 if trimmed.is_empty() {
129 return Vec::new();
130 }
131 let offset = text.as_ptr() as usize;
132 let trimmed_offset = trimmed.as_ptr() as usize - offset;
133 vec![Token::new(
134 trimmed.to_string(),
135 0,
136 trimmed_offset,
137 trimmed_offset + trimmed.len(),
138 )]
139 }
140}
141
142#[derive(Debug, Clone, Default)]
146pub struct RawCiTokenizer;
147
148impl Tokenizer for RawCiTokenizer {
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 lowercase_word(trimmed),
158 0,
159 trimmed_offset,
160 trimmed_offset + trimmed.len(),
161 )]
162 }
163}
164
165#[inline]
169fn lowercase_word(word: &str) -> String {
170 if word.is_ascii() {
171 if word.bytes().all(|b| !b.is_ascii_uppercase()) {
172 return word.to_string();
173 }
174 let mut s = word.to_string();
175 s.make_ascii_lowercase();
176 s
177 } else {
178 word.chars().flat_map(|c| c.to_lowercase()).collect()
179 }
180}
181
182#[inline]
187pub(super) fn clean_word(word: &str) -> String {
188 if word.is_ascii() {
189 let bytes = word.as_bytes();
190 if bytes
192 .iter()
193 .all(|&b| b.is_ascii_lowercase() || b.is_ascii_digit())
194 {
195 return word.to_string();
196 }
197 let mut result = String::with_capacity(bytes.len());
199 for &b in bytes {
200 if b.is_ascii_alphanumeric() {
201 result.push(b.to_ascii_lowercase() as char);
202 }
203 }
204 result
205 } else {
206 word.chars()
208 .filter(|c| c.is_alphanumeric())
209 .flat_map(|c| c.to_lowercase())
210 .collect()
211 }
212}
213
214fn tokenize_and_clean(text: &str, transform: impl Fn(String) -> String) -> Vec<Token> {
220 tokenize_and_clean_filtered(text, |word| Some(transform(word)))
221}
222
223fn tokenize_and_clean_filtered(
228 text: &str,
229 transform: impl Fn(String) -> Option<String>,
230) -> Vec<Token> {
231 let mut tokens = Vec::with_capacity(text.len() / 5);
232 let mut position = 0u32;
233 for (offset, word) in split_whitespace_with_offsets(text) {
234 if !word.is_empty() {
235 let cleaned = clean_word(word);
236 if !cleaned.is_empty() {
237 if let Some(text) = transform(cleaned) {
238 tokens.push(Token::new(text, position, offset, offset + word.len()));
239 }
240 position += 1;
241 }
242 }
243 }
244 tokens
245}
246
247pub(super) fn split_whitespace_with_offsets(text: &str) -> impl Iterator<Item = (usize, &str)> {
252 let base = text.as_ptr() as usize;
253 text.split_whitespace()
254 .map(move |word| (word.as_ptr() as usize - base, word))
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
259#[allow(missing_docs)]
260#[derive(Default)]
261pub enum Language {
262 Arabic,
263 Danish,
264 Dutch,
265 #[default]
266 English,
267 Finnish,
268 French,
269 German,
270 Greek,
271 Hungarian,
272 Italian,
273 Norwegian,
274 Portuguese,
275 Romanian,
276 Russian,
277 Spanish,
278 Swedish,
279 Tamil,
280 Turkish,
281}
282
283impl Language {
284 fn to_algorithm(self) -> Algorithm {
285 match self {
286 Language::Arabic => Algorithm::Arabic,
287 Language::Danish => Algorithm::Danish,
288 Language::Dutch => Algorithm::Dutch,
289 Language::English => Algorithm::English,
290 Language::Finnish => Algorithm::Finnish,
291 Language::French => Algorithm::French,
292 Language::German => Algorithm::German,
293 Language::Greek => Algorithm::Greek,
294 Language::Hungarian => Algorithm::Hungarian,
295 Language::Italian => Algorithm::Italian,
296 Language::Norwegian => Algorithm::Norwegian,
297 Language::Portuguese => Algorithm::Portuguese,
298 Language::Romanian => Algorithm::Romanian,
299 Language::Russian => Algorithm::Russian,
300 Language::Spanish => Algorithm::Spanish,
301 Language::Swedish => Algorithm::Swedish,
302 Language::Tamil => Algorithm::Tamil,
303 Language::Turkish => Algorithm::Turkish,
304 }
305 }
306
307 pub(super) fn to_stop_words_language(self) -> LANGUAGE {
308 match self {
309 Language::Arabic => LANGUAGE::Arabic,
310 Language::Danish => LANGUAGE::Danish,
311 Language::Dutch => LANGUAGE::Dutch,
312 Language::English => LANGUAGE::English,
313 Language::Finnish => LANGUAGE::Finnish,
314 Language::French => LANGUAGE::French,
315 Language::German => LANGUAGE::German,
316 Language::Greek => LANGUAGE::Greek,
317 Language::Hungarian => LANGUAGE::Hungarian,
318 Language::Italian => LANGUAGE::Italian,
319 Language::Norwegian => LANGUAGE::Norwegian,
320 Language::Portuguese => LANGUAGE::Portuguese,
321 Language::Romanian => LANGUAGE::Romanian,
322 Language::Russian => LANGUAGE::Russian,
323 Language::Spanish => LANGUAGE::Spanish,
324 Language::Swedish => LANGUAGE::Swedish,
325 Language::Tamil => LANGUAGE::Tamil,
326 Language::Turkish => LANGUAGE::Turkish,
327 }
328 }
329}
330
331#[derive(Debug, Clone)]
335pub struct StopWordTokenizer<T: Tokenizer> {
336 inner: T,
337 stop_words: HashSet<String>,
338}
339
340use std::collections::HashSet;
341
342impl<T: Tokenizer> StopWordTokenizer<T> {
343 pub fn new(inner: T, language: Language) -> Self {
345 let stop_words: HashSet<String> = stop_words::get(language.to_stop_words_language())
346 .iter()
347 .map(|s| s.to_string())
348 .collect();
349 Self { inner, stop_words }
350 }
351
352 pub fn english(inner: T) -> Self {
354 Self::new(inner, Language::English)
355 }
356
357 pub fn with_custom_stop_words(inner: T, stop_words: HashSet<String>) -> Self {
359 Self { inner, stop_words }
360 }
361
362 pub fn is_stop_word(&self, word: &str) -> bool {
364 self.stop_words.contains(word)
365 }
366}
367
368impl<T: Tokenizer> Tokenizer for StopWordTokenizer<T> {
369 fn tokenize(&self, text: &str) -> Vec<Token> {
370 self.inner
371 .tokenize(text)
372 .into_iter()
373 .filter(|token| !self.stop_words.contains(token.text.as_str()))
374 .collect()
375 }
376}
377
378#[derive(Debug, Clone)]
383pub struct StemmerTokenizer {
384 language: Language,
385}
386
387impl StemmerTokenizer {
388 pub fn new(language: Language) -> Self {
390 Self { language }
391 }
392
393 pub fn english() -> Self {
395 Self::new(Language::English)
396 }
397}
398
399impl Default for StemmerTokenizer {
400 fn default() -> Self {
401 Self::english()
402 }
403}
404
405impl Tokenizer for StemmerTokenizer {
406 fn tokenize(&self, text: &str) -> Vec<Token> {
407 let stemmer = rust_stemmers::Stemmer::create(self.language.to_algorithm());
408 tokenize_and_clean(text, |s| stemmer.stem(&s).into_owned())
409 }
410}
411
412#[derive(Debug, Clone)]
417pub struct MultiLanguageStemmer {
418 default_language: Language,
419}
420
421impl MultiLanguageStemmer {
422 pub fn new(default_language: Language) -> Self {
424 Self { default_language }
425 }
426
427 pub fn tokenize_with_language(&self, text: &str, language: Language) -> Vec<Token> {
429 let stemmer = rust_stemmers::Stemmer::create(language.to_algorithm());
430 tokenize_and_clean(text, |s| stemmer.stem(&s).into_owned())
431 }
432
433 pub fn default_language(&self) -> Language {
435 self.default_language
436 }
437}
438
439impl Default for MultiLanguageStemmer {
440 fn default() -> Self {
441 Self::new(Language::English)
442 }
443}
444
445impl Tokenizer for MultiLanguageStemmer {
446 fn tokenize(&self, text: &str) -> Vec<Token> {
447 self.tokenize_with_language(text, self.default_language)
448 }
449}
450
451#[derive(Clone)]
456pub struct LanguageAwareTokenizer<F>
457where
458 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
459{
460 language_selector: F,
461 stemmer: MultiLanguageStemmer,
462}
463
464impl<F> LanguageAwareTokenizer<F>
465where
466 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
467{
468 pub fn new(language_selector: F) -> Self {
485 Self {
486 language_selector,
487 stemmer: MultiLanguageStemmer::default(),
488 }
489 }
490
491 pub fn tokenize_with_hint(&self, text: &str, language_hint: &str) -> Vec<Token> {
495 let language = (self.language_selector)(language_hint);
496 self.stemmer.tokenize_with_language(text, language)
497 }
498}
499
500impl<F> Tokenizer for LanguageAwareTokenizer<F>
501where
502 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
503{
504 fn tokenize(&self, text: &str) -> Vec<Token> {
505 self.stemmer.tokenize_with_language(text, Language::English)
507 }
508
509 fn tokenize_with(&self, text: &str, hint: Option<&str>, _purpose: Purpose) -> Vec<Token> {
510 match hint {
511 Some(hint) => self.tokenize_with_hint(text, hint),
512 None => Tokenizer::tokenize(self, text),
513 }
514 }
515}
516
517pub fn parse_language(s: &str) -> Language {
521 match s.to_lowercase().as_str() {
522 "ar" | "arabic" => Language::Arabic,
523 "da" | "danish" => Language::Danish,
524 "nl" | "dutch" => Language::Dutch,
525 "en" | "english" => Language::English,
526 "fi" | "finnish" => Language::Finnish,
527 "fr" | "french" => Language::French,
528 "de" | "german" => Language::German,
529 "el" | "greek" => Language::Greek,
530 "hu" | "hungarian" => Language::Hungarian,
531 "it" | "italian" => Language::Italian,
532 "no" | "norwegian" => Language::Norwegian,
533 "pt" | "portuguese" => Language::Portuguese,
534 "ro" | "romanian" => Language::Romanian,
535 "ru" | "russian" => Language::Russian,
536 "es" | "spanish" => Language::Spanish,
537 "sv" | "swedish" => Language::Swedish,
538 "ta" | "tamil" => Language::Tamil,
539 "tr" | "turkish" => Language::Turkish,
540 _ => Language::English, }
542}
543
544pub fn parse_language_opt(s: &str) -> Option<Language> {
549 Some(match s.trim().to_lowercase().as_str() {
550 "ar" | "arabic" => Language::Arabic,
551 "da" | "danish" => Language::Danish,
552 "nl" | "dutch" => Language::Dutch,
553 "en" | "english" => Language::English,
554 "fi" | "finnish" => Language::Finnish,
555 "fr" | "french" => Language::French,
556 "de" | "german" => Language::German,
557 "el" | "greek" => Language::Greek,
558 "hu" | "hungarian" => Language::Hungarian,
559 "it" | "italian" => Language::Italian,
560 "no" | "norwegian" => Language::Norwegian,
561 "pt" | "portuguese" => Language::Portuguese,
562 "ro" | "romanian" => Language::Romanian,
563 "ru" | "russian" => Language::Russian,
564 "es" | "spanish" => Language::Spanish,
565 "sv" | "swedish" => Language::Swedish,
566 "ta" | "tamil" => Language::Tamil,
567 "tr" | "turkish" => Language::Turkish,
568 _ => return None,
569 })
570}
571
572#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
574pub enum Script {
575 Latin,
576 Cyrillic,
577 Greek,
578 Arabic,
579 Tamil,
580 Other,
582}
583
584impl Script {
585 pub fn of_token(token: &str) -> Script {
587 let Some(c) = token.chars().find(|c| c.is_alphabetic()) else {
588 return Script::Other;
589 };
590 Script::of_char(c)
591 }
592
593 fn of_char(c: char) -> Script {
594 match c as u32 {
595 0x0041..=0x024F | 0x1E00..=0x1EFF => Script::Latin,
597 0x0370..=0x03FF | 0x1F00..=0x1FFF => Script::Greek,
598 0x0400..=0x052F => Script::Cyrillic,
599 0x0600..=0x06FF | 0x0750..=0x077F | 0x08A0..=0x08FF => Script::Arabic,
600 0x0B80..=0x0BFF => Script::Tamil,
601 _ => Script::Other,
602 }
603 }
604}
605
606impl Language {
607 pub fn script(self) -> Script {
609 match self {
610 Language::Russian => Script::Cyrillic,
611 Language::Greek => Script::Greek,
612 Language::Arabic => Script::Arabic,
613 Language::Tamil => Script::Tamil,
614 _ => Script::Latin,
615 }
616 }
617}
618
619thread_local! {
620 static STEMMER_CACHE: std::cell::RefCell<HashMap<Language, rust_stemmers::Stemmer>> =
621 std::cell::RefCell::new(HashMap::new());
622}
623
624pub(super) fn with_stemmers<R>(
629 languages: &[Language],
630 f: impl FnOnce(&[&rust_stemmers::Stemmer]) -> R,
631) -> R {
632 STEMMER_CACHE.with(|cache| {
633 let mut cache = cache.borrow_mut();
634 for language in languages {
635 cache
636 .entry(*language)
637 .or_insert_with(|| rust_stemmers::Stemmer::create(language.to_algorithm()));
638 }
639 let stemmers: Vec<&rust_stemmers::Stemmer> =
640 languages.iter().map(|language| &cache[language]).collect();
641 f(&stemmers)
642 })
643}
644
645pub fn language_code(language: Language) -> &'static str {
647 match language {
648 Language::Arabic => "ar",
649 Language::Danish => "da",
650 Language::Dutch => "nl",
651 Language::English => "en",
652 Language::Finnish => "fi",
653 Language::French => "fr",
654 Language::German => "de",
655 Language::Greek => "el",
656 Language::Hungarian => "hu",
657 Language::Italian => "it",
658 Language::Norwegian => "no",
659 Language::Portuguese => "pt",
660 Language::Romanian => "ro",
661 Language::Russian => "ru",
662 Language::Spanish => "es",
663 Language::Swedish => "sv",
664 Language::Tamil => "ta",
665 Language::Turkish => "tr",
666 }
667}
668
669pub type BoxedTokenizer = Box<dyn TokenizerClone>;
671
672pub trait TokenizerClone: Send + Sync {
673 fn tokenize(&self, text: &str) -> Vec<Token>;
674 fn tokenize_with(&self, text: &str, hint: Option<&str>, purpose: Purpose) -> Vec<Token>;
676 fn clone_box(&self) -> BoxedTokenizer;
677}
678
679impl<T: Tokenizer> TokenizerClone for T {
680 fn tokenize(&self, text: &str) -> Vec<Token> {
681 Tokenizer::tokenize(self, text)
682 }
683
684 fn tokenize_with(&self, text: &str, hint: Option<&str>, purpose: Purpose) -> Vec<Token> {
685 Tokenizer::tokenize_with(self, text, hint, purpose)
686 }
687
688 fn clone_box(&self) -> BoxedTokenizer {
689 Box::new(self.clone())
690 }
691}
692
693impl Clone for BoxedTokenizer {
694 fn clone(&self) -> Self {
695 self.clone_box()
696 }
697}
698
699#[derive(Clone)]
704pub struct TokenizerRegistry {
705 tokenizers: Arc<RwLock<HashMap<String, BoxedTokenizer>>>,
706 dynamic: Arc<RwLock<HashMap<String, BoxedTokenizer>>>,
710}
711
712impl TokenizerRegistry {
713 pub fn new() -> Self {
715 let registry = Self {
716 tokenizers: Arc::new(RwLock::new(HashMap::new())),
717 dynamic: Arc::new(RwLock::new(HashMap::new())),
718 };
719 registry.register_defaults();
720 registry
721 }
722
723 fn register_defaults(&self) {
725 self.register("simple", SimpleTokenizer);
727 self.register("default", SimpleTokenizer);
728 self.register("raw", RawTokenizer);
729 self.register("raw_ci", RawCiTokenizer);
730
731 self.register("en_stem", StemmerTokenizer::new(Language::English));
733 self.register("english", StemmerTokenizer::new(Language::English));
734
735 self.register("ar_stem", StemmerTokenizer::new(Language::Arabic));
737 self.register("arabic", StemmerTokenizer::new(Language::Arabic));
738 self.register("da_stem", StemmerTokenizer::new(Language::Danish));
739 self.register("danish", StemmerTokenizer::new(Language::Danish));
740 self.register("nl_stem", StemmerTokenizer::new(Language::Dutch));
741 self.register("dutch", StemmerTokenizer::new(Language::Dutch));
742 self.register("fi_stem", StemmerTokenizer::new(Language::Finnish));
743 self.register("finnish", StemmerTokenizer::new(Language::Finnish));
744 self.register("fr_stem", StemmerTokenizer::new(Language::French));
745 self.register("french", StemmerTokenizer::new(Language::French));
746 self.register("de_stem", StemmerTokenizer::new(Language::German));
747 self.register("german", StemmerTokenizer::new(Language::German));
748 self.register("el_stem", StemmerTokenizer::new(Language::Greek));
749 self.register("greek", StemmerTokenizer::new(Language::Greek));
750 self.register("hu_stem", StemmerTokenizer::new(Language::Hungarian));
751 self.register("hungarian", StemmerTokenizer::new(Language::Hungarian));
752 self.register("it_stem", StemmerTokenizer::new(Language::Italian));
753 self.register("italian", StemmerTokenizer::new(Language::Italian));
754 self.register("no_stem", StemmerTokenizer::new(Language::Norwegian));
755 self.register("norwegian", StemmerTokenizer::new(Language::Norwegian));
756 self.register("pt_stem", StemmerTokenizer::new(Language::Portuguese));
757 self.register("portuguese", StemmerTokenizer::new(Language::Portuguese));
758 self.register("ro_stem", StemmerTokenizer::new(Language::Romanian));
759 self.register("romanian", StemmerTokenizer::new(Language::Romanian));
760 self.register("ru_stem", StemmerTokenizer::new(Language::Russian));
761 self.register("russian", StemmerTokenizer::new(Language::Russian));
762 self.register("es_stem", StemmerTokenizer::new(Language::Spanish));
763 self.register("spanish", StemmerTokenizer::new(Language::Spanish));
764 self.register("sv_stem", StemmerTokenizer::new(Language::Swedish));
765 self.register("swedish", StemmerTokenizer::new(Language::Swedish));
766 self.register("ta_stem", StemmerTokenizer::new(Language::Tamil));
767 self.register("tamil", StemmerTokenizer::new(Language::Tamil));
768 self.register("tr_stem", StemmerTokenizer::new(Language::Turkish));
769 self.register("turkish", StemmerTokenizer::new(Language::Turkish));
770
771 self.register(
773 "en_stop",
774 StopWordTokenizer::new(SimpleTokenizer, Language::English),
775 );
776 self.register(
777 "de_stop",
778 StopWordTokenizer::new(SimpleTokenizer, Language::German),
779 );
780 self.register(
781 "fr_stop",
782 StopWordTokenizer::new(SimpleTokenizer, Language::French),
783 );
784 self.register(
785 "ru_stop",
786 StopWordTokenizer::new(SimpleTokenizer, Language::Russian),
787 );
788 self.register(
789 "es_stop",
790 StopWordTokenizer::new(SimpleTokenizer, Language::Spanish),
791 );
792
793 self.register(
795 "en_stem_stop",
796 StopWordTokenizer::new(StemmerTokenizer::new(Language::English), Language::English),
797 );
798 self.register(
799 "de_stem_stop",
800 StopWordTokenizer::new(StemmerTokenizer::new(Language::German), Language::German),
801 );
802 self.register(
803 "fr_stem_stop",
804 StopWordTokenizer::new(StemmerTokenizer::new(Language::French), Language::French),
805 );
806 self.register(
807 "ru_stem_stop",
808 StopWordTokenizer::new(StemmerTokenizer::new(Language::Russian), Language::Russian),
809 );
810 self.register(
811 "es_stem_stop",
812 StopWordTokenizer::new(StemmerTokenizer::new(Language::Spanish), Language::Spanish),
813 );
814 }
815
816 pub fn register<T: Tokenizer>(&self, name: &str, tokenizer: T) {
818 let mut tokenizers = self.tokenizers.write();
819 tokenizers.insert(name.to_string(), Box::new(tokenizer));
820 }
821
822 pub fn get(&self, name: &str) -> Option<BoxedTokenizer> {
827 if name.starts_with("lex(") {
828 if let Some(tokenizer) = self.dynamic.read().get(name) {
829 return Some(tokenizer.clone());
830 }
831 let tokenizer = TokenizerSpec::parse(name)
832 .ok()
833 .and_then(|spec| spec.dynamic_tokenizer())?;
834 self.dynamic
835 .write()
836 .entry(name.to_string())
837 .or_insert_with(|| tokenizer.clone());
838 return Some(tokenizer);
839 }
840 let tokenizers = self.tokenizers.read();
841 tokenizers.get(name).cloned()
842 }
843
844 pub fn contains(&self, name: &str) -> bool {
846 let tokenizers = self.tokenizers.read();
847 tokenizers.contains_key(name)
848 }
849
850 pub fn names(&self) -> Vec<String> {
852 let tokenizers = self.tokenizers.read();
853 tokenizers.keys().cloned().collect()
854 }
855}
856
857impl Default for TokenizerRegistry {
858 fn default() -> Self {
859 Self::new()
860 }
861}
862
863#[cfg(test)]
864mod tests {
865 use super::*;
866
867 #[test]
868 fn test_simple_tokenizer() {
869 let tokenizer = SimpleTokenizer;
870 let tokens = Tokenizer::tokenize(&tokenizer, "Hello World");
871
872 assert_eq!(tokens.len(), 2);
873 assert_eq!(tokens[0].text, "hello");
874 assert_eq!(tokens[0].position, 0);
875 assert_eq!(tokens[1].text, "world");
876 assert_eq!(tokens[1].position, 1);
877 }
878
879 #[test]
880 fn test_raw_tokenizer() {
881 let tokenizer = RawTokenizer;
882 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
884 assert_eq!(tokens.len(), 1);
885 assert_eq!(tokens[0].text, "Hello, World!");
886 assert_eq!(tokens[0].position, 0);
887 }
888
889 #[test]
890 fn test_raw_tokenizer_trims() {
891 let tokenizer = RawTokenizer;
892 let tokens = Tokenizer::tokenize(&tokenizer, " spaced ");
893 assert_eq!(tokens.len(), 1);
894 assert_eq!(tokens[0].text, "spaced");
895 assert_eq!(tokens[0].offset_from, 2);
896 }
897
898 #[test]
899 fn test_raw_tokenizer_empty() {
900 let tokenizer = RawTokenizer;
901 assert!(Tokenizer::tokenize(&tokenizer, "").is_empty());
902 assert!(Tokenizer::tokenize(&tokenizer, " ").is_empty());
903 }
904
905 #[test]
906 fn test_raw_ci_tokenizer() {
907 let tokenizer = RawCiTokenizer;
908 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
910 assert_eq!(tokens.len(), 1);
911 assert_eq!(tokens[0].text, "hello, world!");
912 assert_eq!(tokens[0].position, 0);
913 }
914
915 #[test]
916 fn test_raw_ci_tokenizer_preserves_structure() {
917 let tokenizer = RawCiTokenizer;
918 let tokens = Tokenizer::tokenize(&tokenizer, "HTTPS://Example.COM/Page");
919 assert_eq!(tokens.len(), 1);
920 assert_eq!(tokens[0].text, "https://example.com/page");
921 }
922
923 #[test]
924 fn test_simple_tokenizer_strips_punctuation() {
925 let tokenizer = SimpleTokenizer;
926 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
927
928 assert_eq!(tokens.len(), 2);
929 assert_eq!(tokens[0].text, "hello");
930 assert_eq!(tokens[1].text, "world");
931 }
932
933 #[test]
934 fn test_empty_text() {
935 let tokenizer = SimpleTokenizer;
936 let tokens = Tokenizer::tokenize(&tokenizer, "");
937 assert!(tokens.is_empty());
938 }
939
940 #[test]
941 fn test_stemmer_tokenizer_english() {
942 let tokenizer = StemmerTokenizer::english();
943 let tokens = Tokenizer::tokenize(&tokenizer, "Dogs are running quickly");
944
945 assert_eq!(tokens.len(), 4);
946 assert_eq!(tokens[0].text, "dog"); assert_eq!(tokens[1].text, "are"); assert_eq!(tokens[2].text, "run"); assert_eq!(tokens[3].text, "quick"); }
951
952 #[test]
953 fn test_stemmer_tokenizer_preserves_offsets() {
954 let tokenizer = StemmerTokenizer::english();
955 let tokens = Tokenizer::tokenize(&tokenizer, "Running dogs");
956
957 assert_eq!(tokens.len(), 2);
958 assert_eq!(tokens[0].text, "run");
959 assert_eq!(tokens[0].offset_from, 0);
960 assert_eq!(tokens[0].offset_to, 7); assert_eq!(tokens[1].text, "dog");
962 assert_eq!(tokens[1].offset_from, 8);
963 assert_eq!(tokens[1].offset_to, 12); }
965
966 #[test]
967 fn test_stemmer_tokenizer_german() {
968 let tokenizer = StemmerTokenizer::new(Language::German);
969 let tokens = Tokenizer::tokenize(&tokenizer, "Häuser Bücher");
970
971 assert_eq!(tokens.len(), 2);
972 assert_eq!(tokens[0].text, "haus"); assert_eq!(tokens[1].text, "buch"); }
976
977 #[test]
978 fn test_stemmer_tokenizer_russian() {
979 let tokenizer = StemmerTokenizer::new(Language::Russian);
980 let tokens = Tokenizer::tokenize(&tokenizer, "бегущие собаки");
981
982 assert_eq!(tokens.len(), 2);
983 assert_eq!(tokens[0].text, "бегущ"); assert_eq!(tokens[1].text, "собак"); }
987
988 #[test]
989 fn test_multi_language_stemmer() {
990 let stemmer = MultiLanguageStemmer::new(Language::English);
991
992 let tokens = stemmer.tokenize_with_language("running dogs", Language::English);
994 assert_eq!(tokens[0].text, "run");
995 assert_eq!(tokens[1].text, "dog");
996
997 let tokens = stemmer.tokenize_with_language("Häuser Bücher", Language::German);
999 assert_eq!(tokens[0].text, "haus");
1000 assert_eq!(tokens[1].text, "buch");
1001
1002 let tokens = stemmer.tokenize_with_language("бегущие собаки", Language::Russian);
1004 assert_eq!(tokens[0].text, "бегущ");
1005 assert_eq!(tokens[1].text, "собак");
1006 }
1007
1008 #[test]
1009 fn test_language_aware_tokenizer() {
1010 let tokenizer = LanguageAwareTokenizer::new(parse_language);
1011
1012 let tokens = tokenizer.tokenize_with_hint("running dogs", "en");
1014 assert_eq!(tokens[0].text, "run");
1015 assert_eq!(tokens[1].text, "dog");
1016
1017 let tokens = tokenizer.tokenize_with_hint("Häuser Bücher", "de");
1019 assert_eq!(tokens[0].text, "haus");
1020 assert_eq!(tokens[1].text, "buch");
1021
1022 let tokens = tokenizer.tokenize_with_hint("бегущие собаки", "russian");
1024 assert_eq!(tokens[0].text, "бегущ");
1025 assert_eq!(tokens[1].text, "собак");
1026 }
1027
1028 #[test]
1029 fn test_parse_language() {
1030 assert_eq!(parse_language("en"), Language::English);
1031 assert_eq!(parse_language("english"), Language::English);
1032 assert_eq!(parse_language("English"), Language::English);
1033 assert_eq!(parse_language("de"), Language::German);
1034 assert_eq!(parse_language("german"), Language::German);
1035 assert_eq!(parse_language("ru"), Language::Russian);
1036 assert_eq!(parse_language("russian"), Language::Russian);
1037 assert_eq!(parse_language("unknown"), Language::English); }
1039
1040 #[test]
1041 fn test_tokenizer_registry_defaults() {
1042 let registry = TokenizerRegistry::new();
1043
1044 assert!(registry.contains("simple"));
1046 assert!(registry.contains("raw"));
1047 assert!(registry.contains("raw_ci"));
1048 assert!(registry.contains("raw"));
1049 assert!(registry.contains("raw_ci"));
1050 assert!(registry.contains("en_stem"));
1051 assert!(registry.contains("german"));
1052 assert!(registry.contains("russian"));
1053 }
1054
1055 #[test]
1056 fn test_tokenizer_registry_get() {
1057 let registry = TokenizerRegistry::new();
1058
1059 let tokenizer = registry.get("en_stem").unwrap();
1061 let tokens = tokenizer.tokenize("running dogs");
1062 assert_eq!(tokens[0].text, "run");
1063 assert_eq!(tokens[1].text, "dog");
1064
1065 let tokenizer = registry.get("german").unwrap();
1067 let tokens = tokenizer.tokenize("Häuser Bücher");
1068 assert_eq!(tokens[0].text, "haus");
1069 assert_eq!(tokens[1].text, "buch");
1070 }
1071
1072 #[test]
1073 fn test_tokenizer_registry_custom() {
1074 let registry = TokenizerRegistry::new();
1075
1076 registry.register("my_tokenizer", SimpleTokenizer);
1078
1079 assert!(registry.contains("my_tokenizer"));
1080 let tokenizer = registry.get("my_tokenizer").unwrap();
1081 let tokens = tokenizer.tokenize("Hello World");
1082 assert_eq!(tokens[0].text, "hello");
1083 assert_eq!(tokens[1].text, "world");
1084 }
1085
1086 #[test]
1087 fn test_tokenizer_registry_nonexistent() {
1088 let registry = TokenizerRegistry::new();
1089 assert!(registry.get("nonexistent").is_none());
1090 }
1091
1092 #[test]
1093 fn test_stop_word_tokenizer_english() {
1094 let tokenizer = StopWordTokenizer::english(SimpleTokenizer);
1095 let tokens = Tokenizer::tokenize(&tokenizer, "The quick brown fox jumps over the lazy dog");
1096
1097 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1099 assert!(!texts.contains(&"the"));
1100 assert!(!texts.contains(&"over"));
1101 assert!(texts.contains(&"quick"));
1102 assert!(texts.contains(&"brown"));
1103 assert!(texts.contains(&"fox"));
1104 assert!(texts.contains(&"jumps"));
1105 assert!(texts.contains(&"lazy"));
1106 assert!(texts.contains(&"dog"));
1107 }
1108
1109 #[test]
1110 fn test_stop_word_tokenizer_with_stemmer() {
1111 let tokenizer = StopWordTokenizer::new(StemmerTokenizer::english(), Language::English);
1115 let tokens = Tokenizer::tokenize(&tokenizer, "elephants galaxies quantum");
1116
1117 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1118 assert!(texts.contains(&"eleph")); assert!(texts.contains(&"galaxi")); assert!(texts.contains(&"quantum")); }
1123
1124 #[test]
1125 fn test_stop_word_tokenizer_german() {
1126 let tokenizer = StopWordTokenizer::new(SimpleTokenizer, Language::German);
1127 let tokens = Tokenizer::tokenize(&tokenizer, "Der Hund und die Katze");
1128
1129 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1131 assert!(!texts.contains(&"der"));
1132 assert!(!texts.contains(&"und"));
1133 assert!(!texts.contains(&"die"));
1134 assert!(texts.contains(&"hund"));
1135 assert!(texts.contains(&"katze"));
1136 }
1137
1138 #[test]
1139 fn test_stop_word_tokenizer_custom() {
1140 let custom_stops: HashSet<String> = ["foo", "bar"].iter().map(|s| s.to_string()).collect();
1141 let tokenizer = StopWordTokenizer::with_custom_stop_words(SimpleTokenizer, custom_stops);
1142 let tokens = Tokenizer::tokenize(&tokenizer, "foo baz bar qux");
1143
1144 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1145 assert!(!texts.contains(&"foo"));
1146 assert!(!texts.contains(&"bar"));
1147 assert!(texts.contains(&"baz"));
1148 assert!(texts.contains(&"qux"));
1149 }
1150
1151 #[test]
1152 fn test_stop_word_tokenizer_is_stop_word() {
1153 let tokenizer = StopWordTokenizer::english(SimpleTokenizer);
1154 assert!(tokenizer.is_stop_word("the"));
1155 assert!(tokenizer.is_stop_word("and"));
1156 assert!(tokenizer.is_stop_word("is"));
1157 assert!(!tokenizer.is_stop_word("elephant"));
1159 assert!(!tokenizer.is_stop_word("quantum"));
1160 }
1161
1162 #[test]
1163 fn test_tokenizer_registry_stop_word_tokenizers() {
1164 let registry = TokenizerRegistry::new();
1165
1166 assert!(registry.contains("en_stop"));
1168 assert!(registry.contains("en_stem_stop"));
1169 assert!(registry.contains("de_stop"));
1170 assert!(registry.contains("ru_stop"));
1171
1172 let tokenizer = registry.get("en_stop").unwrap();
1174 let tokens = tokenizer.tokenize("The quick fox");
1175 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1176 assert!(!texts.contains(&"the"));
1177 assert!(texts.contains(&"quick"));
1178 assert!(texts.contains(&"fox"));
1179
1180 let tokenizer = registry.get("en_stem_stop").unwrap();
1182 let tokens = tokenizer.tokenize("elephants galaxies");
1183 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1184 assert!(texts.contains(&"eleph")); assert!(texts.contains(&"galaxi")); }
1187
1188 fn hinted<T: Tokenizer>(tokenizer: &T, text: &str, hint: Option<&str>) -> Vec<Token> {
1189 Tokenizer::tokenize_with(tokenizer, text, hint, Purpose::Index)
1190 }
1191
1192 fn lex(spec: &str) -> LexTokenizer {
1193 LexTokenizer::new(LexOptions::parse(spec).unwrap())
1194 }
1195
1196 fn texts(tokens: &[Token]) -> Vec<&str> {
1197 tokens.iter().map(|t| t.text.as_str()).collect()
1198 }
1199
1200 fn positions(tokens: &[Token]) -> Vec<u32> {
1201 tokens.iter().map(|t| t.position).collect()
1202 }
1203
1204 #[test]
1205 fn unicode_segmenter_splits_on_word_boundaries_and_folds() {
1206 let plain = lex("by: languages, segmenter: unicode, stem: snowball, variants: false");
1207 let tokens = hinted(
1208 &plain,
1209 "Float-zero determinants: p53/CO2, 10.1007/s1 résumé",
1210 None,
1211 );
1212 assert_eq!(
1213 texts(&tokens),
1214 vec![
1215 "float",
1216 "zero",
1217 "determinants",
1218 "p53",
1219 "co2",
1220 "10.1007",
1221 "s1",
1222 "resume"
1223 ]
1224 );
1225 assert_eq!(positions(&tokens), (0..8).collect::<Vec<u32>>());
1226 assert_eq!(
1228 &"Float-zero determinants: p53/CO2, 10.1007/s1 résumé"
1229 [tokens[7].offset_from..tokens[7].offset_to],
1230 "résumé"
1231 );
1232
1233 let english = lex("by: languages, segmenter: unicode, stem: snowball, variants: false");
1236 assert_eq!(
1237 texts(&hinted(&english, "Running foxes' café", Some("en"))),
1238 vec!["run", "fox", "cafe"]
1239 );
1240 assert_eq!(
1241 texts(&hinted(
1242 &lex("by: languages, segmenter: simple, stem: snowball, variants: false"),
1243 "Float-zero café",
1244 Some("en")
1245 )),
1246 vec!["float", "zero", "cafe"]
1247 );
1248 assert_eq!(texts(&hinted(&plain, "ёлка", None)), vec!["елка"]);
1250 let stopping = lex(
1252 "by: languages, stop_words: true, segmenter: unicode, stem: snowball, variants: false",
1253 );
1254 let tokens = hinted(&stopping, "state-of-the-art résumé", Some("en"));
1255 assert_eq!(tokens.len(), 3, "{:?}", texts(&tokens));
1256 assert_eq!(&texts(&tokens)[..2], ["state", "art"]);
1257 assert!(tokens[2].text.starts_with("resum"), "{:?}", tokens[2].text);
1258 assert!(tokens[2].text.is_ascii(), "folded after stemming");
1259 assert_eq!(positions(&tokens), vec![0, 3, 4]);
1260 }
1261
1262 #[test]
1263 fn unicode_segmenter_bigrams_cjk_runs() {
1264 let t = lex("by: languages, segmenter: unicode, stem: snowball, variants: false");
1265 let tokens = hinted(&t, "東京都 tower", Some("en"));
1266 assert_eq!(texts(&tokens), vec!["東京", "京都", "tower"]);
1267 assert_eq!(positions(&tokens), vec![0, 1, 2]);
1268 assert_eq!(
1269 &"東京都 tower"[tokens[1].offset_from..tokens[1].offset_to],
1270 "京都"
1271 );
1272 assert_eq!(
1274 texts(&hinted(&t, "東 tower 京都", None)),
1275 vec!["東", "tower", "京都"]
1276 );
1277 assert_eq!(
1280 texts(&hinted(&t, "トウキョウ タワー", None)),
1281 vec!["トウ", "ウキ", "キョ", "ョウ", "タワ", "ワー"]
1282 );
1283 assert_eq!(texts(&hinted(&t, "東京tower", None)), vec!["東京", "tower"]);
1286 let query = hinted(&t, "東京都", None);
1288 assert_eq!(positions(&query), vec![0, 1]);
1289 }
1290
1291 #[test]
1292 fn dynamic_stemmer_drops_stop_words_but_keeps_positions() {
1293 let stemmer = lex(
1294 "by: languages, stop_words: true, segmenter: simple, stem: snowball, variants: false",
1295 );
1296 let tokens = hinted(&stemmer, "Quantum of the Art", Some("en"));
1297 assert_eq!(texts(&tokens), vec!["quantum", "art"]);
1298 assert_eq!(positions(&tokens), vec![0, 3]);
1299 assert_eq!(tokens[1].offset_from, "Quantum of the ".len());
1301
1302 let tokens = hinted(&stemmer, "бегущие и собаки the foxes", Some("ru,en"));
1304 assert_eq!(texts(&tokens), vec!["бегущ", "собак", "fox"]);
1305 assert_eq!(positions(&tokens), vec![0, 2, 4]);
1306
1307 assert_eq!(
1309 texts(&hinted(&stemmer, "the 日本語 fox", Some("en"))),
1310 vec!["日本語", "fox"]
1311 );
1312
1313 assert_eq!(
1315 texts(&hinted(&stemmer, "the fox", None)),
1316 vec!["the", "fox"]
1317 );
1318 let english = lex(
1320 "by: languages, default: en, stop_words: true, segmenter: simple, stem: snowball, variants: false",
1321 );
1322 assert_eq!(texts(&hinted(&english, "the fox", None)), vec!["fox"]);
1323 assert_eq!(
1325 texts(&hinted(
1326 &lex("by: languages, segmenter: simple, stem: snowball, variants: false"),
1327 "the fox",
1328 Some("en")
1329 )),
1330 vec!["the", "fox"]
1331 );
1332 assert!(hinted(&stemmer, "to be or not to be", Some("en")).is_empty());
1334 }
1335
1336 #[test]
1337 fn dynamic_stemmer_selects_language_from_hint() {
1338 let stemmer = lex("by: languages, segmenter: simple, stem: snowball, variants: false");
1339 assert_eq!(
1340 texts(&hinted(&stemmer, "Running Foxes", Some("en"))),
1341 vec!["run", "fox"]
1342 );
1343 assert_eq!(
1344 texts(&hinted(&stemmer, "бегущие собаки", Some("ru"))),
1345 vec!["бегущ", "собак"]
1346 );
1347 assert_eq!(
1349 texts(&hinted(&stemmer, "Running Foxes", Some("xx"))),
1350 vec!["running", "foxes"]
1351 );
1352 assert_eq!(
1353 texts(&hinted(&stemmer, "Running Foxes", None)),
1354 vec!["running", "foxes"]
1355 );
1356 assert_eq!(
1357 texts(&Tokenizer::tokenize(&stemmer, "Running, Foxes!")),
1358 vec!["running", "foxes"]
1359 );
1360 let english =
1362 lex("by: languages, default: en, segmenter: simple, stem: snowball, variants: false");
1363 assert_eq!(
1364 texts(&hinted(&english, "Running Foxes", None)),
1365 vec!["run", "fox"]
1366 );
1367 }
1368
1369 #[test]
1370 fn dynamic_stemmer_routes_tokens_by_script() {
1371 let stemmer = lex("by: languages, segmenter: simple, stem: snowball, variants: false");
1372 assert_eq!(
1374 texts(&hinted(&stemmer, "бегущие foxes", Some("ru,en"))),
1375 vec!["бегущ", "fox"]
1376 );
1377 assert_eq!(
1378 texts(&hinted(&stemmer, "бегущие foxes", Some("en, ru"))),
1379 vec!["бегущ", "fox"]
1380 );
1381 assert_eq!(
1383 texts(&hinted(&stemmer, "бегущие foxes", Some("ru"))),
1384 vec!["бегущ", "foxes"]
1385 );
1386 assert_eq!(
1387 texts(&hinted(&stemmer, "бегущие foxes", Some("en"))),
1388 vec!["бегущие", "fox"]
1389 );
1390 assert_eq!(
1392 texts(&hinted(&stemmer, "running", Some("de,en"))),
1393 vec!["running"]
1394 );
1395 assert_eq!(
1396 texts(&hinted(&stemmer, "running", Some("en,de"))),
1397 vec!["run"]
1398 );
1399 let tokens = hinted(&stemmer, "бегущие foxes run", Some("ru,en"));
1401 assert_eq!(
1402 tokens.iter().map(|t| t.position).collect::<Vec<_>>(),
1403 vec![0, 1, 2]
1404 );
1405 }
1406
1407 #[test]
1408 fn script_detection_covers_supported_stemmer_scripts() {
1409 assert_eq!(Script::of_token("hello"), Script::Latin);
1410 assert_eq!(Script::of_token("straße"), Script::Latin);
1411 assert_eq!(Script::of_token("собака"), Script::Cyrillic);
1412 assert_eq!(Script::of_token("γεια"), Script::Greek);
1413 assert_eq!(Script::of_token("مرحبا"), Script::Arabic);
1414 assert_eq!(Script::of_token("தமிழ்"), Script::Tamil);
1415 assert_eq!(Script::of_token("日本語"), Script::Other);
1416 assert_eq!(Script::of_token("2024"), Script::Other);
1417 assert_eq!(Language::Russian.script(), Script::Cyrillic);
1418 assert_eq!(Language::Turkish.script(), Script::Latin);
1419 }
1420
1421 #[test]
1422 fn registry_builds_lex_tokenizers_from_specs() {
1423 let registry = TokenizerRegistry::new();
1424 let tokenizer = registry
1425 .get("lex(by: languages, segmenter: simple, stem: snowball, variants: false)")
1426 .expect("lex spec resolves without registration");
1427 assert_eq!(
1428 texts(&tokenizer.tokenize_with("Running Foxes", Some("en"), Purpose::Index)),
1429 vec!["run", "fox"]
1430 );
1431 assert_eq!(
1432 texts(&tokenizer.tokenize("Running Foxes")),
1433 vec!["running", "foxes"]
1434 );
1435 assert!(
1436 registry
1437 .get("lex(by: languages, default: klingon)")
1438 .is_none()
1439 );
1440 let stopping = registry
1441 .get("lex(by: languages, stop_words: true, segmenter: simple, stem: snowball, variants: false)")
1442 .expect("stop-word spec resolves");
1443 let tokens = stopping.tokenize_with("the running foxes", Some("en"), Purpose::Index);
1444 assert_eq!(texts(&tokens), vec!["run", "fox"]);
1445 assert_eq!(positions(&tokens), vec![1, 2]);
1446 let simple = registry.get("en_stem").unwrap();
1448 assert_eq!(
1449 texts(&simple.tokenize_with("Running Foxes", Some("ru"), Purpose::Exact)),
1450 vec!["run", "fox"]
1451 );
1452 }
1453
1454 #[test]
1455 fn spec_without_by_is_a_fixed_tokenizer_that_ignores_hints() {
1456 let spec = TokenizerSpec::parse("lex(stop_words: true, stem: none)").unwrap();
1457 assert_eq!(spec.hint_field(), None);
1458 let tokenizer = spec.dynamic_tokenizer().unwrap();
1459 let plain_tokens = tokenizer.tokenize("The running cells");
1460 let plain = texts(&plain_tokens);
1461 assert_eq!(plain, vec!["the", "running", "cells"]);
1463 let hinted_tokens =
1464 tokenizer.tokenize_with("The running cells", Some("en"), Purpose::Index);
1465 assert_eq!(
1466 texts(&hinted_tokens),
1467 plain,
1468 "hints are ignored without `by`"
1469 );
1470
1471 let english = TokenizerSpec::parse(
1473 "lex(default: en, stop_words: true, stem: snowball, variants: false)",
1474 )
1475 .unwrap();
1476 let tokenizer = english.dynamic_tokenizer().unwrap();
1477 assert_eq!(
1478 texts(&tokenizer.tokenize_with("The running cells", Some("ru"), Purpose::Index)),
1479 vec!["run", "cell"]
1480 );
1481 }
1482
1483 #[test]
1484 fn parse_language_opt_rejects_unknown_codes() {
1485 assert_eq!(parse_language_opt(" RU "), Some(Language::Russian));
1486 assert_eq!(parse_language_opt("german"), Some(Language::German));
1487 assert_eq!(parse_language_opt("xx"), None);
1488 assert_eq!(parse_language_opt(""), None);
1489 for language in [Language::English, Language::Russian, Language::Tamil] {
1490 assert_eq!(parse_language_opt(language_code(language)), Some(language));
1491 }
1492 }
1493}