#[cfg(feature = "advanced-tokenizer")]
use tantivy::tokenizer::{
AsciiFoldingFilter, Language, LowerCaser, RemoveLongFilter, SimpleTokenizer, Stemmer,
StopWordFilter, TextAnalyzer, TokenStream,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AdvancedTokenizerConfig {
pub language: Language,
pub max_token_length: usize,
pub remove_stopwords: bool,
pub apply_stemming: bool,
}
impl Default for AdvancedTokenizerConfig {
fn default() -> Self {
Self {
language: Language::English,
max_token_length: 40,
remove_stopwords: true,
apply_stemming: true,
}
}
}
#[cfg(feature = "advanced-tokenizer")]
pub fn tokenize_advanced(text: &str, config: &AdvancedTokenizerConfig) -> Vec<String> {
let mut tokenizer = if config.apply_stemming && config.remove_stopwords {
if let Some(stopword_filter) = StopWordFilter::new(config.language) {
TextAnalyzer::builder(SimpleTokenizer::default())
.filter(RemoveLongFilter::limit(config.max_token_length))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.filter(Stemmer::new(config.language))
.filter(stopword_filter)
.build()
} else {
TextAnalyzer::builder(SimpleTokenizer::default())
.filter(RemoveLongFilter::limit(config.max_token_length))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.filter(Stemmer::new(config.language))
.build()
}
} else if config.apply_stemming {
TextAnalyzer::builder(SimpleTokenizer::default())
.filter(RemoveLongFilter::limit(config.max_token_length))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.filter(Stemmer::new(config.language))
.build()
} else if config.remove_stopwords {
if let Some(stopword_filter) = StopWordFilter::new(config.language) {
TextAnalyzer::builder(SimpleTokenizer::default())
.filter(RemoveLongFilter::limit(config.max_token_length))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.filter(stopword_filter)
.build()
} else {
TextAnalyzer::builder(SimpleTokenizer::default())
.filter(RemoveLongFilter::limit(config.max_token_length))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.build()
}
} else {
TextAnalyzer::builder(SimpleTokenizer::default())
.filter(RemoveLongFilter::limit(config.max_token_length))
.filter(LowerCaser)
.filter(AsciiFoldingFilter)
.build()
};
let mut stream = tokenizer.token_stream(text);
let mut tokens = Vec::new();
while let Some(token) = stream.next() {
tokens.push(token.text.to_string());
}
tokens
}
#[cfg(feature = "advanced-tokenizer")]
pub fn tokenize_advanced_default(text: &str) -> Vec<String> {
tokenize_advanced(text, &AdvancedTokenizerConfig::default())
}
pub fn is_advanced_tokenizer_available() -> bool {
cfg!(feature = "advanced-tokenizer")
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "advanced-tokenizer")]
#[test]
fn test_advanced_tokenizer_basic() {
let text = "The quick brown fox jumps over the lazy dog";
let tokens = tokenize_advanced_default(text);
assert!(!tokens.is_empty());
assert!(tokens
.iter()
.any(|t| t.contains("quick") || t.contains("brown")));
}
#[cfg(feature = "advanced-tokenizer")]
#[test]
fn test_advanced_tokenizer_stemming() {
let config = AdvancedTokenizerConfig {
apply_stemming: true,
remove_stopwords: false,
..Default::default()
};
let text = "The jumping fox runs quickly";
let tokens = tokenize_advanced(text, &config);
assert!(tokens.iter().any(|t| t == "jump" || t.contains("jump")));
assert!(tokens.iter().any(|t| t.contains("quick")));
}
#[cfg(feature = "advanced-tokenizer")]
#[test]
fn test_advanced_tokenizer_stopwords() {
let config_with_stopwords = AdvancedTokenizerConfig {
remove_stopwords: true,
apply_stemming: false,
..Default::default()
};
let config_without_stopwords = AdvancedTokenizerConfig {
remove_stopwords: false,
apply_stemming: false,
..Default::default()
};
let text = "The quick brown fox jumps over the lazy dog";
let tokens_with = tokenize_advanced(text, &config_with_stopwords);
let tokens_without = tokenize_advanced(text, &config_without_stopwords);
assert!(tokens_with.len() < tokens_without.len());
assert!(!tokens_with.iter().any(|t| t == "the"));
assert!(!tokens_with.iter().any(|t| t == "is"));
}
#[cfg(feature = "advanced-tokenizer")]
#[test]
fn test_advanced_tokenizer_multilingual_spanish() {
let config = AdvancedTokenizerConfig {
language: Language::Spanish,
..Default::default()
};
let text = "El perro rápido salta sobre el perro perezoso";
let tokens = tokenize_advanced(text, &config);
assert!(!tokens.is_empty());
assert!(!tokens.iter().any(|t| t == "el"));
assert!(!tokens.iter().any(|t| t == "sobre"));
}
#[cfg(feature = "advanced-tokenizer")]
#[test]
fn test_advanced_tokenizer_multilingual_french() {
let config = AdvancedTokenizerConfig {
language: Language::French,
..Default::default()
};
let text = "Le chien rapide saute par-dessus le chien paresseux";
let tokens = tokenize_advanced(text, &config);
assert!(!tokens.is_empty());
}
#[cfg(feature = "advanced-tokenizer")]
#[test]
fn test_advanced_tokenizer_multilingual_german() {
let config = AdvancedTokenizerConfig {
language: Language::German,
..Default::default()
};
let text = "Der schnelle Hund springt über den faulen Hund";
let tokens = tokenize_advanced(text, &config);
assert!(!tokens.is_empty());
}
#[cfg(feature = "advanced-tokenizer")]
#[test]
fn test_advanced_tokenizer_length_filter() {
let config = AdvancedTokenizerConfig {
max_token_length: 5,
..Default::default()
};
let text = "The quick brown fox jumps over the lazy dog";
let tokens = tokenize_advanced(text, &config);
for token in &tokens {
assert!(token.len() <= 5);
}
}
#[cfg(feature = "advanced-tokenizer")]
#[test]
fn test_advanced_tokenizer_empty_text() {
let text = "";
let tokens = tokenize_advanced_default(text);
assert!(tokens.is_empty());
}
#[cfg(feature = "advanced-tokenizer")]
#[test]
fn test_advanced_tokenizer_unicode_folding() {
let text = "Café naïve résumé";
let tokens = tokenize_advanced_default(text);
assert!(!tokens.is_empty());
assert!(tokens.iter().any(|t| t.contains("cafe")));
}
#[cfg(feature = "advanced-tokenizer")]
#[test]
fn test_advanced_tokenizer_combined_features() {
let config = AdvancedTokenizerConfig {
apply_stemming: true,
remove_stopwords: true,
..Default::default()
};
let text = "The jumping fox runs quickly and the lazy dog";
let tokens = tokenize_advanced(text, &config);
assert!(!tokens.is_empty());
assert!(!tokens.iter().any(|t| t == "the"));
assert!(!tokens.iter().any(|t| t == "and"));
assert!(tokens.iter().any(|t| t.contains("jump")));
}
#[test]
fn test_advanced_tokenizer_availability() {
let _ = is_advanced_tokenizer_available();
}
}