1use std::collections::HashSet;
4use std::sync::LazyLock;
5
6use crate::TokenFilter;
7
8#[derive(Debug, Default, Clone)]
10pub struct LowercaseFilter;
11
12impl TokenFilter for LowercaseFilter {
13 fn apply(&self, token: String) -> Option<String> {
14 Some(token.to_lowercase())
15 }
16}
17
18#[derive(Debug, Clone)]
20pub struct MinLengthFilter(pub usize);
21
22impl TokenFilter for MinLengthFilter {
23 fn apply(&self, token: String) -> Option<String> {
24 if token.chars().count() >= self.0 {
25 Some(token)
26 } else {
27 None
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct MaxLengthFilter(pub usize);
35
36impl Default for MaxLengthFilter {
37 fn default() -> Self {
38 Self(40)
39 }
40}
41
42impl TokenFilter for MaxLengthFilter {
43 fn apply(&self, token: String) -> Option<String> {
44 if token.chars().count() <= self.0 {
45 Some(token)
46 } else {
47 None
48 }
49 }
50}
51
52#[derive(Debug, Default, Clone)]
54pub struct StopWordFilter;
55
56static EN_STOP_WORDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
57 [
58 "a",
59 "about",
60 "above",
61 "after",
62 "again",
63 "against",
64 "all",
65 "am",
66 "an",
67 "and",
68 "any",
69 "are",
70 "as",
71 "at",
72 "be",
73 "because",
74 "been",
75 "before",
76 "being",
77 "below",
78 "between",
79 "both",
80 "but",
81 "by",
82 "can",
83 "could",
84 "did",
85 "do",
86 "does",
87 "doing",
88 "don't",
89 "down",
90 "during",
91 "each",
92 "few",
93 "for",
94 "from",
95 "further",
96 "get",
97 "got",
98 "had",
99 "has",
100 "have",
101 "having",
102 "he",
103 "her",
104 "here",
105 "hers",
106 "herself",
107 "him",
108 "himself",
109 "his",
110 "how",
111 "i",
112 "if",
113 "in",
114 "into",
115 "is",
116 "it",
117 "its",
118 "itself",
119 "just",
120 "me",
121 "more",
122 "most",
123 "my",
124 "myself",
125 "no",
126 "nor",
127 "not",
128 "now",
129 "of",
130 "off",
131 "on",
132 "once",
133 "only",
134 "or",
135 "other",
136 "our",
137 "ours",
138 "ourselves",
139 "out",
140 "over",
141 "own",
142 "same",
143 "she",
144 "should",
145 "so",
146 "some",
147 "such",
148 "than",
149 "that",
150 "the",
151 "their",
152 "theirs",
153 "them",
154 "themselves",
155 "then",
156 "there",
157 "these",
158 "they",
159 "this",
160 "those",
161 "through",
162 "to",
163 "too",
164 "under",
165 "until",
166 "up",
167 "us",
168 "very",
169 "was",
170 "we",
171 "were",
172 "what",
173 "when",
174 "where",
175 "which",
176 "while",
177 "who",
178 "whom",
179 "why",
180 "will",
181 "with",
182 "would",
183 "you",
184 "your",
185 "yours",
186 "yourself",
187 "yourselves",
188 ]
189 .into_iter()
190 .collect()
191});
192
193impl TokenFilter for StopWordFilter {
194 fn apply(&self, token: String) -> Option<String> {
195 if EN_STOP_WORDS.contains(token.as_str()) {
196 None
197 } else {
198 Some(token)
199 }
200 }
201}
202
203#[derive(Debug, Default, Clone)]
207pub struct Bm25StopWordFilter;
208
209static BM25_STOP_WORDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
210 [
211 "a", "an", "and", "are", "as", "at", "be", "been", "being", "but", "by", "can", "did",
212 "do", "does", "doing", "done", "for", "from", "had", "has", "have", "having", "he", "her",
213 "here", "hers", "him", "his", "how", "i", "if", "in", "into", "is", "it", "its", "just",
214 "may", "me", "might", "my", "no", "nor", "not", "of", "on", "or", "our", "out", "own",
215 "say", "she", "should", "so", "some", "such", "than", "that", "the", "their", "them",
216 "then", "there", "these", "they", "this", "those", "through", "to", "too", "up", "us",
217 "very", "was", "we", "were", "what", "when", "where", "which", "while", "who", "whom",
218 "why", "will", "with", "would", "you", "your",
219 ]
220 .into_iter()
221 .collect()
222});
223
224impl TokenFilter for Bm25StopWordFilter {
225 fn apply(&self, token: String) -> Option<String> {
226 if BM25_STOP_WORDS.contains(token.as_str()) {
227 None
228 } else {
229 Some(token)
230 }
231 }
232}
233
234#[cfg(feature = "stem")]
236pub struct SnowballStemmer(rust_stemmers::Stemmer);
237
238#[cfg(feature = "stem")]
239impl SnowballStemmer {
240 pub fn english() -> Self {
242 Self(rust_stemmers::Stemmer::create(
243 rust_stemmers::Algorithm::English,
244 ))
245 }
246
247 pub fn for_algorithm(algo: rust_stemmers::Algorithm) -> Self {
249 Self(rust_stemmers::Stemmer::create(algo))
250 }
251}
252
253#[cfg(feature = "stem")]
254impl TokenFilter for SnowballStemmer {
255 fn apply(&self, token: String) -> Option<String> {
256 if token.chars().all(|c| c.is_ascii_alphabetic()) {
257 Some(self.0.stem(&token).into_owned())
258 } else {
259 Some(token)
260 }
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn lowercase_ascii() {
270 assert_eq!(LowercaseFilter.apply("HELLO".into()), Some("hello".into()));
271 }
272
273 #[test]
274 fn lowercase_unicode() {
275 assert_eq!(
276 LowercaseFilter.apply("Straße".into()),
277 Some("straße".into())
278 );
279 }
280
281 #[test]
282 fn min_length_boundary() {
283 let f = MinLengthFilter(3);
284 assert_eq!(f.apply("ab".into()), None);
285 assert_eq!(f.apply("abc".into()), Some("abc".into()));
286 }
287
288 #[test]
289 fn max_length_boundary() {
290 let f = MaxLengthFilter(5);
291 assert_eq!(f.apply("hello".into()), Some("hello".into()));
292 assert_eq!(f.apply("helloo".into()), None);
293 }
294
295 #[test]
296 fn max_length_default_is_40() {
297 let f = MaxLengthFilter::default();
298 assert_eq!(f.0, 40);
299 }
300
301 #[test]
302 fn stop_word_drops() {
303 assert_eq!(StopWordFilter.apply("the".into()), None);
304 assert_eq!(StopWordFilter.apply("is".into()), None);
305 assert_eq!(
306 StopWordFilter.apply("transformer".into()),
307 Some("transformer".into())
308 );
309 }
310
311 #[test]
312 fn stop_word_case_sensitive() {
313 assert_eq!(StopWordFilter.apply("The".into()), Some("The".into()));
315 }
316}