74befbc72baf | atagen | 2026-10-25 11:18 UTC | 1 | use tantivy::{ |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 2 | Term, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 3 | query::{BooleanQuery, Occur, PhraseQuery, Query, TermQuery}, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 4 | schema::{Field, IndexRecordOption}, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 5 | tokenizer::{ |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 6 | AsciiFoldingFilter, LowerCaser, RawTokenizer, TextAnalyzer, Token, TokenStream, Tokenizer, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 7 | }, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 8 | }; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 9 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 10 | use crate::{Error, ErrorKind}; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 11 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 12 | pub(crate) const TOKENIZER: &str = "macltok"; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 13 | /// longer runs are hashes, keys or noise, and in chars so cyrillic and greek words fit |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 14 | const MAX_WORD_CHARS: usize = 64; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 15 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 16 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 17 | enum Kind { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 18 | Word, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 19 | /// one char of a han, kana, hangul or thai run, or an emoji |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 20 | Single, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 21 | /// two neighbouring chars of such a run, sharing the first one's position |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 22 | Pair, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 23 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 24 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 25 | #[derive(Clone, Debug, Eq, PartialEq)] |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 26 | struct Piece { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 27 | from: usize, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 28 | to: usize, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 29 | position: usize, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 30 | kind: Kind, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 31 | /// a single inside a run of two or more, which its pairs already cover in a query |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 32 | paired: bool, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 33 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 34 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 35 | /// scripts written without spaces, where a char is the only boundary there is |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 36 | fn unspaced(c: char) -> bool { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 37 | matches!(c, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 38 | '\u{3040}'..='\u{30FF}' | '\u{31F0}'..='\u{31FF}' | '\u{3400}'..='\u{4DBF}' |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 39 | | '\u{4E00}'..='\u{9FFF}' | '\u{F900}'..='\u{FAFF}' | '\u{FF66}'..='\u{FF9F}' |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 40 | | '\u{1100}'..='\u{11FF}' | '\u{3130}'..='\u{318F}' | '\u{AC00}'..='\u{D7AF}' |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 41 | | '\u{0E00}'..='\u{0E7F}' | '\u{20000}'..='\u{2FA1F}') |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 42 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 43 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 44 | fn emoji(c: char) -> bool { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 45 | matches!(c, '\u{1F300}'..='\u{1FAFF}' | '\u{2600}'..='\u{27BF}') |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 46 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 47 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 48 | fn pieces(text: &str) -> Vec<Piece> { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 49 | let mut pieces = Vec::new(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 50 | let mut position = 0; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 51 | let mut chars = text.char_indices().peekable(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 52 | while let Some((from, c)) = chars.next() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 53 | if unspaced(c) { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 54 | let mut run = vec![(from, from + c.len_utf8())]; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 55 | while let Some(&(at, next)) = chars.peek() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 56 | if !unspaced(next) { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 57 | break; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 58 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 59 | run.push((at, at + next.len_utf8())); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 60 | chars.next(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 61 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 62 | let paired = run.len() > 1; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 63 | for (index, &(from, to)) in run.iter().enumerate() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 64 | pieces.push(Piece { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 65 | from, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 66 | to, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 67 | position, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 68 | kind: Kind::Single, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 69 | paired, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 70 | }); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 71 | if let Some(&(_, next_to)) = run.get(index + 1) { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 72 | pieces.push(Piece { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 73 | from, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 74 | to: next_to, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 75 | position, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 76 | kind: Kind::Pair, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 77 | paired: false, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 78 | }); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 79 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 80 | position += 1; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 81 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 82 | } else if emoji(c) { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 83 | pieces.push(Piece { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 84 | from, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 85 | to: from + c.len_utf8(), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 86 | position, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 87 | kind: Kind::Single, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 88 | paired: false, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 89 | }); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 90 | position += 1; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 91 | } else if c.is_alphanumeric() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 92 | let mut to = from + c.len_utf8(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 93 | let mut count = 1; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 94 | while let Some(&(at, next)) = chars.peek() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 95 | if !next.is_alphanumeric() || unspaced(next) { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 96 | break; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 97 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 98 | to = at + next.len_utf8(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 99 | count += 1; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 100 | chars.next(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 101 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 102 | if count <= MAX_WORD_CHARS { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 103 | pieces.push(Piece { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 104 | from, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 105 | to, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 106 | position, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 107 | kind: Kind::Word, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 108 | paired: false, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 109 | }); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 110 | position += 1; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 111 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 112 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 113 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 114 | pieces |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 115 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 116 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 117 | #[derive(Clone)] |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 118 | pub(crate) struct MacltokTokenizer; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 119 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 120 | pub(crate) struct PieceStream { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 121 | tokens: std::vec::IntoIter<Token>, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 122 | token: Token, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 123 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 124 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 125 | impl TokenStream for PieceStream { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 126 | fn advance(&mut self) -> bool { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 127 | match self.tokens.next() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 128 | Some(token) => { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 129 | self.token = token; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 130 | true |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 131 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 132 | None => false, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 133 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 134 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 135 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 136 | fn token(&self) -> &Token { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 137 | &self.token |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 138 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 139 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 140 | fn token_mut(&mut self) -> &mut Token { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 141 | &mut self.token |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 142 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 143 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 144 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 145 | impl Tokenizer for MacltokTokenizer { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 146 | type TokenStream<'a> = PieceStream; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 147 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 148 | fn token_stream<'a>(&'a mut self, text: &'a str) -> PieceStream { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 149 | let tokens: Vec<_> = pieces(text) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 150 | .into_iter() |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 151 | .map(|piece| Token { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 152 | offset_from: piece.from, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 153 | offset_to: piece.to, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 154 | position: piece.position, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 155 | text: text[piece.from..piece.to].to_owned(), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 156 | position_length: 1, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 157 | }) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 158 | .collect(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 159 | PieceStream { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 160 | tokens: tokens.into_iter(), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 161 | token: Token::default(), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 162 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 163 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 164 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 165 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 166 | pub(crate) fn analyzer() -> TextAnalyzer { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 167 | TextAnalyzer::builder(MacltokTokenizer) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 168 | .filter(LowerCaser) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 169 | .filter(AsciiFoldingFilter) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 170 | .build() |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 171 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 172 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 173 | fn fold(text: &str) -> String { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 174 | let mut folder = TextAnalyzer::builder(RawTokenizer::default()) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 175 | .filter(LowerCaser) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 176 | .filter(AsciiFoldingFilter) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 177 | .build(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 178 | let mut stream = folder.token_stream(text); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 179 | if stream.advance() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 180 | stream.token().text.clone() |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 181 | } else { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 182 | String::new() |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 183 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 184 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 185 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 186 | /// the byte ranges in `text` whose analysed token is one of `terms`, overlaps joined |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 187 | pub(crate) fn match_ranges(text: &str, terms: &[String]) -> Vec<(usize, usize)> { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 188 | if terms.is_empty() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 189 | return Vec::new(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 190 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 191 | let mut analyzer = analyzer(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 192 | let mut stream = analyzer.token_stream(text); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 193 | let mut ranges: Vec<(usize, usize)> = Vec::new(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 194 | while stream.advance() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 195 | let token = stream.token(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 196 | if !terms.iter().any(|term| *term == token.text) { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 197 | continue; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 198 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 199 | match ranges.last_mut() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 200 | Some(last) if token.offset_from < last.1 => last.1 = last.1.max(token.offset_to), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 201 | _ => ranges.push((token.offset_from, token.offset_to)), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 202 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 203 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 204 | ranges |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 205 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 206 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 207 | enum Clause { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 208 | Words(String), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 209 | Phrase(String), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 210 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 211 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 212 | /// quotes make a phrase, a leading `-` excludes, anything else is a word that must match. |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 213 | /// no field syntax, so `re: deploy` or a url is just its words |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 214 | fn clauses(query: &str) -> Vec<(bool, Clause)> { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 215 | let mut clauses = Vec::new(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 216 | let mut rest = query; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 217 | loop { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 218 | rest = rest.trim_start(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 219 | if rest.is_empty() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 220 | return clauses; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 221 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 222 | let (excluded, body) = match rest.strip_prefix('-') { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 223 | Some(body) if body.starts_with(|c: char| !c.is_whitespace()) => (true, body), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 224 | _ => (false, rest), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 225 | }; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 226 | if let Some(quoted) = body.strip_prefix('"') { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 227 | let end = quoted.find('"').unwrap_or(quoted.len()); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 228 | clauses.push((excluded, Clause::Phrase(quoted[..end].to_owned()))); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 229 | rest = quoted.get(end + 1..).unwrap_or(""); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 230 | } else { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 231 | let end = body.find(char::is_whitespace).unwrap_or(body.len()); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 232 | clauses.push((excluded, Clause::Words(body[..end].to_owned()))); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 233 | rest = &body[end..]; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 234 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 235 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 236 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 237 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 238 | fn term(field: Field, text: &str) -> Box<dyn Query> { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 239 | Box::new(TermQuery::new( |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 240 | Term::from_field_text(field, text), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 241 | IndexRecordOption::WithFreqs, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 242 | )) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 243 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 244 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 245 | /// every term the clause needs, and its query, `None` when nothing in it is searchable |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 246 | fn clause_query(field: Field, clause: &Clause) -> Option<(Box<dyn Query>, Vec<String>)> { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 247 | let text = match clause { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 248 | Clause::Words(text) | Clause::Phrase(text) => text, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 249 | }; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 250 | let found = pieces(text); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 251 | if let Clause::Phrase(_) = clause { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 252 | let ordered: Vec<_> = found |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 253 | .iter() |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 254 | .filter(|piece| piece.kind != Kind::Pair) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 255 | .map(|piece| (piece.position, fold(&text[piece.from..piece.to]))) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 256 | .filter(|(_, folded)| !folded.is_empty()) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 257 | .collect(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 258 | let terms: Vec<String> = ordered.iter().map(|(_, folded)| folded.clone()).collect(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 259 | return match ordered.len() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 260 | 0 => None, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 261 | 1 => Some((term(field, &ordered[0].1), terms)), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 262 | _ => { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 263 | let first = ordered[0].0; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 264 | let offsets = ordered |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 265 | .iter() |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 266 | .map(|(position, folded)| { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 267 | (position - first, Term::from_field_text(field, folded)) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 268 | }) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 269 | .collect(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 270 | Some((Box::new(PhraseQuery::new_with_offset(offsets)), terms)) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 271 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 272 | }; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 273 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 274 | let mut terms: Vec<String> = found |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 275 | .iter() |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 276 | .filter(|piece| !piece.paired) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 277 | .map(|piece| fold(&text[piece.from..piece.to])) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 278 | .filter(|folded| !folded.is_empty()) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 279 | .collect(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 280 | terms.dedup(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 281 | match terms.len() { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 282 | 0 => None, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 283 | 1 => Some((term(field, &terms[0]), terms)), |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 284 | _ => { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 285 | let musts = terms |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 286 | .iter() |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 287 | .map(|text| (Occur::Must, term(field, text))) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 288 | .collect(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 289 | Some((Box::new(BooleanQuery::new(musts)), terms)) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 290 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 291 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 292 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 293 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 294 | /// the query and the terms to highlight. one with nothing left to find is `Invalid`, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 295 | /// rather than everything in scope |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 296 | pub(crate) fn build_query( |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 297 | field: Field, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 298 | query: &str, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 299 | ) -> Result<(Box<dyn Query>, Vec<String>), Error> { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 300 | let mut subqueries = Vec::new(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 301 | let mut highlights = Vec::new(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 302 | let mut positive = false; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 303 | for (excluded, clause) in clauses(query) { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 304 | let Some((subquery, terms)) = clause_query(field, &clause) else { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 305 | continue; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 306 | }; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 307 | if excluded { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 308 | subqueries.push((Occur::MustNot, subquery)); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 309 | } else { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 310 | positive = true; |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 311 | highlights.extend(terms); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 312 | subqueries.push((Occur::Must, subquery)); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 313 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 314 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 315 | if !positive { |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 316 | return Err(Error::new( |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 317 | ErrorKind::Invalid, |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 318 | "search needs at least one word that isn't excluded", |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 319 | )); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 320 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 321 | highlights.sort(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 322 | highlights.dedup(); |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 323 | Ok((Box::new(BooleanQuery::new(subqueries)), highlights)) |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 324 | } |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 325 | |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 326 | #[cfg(test)] |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 327 | #[path = "text_tests.rs"] |
74befbc72baf | atagen | 2026-10-25 11:18 UTC | 328 | mod tests; |