lance_index/scalar/inverted/
tokenizer.rs1use lance_core::{Error, Result};
5use serde::{Deserialize, Serialize};
6use std::{env, path::PathBuf};
7
8#[cfg(feature = "tokenizer-jieba")]
9mod jieba;
10
11pub mod lance_tokenizer;
12#[cfg(feature = "tokenizer-lindera")]
13mod lindera;
14
15#[cfg(feature = "tokenizer-jieba")]
16use jieba::JiebaTokenizerBuilder;
17
18#[cfg(feature = "tokenizer-lindera")]
19use lindera::LinderaTokenizerBuilder;
20
21use crate::pbold;
22use crate::scalar::inverted::tokenizer::lance_tokenizer::{
23 JsonTokenizer, LanceTokenizer, TextTokenizer,
24};
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28pub struct InvertedIndexParams {
29 pub(crate) lance_tokenizer: Option<String>,
34 pub(crate) base_tokenizer: String,
43
44 pub(crate) language: tantivy::tokenizer::Language,
47
48 #[serde(default)]
53 pub(crate) with_position: bool,
54
55 pub(crate) max_token_length: Option<usize>,
59
60 #[serde(default = "bool_true")]
62 pub(crate) lower_case: bool,
63
64 #[serde(default = "bool_true")]
66 pub(crate) stem: bool,
67
68 #[serde(default = "bool_true")]
70 pub(crate) remove_stop_words: bool,
71
72 pub(crate) custom_stop_words: Option<Vec<String>>,
76
77 #[serde(default = "bool_true")]
79 pub(crate) ascii_folding: bool,
80
81 #[serde(default = "default_min_ngram_length")]
83 pub(crate) min_ngram_length: u32,
84
85 #[serde(default = "default_max_ngram_length")]
87 pub(crate) max_ngram_length: u32,
88
89 #[serde(default)]
91 pub(crate) prefix_only: bool,
92
93 #[serde(default)]
96 pub(crate) skip_merge: bool,
97}
98
99impl TryFrom<&InvertedIndexParams> for pbold::InvertedIndexDetails {
100 type Error = Error;
101
102 fn try_from(params: &InvertedIndexParams) -> Result<Self> {
103 Ok(Self {
104 base_tokenizer: Some(params.base_tokenizer.clone()),
105 language: serde_json::to_string(¶ms.language)?,
106 with_position: params.with_position,
107 max_token_length: params.max_token_length.map(|l| l as u32),
108 lower_case: params.lower_case,
109 stem: params.stem,
110 remove_stop_words: params.remove_stop_words,
111 ascii_folding: params.ascii_folding,
112 min_ngram_length: params.min_ngram_length,
113 max_ngram_length: params.max_ngram_length,
114 prefix_only: params.prefix_only,
115 })
116 }
117}
118
119impl TryFrom<&pbold::InvertedIndexDetails> for InvertedIndexParams {
120 type Error = Error;
121
122 fn try_from(details: &pbold::InvertedIndexDetails) -> Result<Self> {
123 let defaults = Self::default();
124 Ok(Self {
125 lance_tokenizer: defaults.lance_tokenizer,
126 base_tokenizer: details
127 .base_tokenizer
128 .as_ref()
129 .cloned()
130 .unwrap_or(defaults.base_tokenizer),
131 language: serde_json::from_str(details.language.as_str())?,
132 with_position: details.with_position,
133 max_token_length: details.max_token_length.map(|l| l as usize),
134 lower_case: details.lower_case,
135 stem: details.stem,
136 remove_stop_words: details.remove_stop_words,
137 custom_stop_words: defaults.custom_stop_words,
138 ascii_folding: details.ascii_folding,
139 min_ngram_length: details.min_ngram_length,
140 max_ngram_length: details.max_ngram_length,
141 prefix_only: details.prefix_only,
142 skip_merge: defaults.skip_merge,
143 })
144 }
145}
146
147fn bool_true() -> bool {
148 true
149}
150
151fn default_min_ngram_length() -> u32 {
152 3
153}
154
155fn default_max_ngram_length() -> u32 {
156 3
157}
158
159impl Default for InvertedIndexParams {
160 fn default() -> Self {
161 Self::new("simple".to_owned(), tantivy::tokenizer::Language::English)
162 }
163}
164
165impl InvertedIndexParams {
166 pub fn new(base_tokenizer: String, language: tantivy::tokenizer::Language) -> Self {
180 Self {
181 lance_tokenizer: None,
182 base_tokenizer,
183 language,
184 with_position: false,
185 max_token_length: Some(40),
186 lower_case: true,
187 stem: true,
188 remove_stop_words: true,
189 custom_stop_words: None,
190 ascii_folding: true,
191 min_ngram_length: default_min_ngram_length(),
192 max_ngram_length: default_max_ngram_length(),
193 prefix_only: false,
194 skip_merge: false,
195 }
196 }
197
198 pub fn lance_tokenizer(mut self, lance_tokenizer: String) -> Self {
199 self.lance_tokenizer = Some(lance_tokenizer);
200 self
201 }
202
203 pub fn base_tokenizer(mut self, base_tokenizer: String) -> Self {
204 self.base_tokenizer = base_tokenizer;
205 self
206 }
207
208 pub fn language(mut self, language: &str) -> Result<Self> {
209 let language = serde_json::from_str(format!("\"{}\"", language).as_str())?;
211 self.language = language;
212 Ok(self)
213 }
214
215 pub fn with_position(mut self, with_position: bool) -> Self {
221 self.with_position = with_position;
222 self
223 }
224
225 pub fn has_positions(&self) -> bool {
227 self.with_position
228 }
229
230 pub fn max_token_length(mut self, max_token_length: Option<usize>) -> Self {
231 self.max_token_length = max_token_length;
232 self
233 }
234
235 pub fn lower_case(mut self, lower_case: bool) -> Self {
236 self.lower_case = lower_case;
237 self
238 }
239
240 pub fn stem(mut self, stem: bool) -> Self {
241 self.stem = stem;
242 self
243 }
244
245 pub fn remove_stop_words(mut self, remove_stop_words: bool) -> Self {
246 self.remove_stop_words = remove_stop_words;
247 self
248 }
249
250 pub fn custom_stop_words(mut self, custom_stop_words: Option<Vec<String>>) -> Self {
251 self.custom_stop_words = custom_stop_words;
252 self
253 }
254
255 pub fn ascii_folding(mut self, ascii_folding: bool) -> Self {
256 self.ascii_folding = ascii_folding;
257 self
258 }
259
260 pub fn ngram_min_length(mut self, min_length: u32) -> Self {
264 self.min_ngram_length = min_length;
265 self
266 }
267
268 pub fn ngram_max_length(mut self, max_length: u32) -> Self {
272 self.max_ngram_length = max_length;
273 self
274 }
275
276 pub fn ngram_prefix_only(mut self, prefix_only: bool) -> Self {
279 self.prefix_only = prefix_only;
280 self
281 }
282
283 pub fn skip_merge(mut self, skip_merge: bool) -> Self {
285 self.skip_merge = skip_merge;
286 self
287 }
288
289 pub fn build(&self) -> Result<Box<dyn LanceTokenizer>> {
290 let mut builder = self.build_base_tokenizer()?;
291 if let Some(max_token_length) = self.max_token_length {
292 builder = builder.filter_dynamic(tantivy::tokenizer::RemoveLongFilter::limit(
293 max_token_length,
294 ));
295 }
296 if self.lower_case {
297 builder = builder.filter_dynamic(tantivy::tokenizer::LowerCaser);
298 }
299 if self.stem {
300 builder = builder.filter_dynamic(tantivy::tokenizer::Stemmer::new(self.language));
301 }
302 if self.remove_stop_words {
303 let stop_word_filter = match &self.custom_stop_words {
304 Some(words) => tantivy::tokenizer::StopWordFilter::remove(words.iter().cloned()),
305 None => {
306 tantivy::tokenizer::StopWordFilter::new(self.language).ok_or_else(|| {
307 Error::invalid_input(format!(
308 "removing stop words for language {:?} is not supported yet",
309 self.language
310 ))
311 })?
312 }
313 };
314 builder = builder.filter_dynamic(stop_word_filter);
315 }
316 if self.ascii_folding {
317 builder = builder.filter_dynamic(tantivy::tokenizer::AsciiFoldingFilter);
318 }
319 let tokenizer = builder.build();
320
321 match self.lance_tokenizer {
322 Some(ref t) if t == "text" => Ok(Box::new(TextTokenizer::new(tokenizer))),
323 Some(ref t) if t == "json" => Ok(Box::new(JsonTokenizer::new(tokenizer))),
324 None => Ok(Box::new(TextTokenizer::new(tokenizer))),
325 _ => Err(Error::invalid_input(format!(
326 "unknown lance tokenizer {}",
327 self.lance_tokenizer.as_ref().unwrap()
328 ))),
329 }
330 }
331
332 fn build_base_tokenizer(&self) -> Result<tantivy::tokenizer::TextAnalyzerBuilder> {
333 match self.base_tokenizer.as_str() {
334 "simple" => Ok(tantivy::tokenizer::TextAnalyzer::builder(
335 tantivy::tokenizer::SimpleTokenizer::default(),
336 )
337 .dynamic()),
338 "whitespace" => Ok(tantivy::tokenizer::TextAnalyzer::builder(
339 tantivy::tokenizer::WhitespaceTokenizer::default(),
340 )
341 .dynamic()),
342 "raw" => Ok(tantivy::tokenizer::TextAnalyzer::builder(
343 tantivy::tokenizer::RawTokenizer::default(),
344 )
345 .dynamic()),
346 "ngram" => Ok(tantivy::tokenizer::TextAnalyzer::builder(
347 tantivy::tokenizer::NgramTokenizer::new(
348 self.min_ngram_length as usize,
349 self.max_ngram_length as usize,
350 self.prefix_only,
351 )
352 .map_err(|e| Error::invalid_input(e.to_string()))?,
353 )
354 .dynamic()),
355 #[cfg(feature = "tokenizer-lindera")]
356 s if s.starts_with("lindera/") => {
357 let Some(home) = language_model_home() else {
358 return Err(Error::invalid_input(format!(
359 "unknown base tokenizer {}",
360 self.base_tokenizer
361 )));
362 };
363 lindera::LinderaBuilder::load(&home.join(s))?.build()
364 }
365 #[cfg(feature = "tokenizer-jieba")]
366 s if s.starts_with("jieba/") || s == "jieba" => {
367 let s = if s == "jieba" { "jieba/default" } else { s };
368 let Some(home) = language_model_home() else {
369 return Err(Error::invalid_input(format!(
370 "unknown base tokenizer {}",
371 self.base_tokenizer
372 )));
373 };
374 jieba::JiebaBuilder::load(&home.join(s))?.build()
375 }
376 _ => Err(Error::invalid_input(format!(
377 "unknown base tokenizer {}",
378 self.base_tokenizer
379 ))),
380 }
381 }
382}
383
384pub const LANCE_LANGUAGE_MODEL_HOME_ENV_KEY: &str = "LANCE_LANGUAGE_MODEL_HOME";
385
386pub const LANCE_LANGUAGE_MODEL_DEFAULT_DIRECTORY: &str = "lance/language_models";
387
388pub fn language_model_home() -> Option<PathBuf> {
389 match env::var(LANCE_LANGUAGE_MODEL_HOME_ENV_KEY) {
390 Ok(p) => Some(PathBuf::from(p)),
391 Err(_) => dirs::data_local_dir().map(|p| p.join(LANCE_LANGUAGE_MODEL_DEFAULT_DIRECTORY)),
392 }
393}