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 tokenize_and_clean_filtered(text, |word| Some(transform(word)))
181}
182
183fn tokenize_and_clean_filtered(
188 text: &str,
189 transform: impl Fn(String) -> Option<String>,
190) -> Vec<Token> {
191 let mut tokens = Vec::with_capacity(text.len() / 5);
192 let mut position = 0u32;
193 for (offset, word) in split_whitespace_with_offsets(text) {
194 if !word.is_empty() {
195 let cleaned = clean_word(word);
196 if !cleaned.is_empty() {
197 if let Some(text) = transform(cleaned) {
198 tokens.push(Token::new(text, position, offset, offset + word.len()));
199 }
200 position += 1;
201 }
202 }
203 }
204 tokens
205}
206
207fn split_whitespace_with_offsets(text: &str) -> impl Iterator<Item = (usize, &str)> {
212 let base = text.as_ptr() as usize;
213 text.split_whitespace()
214 .map(move |word| (word.as_ptr() as usize - base, word))
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
219#[allow(missing_docs)]
220#[derive(Default)]
221pub enum Language {
222 Arabic,
223 Danish,
224 Dutch,
225 #[default]
226 English,
227 Finnish,
228 French,
229 German,
230 Greek,
231 Hungarian,
232 Italian,
233 Norwegian,
234 Portuguese,
235 Romanian,
236 Russian,
237 Spanish,
238 Swedish,
239 Tamil,
240 Turkish,
241}
242
243impl Language {
244 fn to_algorithm(self) -> Algorithm {
245 match self {
246 Language::Arabic => Algorithm::Arabic,
247 Language::Danish => Algorithm::Danish,
248 Language::Dutch => Algorithm::Dutch,
249 Language::English => Algorithm::English,
250 Language::Finnish => Algorithm::Finnish,
251 Language::French => Algorithm::French,
252 Language::German => Algorithm::German,
253 Language::Greek => Algorithm::Greek,
254 Language::Hungarian => Algorithm::Hungarian,
255 Language::Italian => Algorithm::Italian,
256 Language::Norwegian => Algorithm::Norwegian,
257 Language::Portuguese => Algorithm::Portuguese,
258 Language::Romanian => Algorithm::Romanian,
259 Language::Russian => Algorithm::Russian,
260 Language::Spanish => Algorithm::Spanish,
261 Language::Swedish => Algorithm::Swedish,
262 Language::Tamil => Algorithm::Tamil,
263 Language::Turkish => Algorithm::Turkish,
264 }
265 }
266
267 fn to_stop_words_language(self) -> LANGUAGE {
268 match self {
269 Language::Arabic => LANGUAGE::Arabic,
270 Language::Danish => LANGUAGE::Danish,
271 Language::Dutch => LANGUAGE::Dutch,
272 Language::English => LANGUAGE::English,
273 Language::Finnish => LANGUAGE::Finnish,
274 Language::French => LANGUAGE::French,
275 Language::German => LANGUAGE::German,
276 Language::Greek => LANGUAGE::Greek,
277 Language::Hungarian => LANGUAGE::Hungarian,
278 Language::Italian => LANGUAGE::Italian,
279 Language::Norwegian => LANGUAGE::Norwegian,
280 Language::Portuguese => LANGUAGE::Portuguese,
281 Language::Romanian => LANGUAGE::Romanian,
282 Language::Russian => LANGUAGE::Russian,
283 Language::Spanish => LANGUAGE::Spanish,
284 Language::Swedish => LANGUAGE::Swedish,
285 Language::Tamil => LANGUAGE::Tamil,
286 Language::Turkish => LANGUAGE::Turkish,
287 }
288 }
289}
290
291#[derive(Debug, Clone)]
295pub struct StopWordTokenizer<T: Tokenizer> {
296 inner: T,
297 stop_words: HashSet<String>,
298}
299
300use std::collections::HashSet;
301
302impl<T: Tokenizer> StopWordTokenizer<T> {
303 pub fn new(inner: T, language: Language) -> Self {
305 let stop_words: HashSet<String> = stop_words::get(language.to_stop_words_language())
306 .iter()
307 .map(|s| s.to_string())
308 .collect();
309 Self { inner, stop_words }
310 }
311
312 pub fn english(inner: T) -> Self {
314 Self::new(inner, Language::English)
315 }
316
317 pub fn with_custom_stop_words(inner: T, stop_words: HashSet<String>) -> Self {
319 Self { inner, stop_words }
320 }
321
322 pub fn is_stop_word(&self, word: &str) -> bool {
324 self.stop_words.contains(word)
325 }
326}
327
328impl<T: Tokenizer> Tokenizer for StopWordTokenizer<T> {
329 fn tokenize(&self, text: &str) -> Vec<Token> {
330 self.inner
331 .tokenize(text)
332 .into_iter()
333 .filter(|token| !self.stop_words.contains(token.text.as_str()))
334 .collect()
335 }
336}
337
338#[derive(Debug, Clone)]
343pub struct StemmerTokenizer {
344 language: Language,
345}
346
347impl StemmerTokenizer {
348 pub fn new(language: Language) -> Self {
350 Self { language }
351 }
352
353 pub fn english() -> Self {
355 Self::new(Language::English)
356 }
357}
358
359impl Default for StemmerTokenizer {
360 fn default() -> Self {
361 Self::english()
362 }
363}
364
365impl Tokenizer for StemmerTokenizer {
366 fn tokenize(&self, text: &str) -> Vec<Token> {
367 let stemmer = rust_stemmers::Stemmer::create(self.language.to_algorithm());
368 tokenize_and_clean(text, |s| stemmer.stem(&s).into_owned())
369 }
370}
371
372#[derive(Debug, Clone)]
377pub struct MultiLanguageStemmer {
378 default_language: Language,
379}
380
381impl MultiLanguageStemmer {
382 pub fn new(default_language: Language) -> Self {
384 Self { default_language }
385 }
386
387 pub fn tokenize_with_language(&self, text: &str, language: Language) -> Vec<Token> {
389 let stemmer = rust_stemmers::Stemmer::create(language.to_algorithm());
390 tokenize_and_clean(text, |s| stemmer.stem(&s).into_owned())
391 }
392
393 pub fn default_language(&self) -> Language {
395 self.default_language
396 }
397}
398
399impl Default for MultiLanguageStemmer {
400 fn default() -> Self {
401 Self::new(Language::English)
402 }
403}
404
405impl Tokenizer for MultiLanguageStemmer {
406 fn tokenize(&self, text: &str) -> Vec<Token> {
407 self.tokenize_with_language(text, self.default_language)
408 }
409}
410
411#[derive(Clone)]
416pub struct LanguageAwareTokenizer<F>
417where
418 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
419{
420 language_selector: F,
421 stemmer: MultiLanguageStemmer,
422}
423
424impl<F> LanguageAwareTokenizer<F>
425where
426 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
427{
428 pub fn new(language_selector: F) -> Self {
445 Self {
446 language_selector,
447 stemmer: MultiLanguageStemmer::default(),
448 }
449 }
450
451 pub fn tokenize_with_hint(&self, text: &str, language_hint: &str) -> Vec<Token> {
455 let language = (self.language_selector)(language_hint);
456 self.stemmer.tokenize_with_language(text, language)
457 }
458}
459
460impl<F> Tokenizer for LanguageAwareTokenizer<F>
461where
462 F: Fn(&str) -> Language + Clone + Send + Sync + 'static,
463{
464 fn tokenize(&self, text: &str) -> Vec<Token> {
465 self.stemmer.tokenize_with_language(text, Language::English)
467 }
468
469 fn tokenize_hinted(&self, text: &str, hint: Option<&str>) -> Vec<Token> {
470 match hint {
471 Some(hint) => self.tokenize_with_hint(text, hint),
472 None => Tokenizer::tokenize(self, text),
473 }
474 }
475}
476
477pub fn parse_language(s: &str) -> Language {
481 match s.to_lowercase().as_str() {
482 "ar" | "arabic" => Language::Arabic,
483 "da" | "danish" => Language::Danish,
484 "nl" | "dutch" => Language::Dutch,
485 "en" | "english" => Language::English,
486 "fi" | "finnish" => Language::Finnish,
487 "fr" | "french" => Language::French,
488 "de" | "german" => Language::German,
489 "el" | "greek" => Language::Greek,
490 "hu" | "hungarian" => Language::Hungarian,
491 "it" | "italian" => Language::Italian,
492 "no" | "norwegian" => Language::Norwegian,
493 "pt" | "portuguese" => Language::Portuguese,
494 "ro" | "romanian" => Language::Romanian,
495 "ru" | "russian" => Language::Russian,
496 "es" | "spanish" => Language::Spanish,
497 "sv" | "swedish" => Language::Swedish,
498 "ta" | "tamil" => Language::Tamil,
499 "tr" | "turkish" => Language::Turkish,
500 _ => Language::English, }
502}
503
504pub fn parse_language_opt(s: &str) -> Option<Language> {
509 Some(match s.trim().to_lowercase().as_str() {
510 "ar" | "arabic" => Language::Arabic,
511 "da" | "danish" => Language::Danish,
512 "nl" | "dutch" => Language::Dutch,
513 "en" | "english" => Language::English,
514 "fi" | "finnish" => Language::Finnish,
515 "fr" | "french" => Language::French,
516 "de" | "german" => Language::German,
517 "el" | "greek" => Language::Greek,
518 "hu" | "hungarian" => Language::Hungarian,
519 "it" | "italian" => Language::Italian,
520 "no" | "norwegian" => Language::Norwegian,
521 "pt" | "portuguese" => Language::Portuguese,
522 "ro" | "romanian" => Language::Romanian,
523 "ru" | "russian" => Language::Russian,
524 "es" | "spanish" => Language::Spanish,
525 "sv" | "swedish" => Language::Swedish,
526 "ta" | "tamil" => Language::Tamil,
527 "tr" | "turkish" => Language::Turkish,
528 _ => return None,
529 })
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
534pub enum Script {
535 Latin,
536 Cyrillic,
537 Greek,
538 Arabic,
539 Tamil,
540 Other,
542}
543
544impl Script {
545 pub fn of_token(token: &str) -> Script {
547 let Some(c) = token.chars().find(|c| c.is_alphabetic()) else {
548 return Script::Other;
549 };
550 Script::of_char(c)
551 }
552
553 fn of_char(c: char) -> Script {
554 match c as u32 {
555 0x0041..=0x024F | 0x1E00..=0x1EFF => Script::Latin,
557 0x0370..=0x03FF | 0x1F00..=0x1FFF => Script::Greek,
558 0x0400..=0x052F => Script::Cyrillic,
559 0x0600..=0x06FF | 0x0750..=0x077F | 0x08A0..=0x08FF => Script::Arabic,
560 0x0B80..=0x0BFF => Script::Tamil,
561 _ => Script::Other,
562 }
563 }
564}
565
566impl Language {
567 pub fn script(self) -> Script {
569 match self {
570 Language::Russian => Script::Cyrillic,
571 Language::Greek => Script::Greek,
572 Language::Arabic => Script::Arabic,
573 Language::Tamil => Script::Tamil,
574 _ => Script::Latin,
575 }
576 }
577}
578
579thread_local! {
580 static STEMMER_CACHE: std::cell::RefCell<HashMap<Language, rust_stemmers::Stemmer>> =
581 std::cell::RefCell::new(HashMap::new());
582}
583
584#[inline]
588fn stem_owned(stemmer: &rust_stemmers::Stemmer, word: String) -> String {
589 match stemmer.stem(&word) {
590 std::borrow::Cow::Borrowed(_) => word,
591 std::borrow::Cow::Owned(stemmed) => stemmed,
592 }
593}
594
595fn with_stemmers<R>(languages: &[Language], f: impl FnOnce(&[&rust_stemmers::Stemmer]) -> R) -> R {
600 STEMMER_CACHE.with(|cache| {
601 let mut cache = cache.borrow_mut();
602 for language in languages {
603 cache
604 .entry(*language)
605 .or_insert_with(|| rust_stemmers::Stemmer::create(language.to_algorithm()));
606 }
607 let stemmers: Vec<&rust_stemmers::Stemmer> =
608 languages.iter().map(|language| &cache[language]).collect();
609 f(&stemmers)
610 })
611}
612
613#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
615pub enum Segmenter {
616 #[default]
619 Simple,
620 Unicode,
625}
626
627#[inline]
629fn is_cjk_char(c: char) -> bool {
630 matches!(
631 c as u32,
632 0x3040..=0x30FF | 0x31F0..=0x31FF | 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF | 0xFF66..=0xFF9F | 0x20000..=0x2FA1F )
640}
641
642fn fold_diacritics(word: String) -> String {
646 if word.is_ascii() {
647 return word;
648 }
649 if !matches!(
650 Script::of_token(&word),
651 Script::Latin | Script::Cyrillic | Script::Greek
652 ) {
653 return word;
654 }
655 use unicode_normalization::UnicodeNormalization;
656 use unicode_normalization::char::is_combining_mark;
657 word.nfkd()
658 .filter(|c| !is_combining_mark(*c))
659 .flat_map(|c| c.to_lowercase())
660 .collect()
661}
662
663fn tokenize_unicode_filtered(
669 text: &str,
670 transform: impl Fn(String) -> Option<String>,
671) -> Vec<Token> {
672 use unicode_segmentation::UnicodeSegmentation;
673
674 let mut tokens = Vec::with_capacity(text.len() / 5);
675 let mut position = 0u32;
676 let mut run: Vec<(usize, char)> = Vec::new();
678 let mut run_end = 0usize;
679
680 fn flush_run(run: &mut Vec<(usize, char)>, tokens: &mut Vec<Token>, position: &mut u32) {
681 match run.len() {
682 0 => {}
683 1 => {
684 let (offset, c) = run[0];
685 tokens.push(Token::new(
686 c.to_string(),
687 *position,
688 offset,
689 offset + c.len_utf8(),
690 ));
691 *position += 1;
692 }
693 _ => {
694 for pair in run.windows(2) {
695 let (start, a) = pair[0];
696 let (next, b) = pair[1];
697 let mut text = String::with_capacity(a.len_utf8() + b.len_utf8());
698 text.push(a);
699 text.push(b);
700 tokens.push(Token::new(text, *position, start, next + b.len_utf8()));
701 *position += 1;
702 }
703 }
704 }
705 run.clear();
706 }
707
708 for (offset, word) in text.unicode_word_indices() {
709 if word.chars().all(is_cjk_char) {
710 if !run.is_empty() && offset != run_end {
714 flush_run(&mut run, &mut tokens, &mut position);
715 }
716 let mut at = offset;
717 for c in word.chars() {
718 run.push((at, c));
719 at += c.len_utf8();
720 }
721 run_end = at;
722 continue;
723 }
724 flush_run(&mut run, &mut tokens, &mut position);
725 let cleaned = clean_word(word);
726 if cleaned.is_empty() {
727 continue;
728 }
729 if let Some(out) = transform(cleaned) {
730 tokens.push(Token::new(
731 fold_diacritics(out),
732 position,
733 offset,
734 offset + word.len(),
735 ));
736 }
737 position += 1;
738 }
739 flush_run(&mut run, &mut tokens, &mut position);
740 tokens
741}
742
743fn stop_word_set(language: Language) -> Option<&'static HashSet<String>> {
745 static SETS: std::sync::OnceLock<RwLock<HashMap<Language, &'static HashSet<String>>>> =
746 std::sync::OnceLock::new();
747 let sets = SETS.get_or_init(|| RwLock::new(HashMap::new()));
748 if let Some(set) = sets.read().get(&language) {
749 return Some(set);
750 }
751 let set: &'static HashSet<String> = Box::leak(Box::new(
752 stop_words::get(language.to_stop_words_language())
753 .iter()
754 .map(|word| word.to_string())
755 .collect(),
756 ));
757 Some(*sets.write().entry(language).or_insert(set))
758}
759
760#[derive(Debug, Clone, Default)]
780pub struct DynamicStemmer {
781 default: Option<Language>,
782 stop_words: bool,
783 segmenter: Segmenter,
784}
785
786impl DynamicStemmer {
787 pub fn new(default: Option<Language>) -> Self {
792 Self {
793 default,
794 stop_words: false,
795 segmenter: Segmenter::Simple,
796 }
797 }
798
799 pub fn with_segmenter(mut self, segmenter: Segmenter) -> Self {
801 self.segmenter = segmenter;
802 self
803 }
804
805 pub fn segmenter(&self) -> Segmenter {
807 self.segmenter
808 }
809
810 pub fn with_stop_words(mut self, enabled: bool) -> Self {
812 self.stop_words = enabled;
813 self
814 }
815
816 pub fn strips_stop_words(&self) -> bool {
818 self.stop_words
819 }
820
821 pub fn default_language(&self) -> Option<Language> {
823 self.default
824 }
825
826 pub fn parse_hint(hint: &str) -> Vec<Language> {
828 let mut languages = Vec::new();
829 for part in hint.split(',') {
830 if let Some(language) = parse_language_opt(part)
831 && !languages.contains(&language)
832 {
833 languages.push(language);
834 }
835 }
836 languages
837 }
838
839 fn segment(&self, text: &str, transform: impl Fn(String) -> Option<String>) -> Vec<Token> {
840 match self.segmenter {
841 Segmenter::Simple => tokenize_and_clean_filtered(text, transform),
842 Segmenter::Unicode => tokenize_unicode_filtered(text, transform),
843 }
844 }
845
846 fn tokenize_with_languages(&self, text: &str, languages: &[Language]) -> Vec<Token> {
847 if languages.is_empty() {
848 return self.segment(text, Some);
849 }
850 let stops: Vec<Option<&'static HashSet<String>>> = languages
854 .iter()
855 .map(|language| self.stop_words.then(|| stop_word_set(*language)).flatten())
856 .collect();
857 with_stemmers(languages, |stemmers| {
858 self.segment(text, |s| {
859 let script = Script::of_token(&s);
860 let Some(index) = languages
861 .iter()
862 .position(|language| language.script() == script)
863 else {
864 return Some(s);
865 };
866 if stops[index].is_some_and(|set| set.contains(s.as_str())) {
867 return None;
868 }
869 Some(stem_owned(stemmers[index], s))
870 })
871 })
872 }
873}
874
875impl Tokenizer for DynamicStemmer {
876 fn tokenize(&self, text: &str) -> Vec<Token> {
877 match self.default {
878 Some(language) => self.tokenize_with_languages(text, &[language]),
879 None => self.segment(text, Some),
880 }
881 }
882
883 fn tokenize_hinted(&self, text: &str, hint: Option<&str>) -> Vec<Token> {
884 match hint.map(str::trim).filter(|hint| !hint.is_empty()) {
885 Some(hint) => {
886 let languages = Self::parse_hint(hint);
887 if languages.is_empty() {
888 Tokenizer::tokenize(self, text)
890 } else {
891 self.tokenize_with_languages(text, &languages)
892 }
893 }
894 None => Tokenizer::tokenize(self, text),
895 }
896 }
897}
898
899#[derive(Debug, Clone, PartialEq, Eq)]
908pub enum TokenizerSpec {
909 Named(String),
911 DynamicStem {
913 by: String,
915 default: Option<Language>,
917 stop_words: bool,
919 segmenter: Segmenter,
921 },
922}
923
924impl TokenizerSpec {
925 pub fn parse(spec: &str) -> Result<TokenizerSpec, String> {
927 let spec = spec.trim();
928 let Some(rest) = spec.strip_prefix("stem(") else {
929 if spec.is_empty() || spec.contains(['(', ')', ':', ',']) {
930 return Err(format!("invalid tokenizer spec '{spec}'"));
931 }
932 return Ok(TokenizerSpec::Named(spec.to_string()));
933 };
934 let Some(params) = rest.strip_suffix(')') else {
935 return Err(format!("tokenizer spec '{spec}' is missing ')'"));
936 };
937 let mut by = None;
938 let mut default = None;
939 let mut stop_words = false;
940 let mut segmenter = Segmenter::Simple;
941 for param in params.split(',') {
942 let param = param.trim();
943 if param.is_empty() {
944 continue;
945 }
946 let Some((key, value)) = param.split_once(':') else {
947 return Err(format!(
948 "tokenizer spec '{spec}': parameter '{param}' must be 'key: value'"
949 ));
950 };
951 let (key, value) = (key.trim(), value.trim());
952 match key {
953 "by" if !value.is_empty() => by = Some(value.to_string()),
954 "by" => return Err(format!("tokenizer spec '{spec}': 'by' needs a field name")),
955 "default" => {
956 default = match value {
957 "simple" | "none" => None,
958 other => Some(parse_language_opt(other).ok_or_else(|| {
959 format!("tokenizer spec '{spec}': unknown default language '{other}'")
960 })?),
961 };
962 }
963 "stop_words" => {
964 stop_words = match value {
965 "true" => true,
966 "false" => false,
967 other => {
968 return Err(format!(
969 "tokenizer spec '{spec}': 'stop_words' must be true or false, got '{other}'"
970 ));
971 }
972 };
973 }
974 "segmenter" => {
975 segmenter = match value {
976 "simple" => Segmenter::Simple,
977 "unicode" => Segmenter::Unicode,
978 other => {
979 return Err(format!(
980 "tokenizer spec '{spec}': 'segmenter' must be simple or unicode, got '{other}'"
981 ));
982 }
983 };
984 }
985 other => {
986 return Err(format!(
987 "tokenizer spec '{spec}': unknown parameter '{other}'"
988 ));
989 }
990 }
991 }
992 let by = by.ok_or_else(|| format!("tokenizer spec '{spec}' requires 'by: <field>'"))?;
993 Ok(TokenizerSpec::DynamicStem {
994 by,
995 default,
996 stop_words,
997 segmenter,
998 })
999 }
1000
1001 pub fn hint_field(&self) -> Option<&str> {
1003 match self {
1004 TokenizerSpec::Named(_) => None,
1005 TokenizerSpec::DynamicStem { by, .. } => Some(by),
1006 }
1007 }
1008
1009 pub fn dynamic_tokenizer(&self) -> Option<BoxedTokenizer> {
1011 match self {
1012 TokenizerSpec::Named(_) => None,
1013 TokenizerSpec::DynamicStem {
1014 default,
1015 stop_words,
1016 segmenter,
1017 ..
1018 } => Some(Box::new(
1019 DynamicStemmer::new(*default)
1020 .with_stop_words(*stop_words)
1021 .with_segmenter(*segmenter),
1022 )),
1023 }
1024 }
1025}
1026
1027impl std::fmt::Display for TokenizerSpec {
1028 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1029 match self {
1030 TokenizerSpec::Named(name) => f.write_str(name),
1031 TokenizerSpec::DynamicStem {
1032 by,
1033 default,
1034 stop_words,
1035 segmenter,
1036 } => {
1037 let default = match default {
1038 None => "simple".to_string(),
1039 Some(language) => language_code(*language).to_string(),
1040 };
1041 write!(f, "stem(by: {by}, default: {default}")?;
1042 if *stop_words {
1043 write!(f, ", stop_words: true")?;
1044 }
1045 if *segmenter == Segmenter::Unicode {
1046 write!(f, ", segmenter: unicode")?;
1047 }
1048 write!(f, ")")
1049 }
1050 }
1051 }
1052}
1053
1054pub fn language_code(language: Language) -> &'static str {
1056 match language {
1057 Language::Arabic => "ar",
1058 Language::Danish => "da",
1059 Language::Dutch => "nl",
1060 Language::English => "en",
1061 Language::Finnish => "fi",
1062 Language::French => "fr",
1063 Language::German => "de",
1064 Language::Greek => "el",
1065 Language::Hungarian => "hu",
1066 Language::Italian => "it",
1067 Language::Norwegian => "no",
1068 Language::Portuguese => "pt",
1069 Language::Romanian => "ro",
1070 Language::Russian => "ru",
1071 Language::Spanish => "es",
1072 Language::Swedish => "sv",
1073 Language::Tamil => "ta",
1074 Language::Turkish => "tr",
1075 }
1076}
1077
1078pub type BoxedTokenizer = Box<dyn TokenizerClone>;
1080
1081pub trait TokenizerClone: Send + Sync {
1082 fn tokenize(&self, text: &str) -> Vec<Token>;
1083 fn tokenize_hinted(&self, text: &str, hint: Option<&str>) -> Vec<Token>;
1085 fn clone_box(&self) -> BoxedTokenizer;
1086}
1087
1088impl<T: Tokenizer> TokenizerClone for T {
1089 fn tokenize(&self, text: &str) -> Vec<Token> {
1090 Tokenizer::tokenize(self, text)
1091 }
1092
1093 fn tokenize_hinted(&self, text: &str, hint: Option<&str>) -> Vec<Token> {
1094 Tokenizer::tokenize_hinted(self, text, hint)
1095 }
1096
1097 fn clone_box(&self) -> BoxedTokenizer {
1098 Box::new(self.clone())
1099 }
1100}
1101
1102impl Clone for BoxedTokenizer {
1103 fn clone(&self) -> Self {
1104 self.clone_box()
1105 }
1106}
1107
1108#[derive(Clone)]
1113pub struct TokenizerRegistry {
1114 tokenizers: Arc<RwLock<HashMap<String, BoxedTokenizer>>>,
1115 dynamic: Arc<RwLock<HashMap<String, BoxedTokenizer>>>,
1119}
1120
1121impl TokenizerRegistry {
1122 pub fn new() -> Self {
1124 let registry = Self {
1125 tokenizers: Arc::new(RwLock::new(HashMap::new())),
1126 dynamic: Arc::new(RwLock::new(HashMap::new())),
1127 };
1128 registry.register_defaults();
1129 registry
1130 }
1131
1132 fn register_defaults(&self) {
1134 self.register("simple", SimpleTokenizer);
1136 self.register("default", SimpleTokenizer);
1137 self.register("raw", RawTokenizer);
1138 self.register("raw_ci", RawCiTokenizer);
1139
1140 self.register("en_stem", StemmerTokenizer::new(Language::English));
1142 self.register("english", StemmerTokenizer::new(Language::English));
1143
1144 self.register("ar_stem", StemmerTokenizer::new(Language::Arabic));
1146 self.register("arabic", StemmerTokenizer::new(Language::Arabic));
1147 self.register("da_stem", StemmerTokenizer::new(Language::Danish));
1148 self.register("danish", StemmerTokenizer::new(Language::Danish));
1149 self.register("nl_stem", StemmerTokenizer::new(Language::Dutch));
1150 self.register("dutch", StemmerTokenizer::new(Language::Dutch));
1151 self.register("fi_stem", StemmerTokenizer::new(Language::Finnish));
1152 self.register("finnish", StemmerTokenizer::new(Language::Finnish));
1153 self.register("fr_stem", StemmerTokenizer::new(Language::French));
1154 self.register("french", StemmerTokenizer::new(Language::French));
1155 self.register("de_stem", StemmerTokenizer::new(Language::German));
1156 self.register("german", StemmerTokenizer::new(Language::German));
1157 self.register("el_stem", StemmerTokenizer::new(Language::Greek));
1158 self.register("greek", StemmerTokenizer::new(Language::Greek));
1159 self.register("hu_stem", StemmerTokenizer::new(Language::Hungarian));
1160 self.register("hungarian", StemmerTokenizer::new(Language::Hungarian));
1161 self.register("it_stem", StemmerTokenizer::new(Language::Italian));
1162 self.register("italian", StemmerTokenizer::new(Language::Italian));
1163 self.register("no_stem", StemmerTokenizer::new(Language::Norwegian));
1164 self.register("norwegian", StemmerTokenizer::new(Language::Norwegian));
1165 self.register("pt_stem", StemmerTokenizer::new(Language::Portuguese));
1166 self.register("portuguese", StemmerTokenizer::new(Language::Portuguese));
1167 self.register("ro_stem", StemmerTokenizer::new(Language::Romanian));
1168 self.register("romanian", StemmerTokenizer::new(Language::Romanian));
1169 self.register("ru_stem", StemmerTokenizer::new(Language::Russian));
1170 self.register("russian", StemmerTokenizer::new(Language::Russian));
1171 self.register("es_stem", StemmerTokenizer::new(Language::Spanish));
1172 self.register("spanish", StemmerTokenizer::new(Language::Spanish));
1173 self.register("sv_stem", StemmerTokenizer::new(Language::Swedish));
1174 self.register("swedish", StemmerTokenizer::new(Language::Swedish));
1175 self.register("ta_stem", StemmerTokenizer::new(Language::Tamil));
1176 self.register("tamil", StemmerTokenizer::new(Language::Tamil));
1177 self.register("tr_stem", StemmerTokenizer::new(Language::Turkish));
1178 self.register("turkish", StemmerTokenizer::new(Language::Turkish));
1179
1180 self.register(
1182 "en_stop",
1183 StopWordTokenizer::new(SimpleTokenizer, Language::English),
1184 );
1185 self.register(
1186 "de_stop",
1187 StopWordTokenizer::new(SimpleTokenizer, Language::German),
1188 );
1189 self.register(
1190 "fr_stop",
1191 StopWordTokenizer::new(SimpleTokenizer, Language::French),
1192 );
1193 self.register(
1194 "ru_stop",
1195 StopWordTokenizer::new(SimpleTokenizer, Language::Russian),
1196 );
1197 self.register(
1198 "es_stop",
1199 StopWordTokenizer::new(SimpleTokenizer, Language::Spanish),
1200 );
1201
1202 self.register(
1204 "en_stem_stop",
1205 StopWordTokenizer::new(StemmerTokenizer::new(Language::English), Language::English),
1206 );
1207 self.register(
1208 "de_stem_stop",
1209 StopWordTokenizer::new(StemmerTokenizer::new(Language::German), Language::German),
1210 );
1211 self.register(
1212 "fr_stem_stop",
1213 StopWordTokenizer::new(StemmerTokenizer::new(Language::French), Language::French),
1214 );
1215 self.register(
1216 "ru_stem_stop",
1217 StopWordTokenizer::new(StemmerTokenizer::new(Language::Russian), Language::Russian),
1218 );
1219 self.register(
1220 "es_stem_stop",
1221 StopWordTokenizer::new(StemmerTokenizer::new(Language::Spanish), Language::Spanish),
1222 );
1223 }
1224
1225 pub fn register<T: Tokenizer>(&self, name: &str, tokenizer: T) {
1227 let mut tokenizers = self.tokenizers.write();
1228 tokenizers.insert(name.to_string(), Box::new(tokenizer));
1229 }
1230
1231 pub fn get(&self, name: &str) -> Option<BoxedTokenizer> {
1236 if name.starts_with("stem(") {
1237 if let Some(tokenizer) = self.dynamic.read().get(name) {
1238 return Some(tokenizer.clone());
1239 }
1240 let tokenizer = TokenizerSpec::parse(name)
1241 .ok()
1242 .and_then(|spec| spec.dynamic_tokenizer())?;
1243 self.dynamic
1244 .write()
1245 .entry(name.to_string())
1246 .or_insert_with(|| tokenizer.clone());
1247 return Some(tokenizer);
1248 }
1249 let tokenizers = self.tokenizers.read();
1250 tokenizers.get(name).cloned()
1251 }
1252
1253 pub fn contains(&self, name: &str) -> bool {
1255 let tokenizers = self.tokenizers.read();
1256 tokenizers.contains_key(name)
1257 }
1258
1259 pub fn names(&self) -> Vec<String> {
1261 let tokenizers = self.tokenizers.read();
1262 tokenizers.keys().cloned().collect()
1263 }
1264}
1265
1266impl Default for TokenizerRegistry {
1267 fn default() -> Self {
1268 Self::new()
1269 }
1270}
1271
1272#[cfg(test)]
1273mod tests {
1274 use super::*;
1275
1276 #[test]
1277 fn test_simple_tokenizer() {
1278 let tokenizer = SimpleTokenizer;
1279 let tokens = Tokenizer::tokenize(&tokenizer, "Hello World");
1280
1281 assert_eq!(tokens.len(), 2);
1282 assert_eq!(tokens[0].text, "hello");
1283 assert_eq!(tokens[0].position, 0);
1284 assert_eq!(tokens[1].text, "world");
1285 assert_eq!(tokens[1].position, 1);
1286 }
1287
1288 #[test]
1289 fn test_raw_tokenizer() {
1290 let tokenizer = RawTokenizer;
1291 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
1293 assert_eq!(tokens.len(), 1);
1294 assert_eq!(tokens[0].text, "Hello, World!");
1295 assert_eq!(tokens[0].position, 0);
1296 }
1297
1298 #[test]
1299 fn test_raw_tokenizer_trims() {
1300 let tokenizer = RawTokenizer;
1301 let tokens = Tokenizer::tokenize(&tokenizer, " spaced ");
1302 assert_eq!(tokens.len(), 1);
1303 assert_eq!(tokens[0].text, "spaced");
1304 assert_eq!(tokens[0].offset_from, 2);
1305 }
1306
1307 #[test]
1308 fn test_raw_tokenizer_empty() {
1309 let tokenizer = RawTokenizer;
1310 assert!(Tokenizer::tokenize(&tokenizer, "").is_empty());
1311 assert!(Tokenizer::tokenize(&tokenizer, " ").is_empty());
1312 }
1313
1314 #[test]
1315 fn test_raw_ci_tokenizer() {
1316 let tokenizer = RawCiTokenizer;
1317 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
1319 assert_eq!(tokens.len(), 1);
1320 assert_eq!(tokens[0].text, "hello, world!");
1321 assert_eq!(tokens[0].position, 0);
1322 }
1323
1324 #[test]
1325 fn test_raw_ci_tokenizer_preserves_structure() {
1326 let tokenizer = RawCiTokenizer;
1327 let tokens = Tokenizer::tokenize(&tokenizer, "HTTPS://Example.COM/Page");
1328 assert_eq!(tokens.len(), 1);
1329 assert_eq!(tokens[0].text, "https://example.com/page");
1330 }
1331
1332 #[test]
1333 fn test_simple_tokenizer_strips_punctuation() {
1334 let tokenizer = SimpleTokenizer;
1335 let tokens = Tokenizer::tokenize(&tokenizer, "Hello, World!");
1336
1337 assert_eq!(tokens.len(), 2);
1338 assert_eq!(tokens[0].text, "hello");
1339 assert_eq!(tokens[1].text, "world");
1340 }
1341
1342 #[test]
1343 fn test_empty_text() {
1344 let tokenizer = SimpleTokenizer;
1345 let tokens = Tokenizer::tokenize(&tokenizer, "");
1346 assert!(tokens.is_empty());
1347 }
1348
1349 #[test]
1350 fn test_stemmer_tokenizer_english() {
1351 let tokenizer = StemmerTokenizer::english();
1352 let tokens = Tokenizer::tokenize(&tokenizer, "Dogs are running quickly");
1353
1354 assert_eq!(tokens.len(), 4);
1355 assert_eq!(tokens[0].text, "dog"); assert_eq!(tokens[1].text, "are"); assert_eq!(tokens[2].text, "run"); assert_eq!(tokens[3].text, "quick"); }
1360
1361 #[test]
1362 fn test_stemmer_tokenizer_preserves_offsets() {
1363 let tokenizer = StemmerTokenizer::english();
1364 let tokens = Tokenizer::tokenize(&tokenizer, "Running dogs");
1365
1366 assert_eq!(tokens.len(), 2);
1367 assert_eq!(tokens[0].text, "run");
1368 assert_eq!(tokens[0].offset_from, 0);
1369 assert_eq!(tokens[0].offset_to, 7); assert_eq!(tokens[1].text, "dog");
1371 assert_eq!(tokens[1].offset_from, 8);
1372 assert_eq!(tokens[1].offset_to, 12); }
1374
1375 #[test]
1376 fn test_stemmer_tokenizer_german() {
1377 let tokenizer = StemmerTokenizer::new(Language::German);
1378 let tokens = Tokenizer::tokenize(&tokenizer, "Häuser Bücher");
1379
1380 assert_eq!(tokens.len(), 2);
1381 assert_eq!(tokens[0].text, "haus"); assert_eq!(tokens[1].text, "buch"); }
1385
1386 #[test]
1387 fn test_stemmer_tokenizer_russian() {
1388 let tokenizer = StemmerTokenizer::new(Language::Russian);
1389 let tokens = Tokenizer::tokenize(&tokenizer, "бегущие собаки");
1390
1391 assert_eq!(tokens.len(), 2);
1392 assert_eq!(tokens[0].text, "бегущ"); assert_eq!(tokens[1].text, "собак"); }
1396
1397 #[test]
1398 fn test_multi_language_stemmer() {
1399 let stemmer = MultiLanguageStemmer::new(Language::English);
1400
1401 let tokens = stemmer.tokenize_with_language("running dogs", Language::English);
1403 assert_eq!(tokens[0].text, "run");
1404 assert_eq!(tokens[1].text, "dog");
1405
1406 let tokens = stemmer.tokenize_with_language("Häuser Bücher", Language::German);
1408 assert_eq!(tokens[0].text, "haus");
1409 assert_eq!(tokens[1].text, "buch");
1410
1411 let tokens = stemmer.tokenize_with_language("бегущие собаки", Language::Russian);
1413 assert_eq!(tokens[0].text, "бегущ");
1414 assert_eq!(tokens[1].text, "собак");
1415 }
1416
1417 #[test]
1418 fn test_language_aware_tokenizer() {
1419 let tokenizer = LanguageAwareTokenizer::new(parse_language);
1420
1421 let tokens = tokenizer.tokenize_with_hint("running dogs", "en");
1423 assert_eq!(tokens[0].text, "run");
1424 assert_eq!(tokens[1].text, "dog");
1425
1426 let tokens = tokenizer.tokenize_with_hint("Häuser Bücher", "de");
1428 assert_eq!(tokens[0].text, "haus");
1429 assert_eq!(tokens[1].text, "buch");
1430
1431 let tokens = tokenizer.tokenize_with_hint("бегущие собаки", "russian");
1433 assert_eq!(tokens[0].text, "бегущ");
1434 assert_eq!(tokens[1].text, "собак");
1435 }
1436
1437 #[test]
1438 fn test_parse_language() {
1439 assert_eq!(parse_language("en"), Language::English);
1440 assert_eq!(parse_language("english"), Language::English);
1441 assert_eq!(parse_language("English"), Language::English);
1442 assert_eq!(parse_language("de"), Language::German);
1443 assert_eq!(parse_language("german"), Language::German);
1444 assert_eq!(parse_language("ru"), Language::Russian);
1445 assert_eq!(parse_language("russian"), Language::Russian);
1446 assert_eq!(parse_language("unknown"), Language::English); }
1448
1449 #[test]
1450 fn test_tokenizer_registry_defaults() {
1451 let registry = TokenizerRegistry::new();
1452
1453 assert!(registry.contains("simple"));
1455 assert!(registry.contains("raw"));
1456 assert!(registry.contains("raw_ci"));
1457 assert!(registry.contains("raw"));
1458 assert!(registry.contains("raw_ci"));
1459 assert!(registry.contains("en_stem"));
1460 assert!(registry.contains("german"));
1461 assert!(registry.contains("russian"));
1462 }
1463
1464 #[test]
1465 fn test_tokenizer_registry_get() {
1466 let registry = TokenizerRegistry::new();
1467
1468 let tokenizer = registry.get("en_stem").unwrap();
1470 let tokens = tokenizer.tokenize("running dogs");
1471 assert_eq!(tokens[0].text, "run");
1472 assert_eq!(tokens[1].text, "dog");
1473
1474 let tokenizer = registry.get("german").unwrap();
1476 let tokens = tokenizer.tokenize("Häuser Bücher");
1477 assert_eq!(tokens[0].text, "haus");
1478 assert_eq!(tokens[1].text, "buch");
1479 }
1480
1481 #[test]
1482 fn test_tokenizer_registry_custom() {
1483 let registry = TokenizerRegistry::new();
1484
1485 registry.register("my_tokenizer", SimpleTokenizer);
1487
1488 assert!(registry.contains("my_tokenizer"));
1489 let tokenizer = registry.get("my_tokenizer").unwrap();
1490 let tokens = tokenizer.tokenize("Hello World");
1491 assert_eq!(tokens[0].text, "hello");
1492 assert_eq!(tokens[1].text, "world");
1493 }
1494
1495 #[test]
1496 fn test_tokenizer_registry_nonexistent() {
1497 let registry = TokenizerRegistry::new();
1498 assert!(registry.get("nonexistent").is_none());
1499 }
1500
1501 #[test]
1502 fn test_stop_word_tokenizer_english() {
1503 let tokenizer = StopWordTokenizer::english(SimpleTokenizer);
1504 let tokens = Tokenizer::tokenize(&tokenizer, "The quick brown fox jumps over the lazy dog");
1505
1506 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1508 assert!(!texts.contains(&"the"));
1509 assert!(!texts.contains(&"over"));
1510 assert!(texts.contains(&"quick"));
1511 assert!(texts.contains(&"brown"));
1512 assert!(texts.contains(&"fox"));
1513 assert!(texts.contains(&"jumps"));
1514 assert!(texts.contains(&"lazy"));
1515 assert!(texts.contains(&"dog"));
1516 }
1517
1518 #[test]
1519 fn test_stop_word_tokenizer_with_stemmer() {
1520 let tokenizer = StopWordTokenizer::new(StemmerTokenizer::english(), Language::English);
1524 let tokens = Tokenizer::tokenize(&tokenizer, "elephants galaxies quantum");
1525
1526 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1527 assert!(texts.contains(&"eleph")); assert!(texts.contains(&"galaxi")); assert!(texts.contains(&"quantum")); }
1532
1533 #[test]
1534 fn test_stop_word_tokenizer_german() {
1535 let tokenizer = StopWordTokenizer::new(SimpleTokenizer, Language::German);
1536 let tokens = Tokenizer::tokenize(&tokenizer, "Der Hund und die Katze");
1537
1538 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1540 assert!(!texts.contains(&"der"));
1541 assert!(!texts.contains(&"und"));
1542 assert!(!texts.contains(&"die"));
1543 assert!(texts.contains(&"hund"));
1544 assert!(texts.contains(&"katze"));
1545 }
1546
1547 #[test]
1548 fn test_stop_word_tokenizer_custom() {
1549 let custom_stops: HashSet<String> = ["foo", "bar"].iter().map(|s| s.to_string()).collect();
1550 let tokenizer = StopWordTokenizer::with_custom_stop_words(SimpleTokenizer, custom_stops);
1551 let tokens = Tokenizer::tokenize(&tokenizer, "foo baz bar qux");
1552
1553 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1554 assert!(!texts.contains(&"foo"));
1555 assert!(!texts.contains(&"bar"));
1556 assert!(texts.contains(&"baz"));
1557 assert!(texts.contains(&"qux"));
1558 }
1559
1560 #[test]
1561 fn test_stop_word_tokenizer_is_stop_word() {
1562 let tokenizer = StopWordTokenizer::english(SimpleTokenizer);
1563 assert!(tokenizer.is_stop_word("the"));
1564 assert!(tokenizer.is_stop_word("and"));
1565 assert!(tokenizer.is_stop_word("is"));
1566 assert!(!tokenizer.is_stop_word("elephant"));
1568 assert!(!tokenizer.is_stop_word("quantum"));
1569 }
1570
1571 #[test]
1572 fn test_tokenizer_registry_stop_word_tokenizers() {
1573 let registry = TokenizerRegistry::new();
1574
1575 assert!(registry.contains("en_stop"));
1577 assert!(registry.contains("en_stem_stop"));
1578 assert!(registry.contains("de_stop"));
1579 assert!(registry.contains("ru_stop"));
1580
1581 let tokenizer = registry.get("en_stop").unwrap();
1583 let tokens = tokenizer.tokenize("The quick fox");
1584 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1585 assert!(!texts.contains(&"the"));
1586 assert!(texts.contains(&"quick"));
1587 assert!(texts.contains(&"fox"));
1588
1589 let tokenizer = registry.get("en_stem_stop").unwrap();
1591 let tokens = tokenizer.tokenize("elephants galaxies");
1592 let texts: Vec<&str> = tokens.iter().map(|t| t.text.as_str()).collect();
1593 assert!(texts.contains(&"eleph")); assert!(texts.contains(&"galaxi")); }
1596
1597 fn hinted<T: Tokenizer>(tokenizer: &T, text: &str, hint: Option<&str>) -> Vec<Token> {
1598 Tokenizer::tokenize_hinted(tokenizer, text, hint)
1599 }
1600
1601 fn texts(tokens: &[Token]) -> Vec<&str> {
1602 tokens.iter().map(|t| t.text.as_str()).collect()
1603 }
1604
1605 fn positions(tokens: &[Token]) -> Vec<u32> {
1606 tokens.iter().map(|t| t.position).collect()
1607 }
1608
1609 #[test]
1610 fn unicode_segmenter_splits_on_word_boundaries_and_folds() {
1611 let plain = DynamicStemmer::new(None).with_segmenter(Segmenter::Unicode);
1612 let tokens = hinted(
1613 &plain,
1614 "Float-zero determinants: p53/CO2, 10.1007/s1 résumé",
1615 None,
1616 );
1617 assert_eq!(
1618 texts(&tokens),
1619 vec![
1620 "float",
1621 "zero",
1622 "determinants",
1623 "p53",
1624 "co2",
1625 "101007",
1626 "s1",
1627 "resume"
1628 ]
1629 );
1630 assert_eq!(positions(&tokens), (0..8).collect::<Vec<u32>>());
1631 assert_eq!(
1633 &"Float-zero determinants: p53/CO2, 10.1007/s1 résumé"
1634 [tokens[7].offset_from..tokens[7].offset_to],
1635 "résumé"
1636 );
1637
1638 let english = DynamicStemmer::new(None).with_segmenter(Segmenter::Unicode);
1641 assert_eq!(
1642 texts(&hinted(&english, "Running foxes' café", Some("en"))),
1643 vec!["run", "fox", "cafe"]
1644 );
1645 assert_eq!(
1646 texts(&hinted(
1647 &DynamicStemmer::new(None),
1648 "Float-zero café",
1649 Some("en")
1650 )),
1651 vec!["floatzero", "café"]
1652 );
1653 assert_eq!(texts(&hinted(&plain, "ёлка", None)), vec!["елка"]);
1655 let stopping = DynamicStemmer::new(None)
1657 .with_stop_words(true)
1658 .with_segmenter(Segmenter::Unicode);
1659 let tokens = hinted(&stopping, "state-of-the-art résumé", Some("en"));
1660 assert_eq!(tokens.len(), 3, "{:?}", texts(&tokens));
1661 assert_eq!(&texts(&tokens)[..2], ["state", "art"]);
1662 assert!(tokens[2].text.starts_with("resum"), "{:?}", tokens[2].text);
1663 assert!(tokens[2].text.is_ascii(), "folded after stemming");
1664 assert_eq!(positions(&tokens), vec![0, 3, 4]);
1665 }
1666
1667 #[test]
1668 fn unicode_segmenter_bigrams_cjk_runs() {
1669 let t = DynamicStemmer::new(None).with_segmenter(Segmenter::Unicode);
1670 let tokens = hinted(&t, "東京都 tower", Some("en"));
1671 assert_eq!(texts(&tokens), vec!["東京", "京都", "tower"]);
1672 assert_eq!(positions(&tokens), vec![0, 1, 2]);
1673 assert_eq!(
1674 &"東京都 tower"[tokens[1].offset_from..tokens[1].offset_to],
1675 "京都"
1676 );
1677 assert_eq!(
1679 texts(&hinted(&t, "東 tower 京都", None)),
1680 vec!["東", "tower", "京都"]
1681 );
1682 assert_eq!(
1685 texts(&hinted(&t, "トウキョウ タワー", None)),
1686 vec!["トウ", "ウキ", "キョ", "ョウ", "タワ", "ワー"]
1687 );
1688 assert_eq!(texts(&hinted(&t, "東京tower", None)), vec!["東京", "tower"]);
1691 let query = hinted(&t, "東京都", None);
1693 assert_eq!(positions(&query), vec![0, 1]);
1694 }
1695
1696 #[test]
1697 fn dynamic_stemmer_drops_stop_words_but_keeps_positions() {
1698 let stemmer = DynamicStemmer::new(None).with_stop_words(true);
1699 let tokens = hinted(&stemmer, "Quantum of the Art", Some("en"));
1700 assert_eq!(texts(&tokens), vec!["quantum", "art"]);
1701 assert_eq!(positions(&tokens), vec![0, 3]);
1702 assert_eq!(tokens[1].offset_from, "Quantum of the ".len());
1704
1705 let tokens = hinted(&stemmer, "бегущие и собаки the foxes", Some("ru,en"));
1707 assert_eq!(texts(&tokens), vec!["бегущ", "собак", "fox"]);
1708 assert_eq!(positions(&tokens), vec![0, 2, 4]);
1709
1710 assert_eq!(
1712 texts(&hinted(&stemmer, "the 日本語 fox", Some("en"))),
1713 vec!["日本語", "fox"]
1714 );
1715
1716 assert_eq!(
1718 texts(&hinted(&stemmer, "the fox", None)),
1719 vec!["the", "fox"]
1720 );
1721 let english = DynamicStemmer::new(Some(Language::English)).with_stop_words(true);
1723 assert_eq!(texts(&hinted(&english, "the fox", None)), vec!["fox"]);
1724 assert_eq!(
1726 texts(&hinted(&DynamicStemmer::new(None), "the fox", Some("en"))),
1727 vec!["the", "fox"]
1728 );
1729 assert!(hinted(&stemmer, "to be or not to be", Some("en")).is_empty());
1731 }
1732
1733 #[test]
1734 fn dynamic_stemmer_selects_language_from_hint() {
1735 let stemmer = DynamicStemmer::new(None);
1736 assert_eq!(
1737 texts(&hinted(&stemmer, "Running Foxes", Some("en"))),
1738 vec!["run", "fox"]
1739 );
1740 assert_eq!(
1741 texts(&hinted(&stemmer, "бегущие собаки", Some("ru"))),
1742 vec!["бегущ", "собак"]
1743 );
1744 assert_eq!(
1746 texts(&hinted(&stemmer, "Running Foxes", Some("xx"))),
1747 vec!["running", "foxes"]
1748 );
1749 assert_eq!(
1750 texts(&hinted(&stemmer, "Running Foxes", None)),
1751 vec!["running", "foxes"]
1752 );
1753 assert_eq!(
1754 texts(&Tokenizer::tokenize(&stemmer, "Running, Foxes!")),
1755 vec!["running", "foxes"]
1756 );
1757 let english = DynamicStemmer::new(Some(Language::English));
1759 assert_eq!(
1760 texts(&hinted(&english, "Running Foxes", None)),
1761 vec!["run", "fox"]
1762 );
1763 }
1764
1765 #[test]
1766 fn dynamic_stemmer_routes_tokens_by_script() {
1767 let stemmer = DynamicStemmer::new(None);
1768 assert_eq!(
1770 texts(&hinted(&stemmer, "бегущие foxes", Some("ru,en"))),
1771 vec!["бегущ", "fox"]
1772 );
1773 assert_eq!(
1774 texts(&hinted(&stemmer, "бегущие foxes", Some("en, ru"))),
1775 vec!["бегущ", "fox"]
1776 );
1777 assert_eq!(
1779 texts(&hinted(&stemmer, "бегущие foxes", Some("ru"))),
1780 vec!["бегущ", "foxes"]
1781 );
1782 assert_eq!(
1783 texts(&hinted(&stemmer, "бегущие foxes", Some("en"))),
1784 vec!["бегущие", "fox"]
1785 );
1786 assert_eq!(
1788 texts(&hinted(&stemmer, "running", Some("de,en"))),
1789 vec!["running"]
1790 );
1791 assert_eq!(
1792 texts(&hinted(&stemmer, "running", Some("en,de"))),
1793 vec!["run"]
1794 );
1795 let tokens = hinted(&stemmer, "бегущие foxes run", Some("ru,en"));
1797 assert_eq!(
1798 tokens.iter().map(|t| t.position).collect::<Vec<_>>(),
1799 vec![0, 1, 2]
1800 );
1801 }
1802
1803 #[test]
1804 fn script_detection_covers_supported_stemmer_scripts() {
1805 assert_eq!(Script::of_token("hello"), Script::Latin);
1806 assert_eq!(Script::of_token("straße"), Script::Latin);
1807 assert_eq!(Script::of_token("собака"), Script::Cyrillic);
1808 assert_eq!(Script::of_token("γεια"), Script::Greek);
1809 assert_eq!(Script::of_token("مرحبا"), Script::Arabic);
1810 assert_eq!(Script::of_token("தமிழ்"), Script::Tamil);
1811 assert_eq!(Script::of_token("日本語"), Script::Other);
1812 assert_eq!(Script::of_token("2024"), Script::Other);
1813 assert_eq!(Language::Russian.script(), Script::Cyrillic);
1814 assert_eq!(Language::Turkish.script(), Script::Latin);
1815 }
1816
1817 #[test]
1818 fn tokenizer_spec_parses_and_renders_canonically() {
1819 assert_eq!(
1820 TokenizerSpec::parse("en_stem").unwrap(),
1821 TokenizerSpec::Named("en_stem".to_string())
1822 );
1823 let spec = TokenizerSpec::parse("stem(by:languages,default:simple)").unwrap();
1824 assert_eq!(
1825 spec,
1826 TokenizerSpec::DynamicStem {
1827 by: "languages".to_string(),
1828 default: None,
1829 stop_words: false,
1830 segmenter: Segmenter::Simple,
1831 }
1832 );
1833 assert_eq!(spec.to_string(), "stem(by: languages, default: simple)");
1834 assert_eq!(spec.hint_field(), Some("languages"));
1835
1836 let spec = TokenizerSpec::parse("stem(by: lang, default: english)").unwrap();
1837 assert_eq!(spec.to_string(), "stem(by: lang, default: en)");
1838 assert_eq!(
1839 TokenizerSpec::parse("stem(by: lang)").unwrap(),
1840 TokenizerSpec::DynamicStem {
1841 by: "lang".to_string(),
1842 default: None,
1843 stop_words: false,
1844 segmenter: Segmenter::Simple,
1845 }
1846 );
1847
1848 let spec =
1849 TokenizerSpec::parse("stem(by: languages, default: simple, stop_words: true)").unwrap();
1850 assert_eq!(
1851 spec,
1852 TokenizerSpec::DynamicStem {
1853 by: "languages".to_string(),
1854 default: None,
1855 stop_words: true,
1856 segmenter: Segmenter::Simple,
1857 }
1858 );
1859 assert_eq!(
1860 spec.to_string(),
1861 "stem(by: languages, default: simple, stop_words: true)"
1862 );
1863 assert_eq!(
1865 TokenizerSpec::parse("stem(by: lang, stop_words: false)")
1866 .unwrap()
1867 .to_string(),
1868 "stem(by: lang, default: simple)"
1869 );
1870 assert!(TokenizerSpec::parse("stem(by: lang, stop_words: maybe)").is_err());
1871 let spec =
1872 TokenizerSpec::parse("stem(by: languages, stop_words: true, segmenter: unicode)")
1873 .unwrap();
1874 assert_eq!(
1875 spec,
1876 TokenizerSpec::DynamicStem {
1877 by: "languages".to_string(),
1878 default: None,
1879 stop_words: true,
1880 segmenter: Segmenter::Unicode,
1881 }
1882 );
1883 assert_eq!(
1884 spec.to_string(),
1885 "stem(by: languages, default: simple, stop_words: true, segmenter: unicode)"
1886 );
1887 assert!(TokenizerSpec::parse("stem(by: lang, segmenter: icu)").is_err());
1888
1889 assert!(TokenizerSpec::parse("stem(default: en)").is_err());
1890 assert!(TokenizerSpec::parse("stem(by: lang, default: klingon)").is_err());
1891 assert!(TokenizerSpec::parse("stem(by: lang").is_err());
1892 assert!(TokenizerSpec::parse("stem(by: lang, color: red)").is_err());
1893 assert!(TokenizerSpec::parse("en_stem(foo)").is_err());
1894 assert!(TokenizerSpec::parse("").is_err());
1895 }
1896
1897 #[test]
1898 fn registry_builds_dynamic_stemmer_from_spec() {
1899 let registry = TokenizerRegistry::new();
1900 let tokenizer = registry
1901 .get("stem(by: languages, default: simple)")
1902 .expect("dynamic spec resolves without registration");
1903 assert_eq!(
1904 texts(&tokenizer.tokenize_hinted("Running Foxes", Some("en"))),
1905 vec!["run", "fox"]
1906 );
1907 assert_eq!(
1908 texts(&tokenizer.tokenize("Running Foxes")),
1909 vec!["running", "foxes"]
1910 );
1911 assert!(
1912 registry
1913 .get("stem(by: languages, default: klingon)")
1914 .is_none()
1915 );
1916 let stopping = registry
1917 .get("stem(by: languages, default: simple, stop_words: true)")
1918 .expect("stop-word spec resolves");
1919 let tokens = stopping.tokenize_hinted("the running foxes", Some("en"));
1920 assert_eq!(texts(&tokens), vec!["run", "fox"]);
1921 assert_eq!(positions(&tokens), vec![1, 2]);
1922 let simple = registry.get("en_stem").unwrap();
1924 assert_eq!(
1925 texts(&simple.tokenize_hinted("Running Foxes", Some("ru"))),
1926 vec!["run", "fox"]
1927 );
1928 }
1929
1930 #[test]
1931 fn parse_language_opt_rejects_unknown_codes() {
1932 assert_eq!(parse_language_opt(" RU "), Some(Language::Russian));
1933 assert_eq!(parse_language_opt("german"), Some(Language::German));
1934 assert_eq!(parse_language_opt("xx"), None);
1935 assert_eq!(parse_language_opt(""), None);
1936 for language in [Language::English, Language::Russian, Language::Tamil] {
1937 assert_eq!(parse_language_opt(language_code(language)), Some(language));
1938 }
1939 }
1940}