///|
/// 分词器配置。
///
/// 默认行为:
/// - 英文/数字按字母数字串切分并转小写,过滤长度小于 `min_token_len` 的词
/// - 中日韩文字按单字 + 相邻二字 bigram 切分
/// - 可配置停用词与英文词干化
pub struct Tokenizer {
min_token_len : Int
bigram_cjk : Bool
stem_english : Bool
stopwords : Array[String]
} derive(ToJson, FromJson)
///|
pub fn Tokenizer::new(
min_token_len? : Int = 2,
bigram_cjk? : Bool = true,
stem_english? : Bool = false,
stopwords? : Array[String] = [],
) -> Tokenizer {
{ min_token_len, bigram_cjk, stem_english, stopwords, }
}
///|
/// 常用中文停用词。
pub fn default_stopwords() -> Array[String] {
[
"的", "了", "和", "是", "在", "与", "及", "或", "等", "之", "一个",
"我们", "你们", "他们", "这个", "那个", "可以", "进行", "以及",
"the", "a", "an", "is", "are", "was", "were", "to", "of", "in", "on", "for",
"and", "or", "with", "this", "that", "it",
]
}
///|
/// 对文本分词,返回词条数组。
pub fn Tokenizer::tokenize(self : Tokenizer, text : String) -> Array[String] {
let out : Array[String] = []
let ascii : Array[Char] = []
let mut last_cjk : Char? = None
for c in text.iter() {
if is_ascii_alnum(c) {
last_cjk = None
ascii.push(ascii_lower(c))
} else if is_cjk(c) {
self.flush_ascii(out, ascii)
ascii.clear()
let unigram = StringView::from_iter([c].iter()).to_owned()
// 中文单字不卡最小长度,保证单字查询可用;仅过滤停用词。
if !self.stopwords.contains(unigram) {
out.push(unigram)
}
if self.bigram_cjk {
match last_cjk {
Some(prev) => {
let bigram = StringView::from_iter([prev, c].iter()).to_owned()
if self.keep(bigram) {
out.push(bigram)
}
}
None => ()
}
}
last_cjk = Some(c)
} else {
self.flush_ascii(out, ascii)
ascii.clear()
last_cjk = None
}
}
self.flush_ascii(out, ascii)
out
}
///|
/// 估算文本的 token 数(按当前分词结果计数,供上下文预算使用)。
pub fn Tokenizer::estimate_tokens(self : Tokenizer, text : String) -> Int {
self.tokenize(text).length()
}
///|
fn Tokenizer::flush_ascii(
self : Tokenizer,
out : Array[String],
chars : Array[Char],
) -> Unit {
if chars.is_empty() {
return
}
let word = StringView::from_iter(chars.iter()).to_owned()
let result = if self.stem_english { stem_word(word) } else { word }
if self.keep(result) {
out.push(result)
}
}
///|
fn Tokenizer::keep(self : Tokenizer, token : String) -> Bool {
token.length() >= self.min_token_len && !self.stopwords.contains(token)
}
///|
fn is_ascii_alnum(c : Char) -> Bool {
let n = c.to_int()
(n >= 48 && n <= 57) || (n >= 65 && n <= 90) || (n >= 97 && n <= 122)
}
///|
fn is_cjk(c : Char) -> Bool {
let n = c.to_int()
(n >= 0x3400 && n <= 0x4DBF) ||
(n >= 0x4E00 && n <= 0x9FFF) ||
(n >= 0xF900 && n <= 0xFAFF) ||
(n >= 0x3040 && n <= 0x30FF)
}
///|
fn ascii_lower(c : Char) -> Char {
let n = c.to_int()
if n >= 65 && n <= 90 {
(n + 32).unsafe_to_char()
} else {
c
}
}
///|
fn stem_word(w : String) -> String {
let n = w.length()
if n > 5 && w.has_suffix("ing") {
w.exact_view(start=0, end=n - 3).to_owned()
} else if n > 4 && w.has_suffix("ed") {
w.exact_view(start=0, end=n - 2).to_owned()
} else if n > 4 && w.has_suffix("ly") {
w.exact_view(start=0, end=n - 2).to_owned()
} else if n > 4 && w.has_suffix("es") {
w.exact_view(start=0, end=n - 2).to_owned()
} else if n > 3 && w.has_suffix("s") && !w.has_suffix("ss") {
w.exact_view(start=0, end=n - 1).to_owned()
} else {
w
}
}