1use std::collections::HashSet;
7
8pub struct Tokenizer {
10 keep_stopwords: bool,
12
13 stopwords_en: HashSet<String>,
15
16 stopwords_zh: HashSet<String>,
18}
19
20impl Tokenizer {
21 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 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 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 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 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 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 if !current_chinese.is_empty() {
210 terms.extend(self.tokenize_chinese_segment(¤t_chinese));
212 current_chinese.clear();
213 }
214 current_english.push(ch.to_ascii_lowercase());
215 } else if Self::is_chinese_char(ch) {
216 if !current_english.is_empty() {
218 terms.push(current_english.clone());
220 current_english.clear();
221 }
222 current_chinese.push(ch);
223 } else {
224 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(¤t_chinese));
231 current_chinese.clear();
232 }
233 }
234 }
235
236 if !current_english.is_empty() {
238 terms.push(current_english);
239 }
240 if !current_chinese.is_empty() {
241 terms.extend(self.tokenize_chinese_segment(¤t_chinese));
242 }
243
244 terms
245 }
246
247 fn is_chinese_char(ch: char) -> bool {
249 ('\u{4E00}'..='\u{9FFF}').contains(&ch) ||
251 ('\u{3400}'..='\u{4DBF}').contains(&ch) ||
253 ('\u{3000}'..='\u{303F}').contains(&ch)
255 }
256
257 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 for ch in &chars {
264 terms.push(ch.to_string());
265 }
266
267 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 fn is_stopword(&self, word: &str) -> bool {
278 if self.stopwords_en.contains(word) {
280 return true;
281 }
282
283 if self.stopwords_zh.contains(word) {
285 return true;
286 }
287
288 false
289 }
290
291 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 pub fn tokenize_chinese(&self, text: &str) -> Vec<String> {
314 let mut terms = Vec::new();
315
316 let chars: Vec<char> = text
318 .chars()
319 .filter(|ch| Self::is_chinese_char(*ch))
320 .collect();
321
322 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 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 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 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 assert!(terms.contains(&"rust".to_string()));
397
398 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 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 assert!(!terms.contains(&"的".to_string()));
426 assert!(terms.contains(&"编".to_string()));
427 assert!(terms.contains(&"程".to_string()));
428 }
429}