1use serde::{Deserialize, Serialize};
14
15use crate::char_filter::CharFilter;
16use crate::error::AnalysisResult;
17use crate::token_filter::TokenFilter;
18use crate::tokenizer::Tokenizer;
19
20mod compiled;
21mod diagnostics;
22pub use compiled::CompiledAnalyzer;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Analyzer {
26 #[serde(default = "default_tokenizer")]
27 pub tokenizer: Tokenizer,
28 #[serde(default)]
29 pub token_filters: Vec<TokenFilter>,
30 #[serde(default)]
31 pub char_filters: Vec<CharFilter>,
32}
33
34fn default_tokenizer() -> Tokenizer {
35 Tokenizer::Whitespace
36}
37
38impl Default for Analyzer {
39 fn default() -> Self {
40 Self {
41 tokenizer: default_tokenizer(),
42 token_filters: Vec::new(),
43 char_filters: Vec::new(),
44 }
45 }
46}
47
48impl Analyzer {
49 pub fn uses_korean_stages(&self) -> bool {
51 #[cfg(feature = "nori")]
52 {
53 matches!(self.tokenizer, Tokenizer::Nori(_))
54 || self.token_filters.iter().any(|filter| {
55 matches!(
56 filter,
57 TokenFilter::NoriPartOfSpeech(_)
58 | TokenFilter::NoriReadingForm(_)
59 | TokenFilter::UnicodeSimpleLowercase(_)
60 | TokenFilter::NoriNumber(_)
61 )
62 })
63 }
64 #[cfg(not(feature = "nori"))]
65 {
66 false
67 }
68 }
69
70 pub fn new(
71 tokenizer: Tokenizer,
72 token_filters: Vec<TokenFilter>,
73 char_filters: Vec<CharFilter>,
74 ) -> Self {
75 Self {
76 tokenizer,
77 token_filters,
78 char_filters,
79 }
80 }
81
82 pub fn analyze(&self, text: &str) -> AnalysisResult<Vec<String>> {
83 self.analyze_tokens(text)?.into_terms()
84 }
85
86 pub fn analyze_tokens(&self, text: &str) -> AnalysisResult<crate::AnalyzedText> {
101 Ok(self
102 .analyze_tokens_budgeted(
103 text,
104 &uqa_core::memory::MemoryBudget::new(usize::MAX),
105 || Ok(()),
106 )?
107 .into_parts()
108 .0)
109 }
110
111 pub fn analyze_tokens_budgeted(
115 &self,
116 text: &str,
117 budget: &uqa_core::memory::MemoryBudget,
118 mut poll: impl FnMut() -> AnalysisResult<()>,
119 ) -> AnalysisResult<uqa_core::memory::Budgeted<crate::AnalyzedText>> {
120 poll()?;
121 let mut filtered = crate::FilteredText::new(text);
122 for filter in &self.char_filters {
123 filtered = filter
124 .prepare()?
125 .filter_mapped_budgeted(filtered, budget, &mut poll)?;
126 }
127 let mut tokens = self
128 .tokenizer
129 .prepare()?
130 .tokenize_mapped_budgeted(&filtered, budget, &mut poll)?;
131 drop(filtered);
132 for filter in &self.token_filters {
133 tokens = filter.filter_analyzed_budgeted(tokens, &mut poll)?;
134 }
135 poll()?;
136 Ok(tokens)
137 }
138
139 pub fn validate(&self) -> AnalysisResult<()> {
146 for char_filter in &self.char_filters {
147 char_filter.validate()?;
148 }
149 self.tokenizer.validate()?;
150 for token_filter in &self.token_filters {
151 token_filter.validate()?;
152 }
153 Ok(())
154 }
155}
156
157pub fn whitespace_analyzer() -> Analyzer {
159 Analyzer::new(
160 Tokenizer::Whitespace,
161 vec![TokenFilter::Lowercase],
162 Vec::new(),
163 )
164}
165
166pub fn standard_analyzer(language: &str) -> Analyzer {
168 Analyzer::new(
169 Tokenizer::Standard,
170 vec![
171 TokenFilter::Lowercase,
172 TokenFilter::ASCIIFolding,
173 TokenFilter::Stop {
174 language: language.to_string(),
175 custom_words: Vec::new(),
176 },
177 TokenFilter::PorterStem,
178 ],
179 Vec::new(),
180 )
181}
182
183pub fn standard_cjk_analyzer(language: &str) -> Analyzer {
186 Analyzer::new(
187 Tokenizer::Standard,
188 vec![
189 TokenFilter::Lowercase,
190 TokenFilter::ASCIIFolding,
191 TokenFilter::Stop {
192 language: language.to_string(),
193 custom_words: Vec::new(),
194 },
195 TokenFilter::PorterStem,
196 TokenFilter::Ngram {
197 min_gram: 2,
198 max_gram: 3,
199 keep_short: true,
200 },
201 ],
202 Vec::new(),
203 )
204}
205
206pub fn keyword_analyzer() -> Analyzer {
208 Analyzer::new(Tokenizer::Keyword, Vec::new(), Vec::new())
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn standard_pipeline_lowers_stops_and_stems() {
217 let a = standard_analyzer("english");
218 assert_eq!(a.analyze("The Running").unwrap(), vec!["run"]);
224 }
225
226 #[test]
227 fn whitespace_pipeline_just_lowers() {
228 let a = whitespace_analyzer();
229 assert_eq!(a.analyze("Hello WORLD").unwrap(), vec!["hello", "world"]);
230 }
231
232 #[test]
233 fn keyword_pipeline_emits_whole_input() {
234 let a = keyword_analyzer();
235 assert_eq!(
236 a.analyze("the quick brown").unwrap(),
237 vec!["the quick brown"]
238 );
239 }
240
241 #[test]
242 fn round_trips_via_serde_json() {
243 let a = standard_analyzer("english");
244 let s = serde_json::to_string(&a).unwrap();
245 let back: Analyzer = serde_json::from_str(&s).unwrap();
246 assert_eq!(
247 back.analyze("The Running").unwrap(),
248 a.analyze("The Running").unwrap()
249 );
250 }
251}