use std::borrow::Cow;
use std::iter::Peekable;
use std::sync::LazyLock;
use std::time::Instant;
use hashbrown::HashSet;
use regex::Regex;
use unicode_segmentation::UnicodeSegmentation;
use whatlang::Lang;
use crate::config::{ConfigNormalization, ConfigStopwords, ConfigTokenization};
use crate::lexer::stopwords::is_stopword;
use crate::query::QueryGenericLang;
use crate::store::identifiers::{StoreTermHash, StoreTermHashed};
use super::stopwords::LexerStopWord;
pub struct TokenLexerBuilder;
type TokensIter<'s> = Box<dyn Iterator<Item = Token<'s>> + 's>;
static SPECIAL_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(concat!(
r"(?P<email>[\w.+-]+@[\w-]+\.[\w.-]+)",
r"|(?P<username>@[^\s]*\w)",
r"|(?P<url>\w{2,}://[^\s]*[^\s.])",
r"|(?P<ipv4>\d{1,3}(?:\.\d{1,3}){3})(?:[^\.\d]|$)",
r"|(?P<phone>\+?\d+(?:[\s\.-]?\d+){4,})",
r"|(?P<domain>[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})",
r"|(?P<id>[\w\d:_-]*[\d_][\w\d:-]*)"
))
.unwrap()
});
pub struct Tokenizer<'s> {
config: ConfigTokenization,
text: &'s str,
lang: Option<Lang>,
regex_matches: Peekable<regex::CaptureMatches<'static, 's>>,
regex_cursor: usize,
tokens: Option<(TokensIter<'s>, usize)>,
}
impl<'s> Tokenizer<'s> {
fn new(text: &'s str, lang: Option<Lang>, config: &ConfigTokenization) -> Self {
let regex_matches = if config.detect_special_patterns {
SPECIAL_PATTERNS.captures_iter(text).peekable()
} else {
static NOOP_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^$").unwrap());
NOOP_REGEX.captures_iter(" ").peekable()
};
Self {
config: *config,
lang,
regex_matches,
text,
regex_cursor: 0,
tokens: None,
}
}
}
fn tokenize<'s>(text: &'s str, lang: Option<Lang>) -> Box<dyn Iterator<Item = &'s str> + 's> {
match lang {
#[cfg(feature = "tokenizer-chinese")]
Some(Lang::Cmn) => Box::from(
TOKENIZER_JIEBA
.cut(text, false)
.into_iter()
.map(|token| token.word),
),
#[cfg(feature = "tokenizer-japanese")]
Some(Lang::Jpn) => match TOKENIZER_LINDERA.tokenize(text) {
Ok(tokens) => Box::from(tokens.into_iter()),
Err(err) => {
tracing::warn!("unable to tokenize japanese, falling back: {}", err);
Box::from(text.unicode_words())
}
},
_ => Box::from(text.unicode_words()),
}
}
impl<'s> Iterator for Tokenizer<'s> {
type Item = Token<'s>;
fn next(&mut self) -> Option<Self::Item> {
if let Some((words, end)) = self.tokens.as_mut() {
match words.next() {
Some(word) => return Some(word),
None => {
self.regex_cursor = *end;
self.tokens = None;
}
}
}
match self.regex_matches.peek() {
Some(captures) => {
let regex_match = captures.get_match();
let start = regex_match.start();
let end = regex_match.end();
if start > self.regex_cursor {
let gap = &self.text[self.regex_cursor..start];
let mut tokens = Box::new(tokenize(gap, self.lang).map(Token::Word));
if let Some(token) = tokens.next() {
self.tokens = Some((tokens, end));
return Some(token);
}
}
let next = if self.config.compat_split_special_patterns {
let regex_match = captures.get_match().as_str();
let words = tokenize(regex_match, self.lang);
let mut words = Box::new(words.map(|raw| Token::Special {
raw,
normalized: Cow::Borrowed(raw),
}));
let next = words.next().unwrap_or(Token::Special {
raw: regex_match,
normalized: Cow::Borrowed(regex_match),
});
self.tokens = Some((words, end));
Some(next)
} else {
Some(Token::special(captures))
};
self.regex_matches.next();
self.regex_cursor = end;
next
}
None => {
let gap = &self.text[self.regex_cursor..];
let mut tokens = Box::from(tokenize(gap, self.lang).map(Token::Word));
if let Some(token) = tokens.next() {
self.tokens = Some((tokens, self.text.len()));
return Some(token);
}
None
}
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Token<'s> {
Word(&'s str),
Special {
raw: &'s str,
normalized: Cow<'s, str>,
},
}
impl<'s> Token<'s> {
fn special(captures: ®ex::Captures<'s>) -> Self {
let (raw, normalized) = if let Some(m) = captures.name("email") {
(m.as_str(), Cow::Borrowed(m.as_str()))
} else if let Some(m) = captures.name("username") {
(m.as_str(), Cow::Borrowed(m.as_str()))
} else if let Some(m) = captures.name("url") {
(m.as_str(), Cow::Borrowed(m.as_str()))
} else if let Some(m) = captures.name("ipv4") {
(m.as_str(), Cow::Borrowed(m.as_str()))
} else if let Some(m) = captures.name("phone") {
let raw = m.as_str();
let normalized: String = raw
.chars()
.filter(|c| c.is_ascii_digit() || *c == '+')
.collect();
(raw, Cow::Owned(normalized))
} else if let Some(m) = captures.name("domain") {
(m.as_str(), Cow::Borrowed(m.as_str()))
} else if let Some(m) = captures.name("id") {
(m.as_str(), Cow::Borrowed(m.as_str()))
} else {
unreachable!("One name always matches")
};
Self::Special { raw, normalized }
}
}
#[derive(PartialEq, Eq)]
pub enum NormalizedToken {
Word(String),
Special(String),
}
impl std::fmt::Debug for NormalizedToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Word(str) => std::fmt::Debug::fmt(str, f),
Self::Special(str) => f.debug_tuple("Special").field(str).finish(),
}
}
}
impl std::ops::Deref for NormalizedToken {
type Target = String;
fn deref(&self) -> &Self::Target {
match self {
Self::Word(str) => str,
Self::Special(str) => str,
}
}
}
impl NormalizedToken {
pub fn is_special(&self) -> bool {
matches!(self, Self::Special(_))
}
pub fn into_inner(self) -> String {
match self {
Self::Word(str) => str,
Self::Special(str) => str,
}
}
}
impl From<NormalizedToken> for String {
#[inline]
fn from(value: NormalizedToken) -> Self {
value.into_inner()
}
}
pub struct TokenLexer<'a> {
mode: TokenLexerMode,
locale: Option<Lang>,
#[cfg(feature = "stemming")]
snowball_algorithm: Option<snowball::Algorithm>,
tokenizer: Tokenizer<'a>,
yields: HashSet<StoreTermHashed>,
config: ConfigNormalization,
stopwords: &'a ConfigStopwords,
}
#[derive(PartialEq)]
pub enum TokenLexerMode {
NormalizeAndCleanup,
NormalizeOnly,
}
impl TokenLexerMode {
pub fn should_cleanup(&self) -> bool {
match self {
Self::NormalizeAndCleanup => true,
Self::NormalizeOnly => false,
}
}
}
const TEXT_LANG_TRUNCATE_OVER_CHARS: usize = 200;
const TEXT_LANG_DETECT_PROCEED_OVER_CHARS: usize = 20;
const TEXT_LANG_DETECT_NGRAM_UNDER_CHARS: usize = 60;
#[cfg(feature = "tokenizer-chinese")]
static TOKENIZER_JIEBA: LazyLock<jieba_rs::Jieba> = LazyLock::new(jieba_rs::Jieba::new);
#[cfg(feature = "tokenizer-japanese")]
static TOKENIZER_LINDERA: LazyLock<lindera_tokenizer::tokenizer::Tokenizer> = LazyLock::new(|| {
lindera_tokenizer::tokenizer::Tokenizer::from_config(
lindera_tokenizer::tokenizer::TokenizerConfig {
dictionary: lindera_dictionary::DictionaryConfig {
kind: Some(lindera_dictionary::DictionaryKind::UniDic),
path: None,
},
user_dictionary: None,
mode: lindera_core::mode::Mode::Normal,
},
)
.expect("unable to initialize japanese tokenizer")
});
impl TokenLexerBuilder {
pub fn from<'a>(
mode: TokenLexerMode,
lang: Option<Lang>,
text: &'a str,
normalization_config: ConfigNormalization,
tokenization_config: ConfigTokenization,
stopwords_config: &'a ConfigStopwords,
) -> Result<TokenLexer<'a>, ()> {
let locale = match lang {
Some(hinted_lang) => {
tracing::debug!(
"using hinted locale: {} from lexer text: {}",
hinted_lang,
text
);
lang
}
None => match mode {
TokenLexerMode::NormalizeAndCleanup => {
let locale = Self::detect_lang(text);
tracing::debug!("detected locale: {:?} from lexer text: {}", locale, text);
locale
}
#[cfg(feature = "stemming")]
TokenLexerMode::NormalizeOnly if normalization_config.stemming_enabled => {
let locale = Self::detect_lang(text);
tracing::debug!("detected locale: {:?} from lexer text: {}", locale, text);
locale
}
TokenLexerMode::NormalizeOnly => {
tracing::debug!("not detecting locale from lexer text: {}", text);
None
}
},
};
Ok(TokenLexer::new(
mode,
text,
locale,
normalization_config,
tokenization_config,
stopwords_config,
))
}
fn detect_lang(text: &str) -> Option<Lang> {
tracing::debug!("detecting locale from lexer text: {}", text);
if text.len() < TEXT_LANG_DETECT_PROCEED_OVER_CHARS {
return None;
}
let safe_text = if text.len() > TEXT_LANG_TRUNCATE_OVER_CHARS {
tracing::debug!(
"lexer text needs to be truncated, as it is too long ({}/{}): {}",
text.len(),
TEXT_LANG_TRUNCATE_OVER_CHARS,
text
);
let end_index = text.floor_char_boundary(TEXT_LANG_TRUNCATE_OVER_CHARS);
&text[..end_index]
} else {
text
};
tracing::debug!("will detect locale for lexer safe text: {}", safe_text);
if safe_text.len() < TEXT_LANG_DETECT_NGRAM_UNDER_CHARS {
tracing::debug!(
"lexer text is shorter than {} characters, using the slow method",
TEXT_LANG_DETECT_NGRAM_UNDER_CHARS
);
Self::detect_lang_slow(safe_text)
} else {
tracing::debug!(
"lexer text is equal or longer than {} characters, using the fast method",
TEXT_LANG_DETECT_NGRAM_UNDER_CHARS
);
Self::detect_lang_fast(safe_text)
}
}
fn detect_lang_slow(safe_text: &str) -> Option<Lang> {
let ngram_start = Instant::now();
match whatlang::detect(safe_text) {
Some(info) => {
let ngram_took = ngram_start.elapsed();
let mut locale = info.lang();
tracing::info!(
"[slow lexer] locale detected from text: {} ({} from {} at {}/1; {}s + {}ms)",
safe_text,
locale,
info.script(),
info.confidence(),
ngram_took.as_secs(),
ngram_took.subsec_millis()
);
if !info.is_reliable() {
tracing::debug!("[slow lexer] trying to detect locale from stopwords instead");
if let Some(alternate_locale) =
LexerStopWord::guess_lang(safe_text, info.script())
{
tracing::info!(
"[slow lexer] detected more accurate locale from stopwords: {}",
alternate_locale
);
locale = alternate_locale;
}
}
Some(locale)
}
None => {
tracing::info!(
"[slow lexer] no locale could be detected from text: {}",
safe_text
);
None
}
}
}
fn detect_lang_fast(safe_text: &str) -> Option<Lang> {
let stopwords_start = Instant::now();
match whatlang::detect_script(safe_text) {
Some(script) => {
if let Some(locale) = LexerStopWord::guess_lang(safe_text, script) {
let stopwords_took = stopwords_start.elapsed();
tracing::info!(
"[fast lexer] locale detected from text: {} ({}; {}s + {}ms)",
safe_text,
locale,
stopwords_took.as_secs(),
stopwords_took.subsec_millis()
);
Some(locale)
} else {
tracing::debug!(
"[fast lexer] trying to detect locale from fallback ngram instead"
);
whatlang::detect_lang(safe_text)
}
}
None => {
tracing::info!(
"[fast lexer] no script could be detected from text: {}",
safe_text
);
None
}
}
}
}
impl<'a> TokenLexer<'a> {
fn new(
mode: TokenLexerMode,
text: &'a str,
locale: Option<Lang>,
normalization_config: ConfigNormalization,
tokenization_config: ConfigTokenization,
stopwords_config: &'a ConfigStopwords,
) -> TokenLexer<'a> {
let tokenizer = Tokenizer::new(text, locale, &tokenization_config);
#[cfg(feature = "stemming")]
let snowball_algorithm = match &locale {
Some(locale) => super::stemming::snowball_algorithm(locale),
None => None,
};
TokenLexer {
mode,
locale,
#[cfg(feature = "stemming")]
snowball_algorithm,
tokenizer,
yields: HashSet::new(),
config: normalization_config,
stopwords: stopwords_config,
}
}
}
impl TokenLexerMode {
pub fn from_query_lang(lang: &Option<QueryGenericLang>) -> TokenLexerMode {
match lang {
Some(QueryGenericLang::Enabled(_)) => {
TokenLexerMode::NormalizeAndCleanup
}
Some(QueryGenericLang::Disabled) => {
TokenLexerMode::NormalizeOnly
}
None => {
TokenLexerMode::NormalizeAndCleanup
}
}
}
}
impl<'a> Iterator for TokenLexer<'a> {
type Item = (NormalizedToken, StoreTermHashed, usize);
fn next(&mut self) -> Option<Self::Item> {
'tokenize: for token in self.tokenizer.by_ref() {
let (mut word, original_len) = match token {
Token::Word(original_word) => {
let original_len = original_word.len();
let mut new_word = String::with_capacity(original_len);
#[cfg(debug_assertions)]
let mut current_word = new_word.clone();
let chars = caseless::Caseless::default_case_fold(original_word.chars());
for char in chars {
if self.config.diacritic_folding_enabled {
use unicode_normalization::UnicodeNormalization as _;
use unicode_normalization::char::is_combining_mark;
for char in char.nfd().filter(|c| !is_combining_mark(*c)) {
new_word.push(char);
}
} else {
new_word.push(char);
}
}
#[cfg(debug_assertions)]
{
tracing::trace!(
"Case (+ diacritic?) folding: {current_word:?} -> {new_word:?}"
);
current_word = new_word.clone();
}
#[cfg(feature = "stemming")]
if self.config.stemming_enabled {
if let Some(algo) = self.snowball_algorithm {
new_word = String::from(snowball::stem(algo, &new_word));
tracing::debug!(
"lexer stemmed word {original_word:?} into {new_word:?} using Snowball algorithm {algo:?}"
);
#[cfg(debug_assertions)]
{
tracing::trace!("Stemming: {current_word:?} -> {new_word:?}");
current_word = new_word.clone();
}
}
}
(NormalizedToken::Word(new_word), original_len)
}
Token::Special { normalized, .. } => {
let len = normalized.len();
(NormalizedToken::Special(normalized.into_owned()), len)
}
};
if self.mode.should_cleanup() && is_stopword(&word, self.locale, &self.stopwords) {
tracing::debug!("lexer did not yield word {word:?}: word is a stop-word");
continue 'tokenize;
}
if let Some(normalization) = self.config.unicode_normalization {
if let NormalizedToken::Word(word) = &mut word {
use unicode_normalization::UnicodeNormalization as _;
match normalization {
crate::config::UnicodeNormalization::Nfc => *word = word.nfc().to_string(),
crate::config::UnicodeNormalization::Nfkc => {
*word = word.nfkc().to_string()
}
}
}
}
let term_hash = StoreTermHash::from(&word);
if self.yields.contains(&term_hash) {
tracing::debug!("lexer did not yield word {word:?}: word already yielded");
continue 'tokenize;
}
tracing::debug!("lexer yielded word: {word:?}");
self.yields.insert(term_hash);
return Some((word, term_hash, original_len));
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
const NORMALIZATION_CONFIG: ConfigNormalization = ConfigNormalization {
unicode_normalization: None,
diacritic_folding_enabled: false,
stemming_enabled: false,
};
const TOKENIZATION_CONFIG: ConfigTokenization = ConfigTokenization {
detect_special_patterns: true,
compat_split_special_patterns: false,
};
static STOPWORDS_CONFIG: LazyLock<ConfigStopwords> = LazyLock::new(|| ConfigStopwords {
allow: Default::default(),
deny: Default::default(),
});
#[test]
fn test_tokenizer() {
fn test(sentence: &str, expected: Vec<Token>) {
let tokens = Tokenizer::new(sentence, Some(Lang::Eng), &TOKENIZATION_CONFIG)
.take(256) .collect::<Vec<_>>();
assert_eq!(tokens, expected, "{sentence:?}");
}
test(
"Contact jane.doe@example.org, alice@example.org or bob+foo@example.org for support.",
vec![
Token::Word("Contact"),
Token::Special {
raw: "jane.doe@example.org",
normalized: Cow::Borrowed("jane.doe@example.org"),
},
Token::Special {
raw: "alice@example.org",
normalized: Cow::Borrowed("alice@example.org"),
},
Token::Word("or"),
Token::Special {
raw: "bob+foo@example.org",
normalized: Cow::Borrowed("bob+foo@example.org"),
},
Token::Word("for"),
Token::Word("support"),
],
);
test(
"You can also call me at 555-123-4567 or +33 6 12 34 56 78 (06.12.34.56.78 / 06 12 34 56 78).",
vec![
Token::Word("You"),
Token::Word("can"),
Token::Word("also"),
Token::Word("call"),
Token::Word("me"),
Token::Word("at"),
Token::Special {
raw: "555-123-4567",
normalized: Cow::Borrowed("5551234567"),
},
Token::Word("or"),
Token::Special {
raw: "+33 6 12 34 56 78",
normalized: Cow::Borrowed("+33612345678"),
},
Token::Special {
raw: "06.12.34.56.78",
normalized: Cow::Borrowed("0612345678"),
},
Token::Special {
raw: "06 12 34 56 78",
normalized: Cow::Borrowed("0612345678"),
},
],
);
test(
"My account is 6db14cb4-b82e-4e49-8016-ef76c4290a2f.",
vec![
Token::Word("My"),
Token::Word("account"),
Token::Word("is"),
Token::Special {
raw: "6db14cb4-b82e-4e49-8016-ef76c4290a2f",
normalized: Cow::Borrowed("6db14cb4-b82e-4e49-8016-ef76c4290a2f"),
},
],
);
test(
"Check out b244423d417369795292e9f4530d0c0e6fa07625 and 927ff7701795282232dda41e023c7c6ba29d5a15 (927ff77).",
vec![
Token::Word("Check"),
Token::Word("out"),
Token::Special {
raw: "b244423d417369795292e9f4530d0c0e6fa07625",
normalized: Cow::Borrowed("b244423d417369795292e9f4530d0c0e6fa07625"),
},
Token::Word("and"),
Token::Special {
raw: "927ff7701795282232dda41e023c7c6ba29d5a15",
normalized: Cow::Borrowed("927ff7701795282232dda41e023c7c6ba29d5a15"),
},
Token::Special {
raw: "927ff77",
normalized: Cow::Borrowed("927ff77"),
},
],
);
test(
"Have a look at https://example.org/foo?id=123.",
vec![
Token::Word("Have"),
Token::Word("a"),
Token::Word("look"),
Token::Word("at"),
Token::Special {
raw: "https://example.org/foo?id=123",
normalized: Cow::Borrowed("https://example.org/foo?id=123"),
},
],
);
test(
"My domain name is example.org.",
vec![
Token::Word("My"),
Token::Word("domain"),
Token::Word("name"),
Token::Word("is"),
Token::Special {
raw: "example.org",
normalized: Cow::Borrowed("example.org"),
},
],
);
test(
"I don’t put punctuation correctly .See?",
vec![
Token::Word("I"),
Token::Word("don’t"),
Token::Word("put"),
Token::Word("punctuation"),
Token::Word("correctly"),
Token::Word("See"),
],
);
test(
"Try to ping 192.168.1.0, 0.0.0.0, 2606:4700::6812:1c68, or ::1.",
vec![
Token::Word("Try"),
Token::Word("to"),
Token::Word("ping"),
Token::Special {
raw: "192.168.1.0",
normalized: Cow::Borrowed("192.168.1.0"),
},
Token::Special {
raw: "0.0.0.0",
normalized: Cow::Borrowed("0.0.0.0"),
},
Token::Special {
raw: "2606:4700::6812:1c68",
normalized: Cow::Borrowed("2606:4700::6812:1c68"),
},
Token::Word("or"),
Token::Special {
raw: "::1",
normalized: Cow::Borrowed("::1"),
},
],
);
test(
"Contact @alice.",
vec![
Token::Word("Contact"),
Token::Special {
raw: "@alice",
normalized: Cow::Borrowed("@alice"),
},
],
);
test(
"It’s tested in test_tokenizer.",
vec![
Token::Word("It’s"),
Token::Word("tested"),
Token::Word("in"),
Token::Special {
raw: "test_tokenizer",
normalized: Cow::Borrowed("test_tokenizer"),
},
],
);
}
#[test]
fn it_cleans_token_english() {
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"The quick brown fox jumps over the lazy dog!",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
assert_eq!(token_cleaner.locale, Some(Lang::Eng));
let mut tokens = token_cleaner.map(|(token, _, _)| token.into_inner());
assert_eq!(tokens.next(), Some("quick".to_owned()));
assert_eq!(tokens.next(), Some("brown".to_owned()));
assert_eq!(tokens.next(), Some("fox".to_owned()));
assert_eq!(tokens.next(), Some("jumps".to_owned()));
assert_eq!(tokens.next(), Some("lazy".to_owned()));
assert_eq!(tokens.next(), Some("dog".to_owned()));
assert_eq!(tokens.next(), None);
}
#[test]
fn it_cleans_token_french() {
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"Le vif renard brun saute par dessus le chien paresseux.",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
assert_eq!(token_cleaner.locale, Some(Lang::Fra));
let mut tokens = token_cleaner.map(|(token, _, _)| token.into_inner());
assert_eq!(tokens.next(), Some("renard".to_owned()));
assert_eq!(tokens.next(), Some("brun".to_owned()));
assert_eq!(tokens.next(), Some("saute".to_owned()));
assert_eq!(tokens.next(), Some("chien".to_owned()));
assert_eq!(tokens.next(), Some("paresseux".to_owned()));
assert_eq!(tokens.next(), None);
}
#[cfg(feature = "tokenizer-chinese")]
#[test]
fn it_cleans_token_chinese_jieba() {
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"我们中出了一个叛徒",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
assert_eq!(token_cleaner.locale, Some(Lang::Cmn));
let mut tokens = token_cleaner.map(|(token, _, _)| token.into_inner());
assert_eq!(tokens.next(), Some("出".to_owned()));
assert_eq!(tokens.next(), Some("一个".to_owned()));
assert_eq!(tokens.next(), Some("叛徒".to_owned()));
assert_eq!(tokens.next(), None);
}
#[cfg(not(feature = "tokenizer-chinese"))]
#[test]
fn it_cleans_token_chinese_naive() {
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"快狐跨懒狗快狐跨懒狗",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
assert_eq!(token_cleaner.locale, Some(Lang::Cmn));
let mut tokens = token_cleaner.map(|(token, _, _)| token.into_inner());
assert_eq!(tokens.next(), Some("快".to_owned()));
assert_eq!(tokens.next(), Some("狐".to_owned()));
assert_eq!(tokens.next(), Some("跨".to_owned()));
assert_eq!(tokens.next(), Some("懒".to_owned()));
assert_eq!(tokens.next(), Some("狗".to_owned()));
assert_eq!(tokens.next(), None);
}
#[cfg(feature = "tokenizer-japanese")]
#[test]
fn it_cleans_token_japanese_lindera_product() {
let mut token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"関西国際空港限定トートバッグ",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
assert_eq!(token_cleaner.locale, Some(Lang::Jpn));
let mut tokens = token_cleaner.map(|(token, _, _)| token.into_inner());
assert_eq!(tokens.next(), Some("関西".to_owned()));
assert_eq!(tokens.next(), Some("国際".to_owned()));
assert_eq!(tokens.next(), Some("空港".to_owned()));
assert_eq!(tokens.next(), Some("限定".to_owned()));
assert_eq!(tokens.next(), Some("トート".to_owned()));
assert_eq!(tokens.next(), Some("バッグ".to_owned()));
assert_eq!(tokens.next(), None);
}
#[cfg(feature = "tokenizer-japanese")]
#[test]
fn it_cleans_token_japanese_lindera_food() {
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"𠮷野家",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
assert_eq!(token_cleaner.locale, None);
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"ヱビスビール",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
assert_eq!(token_cleaner.locale, None);
}
#[cfg(feature = "tokenizer-japanese")]
#[test]
fn it_cleans_token_japanese_lindera_sentence() {
let mut token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"𠮷野家でヱビスビールを飲んだ",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
assert_eq!(token_cleaner.locale, Some(Lang::Jpn));
let mut tokens = token_cleaner.map(|(token, _, _)| token.into_inner());
assert_eq!(tokens.next(), Some("𠮷".to_owned()));
assert_eq!(tokens.next(), Some("野家".to_owned()));
assert_eq!(tokens.next(), Some("ヱビス".to_owned()));
assert_eq!(tokens.next(), Some("ビール".to_owned()));
assert_eq!(tokens.next(), Some("飲ん".to_owned()));
assert_eq!(tokens.next(), None);
}
#[test]
fn it_cleans_token_emojis() {
let mut token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"🚀 🙋♂️🙋♂️🙋♂️",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
assert_eq!(token_cleaner.locale, None);
assert_eq!(token_cleaner.next(), None);
}
#[test]
fn it_cleans_token_lang_hinted() {
let token_cleaner_right = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
Some(Lang::Eng),
"This will be cleaned properly, as English was hinted rightfully so.",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
let token_cleaner_wrong = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
Some(Lang::Fra),
"This will not be cleaned properly, as French was hinted but this is English.",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
assert_eq!(token_cleaner_right.locale, Some(Lang::Eng));
assert_eq!(token_cleaner_wrong.locale, Some(Lang::Fra));
let mut tokens_right = token_cleaner_right.map(|(token, _, _)| token.into_inner());
let mut tokens_wrong = token_cleaner_wrong.map(|(token, _, _)| token.into_inner());
assert_eq!(tokens_right.next(), Some("cleaned".to_owned()));
assert_eq!(tokens_wrong.next(), Some("this".to_owned()));
}
#[test]
fn it_detects_lang_english_regular() {
assert_eq!(
TokenLexerBuilder::detect_lang("The quick brown fox jumps over the lazy dog!"),
Some(Lang::Eng)
);
}
#[test]
fn it_detects_lang_english_long() {
assert_eq!(
TokenLexerBuilder::detect_lang(
r#"Running an electrical current through water splits it into oxygen and hydrogen,
the latter of which can be used as a reliable, zero-emission fuel source. In the past,
the process of purifying water beforehand was too energy intensive for this process to
be useful — but now scientists have figured out how to skip the process altogether and
convert seawater into usable hydrogen"#
),
Some(Lang::Eng)
);
}
#[test]
fn it_doesnt_detect_lang_english_tiny() {
assert_eq!(TokenLexerBuilder::detect_lang("The quick"), None);
}
}
#[cfg(all(feature = "benchmark", test))]
mod benches {
extern crate test;
use super::*;
use test::Bencher;
#[bench]
fn bench_normalize_token_french_build(b: &mut Bencher) {
b.iter(|| {
TokenLexerBuilder::from(
TokenLexerMode::NormalizeOnly,
"Le vif renard brun saute par dessus le chien paresseux.",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
});
}
#[bench]
fn bench_normalize_token_french_exhaust(b: &mut Bencher) {
b.iter(|| {
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeOnly,
"Le vif renard brun saute par dessus le chien paresseux.",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
token_cleaner.map(|value| value.1).collect::<Vec<u32>>()
});
}
#[bench]
fn bench_clean_token_english_regular_build(b: &mut Bencher) {
b.iter(|| {
TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"The quick brown fox jumps over the lazy dog!",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
});
}
#[bench]
fn bench_clean_token_english_regular_exhaust(b: &mut Bencher) {
b.iter(|| {
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"The quick brown fox jumps over the lazy dog!",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
token_cleaner.map(|value| value.1).collect::<Vec<u32>>()
});
}
#[bench]
fn bench_clean_token_english_long_exhaust(b: &mut Bencher) {
b.iter(|| {
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
r#"Running an electrical current through water splits it into oxygen and hydrogen,
the latter of which can be used as a reliable, zero-emission fuel source. In the
past, the process of purifying water beforehand was too energy intensive for this
process to be useful — but now scientists have figured out how to skip the process
altogether and convert seawater into usable hydrogen"#,
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
token_cleaner.map(|value| value.1).collect::<Vec<u32>>()
});
}
#[bench]
fn bench_clean_token_english_hinted_build(b: &mut Bencher) {
b.iter(|| {
TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup(Some(Lang::Eng)),
"The quick brown fox jumps over the lazy dog!",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
});
}
#[bench]
fn bench_clean_token_english_hinted_exhaust(b: &mut Bencher) {
b.iter(|| {
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup(Some(Lang::Eng)),
"The quick brown fox jumps over the lazy dog!",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
token_cleaner.map(|value| value.1).collect::<Vec<u32>>()
});
}
#[bench]
fn bench_clean_token_chinese_build(b: &mut Bencher) {
b.iter(|| {
TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"我们中出了一个叛徒",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
});
}
#[bench]
fn bench_clean_token_chinese_exhaust(b: &mut Bencher) {
b.iter(|| {
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"我们中出了一个叛徒",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
token_cleaner.map(|value| value.1).collect::<Vec<u32>>()
});
}
#[bench]
fn bench_clean_token_japanese_build(b: &mut Bencher) {
b.iter(|| {
TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"関西国際空港限定トートバッグ",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
});
}
#[bench]
fn bench_clean_token_japanese_exhaust(b: &mut Bencher) {
b.iter(|| {
let token_cleaner = TokenLexerBuilder::from(
TokenLexerMode::NormalizeAndCleanup,
None,
"関西国際空港限定トートバッグ",
NORMALIZATION_CONFIG,
TOKENIZATION_CONFIG,
&STOPWORDS_CONFIG,
)
.unwrap();
token_cleaner.map(|value| value.1).collect::<Vec<u32>>()
});
}
#[bench]
fn bench_detect_lang_english_short(b: &mut Bencher) {
b.iter(|| TokenLexerBuilder::detect_lang("The quick brown fox."));
}
#[bench]
fn bench_detect_lang_english_regular(b: &mut Bencher) {
b.iter(|| TokenLexerBuilder::detect_lang("The quick brown fox jumps over the lazy dog!"));
}
#[bench]
fn bench_detect_lang_english_long(b: &mut Bencher) {
b.iter(|| {
TokenLexerBuilder::detect_lang(
r#"Running an electrical current through water splits it into oxygen and hydrogen,
the latter of which can be used as a reliable, zero-emission fuel source. In the past,
the process of purifying water beforehand was too energy intensive for this process to
be useful — but now scientists have figured out how to skip the process altogether and
convert seawater into usable hydrogen"#,
)
});
}
#[bench]
fn bench_dont_detect_lang_english_tiny(b: &mut Bencher) {
b.iter(|| TokenLexerBuilder::detect_lang("The quick"));
}
}