index/fulltext_index/
tokenizer.rs1use common_base::term_token::{self, has_irregular_lowercase};
16
17use crate::Bytes;
18use crate::bloom_filter::element_hash;
19use crate::fulltext_index::error::Result;
20
21pub trait Tokenizer: Send {
23 fn tokenize<'a>(&self, text: &'a str) -> Vec<&'a str>;
24}
25
26#[derive(Debug, Default)]
28pub struct ScriptTokenizer;
29
30impl Tokenizer for ScriptTokenizer {
31 fn tokenize<'a>(&self, text: &'a str) -> Vec<&'a str> {
32 term_token::tokenize(text)
33 }
34}
35
36pub struct Analyzer {
42 tokenizer: Box<dyn Tokenizer>,
43 case_sensitive: bool,
44}
45
46impl Analyzer {
47 pub fn new(tokenizer: Box<dyn Tokenizer>, case_sensitive: bool) -> Self {
49 Self {
50 tokenizer,
51 case_sensitive,
52 }
53 }
54
55 pub fn analyze_text_hashes<'a>(
61 &self,
62 text: &'a str,
63 buf: &'a mut Vec<u8>,
64 ) -> impl Iterator<Item = u64> + use<'a> {
65 let case_sensitive = self.case_sensitive;
66 let lowercased_text_hashes = self
67 .irregular_lowercased_text(text)
68 .map(|lowered| {
69 self.tokenizer
70 .tokenize(&lowered)
71 .into_iter()
72 .map(|token| element_hash(token.as_bytes()))
73 .collect::<Vec<_>>()
74 })
75 .unwrap_or_default();
76 self.tokenizer
77 .tokenize(text)
78 .into_iter()
79 .map(move |token| {
80 if case_sensitive {
81 element_hash(token.as_bytes())
82 } else if token.is_ascii() {
83 buf.clear();
84 buf.extend(token.bytes().map(|b| b.to_ascii_lowercase()));
85 element_hash(buf)
86 } else {
87 element_hash(token.to_lowercase().as_bytes())
88 }
89 })
90 .chain(lowercased_text_hashes)
91 }
92
93 pub fn analyze_text(&self, text: &str) -> Result<Vec<Bytes>> {
95 let mut res = self
96 .tokenizer
97 .tokenize(text)
98 .iter()
99 .map(|s| {
100 if self.case_sensitive {
101 s.as_bytes().to_vec()
102 } else {
103 s.to_lowercase().as_bytes().to_vec()
104 }
105 })
106 .collect::<Vec<_>>();
107 if let Some(lowered) = self.irregular_lowercased_text(text) {
108 res.extend(
109 self.tokenizer
110 .tokenize(&lowered)
111 .into_iter()
112 .map(|token| token.as_bytes().to_vec()),
113 );
114 }
115 Ok(res)
116 }
117
118 fn irregular_lowercased_text(&self, text: &str) -> Option<String> {
120 (!self.case_sensitive && has_irregular_lowercase(text)).then(|| text.to_lowercase())
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_analyze_text_hashes_matches_analyze_text() {
130 let text = "Hello, WORLD ship_Ship 清洁表面 ÄÖÜ straße İstanbul x";
131 for case_sensitive in [false, true] {
132 let analyzer = Analyzer::new(Box::new(ScriptTokenizer), case_sensitive);
133 let expected = analyzer
134 .analyze_text(text)
135 .unwrap()
136 .iter()
137 .map(|t| element_hash(t))
138 .collect::<Vec<_>>();
139 let hashes = analyzer
140 .analyze_text_hashes(text, &mut Vec::new())
141 .collect::<Vec<_>>();
142 assert_eq!(expected, hashes);
143 }
144 }
145
146 #[test]
147 fn test_analyzer() {
148 let analyzer = Analyzer::new(Box::new(ScriptTokenizer), false);
149 let tokens = analyzer.analyze_text("Hello, World_Two").unwrap();
150 assert_eq!(
151 tokens,
152 vec![b"hello".to_vec(), b"world".to_vec(), b"two".to_vec()]
153 );
154 }
155}