1use crate::analyzer::language::stemmer::{LanguageAnalyzer, NoStemAnalyzer};
14use crate::analyzer::pipeline::{TextAnalyzer, analyze};
15use crate::analyzer::standard::StandardAnalyzer;
16use crate::backend::FtsBackend;
17use crate::index::FtsIndex;
18use crate::scope::IndexScope;
19
20impl<B: FtsBackend> FtsIndex<B> {
21 pub fn set_collection_analyzer(
23 &self,
24 database_id: u64,
25 tid: u64,
26 collection: &str,
27 analyzer_name: &str,
28 ) -> Result<(), B::Error> {
29 self.backend.write_meta(
30 database_id,
31 tid,
32 IndexScope::document(collection),
33 "analyzer",
34 analyzer_name.as_bytes(),
35 )
36 }
37
38 pub fn set_collection_language(
40 &self,
41 database_id: u64,
42 tid: u64,
43 collection: &str,
44 lang_code: &str,
45 ) -> Result<(), B::Error> {
46 self.backend.write_meta(
47 database_id,
48 tid,
49 IndexScope::document(collection),
50 "language",
51 lang_code.as_bytes(),
52 )
53 }
54
55 pub fn set_collection_fuzzy(
58 &self,
59 database_id: u64,
60 tid: u64,
61 collection: &str,
62 fuzzy: bool,
63 ) -> Result<(), B::Error> {
64 self.backend.write_meta(
65 database_id,
66 tid,
67 IndexScope::document(collection),
68 "fuzzy",
69 if fuzzy { b"1" } else { b"0" },
70 )
71 }
72
73 pub fn get_collection_fuzzy(
75 &self,
76 database_id: u64,
77 tid: u64,
78 collection: &str,
79 ) -> Result<bool, B::Error> {
80 Ok(self
81 .backend
82 .read_meta(database_id, tid, IndexScope::document(collection), "fuzzy")?
83 .is_some_and(|bytes| bytes.as_slice() == b"1"))
84 }
85
86 pub fn get_collection_analyzer(
88 &self,
89 database_id: u64,
90 tid: u64,
91 collection: &str,
92 ) -> Result<Option<String>, B::Error> {
93 match self.backend.read_meta(
94 database_id,
95 tid,
96 IndexScope::document(collection),
97 "analyzer",
98 )? {
99 Some(bytes) => Ok(std::str::from_utf8(&bytes).ok().map(String::from)),
100 None => Ok(None),
101 }
102 }
103
104 pub fn get_collection_language(
106 &self,
107 database_id: u64,
108 tid: u64,
109 collection: &str,
110 ) -> Result<Option<String>, B::Error> {
111 match self.backend.read_meta(
112 database_id,
113 tid,
114 IndexScope::document(collection),
115 "language",
116 )? {
117 Some(bytes) => Ok(std::str::from_utf8(&bytes).ok().map(String::from)),
118 None => Ok(None),
119 }
120 }
121
122 pub fn analyze_for_collection(
126 &self,
127 database_id: u64,
128 tid: u64,
129 collection: &str,
130 text: &str,
131 ) -> Result<Vec<String>, B::Error> {
132 let analyzer_name = self.get_collection_analyzer(database_id, tid, collection)?;
133 match analyzer_name.as_deref() {
134 Some(name) => Ok(resolve_analyzer(name).analyze(text)),
135 None => Ok(analyze(text)),
136 }
137 }
138
139 pub fn tokenize_raw_for_collection(
141 &self,
142 database_id: u64,
143 tid: u64,
144 collection: &str,
145 text: &str,
146 ) -> Result<Vec<String>, B::Error> {
147 let lang = self.get_collection_language(database_id, tid, collection)?;
148 let lang_code = lang.as_deref().unwrap_or("en");
149 let stop_list = crate::analyzer::language::stop_words::stop_words(lang_code);
150 Ok(crate::analyzer::pipeline::tokenize_raw(
151 text, lang_code, stop_list,
152 ))
153 }
154}
155
156pub fn analyzer_exists(name: &str) -> bool {
165 name == "standard"
166 || LanguageAnalyzer::new(name).is_some()
167 || NoStemAnalyzer::new(name).is_some()
168}
169
170fn resolve_analyzer(name: &str) -> Box<dyn TextAnalyzer> {
172 match name {
173 "standard" => Box::new(StandardAnalyzer),
174 _ => {
175 if let Some(a) = LanguageAnalyzer::new(name) {
176 Box::new(a)
177 } else if let Some(a) = NoStemAnalyzer::new(name) {
178 Box::new(a)
179 } else {
180 Box::new(StandardAnalyzer)
181 }
182 }
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use crate::backend::memory::MemoryBackend;
189 use crate::index::FtsIndex;
190 use crate::test_support::test_governor;
191
192 const DB: u64 = 0;
193 const T: u64 = 1;
194
195 #[test]
196 fn default_analyzer() {
197 let idx = FtsIndex::new(MemoryBackend::new(), test_governor());
198 let tokens = idx
199 .analyze_for_collection(DB, T, "col", "The quick brown fox")
200 .unwrap();
201 assert!(tokens.contains(&"quick".to_string()));
202 assert!(!tokens.contains(&"the".to_string()));
203 }
204
205 #[test]
206 fn configured_german_analyzer() {
207 let idx = FtsIndex::new(MemoryBackend::new(), test_governor());
208 idx.set_collection_analyzer(DB, T, "col", "german").unwrap();
209
210 let tokens = idx
211 .analyze_for_collection(DB, T, "col", "Die Datenbanken sind schnell")
212 .unwrap();
213 assert!(!tokens.iter().any(|t| t == "die" || t == "sind"));
214 assert!(!tokens.is_empty());
215 }
216
217 #[test]
218 fn configured_hindi_no_stem() {
219 let idx = FtsIndex::new(MemoryBackend::new(), test_governor());
220 idx.set_collection_analyzer(DB, T, "col", "hindi").unwrap();
221
222 let tokens = idx
223 .analyze_for_collection(DB, T, "col", "यह एक परीक्षा है")
224 .unwrap();
225 assert!(!tokens.iter().any(|t| t == "यह" || t == "है"));
226 }
227
228 #[test]
229 fn analyzer_persists() {
230 let idx = FtsIndex::new(MemoryBackend::new(), test_governor());
231 idx.set_collection_analyzer(DB, T, "col", "french").unwrap();
232 idx.set_collection_language(DB, T, "col", "fr").unwrap();
233
234 assert_eq!(
235 idx.get_collection_analyzer(DB, T, "col")
236 .unwrap()
237 .as_deref(),
238 Some("french")
239 );
240 assert_eq!(
241 idx.get_collection_language(DB, T, "col")
242 .unwrap()
243 .as_deref(),
244 Some("fr")
245 );
246 }
247}