Skip to main content

lc_rag/bm25/
tokenizer.rs

1// src/retrieval/bm25/tokenizer.rs
2//! 分词器实现
3//!
4//! 支持英文和中文的简单分词
5
6use std::collections::HashSet;
7
8/// 分词器
9pub struct Tokenizer {
10    /// 是否保留停用词
11    keep_stopwords: bool,
12
13    /// 英文停用词表
14    stopwords_en: HashSet<String>,
15
16    /// 中文停用词表
17    stopwords_zh: HashSet<String>,
18}
19
20impl Tokenizer {
21    /// 创建新的分词器(过滤停用词)
22    pub fn new() -> Self {
23        Self {
24            keep_stopwords: false,
25            stopwords_en: Self::default_stopwords_en(),
26            stopwords_zh: Self::default_stopwords_zh(),
27        }
28    }
29
30    /// 创建保留停用词的分词器
31    pub fn with_stopwords() -> Self {
32        Self {
33            keep_stopwords: true,
34            stopwords_en: HashSet::new(),
35            stopwords_zh: HashSet::new(),
36        }
37    }
38
39    /// 默认英文停用词
40    fn default_stopwords_en() -> HashSet<String> {
41        [
42            "a",
43            "an",
44            "the",
45            "is",
46            "are",
47            "was",
48            "were",
49            "be",
50            "been",
51            "being",
52            "have",
53            "has",
54            "had",
55            "do",
56            "does",
57            "did",
58            "will",
59            "would",
60            "could",
61            "should",
62            "may",
63            "might",
64            "must",
65            "shall",
66            "can",
67            "need",
68            "dare",
69            "ought",
70            "used",
71            "to",
72            "of",
73            "in",
74            "for",
75            "on",
76            "with",
77            "at",
78            "by",
79            "from",
80            "as",
81            "into",
82            "through",
83            "during",
84            "before",
85            "after",
86            "above",
87            "below",
88            "between",
89            "under",
90            "again",
91            "further",
92            "then",
93            "once",
94            "here",
95            "there",
96            "when",
97            "where",
98            "why",
99            "how",
100            "all",
101            "each",
102            "few",
103            "more",
104            "most",
105            "other",
106            "some",
107            "such",
108            "no",
109            "nor",
110            "not",
111            "only",
112            "own",
113            "same",
114            "so",
115            "than",
116            "too",
117            "very",
118            "just",
119            "and",
120            "but",
121            "if",
122            "or",
123            "because",
124            "until",
125            "while",
126            "about",
127            "against",
128            "i",
129            "me",
130            "my",
131            "myself",
132            "we",
133            "our",
134            "ours",
135            "ourselves",
136            "you",
137            "your",
138            "yours",
139            "yourself",
140            "yourselves",
141            "he",
142            "him",
143            "his",
144            "himself",
145            "she",
146            "her",
147            "hers",
148            "herself",
149            "it",
150            "its",
151            "itself",
152            "they",
153            "them",
154            "their",
155            "theirs",
156            "themselves",
157            "what",
158            "which",
159            "who",
160            "whom",
161            "this",
162            "that",
163            "these",
164            "those",
165            "am",
166            "aren",
167        ]
168        .iter()
169        .map(|s| s.to_string())
170        .collect()
171    }
172
173    /// 默认中文停用词
174    fn default_stopwords_zh() -> HashSet<String> {
175        [
176            "的", "了", "在", "是", "我", "有", "和", "就", "不", "人", "都", "一", "一个", "上",
177            "也", "很", "到", "说", "要", "去", "你", "会", "着", "没有", "看", "好", "自己", "这",
178            "那", "里", "为", "什么", "他", "她", "它", "们", "这个", "那个", "可以", "把", "能",
179            "被", "与", "及", "等", "或", "而", "但", "如", "若", "则", "因", "所以", "因为",
180            "但是", "然而", "不过", "虽然", "即使", "如果", "只要",
181        ]
182        .iter()
183        .map(|s| s.to_string())
184        .collect()
185    }
186
187    /// 分词(自动检测语言)
188    pub fn tokenize(&self, text: &str) -> Vec<String> {
189        let mut terms = Vec::new();
190
191        for word in self.tokenize_mixed(text) {
192            if self.keep_stopwords || !self.is_stopword(&word) {
193                terms.push(word);
194            }
195        }
196
197        terms
198    }
199
200    /// 中英文混合分词
201    fn tokenize_mixed(&self, text: &str) -> Vec<String> {
202        let mut terms = Vec::new();
203        let mut current_english = String::new();
204        let mut current_chinese = String::new();
205
206        for ch in text.chars() {
207            if ch.is_ascii_alphabetic() || ch.is_ascii_digit() {
208                // 英文/数字字符
209                if !current_chinese.is_empty() {
210                    // 先处理累积的中文
211                    terms.extend(self.tokenize_chinese_segment(&current_chinese));
212                    current_chinese.clear();
213                }
214                current_english.push(ch.to_ascii_lowercase());
215            } else if Self::is_chinese_char(ch) {
216                // 中文字符
217                if !current_english.is_empty() {
218                    // 先处理累积的英文
219                    terms.push(current_english.clone());
220                    current_english.clear();
221                }
222                current_chinese.push(ch);
223            } else {
224                // 其他字符(标点、空格等)
225                if !current_english.is_empty() {
226                    terms.push(current_english.clone());
227                    current_english.clear();
228                }
229                if !current_chinese.is_empty() {
230                    terms.extend(self.tokenize_chinese_segment(&current_chinese));
231                    current_chinese.clear();
232                }
233            }
234        }
235
236        // 处理剩余字符
237        if !current_english.is_empty() {
238            terms.push(current_english);
239        }
240        if !current_chinese.is_empty() {
241            terms.extend(self.tokenize_chinese_segment(&current_chinese));
242        }
243
244        terms
245    }
246
247    /// 判断是否为中文字符
248    fn is_chinese_char(ch: char) -> bool {
249        // CJK 统一汉字范围
250        ('\u{4E00}'..='\u{9FFF}').contains(&ch) ||
251        // CJK 扩展A
252        ('\u{3400}'..='\u{4DBF}').contains(&ch) ||
253        // 中文标点
254        ('\u{3000}'..='\u{303F}').contains(&ch)
255    }
256
257    /// 中文简单分词(单字 + 双字组合)
258    fn tokenize_chinese_segment(&self, text: &str) -> Vec<String> {
259        let chars: Vec<char> = text.chars().collect();
260        let mut terms = Vec::new();
261
262        // 单字
263        for ch in &chars {
264            terms.push(ch.to_string());
265        }
266
267        // 双字组合(n-gram)
268        for i in 0..chars.len().saturating_sub(1) {
269            let bigram = format!("{}{}", chars[i], chars[i + 1]);
270            terms.push(bigram);
271        }
272
273        terms
274    }
275
276    /// 判断是否为停用词
277    fn is_stopword(&self, word: &str) -> bool {
278        // 先检查英文停用词
279        if self.stopwords_en.contains(word) {
280            return true;
281        }
282
283        // 再检查中文停用词(单字和双字)
284        if self.stopwords_zh.contains(word) {
285            return true;
286        }
287
288        false
289    }
290
291    /// 英文分词(空格分割 + 小写化)
292    pub fn tokenize_english(&self, text: &str) -> Vec<String> {
293        let mut terms = Vec::new();
294
295        for word in text.split_whitespace() {
296            let word_lower = word
297                .chars()
298                .filter(|c| c.is_ascii_alphabetic() || c.is_ascii_digit())
299                .collect::<String>()
300                .to_lowercase();
301
302            if !word_lower.is_empty()
303                && (self.keep_stopwords || !self.stopwords_en.contains(&word_lower))
304            {
305                terms.push(word_lower);
306            }
307        }
308
309        terms
310    }
311
312    /// 中文分词(单字 + 双字)
313    pub fn tokenize_chinese(&self, text: &str) -> Vec<String> {
314        let mut terms = Vec::new();
315
316        // 提取中文字符
317        let chars: Vec<char> = text
318            .chars()
319            .filter(|ch| Self::is_chinese_char(*ch))
320            .collect();
321
322        // 单字
323        for ch in &chars {
324            let s: String = ch.to_string();
325            if self.keep_stopwords || !self.stopwords_zh.contains(&s) {
326                terms.push(s);
327            }
328        }
329
330        // 双字组合
331        for i in 0..chars.len().saturating_sub(1) {
332            let bigram = format!("{}{}", chars[i], chars[i + 1]);
333            if self.keep_stopwords || !self.stopwords_zh.contains(&bigram) {
334                terms.push(bigram);
335            }
336        }
337
338        terms
339    }
340}
341
342impl Default for Tokenizer {
343    fn default() -> Self {
344        Self::new()
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn test_tokenize_english() {
354        let tokenizer = Tokenizer::new();
355
356        let terms = tokenizer.tokenize_english("Hello World Rust");
357        assert_eq!(terms, vec!["hello", "world", "rust"]);
358    }
359
360    #[test]
361    fn test_tokenize_english_stopwords() {
362        let tokenizer = Tokenizer::new();
363
364        let terms = tokenizer.tokenize_english("The Rust is a programming language");
365        // "the", "is", "a" 应被过滤
366        assert!(!terms.contains(&"the".to_string()));
367        assert!(!terms.contains(&"is".to_string()));
368        assert!(!terms.contains(&"a".to_string()));
369        assert!(terms.contains(&"rust".to_string()));
370        assert!(terms.contains(&"programming".to_string()));
371        assert!(terms.contains(&"language".to_string()));
372    }
373
374    #[test]
375    fn test_tokenize_chinese() {
376        let tokenizer = Tokenizer::new();
377
378        let terms = tokenizer.tokenize_chinese("编程语言");
379        // 单字 + 双字组合
380        assert!(terms.contains(&"编".to_string()));
381        assert!(terms.contains(&"程".to_string()));
382        assert!(terms.contains(&"语".to_string()));
383        assert!(terms.contains(&"言".to_string()));
384        assert!(terms.contains(&"编程".to_string()));
385        assert!(terms.contains(&"程语".to_string()));
386        assert!(terms.contains(&"语言".to_string()));
387    }
388
389    #[test]
390    fn test_tokenize_mixed() {
391        let tokenizer = Tokenizer::new();
392
393        let terms = tokenizer.tokenize("Rust 编程语言");
394
395        // 英文词
396        assert!(terms.contains(&"rust".to_string()));
397
398        // 中文单字
399        assert!(terms.contains(&"编".to_string()));
400        assert!(terms.contains(&"程".to_string()));
401        assert!(terms.contains(&"语".to_string()));
402        assert!(terms.contains(&"言".to_string()));
403
404        // 中文双字
405        assert!(terms.contains(&"编程".to_string()));
406        assert!(terms.contains(&"语言".to_string()));
407    }
408
409    #[test]
410    fn test_tokenize_with_stopwords() {
411        let tokenizer = Tokenizer::with_stopwords();
412
413        let terms = tokenizer.tokenize("The programming language");
414        assert!(terms.contains(&"the".to_string()));
415        assert!(terms.contains(&"programming".to_string()));
416        assert!(terms.contains(&"language".to_string()));
417    }
418
419    #[test]
420    fn test_chinese_stopwords() {
421        let tokenizer = Tokenizer::new();
422
423        let terms = tokenizer.tokenize_chinese("编程的语言");
424        // "的" 应被过滤
425        assert!(!terms.contains(&"的".to_string()));
426        assert!(terms.contains(&"编".to_string()));
427        assert!(terms.contains(&"程".to_string()));
428    }
429}