///|
/// Porter2 English stemming filter compatible with Tantivy's `en_stem` era.
///
/// The filter expects lowercase English terms, so the built-in `en_stem`
/// pipeline places LowerCaseFilter before it. Non-ASCII and mixed-script terms
/// pass through unchanged.
pub struct EnglishStemmerFilter {}
///|
priv struct EnglishStemmerTokenStream {
input : &TokenStream
mut current : Token?
}
///|
pub fn EnglishStemmerFilter::new() -> EnglishStemmerFilter {
EnglishStemmerFilter::{ }
}
///|
fn english_is_vowel(word : Array[Char], index : Int) -> Bool {
if index < 0 || index >= word.length() {
return false
}
match word[index] {
'a' | 'e' | 'i' | 'o' | 'u' | 'y' => true
_ => false
}
}
///|
fn english_word_supported(text : String) -> Bool {
if text.length() == 0 {
return false
}
for ch in text {
if !((ch >= 'a' && ch <= 'z') || ch.to_int() == 0x27) {
return false
}
}
true
}
///|
fn chars_start_with(word : Array[Char], prefix : String) -> Bool {
let expected = prefix.to_array()
if expected.length() > word.length() {
return false
}
for index in 0.. Bool {
let expected = suffix.to_array()
if expected.length() > word.length() {
return false
}
let offset = word.length() - expected.length()
for index in 0.. Array[Char] {
let suffix_length = suffix.length()
let output : Array[Char] = []
let prefix_length = word.length() - suffix_length
for index in 0.. Array[Char] {
let output : Array[Char] = []
for index in 0.. Int {
if start >= word.length() {
return word.length()
}
for index in (start + 1).. Int {
if chars_start_with(word, "gener") {
5
} else if chars_start_with(word, "commun") {
6
} else if chars_start_with(word, "arsen") {
5
} else {
english_region(word, 0)
}
}
///|
fn suffix_in_region(word : Array[Char], suffix : String, region : Int) -> Bool {
chars_end_with(word, suffix) && word.length() - suffix.length() >= region
}
///|
fn english_contains_vowel(word : Array[Char], end : Int) -> Bool {
for index in 0.. Bool {
let length = word.length()
if length == 2 {
return english_is_vowel(word, 0) && !english_is_vowel(word, 1)
}
if length < 3 {
return false
}
let last = word[length - 1]
!english_is_vowel(word, length - 3) &&
english_is_vowel(word, length - 2) &&
!english_is_vowel(word, length - 1) &&
last != 'w' &&
last != 'x' &&
last != 'Y'
}
///|
fn english_exception_one(word : String) -> String? {
match word {
"skis" => Some("ski")
"skies" => Some("sky")
"dying" => Some("die")
"lying" => Some("lie")
"tying" => Some("tie")
"idly" => Some("idl")
"gently" => Some("gentl")
"ugly" => Some("ugli")
"early" => Some("earli")
"only" => Some("onli")
"singly" => Some("singl")
"sky" | "news" | "howe" | "atlas" | "cosmos" | "bias" | "andes" =>
Some(word)
_ => None
}
}
///|
fn english_exception_two(word : Array[Char]) -> Bool {
let text = String::from_array(word[:])
text == "inning" ||
text == "outing" ||
text == "canning" ||
text == "herring" ||
text == "earring" ||
text == "proceed" ||
text == "exceed" ||
text == "succeed"
}
///|
fn english_step_zero(word : Array[Char]) -> Array[Char] {
if chars_end_with(word, "'s'") {
replace_english_suffix(word, "'s'", "")
} else if chars_end_with(word, "'s") {
replace_english_suffix(word, "'s", "")
} else if chars_end_with(word, "'") {
replace_english_suffix(word, "'", "")
} else {
word
}
}
///|
fn english_step_one_a(word : Array[Char]) -> Array[Char] {
if chars_end_with(word, "sses") {
return replace_english_suffix(word, "sses", "ss")
}
if chars_end_with(word, "ied") || chars_end_with(word, "ies") {
let suffix = if chars_end_with(word, "ied") { "ied" } else { "ies" }
let replacement = if word.length() - suffix.length() > 1 {
"i"
} else {
"ie"
}
return replace_english_suffix(word, suffix, replacement)
}
if chars_end_with(word, "us") || chars_end_with(word, "ss") {
return word
}
if chars_end_with(word, "s") &&
word.length() > 2 &&
english_contains_vowel(word, word.length() - 2) {
return replace_english_suffix(word, "s", "")
}
word
}
///|
fn english_step_one_b(original : Array[Char], r1 : Int) -> Array[Char] {
let mut word = original
if chars_end_with(word, "eedly") {
if suffix_in_region(word, "eedly", r1) {
return replace_english_suffix(word, "eedly", "ee")
}
return word
}
if chars_end_with(word, "eed") {
if suffix_in_region(word, "eed", r1) {
return replace_english_suffix(word, "eed", "ee")
}
return word
}
let suffix = if chars_end_with(word, "ingly") {
"ingly"
} else if chars_end_with(word, "edly") {
"edly"
} else if chars_end_with(word, "ing") {
"ing"
} else if chars_end_with(word, "ed") {
"ed"
} else {
""
}
if suffix == "" {
return word
}
let stem_length = word.length() - suffix.length()
if !english_contains_vowel(word, stem_length) {
return word
}
word = truncate_english_word(word, stem_length)
if chars_end_with(word, "at") ||
chars_end_with(word, "bl") ||
chars_end_with(word, "iz") {
return replace_english_suffix(word, "", "e")
}
if chars_end_with(word, "bb") ||
chars_end_with(word, "dd") ||
chars_end_with(word, "ff") ||
chars_end_with(word, "gg") ||
chars_end_with(word, "mm") ||
chars_end_with(word, "nn") ||
chars_end_with(word, "pp") ||
chars_end_with(word, "rr") ||
chars_end_with(word, "tt") {
return truncate_english_word(word, word.length() - 1)
}
if r1 >= word.length() && english_short_syllable(word) {
return replace_english_suffix(word, "", "e")
}
word
}
///|
fn english_step_one_c(word : Array[Char]) -> Array[Char] {
if word.length() > 2 {
let final_index = word.length() - 1
if (word[final_index] == 'y' || word[final_index] == 'Y') &&
!english_is_vowel(word, final_index - 1) {
let output = truncate_english_word(word, final_index)
output.push('i')
return output
}
}
word
}
///|
fn english_step_two(original : Array[Char], r1 : Int) -> Array[Char] {
let word = original
if suffix_in_region(word, "ization", r1) {
replace_english_suffix(word, "ization", "ize")
} else if suffix_in_region(word, "ational", r1) {
replace_english_suffix(word, "ational", "ate")
} else if suffix_in_region(word, "fulness", r1) {
replace_english_suffix(word, "fulness", "ful")
} else if suffix_in_region(word, "ousness", r1) {
replace_english_suffix(word, "ousness", "ous")
} else if suffix_in_region(word, "iveness", r1) {
replace_english_suffix(word, "iveness", "ive")
} else if suffix_in_region(word, "tional", r1) {
replace_english_suffix(word, "tional", "tion")
} else if suffix_in_region(word, "biliti", r1) {
replace_english_suffix(word, "biliti", "ble")
} else if suffix_in_region(word, "lessli", r1) {
replace_english_suffix(word, "lessli", "less")
} else if suffix_in_region(word, "entli", r1) {
replace_english_suffix(word, "entli", "ent")
} else if suffix_in_region(word, "ation", r1) {
replace_english_suffix(word, "ation", "ate")
} else if suffix_in_region(word, "alism", r1) {
replace_english_suffix(word, "alism", "al")
} else if suffix_in_region(word, "aliti", r1) {
replace_english_suffix(word, "aliti", "al")
} else if suffix_in_region(word, "fulli", r1) {
replace_english_suffix(word, "fulli", "ful")
} else if suffix_in_region(word, "ousli", r1) {
replace_english_suffix(word, "ousli", "ous")
} else if suffix_in_region(word, "iviti", r1) {
replace_english_suffix(word, "iviti", "ive")
} else if suffix_in_region(word, "enci", r1) {
replace_english_suffix(word, "enci", "ence")
} else if suffix_in_region(word, "anci", r1) {
replace_english_suffix(word, "anci", "ance")
} else if suffix_in_region(word, "abli", r1) {
replace_english_suffix(word, "abli", "able")
} else if suffix_in_region(word, "izer", r1) {
replace_english_suffix(word, "izer", "ize")
} else if suffix_in_region(word, "ator", r1) {
replace_english_suffix(word, "ator", "ate")
} else if suffix_in_region(word, "alli", r1) {
replace_english_suffix(word, "alli", "al")
} else if suffix_in_region(word, "bli", r1) {
replace_english_suffix(word, "bli", "ble")
} else if suffix_in_region(word, "ogi", r1) {
let prefix_index = word.length() - "ogi".length() - 1
if prefix_index >= 0 && word[prefix_index] == 'l' {
replace_english_suffix(word, "ogi", "og")
} else {
word
}
} else if suffix_in_region(word, "li", r1) {
let prefix_index = word.length() - 3
if prefix_index >= 0 {
match word[prefix_index] {
'c' | 'd' | 'e' | 'g' | 'h' | 'k' | 'm' | 'n' | 'r' | 't' =>
replace_english_suffix(word, "li", "")
_ => word
}
} else {
word
}
} else {
word
}
}
///|
fn english_step_three(word : Array[Char], r1 : Int, r2 : Int) -> Array[Char] {
if suffix_in_region(word, "ational", r1) {
replace_english_suffix(word, "ational", "ate")
} else if suffix_in_region(word, "tional", r1) {
replace_english_suffix(word, "tional", "tion")
} else if suffix_in_region(word, "alize", r1) {
replace_english_suffix(word, "alize", "al")
} else if suffix_in_region(word, "icate", r1) {
replace_english_suffix(word, "icate", "ic")
} else if suffix_in_region(word, "iciti", r1) {
replace_english_suffix(word, "iciti", "ic")
} else if suffix_in_region(word, "ical", r1) {
replace_english_suffix(word, "ical", "ic")
} else if suffix_in_region(word, "ful", r1) {
replace_english_suffix(word, "ful", "")
} else if suffix_in_region(word, "ness", r1) {
replace_english_suffix(word, "ness", "")
} else if suffix_in_region(word, "ative", r2) {
replace_english_suffix(word, "ative", "")
} else {
word
}
}
///|
fn english_step_four(word : Array[Char], r2 : Int) -> Array[Char] {
if suffix_in_region(word, "ement", r2) {
replace_english_suffix(word, "ement", "")
} else if suffix_in_region(word, "ance", r2) {
replace_english_suffix(word, "ance", "")
} else if suffix_in_region(word, "ence", r2) {
replace_english_suffix(word, "ence", "")
} else if suffix_in_region(word, "able", r2) {
replace_english_suffix(word, "able", "")
} else if suffix_in_region(word, "ible", r2) {
replace_english_suffix(word, "ible", "")
} else if suffix_in_region(word, "ment", r2) {
replace_english_suffix(word, "ment", "")
} else if suffix_in_region(word, "ant", r2) {
replace_english_suffix(word, "ant", "")
} else if suffix_in_region(word, "ent", r2) {
replace_english_suffix(word, "ent", "")
} else if suffix_in_region(word, "ism", r2) {
replace_english_suffix(word, "ism", "")
} else if suffix_in_region(word, "ate", r2) {
replace_english_suffix(word, "ate", "")
} else if suffix_in_region(word, "iti", r2) {
replace_english_suffix(word, "iti", "")
} else if suffix_in_region(word, "ous", r2) {
replace_english_suffix(word, "ous", "")
} else if suffix_in_region(word, "ive", r2) {
replace_english_suffix(word, "ive", "")
} else if suffix_in_region(word, "ize", r2) {
replace_english_suffix(word, "ize", "")
} else if suffix_in_region(word, "al", r2) {
replace_english_suffix(word, "al", "")
} else if suffix_in_region(word, "er", r2) {
replace_english_suffix(word, "er", "")
} else if suffix_in_region(word, "ic", r2) {
replace_english_suffix(word, "ic", "")
} else if suffix_in_region(word, "ion", r2) {
let prefix_index = word.length() - 4
if prefix_index >= 0 &&
(word[prefix_index] == 's' || word[prefix_index] == 't') {
replace_english_suffix(word, "ion", "")
} else {
word
}
} else {
word
}
}
///|
fn english_step_five(word : Array[Char], r1 : Int, r2 : Int) -> Array[Char] {
if chars_end_with(word, "e") {
let suffix_index = word.length() - 1
if suffix_index >= r2 {
return truncate_english_word(word, suffix_index)
}
if suffix_index >= r1 {
let stem = truncate_english_word(word, suffix_index)
if !english_short_syllable(stem) {
return stem
}
}
return word
}
if chars_end_with(word, "ll") && word.length() - 1 >= r2 {
return truncate_english_word(word, word.length() - 1)
}
word
}
///|
fn restore_english_y(word : Array[Char]) -> Array[Char] {
let output : Array[Char] = []
for ch in word {
output.push(if ch == 'Y' { 'y' } else { ch })
}
output
}
///|
fn stem_english_word(text : String) -> String {
if !english_word_supported(text) {
return text
}
let mut word = text.to_array()
if word.length() > 0 && word[0].to_int() == 0x27 {
let without_apostrophe : Array[Char] = []
for index in 1.. return stem
None => ()
}
if word[0] == 'y' {
word[0] = 'Y'
}
for index in 1.. {
self.current = Some({
text: stem_english_word(token.text),
position: token.position,
position_length: token.position_length,
start_offset: token.start_offset,
end_offset: token.end_offset,
})
true
}
None => {
self.current = None
false
}
}
}
///|
impl TokenStream for EnglishStemmerTokenStream with fn token(self) {
self.current
}
///|
pub impl TokenFilter for EnglishStemmerFilter with fn transform(_self, input) {
EnglishStemmerTokenStream::{ input, current: None }
}
///|
/// Creates the same high-level pipeline as Tantivy's `en_stem` preset.
pub fn english_stem_analyzer() -> TextAnalyzer {
let analyzer = TextAnalyzer::new(SimpleTokenizer::new())
analyzer.add_filter(RemoveLongFilter::new(40))
analyzer.add_filter(LowerCaseFilter::new())
analyzer.add_filter(EnglishStemmerFilter::new())
analyzer
}