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#[cfg(feature = "stem")]
205pub struct SnowballStemmer(rust_stemmers::Stemmer);
206
207#[cfg(feature = "stem")]
208impl SnowballStemmer {
209 pub fn english() -> Self {
211 Self(rust_stemmers::Stemmer::create(
212 rust_stemmers::Algorithm::English,
213 ))
214 }
215
216 pub fn for_algorithm(algo: rust_stemmers::Algorithm) -> Self {
218 Self(rust_stemmers::Stemmer::create(algo))
219 }
220}
221
222#[cfg(feature = "stem")]
223impl TokenFilter for SnowballStemmer {
224 fn apply(&self, token: String) -> Option<String> {
225 if token.chars().all(|c| c.is_ascii_alphabetic()) {
226 Some(self.0.stem(&token).into_owned())
227 } else {
228 Some(token)
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn lowercase_ascii() {
239 assert_eq!(LowercaseFilter.apply("HELLO".into()), Some("hello".into()));
240 }
241
242 #[test]
243 fn lowercase_unicode() {
244 assert_eq!(
245 LowercaseFilter.apply("Straße".into()),
246 Some("straße".into())
247 );
248 }
249
250 #[test]
251 fn min_length_boundary() {
252 let f = MinLengthFilter(3);
253 assert_eq!(f.apply("ab".into()), None);
254 assert_eq!(f.apply("abc".into()), Some("abc".into()));
255 }
256
257 #[test]
258 fn max_length_boundary() {
259 let f = MaxLengthFilter(5);
260 assert_eq!(f.apply("hello".into()), Some("hello".into()));
261 assert_eq!(f.apply("helloo".into()), None);
262 }
263
264 #[test]
265 fn max_length_default_is_40() {
266 let f = MaxLengthFilter::default();
267 assert_eq!(f.0, 40);
268 }
269
270 #[test]
271 fn stop_word_drops() {
272 assert_eq!(StopWordFilter.apply("the".into()), None);
273 assert_eq!(StopWordFilter.apply("is".into()), None);
274 assert_eq!(
275 StopWordFilter.apply("transformer".into()),
276 Some("transformer".into())
277 );
278 }
279
280 #[test]
281 fn stop_word_case_sensitive() {
282 assert_eq!(StopWordFilter.apply("The".into()), Some("The".into()));
284 }
285}