Skip to main content

lc_rag/bm25/
tokenizer.rs

1// src/retrieval/bm25/tokenizer.rs
2//! Tokenizer implementation
3//!
4//! Provides simple tokenization for both English and Chinese
5
6use std::collections::HashSet;
7
8/// Tokenizer
9pub struct Tokenizer {
10    /// Whether to keep stopwords
11    keep_stopwords: bool,
12
13    /// English stopword list
14    stopwords_en: HashSet<String>,
15
16    /// Chinese stopword list
17    stopwords_zh: HashSet<String>,
18}
19
20impl Tokenizer {
21    /// Creates a new tokenizer (filtering stopwords)
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    /// Creates a tokenizer that keeps stopwords
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    /// Default English stopwords
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    /// Default Chinese stopwords
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    /// Tokenizes text (auto-detects the language)
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    /// Tokenizes mixed English/Chinese text
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                // English/digit characters
209                if !current_chinese.is_empty() {
210                    // First flush the accumulated Chinese
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                // Chinese character
217                if !current_english.is_empty() {
218                    // First flush the accumulated English
219                    terms.push(current_english.clone());
220                    current_english.clear();
221                }
222                current_chinese.push(ch);
223            } else {
224                // Other characters (punctuation, whitespace, etc.)
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        // Flush remaining characters
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    /// Checks whether a character is Chinese
248    fn is_chinese_char(ch: char) -> bool {
249        // CJK unified ideographs
250        ('\u{4E00}'..='\u{9FFF}').contains(&ch) ||
251        // CJK Extension A
252        ('\u{3400}'..='\u{4DBF}').contains(&ch) ||
253        // Chinese punctuation
254        ('\u{3000}'..='\u{303F}').contains(&ch)
255    }
256
257    /// Simple Chinese tokenization (single characters + bigrams)
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        // Single characters
263        for ch in &chars {
264            terms.push(ch.to_string());
265        }
266
267        // Bigrams (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    /// Checks whether a term is a stopword
277    fn is_stopword(&self, word: &str) -> bool {
278        // Check the English stopwords first
279        if self.stopwords_en.contains(word) {
280            return true;
281        }
282
283        // Then check the Chinese stopwords (single characters and bigrams)
284        if self.stopwords_zh.contains(word) {
285            return true;
286        }
287
288        false
289    }
290
291    /// English tokenization (whitespace-split + lowercased)
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    /// Chinese tokenization (single characters + bigrams)
313    pub fn tokenize_chinese(&self, text: &str) -> Vec<String> {
314        let mut terms = Vec::new();
315
316        // Extract the Chinese characters
317        let chars: Vec<char> = text
318            .chars()
319            .filter(|ch| Self::is_chinese_char(*ch))
320            .collect();
321
322        // Single characters
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        // Bigrams
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" should be filtered out
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        // Single characters + bigrams
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        // English terms
396        assert!(terms.contains(&"rust".to_string()));
397
398        // Chinese single characters
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        // Chinese bigrams
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        // The Chinese stopword (de) should be filtered out
425        assert!(!terms.contains(&"的".to_string()));
426        assert!(terms.contains(&"编".to_string()));
427        assert!(terms.contains(&"程".to_string()));
428    }
429}