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