atagen

macltok

public
Star0
Watch0
aced11e80b47de7a887296b10eedff310df5360f
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/search/text.rs

74befbc72bafatagen2026-10-25 11:18 UTC1
use tantivy::{
74befbc72bafatagen2026-10-25 11:18 UTC2
    Term,
74befbc72bafatagen2026-10-25 11:18 UTC3
    query::{BooleanQuery, Occur, PhraseQuery, Query, TermQuery},
74befbc72bafatagen2026-10-25 11:18 UTC4
    schema::{Field, IndexRecordOption},
74befbc72bafatagen2026-10-25 11:18 UTC5
    tokenizer::{
74befbc72bafatagen2026-10-25 11:18 UTC6
        AsciiFoldingFilter, LowerCaser, RawTokenizer, TextAnalyzer, Token, TokenStream, Tokenizer,
74befbc72bafatagen2026-10-25 11:18 UTC7
    },
74befbc72bafatagen2026-10-25 11:18 UTC8
};
74befbc72bafatagen2026-10-25 11:18 UTC9
74befbc72bafatagen2026-10-25 11:18 UTC10
use crate::{Error, ErrorKind};
74befbc72bafatagen2026-10-25 11:18 UTC11
74befbc72bafatagen2026-10-25 11:18 UTC12
pub(crate) const TOKENIZER: &str = "macltok";
74befbc72bafatagen2026-10-25 11:18 UTC13
/// longer runs are hashes, keys or noise, and in chars so cyrillic and greek words fit
74befbc72bafatagen2026-10-25 11:18 UTC14
const MAX_WORD_CHARS: usize = 64;
74befbc72bafatagen2026-10-25 11:18 UTC15
74befbc72bafatagen2026-10-25 11:18 UTC16
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74befbc72bafatagen2026-10-25 11:18 UTC17
enum Kind {
74befbc72bafatagen2026-10-25 11:18 UTC18
    Word,
74befbc72bafatagen2026-10-25 11:18 UTC19
    /// one char of a han, kana, hangul or thai run, or an emoji
74befbc72bafatagen2026-10-25 11:18 UTC20
    Single,
74befbc72bafatagen2026-10-25 11:18 UTC21
    /// two neighbouring chars of such a run, sharing the first one's position
74befbc72bafatagen2026-10-25 11:18 UTC22
    Pair,
74befbc72bafatagen2026-10-25 11:18 UTC23
}
74befbc72bafatagen2026-10-25 11:18 UTC24
74befbc72bafatagen2026-10-25 11:18 UTC25
#[derive(Clone, Debug, Eq, PartialEq)]
74befbc72bafatagen2026-10-25 11:18 UTC26
struct Piece {
74befbc72bafatagen2026-10-25 11:18 UTC27
    from: usize,
74befbc72bafatagen2026-10-25 11:18 UTC28
    to: usize,
74befbc72bafatagen2026-10-25 11:18 UTC29
    position: usize,
74befbc72bafatagen2026-10-25 11:18 UTC30
    kind: Kind,
74befbc72bafatagen2026-10-25 11:18 UTC31
    /// a single inside a run of two or more, which its pairs already cover in a query
74befbc72bafatagen2026-10-25 11:18 UTC32
    paired: bool,
74befbc72bafatagen2026-10-25 11:18 UTC33
}
74befbc72bafatagen2026-10-25 11:18 UTC34
74befbc72bafatagen2026-10-25 11:18 UTC35
/// scripts written without spaces, where a char is the only boundary there is
74befbc72bafatagen2026-10-25 11:18 UTC36
fn unspaced(c: char) -> bool {
74befbc72bafatagen2026-10-25 11:18 UTC37
    matches!(c,
74befbc72bafatagen2026-10-25 11:18 UTC38
        '\u{3040}'..='\u{30FF}' | '\u{31F0}'..='\u{31FF}' | '\u{3400}'..='\u{4DBF}'
74befbc72bafatagen2026-10-25 11:18 UTC39
        | '\u{4E00}'..='\u{9FFF}' | '\u{F900}'..='\u{FAFF}' | '\u{FF66}'..='\u{FF9F}'
74befbc72bafatagen2026-10-25 11:18 UTC40
        | '\u{1100}'..='\u{11FF}' | '\u{3130}'..='\u{318F}' | '\u{AC00}'..='\u{D7AF}'
74befbc72bafatagen2026-10-25 11:18 UTC41
        | '\u{0E00}'..='\u{0E7F}' | '\u{20000}'..='\u{2FA1F}')
74befbc72bafatagen2026-10-25 11:18 UTC42
}
74befbc72bafatagen2026-10-25 11:18 UTC43
74befbc72bafatagen2026-10-25 11:18 UTC44
fn emoji(c: char) -> bool {
74befbc72bafatagen2026-10-25 11:18 UTC45
    matches!(c, '\u{1F300}'..='\u{1FAFF}' | '\u{2600}'..='\u{27BF}')
74befbc72bafatagen2026-10-25 11:18 UTC46
}
74befbc72bafatagen2026-10-25 11:18 UTC47
74befbc72bafatagen2026-10-25 11:18 UTC48
fn pieces(text: &str) -> Vec<Piece> {
74befbc72bafatagen2026-10-25 11:18 UTC49
    let mut pieces = Vec::new();
74befbc72bafatagen2026-10-25 11:18 UTC50
    let mut position = 0;
74befbc72bafatagen2026-10-25 11:18 UTC51
    let mut chars = text.char_indices().peekable();
74befbc72bafatagen2026-10-25 11:18 UTC52
    while let Some((from, c)) = chars.next() {
74befbc72bafatagen2026-10-25 11:18 UTC53
        if unspaced(c) {
74befbc72bafatagen2026-10-25 11:18 UTC54
            let mut run = vec![(from, from + c.len_utf8())];
74befbc72bafatagen2026-10-25 11:18 UTC55
            while let Some(&(at, next)) = chars.peek() {
74befbc72bafatagen2026-10-25 11:18 UTC56
                if !unspaced(next) {
74befbc72bafatagen2026-10-25 11:18 UTC57
                    break;
74befbc72bafatagen2026-10-25 11:18 UTC58
                }
74befbc72bafatagen2026-10-25 11:18 UTC59
                run.push((at, at + next.len_utf8()));
74befbc72bafatagen2026-10-25 11:18 UTC60
                chars.next();
74befbc72bafatagen2026-10-25 11:18 UTC61
            }
74befbc72bafatagen2026-10-25 11:18 UTC62
            let paired = run.len() > 1;
74befbc72bafatagen2026-10-25 11:18 UTC63
            for (index, &(from, to)) in run.iter().enumerate() {
74befbc72bafatagen2026-10-25 11:18 UTC64
                pieces.push(Piece {
74befbc72bafatagen2026-10-25 11:18 UTC65
                    from,
74befbc72bafatagen2026-10-25 11:18 UTC66
                    to,
74befbc72bafatagen2026-10-25 11:18 UTC67
                    position,
74befbc72bafatagen2026-10-25 11:18 UTC68
                    kind: Kind::Single,
74befbc72bafatagen2026-10-25 11:18 UTC69
                    paired,
74befbc72bafatagen2026-10-25 11:18 UTC70
                });
74befbc72bafatagen2026-10-25 11:18 UTC71
                if let Some(&(_, next_to)) = run.get(index + 1) {
74befbc72bafatagen2026-10-25 11:18 UTC72
                    pieces.push(Piece {
74befbc72bafatagen2026-10-25 11:18 UTC73
                        from,
74befbc72bafatagen2026-10-25 11:18 UTC74
                        to: next_to,
74befbc72bafatagen2026-10-25 11:18 UTC75
                        position,
74befbc72bafatagen2026-10-25 11:18 UTC76
                        kind: Kind::Pair,
74befbc72bafatagen2026-10-25 11:18 UTC77
                        paired: false,
74befbc72bafatagen2026-10-25 11:18 UTC78
                    });
74befbc72bafatagen2026-10-25 11:18 UTC79
                }
74befbc72bafatagen2026-10-25 11:18 UTC80
                position += 1;
74befbc72bafatagen2026-10-25 11:18 UTC81
            }
74befbc72bafatagen2026-10-25 11:18 UTC82
        } else if emoji(c) {
74befbc72bafatagen2026-10-25 11:18 UTC83
            pieces.push(Piece {
74befbc72bafatagen2026-10-25 11:18 UTC84
                from,
74befbc72bafatagen2026-10-25 11:18 UTC85
                to: from + c.len_utf8(),
74befbc72bafatagen2026-10-25 11:18 UTC86
                position,
74befbc72bafatagen2026-10-25 11:18 UTC87
                kind: Kind::Single,
74befbc72bafatagen2026-10-25 11:18 UTC88
                paired: false,
74befbc72bafatagen2026-10-25 11:18 UTC89
            });
74befbc72bafatagen2026-10-25 11:18 UTC90
            position += 1;
74befbc72bafatagen2026-10-25 11:18 UTC91
        } else if c.is_alphanumeric() {
74befbc72bafatagen2026-10-25 11:18 UTC92
            let mut to = from + c.len_utf8();
74befbc72bafatagen2026-10-25 11:18 UTC93
            let mut count = 1;
74befbc72bafatagen2026-10-25 11:18 UTC94
            while let Some(&(at, next)) = chars.peek() {
74befbc72bafatagen2026-10-25 11:18 UTC95
                if !next.is_alphanumeric() || unspaced(next) {
74befbc72bafatagen2026-10-25 11:18 UTC96
                    break;
74befbc72bafatagen2026-10-25 11:18 UTC97
                }
74befbc72bafatagen2026-10-25 11:18 UTC98
                to = at + next.len_utf8();
74befbc72bafatagen2026-10-25 11:18 UTC99
                count += 1;
74befbc72bafatagen2026-10-25 11:18 UTC100
                chars.next();
74befbc72bafatagen2026-10-25 11:18 UTC101
            }
74befbc72bafatagen2026-10-25 11:18 UTC102
            if count <= MAX_WORD_CHARS {
74befbc72bafatagen2026-10-25 11:18 UTC103
                pieces.push(Piece {
74befbc72bafatagen2026-10-25 11:18 UTC104
                    from,
74befbc72bafatagen2026-10-25 11:18 UTC105
                    to,
74befbc72bafatagen2026-10-25 11:18 UTC106
                    position,
74befbc72bafatagen2026-10-25 11:18 UTC107
                    kind: Kind::Word,
74befbc72bafatagen2026-10-25 11:18 UTC108
                    paired: false,
74befbc72bafatagen2026-10-25 11:18 UTC109
                });
74befbc72bafatagen2026-10-25 11:18 UTC110
                position += 1;
74befbc72bafatagen2026-10-25 11:18 UTC111
            }
74befbc72bafatagen2026-10-25 11:18 UTC112
        }
74befbc72bafatagen2026-10-25 11:18 UTC113
    }
74befbc72bafatagen2026-10-25 11:18 UTC114
    pieces
74befbc72bafatagen2026-10-25 11:18 UTC115
}
74befbc72bafatagen2026-10-25 11:18 UTC116
74befbc72bafatagen2026-10-25 11:18 UTC117
#[derive(Clone)]
74befbc72bafatagen2026-10-25 11:18 UTC118
pub(crate) struct MacltokTokenizer;
74befbc72bafatagen2026-10-25 11:18 UTC119
74befbc72bafatagen2026-10-25 11:18 UTC120
pub(crate) struct PieceStream {
74befbc72bafatagen2026-10-25 11:18 UTC121
    tokens: std::vec::IntoIter<Token>,
74befbc72bafatagen2026-10-25 11:18 UTC122
    token: Token,
74befbc72bafatagen2026-10-25 11:18 UTC123
}
74befbc72bafatagen2026-10-25 11:18 UTC124
74befbc72bafatagen2026-10-25 11:18 UTC125
impl TokenStream for PieceStream {
74befbc72bafatagen2026-10-25 11:18 UTC126
    fn advance(&mut self) -> bool {
74befbc72bafatagen2026-10-25 11:18 UTC127
        match self.tokens.next() {
74befbc72bafatagen2026-10-25 11:18 UTC128
            Some(token) => {
74befbc72bafatagen2026-10-25 11:18 UTC129
                self.token = token;
74befbc72bafatagen2026-10-25 11:18 UTC130
                true
74befbc72bafatagen2026-10-25 11:18 UTC131
            }
74befbc72bafatagen2026-10-25 11:18 UTC132
            None => false,
74befbc72bafatagen2026-10-25 11:18 UTC133
        }
74befbc72bafatagen2026-10-25 11:18 UTC134
    }
74befbc72bafatagen2026-10-25 11:18 UTC135
74befbc72bafatagen2026-10-25 11:18 UTC136
    fn token(&self) -> &Token {
74befbc72bafatagen2026-10-25 11:18 UTC137
        &self.token
74befbc72bafatagen2026-10-25 11:18 UTC138
    }
74befbc72bafatagen2026-10-25 11:18 UTC139
74befbc72bafatagen2026-10-25 11:18 UTC140
    fn token_mut(&mut self) -> &mut Token {
74befbc72bafatagen2026-10-25 11:18 UTC141
        &mut self.token
74befbc72bafatagen2026-10-25 11:18 UTC142
    }
74befbc72bafatagen2026-10-25 11:18 UTC143
}
74befbc72bafatagen2026-10-25 11:18 UTC144
74befbc72bafatagen2026-10-25 11:18 UTC145
impl Tokenizer for MacltokTokenizer {
74befbc72bafatagen2026-10-25 11:18 UTC146
    type TokenStream<'a> = PieceStream;
74befbc72bafatagen2026-10-25 11:18 UTC147
74befbc72bafatagen2026-10-25 11:18 UTC148
    fn token_stream<'a>(&'a mut self, text: &'a str) -> PieceStream {
74befbc72bafatagen2026-10-25 11:18 UTC149
        let tokens: Vec<_> = pieces(text)
74befbc72bafatagen2026-10-25 11:18 UTC150
            .into_iter()
74befbc72bafatagen2026-10-25 11:18 UTC151
            .map(|piece| Token {
74befbc72bafatagen2026-10-25 11:18 UTC152
                offset_from: piece.from,
74befbc72bafatagen2026-10-25 11:18 UTC153
                offset_to: piece.to,
74befbc72bafatagen2026-10-25 11:18 UTC154
                position: piece.position,
74befbc72bafatagen2026-10-25 11:18 UTC155
                text: text[piece.from..piece.to].to_owned(),
74befbc72bafatagen2026-10-25 11:18 UTC156
                position_length: 1,
74befbc72bafatagen2026-10-25 11:18 UTC157
            })
74befbc72bafatagen2026-10-25 11:18 UTC158
            .collect();
74befbc72bafatagen2026-10-25 11:18 UTC159
        PieceStream {
74befbc72bafatagen2026-10-25 11:18 UTC160
            tokens: tokens.into_iter(),
74befbc72bafatagen2026-10-25 11:18 UTC161
            token: Token::default(),
74befbc72bafatagen2026-10-25 11:18 UTC162
        }
74befbc72bafatagen2026-10-25 11:18 UTC163
    }
74befbc72bafatagen2026-10-25 11:18 UTC164
}
74befbc72bafatagen2026-10-25 11:18 UTC165
74befbc72bafatagen2026-10-25 11:18 UTC166
pub(crate) fn analyzer() -> TextAnalyzer {
74befbc72bafatagen2026-10-25 11:18 UTC167
    TextAnalyzer::builder(MacltokTokenizer)
74befbc72bafatagen2026-10-25 11:18 UTC168
        .filter(LowerCaser)
74befbc72bafatagen2026-10-25 11:18 UTC169
        .filter(AsciiFoldingFilter)
74befbc72bafatagen2026-10-25 11:18 UTC170
        .build()
74befbc72bafatagen2026-10-25 11:18 UTC171
}
74befbc72bafatagen2026-10-25 11:18 UTC172
74befbc72bafatagen2026-10-25 11:18 UTC173
fn fold(text: &str) -> String {
74befbc72bafatagen2026-10-25 11:18 UTC174
    let mut folder = TextAnalyzer::builder(RawTokenizer::default())
74befbc72bafatagen2026-10-25 11:18 UTC175
        .filter(LowerCaser)
74befbc72bafatagen2026-10-25 11:18 UTC176
        .filter(AsciiFoldingFilter)
74befbc72bafatagen2026-10-25 11:18 UTC177
        .build();
74befbc72bafatagen2026-10-25 11:18 UTC178
    let mut stream = folder.token_stream(text);
74befbc72bafatagen2026-10-25 11:18 UTC179
    if stream.advance() {
74befbc72bafatagen2026-10-25 11:18 UTC180
        stream.token().text.clone()
74befbc72bafatagen2026-10-25 11:18 UTC181
    } else {
74befbc72bafatagen2026-10-25 11:18 UTC182
        String::new()
74befbc72bafatagen2026-10-25 11:18 UTC183
    }
74befbc72bafatagen2026-10-25 11:18 UTC184
}
74befbc72bafatagen2026-10-25 11:18 UTC185
74befbc72bafatagen2026-10-25 11:18 UTC186
/// the byte ranges in `text` whose analysed token is one of `terms`, overlaps joined
74befbc72bafatagen2026-10-25 11:18 UTC187
pub(crate) fn match_ranges(text: &str, terms: &[String]) -> Vec<(usize, usize)> {
74befbc72bafatagen2026-10-25 11:18 UTC188
    if terms.is_empty() {
74befbc72bafatagen2026-10-25 11:18 UTC189
        return Vec::new();
74befbc72bafatagen2026-10-25 11:18 UTC190
    }
74befbc72bafatagen2026-10-25 11:18 UTC191
    let mut analyzer = analyzer();
74befbc72bafatagen2026-10-25 11:18 UTC192
    let mut stream = analyzer.token_stream(text);
74befbc72bafatagen2026-10-25 11:18 UTC193
    let mut ranges: Vec<(usize, usize)> = Vec::new();
74befbc72bafatagen2026-10-25 11:18 UTC194
    while stream.advance() {
74befbc72bafatagen2026-10-25 11:18 UTC195
        let token = stream.token();
74befbc72bafatagen2026-10-25 11:18 UTC196
        if !terms.iter().any(|term| *term == token.text) {
74befbc72bafatagen2026-10-25 11:18 UTC197
            continue;
74befbc72bafatagen2026-10-25 11:18 UTC198
        }
74befbc72bafatagen2026-10-25 11:18 UTC199
        match ranges.last_mut() {
74befbc72bafatagen2026-10-25 11:18 UTC200
            Some(last) if token.offset_from < last.1 => last.1 = last.1.max(token.offset_to),
74befbc72bafatagen2026-10-25 11:18 UTC201
            _ => ranges.push((token.offset_from, token.offset_to)),
74befbc72bafatagen2026-10-25 11:18 UTC202
        }
74befbc72bafatagen2026-10-25 11:18 UTC203
    }
74befbc72bafatagen2026-10-25 11:18 UTC204
    ranges
74befbc72bafatagen2026-10-25 11:18 UTC205
}
74befbc72bafatagen2026-10-25 11:18 UTC206
74befbc72bafatagen2026-10-25 11:18 UTC207
enum Clause {
74befbc72bafatagen2026-10-25 11:18 UTC208
    Words(String),
74befbc72bafatagen2026-10-25 11:18 UTC209
    Phrase(String),
74befbc72bafatagen2026-10-25 11:18 UTC210
}
74befbc72bafatagen2026-10-25 11:18 UTC211
74befbc72bafatagen2026-10-25 11:18 UTC212
/// quotes make a phrase, a leading `-` excludes, anything else is a word that must match.
74befbc72bafatagen2026-10-25 11:18 UTC213
/// no field syntax, so `re: deploy` or a url is just its words
74befbc72bafatagen2026-10-25 11:18 UTC214
fn clauses(query: &str) -> Vec<(bool, Clause)> {
74befbc72bafatagen2026-10-25 11:18 UTC215
    let mut clauses = Vec::new();
74befbc72bafatagen2026-10-25 11:18 UTC216
    let mut rest = query;
74befbc72bafatagen2026-10-25 11:18 UTC217
    loop {
74befbc72bafatagen2026-10-25 11:18 UTC218
        rest = rest.trim_start();
74befbc72bafatagen2026-10-25 11:18 UTC219
        if rest.is_empty() {
74befbc72bafatagen2026-10-25 11:18 UTC220
            return clauses;
74befbc72bafatagen2026-10-25 11:18 UTC221
        }
74befbc72bafatagen2026-10-25 11:18 UTC222
        let (excluded, body) = match rest.strip_prefix('-') {
74befbc72bafatagen2026-10-25 11:18 UTC223
            Some(body) if body.starts_with(|c: char| !c.is_whitespace()) => (true, body),
74befbc72bafatagen2026-10-25 11:18 UTC224
            _ => (false, rest),
74befbc72bafatagen2026-10-25 11:18 UTC225
        };
74befbc72bafatagen2026-10-25 11:18 UTC226
        if let Some(quoted) = body.strip_prefix('"') {
74befbc72bafatagen2026-10-25 11:18 UTC227
            let end = quoted.find('"').unwrap_or(quoted.len());
74befbc72bafatagen2026-10-25 11:18 UTC228
            clauses.push((excluded, Clause::Phrase(quoted[..end].to_owned())));
74befbc72bafatagen2026-10-25 11:18 UTC229
            rest = quoted.get(end + 1..).unwrap_or("");
74befbc72bafatagen2026-10-25 11:18 UTC230
        } else {
74befbc72bafatagen2026-10-25 11:18 UTC231
            let end = body.find(char::is_whitespace).unwrap_or(body.len());
74befbc72bafatagen2026-10-25 11:18 UTC232
            clauses.push((excluded, Clause::Words(body[..end].to_owned())));
74befbc72bafatagen2026-10-25 11:18 UTC233
            rest = &body[end..];
74befbc72bafatagen2026-10-25 11:18 UTC234
        }
74befbc72bafatagen2026-10-25 11:18 UTC235
    }
74befbc72bafatagen2026-10-25 11:18 UTC236
}
74befbc72bafatagen2026-10-25 11:18 UTC237
74befbc72bafatagen2026-10-25 11:18 UTC238
fn term(field: Field, text: &str) -> Box<dyn Query> {
74befbc72bafatagen2026-10-25 11:18 UTC239
    Box::new(TermQuery::new(
74befbc72bafatagen2026-10-25 11:18 UTC240
        Term::from_field_text(field, text),
74befbc72bafatagen2026-10-25 11:18 UTC241
        IndexRecordOption::WithFreqs,
74befbc72bafatagen2026-10-25 11:18 UTC242
    ))
74befbc72bafatagen2026-10-25 11:18 UTC243
}
74befbc72bafatagen2026-10-25 11:18 UTC244
74befbc72bafatagen2026-10-25 11:18 UTC245
/// every term the clause needs, and its query, `None` when nothing in it is searchable
74befbc72bafatagen2026-10-25 11:18 UTC246
fn clause_query(field: Field, clause: &Clause) -> Option<(Box<dyn Query>, Vec<String>)> {
74befbc72bafatagen2026-10-25 11:18 UTC247
    let text = match clause {
74befbc72bafatagen2026-10-25 11:18 UTC248
        Clause::Words(text) | Clause::Phrase(text) => text,
74befbc72bafatagen2026-10-25 11:18 UTC249
    };
74befbc72bafatagen2026-10-25 11:18 UTC250
    let found = pieces(text);
74befbc72bafatagen2026-10-25 11:18 UTC251
    if let Clause::Phrase(_) = clause {
74befbc72bafatagen2026-10-25 11:18 UTC252
        let ordered: Vec<_> = found
74befbc72bafatagen2026-10-25 11:18 UTC253
            .iter()
74befbc72bafatagen2026-10-25 11:18 UTC254
            .filter(|piece| piece.kind != Kind::Pair)
74befbc72bafatagen2026-10-25 11:18 UTC255
            .map(|piece| (piece.position, fold(&text[piece.from..piece.to])))
74befbc72bafatagen2026-10-25 11:18 UTC256
            .filter(|(_, folded)| !folded.is_empty())
74befbc72bafatagen2026-10-25 11:18 UTC257
            .collect();
74befbc72bafatagen2026-10-25 11:18 UTC258
        let terms: Vec<String> = ordered.iter().map(|(_, folded)| folded.clone()).collect();
74befbc72bafatagen2026-10-25 11:18 UTC259
        return match ordered.len() {
74befbc72bafatagen2026-10-25 11:18 UTC260
            0 => None,
74befbc72bafatagen2026-10-25 11:18 UTC261
            1 => Some((term(field, &ordered[0].1), terms)),
74befbc72bafatagen2026-10-25 11:18 UTC262
            _ => {
74befbc72bafatagen2026-10-25 11:18 UTC263
                let first = ordered[0].0;
74befbc72bafatagen2026-10-25 11:18 UTC264
                let offsets = ordered
74befbc72bafatagen2026-10-25 11:18 UTC265
                    .iter()
74befbc72bafatagen2026-10-25 11:18 UTC266
                    .map(|(position, folded)| {
74befbc72bafatagen2026-10-25 11:18 UTC267
                        (position - first, Term::from_field_text(field, folded))
74befbc72bafatagen2026-10-25 11:18 UTC268
                    })
74befbc72bafatagen2026-10-25 11:18 UTC269
                    .collect();
74befbc72bafatagen2026-10-25 11:18 UTC270
                Some((Box::new(PhraseQuery::new_with_offset(offsets)), terms))
74befbc72bafatagen2026-10-25 11:18 UTC271
            }
74befbc72bafatagen2026-10-25 11:18 UTC272
        };
74befbc72bafatagen2026-10-25 11:18 UTC273
    }
74befbc72bafatagen2026-10-25 11:18 UTC274
    let mut terms: Vec<String> = found
74befbc72bafatagen2026-10-25 11:18 UTC275
        .iter()
74befbc72bafatagen2026-10-25 11:18 UTC276
        .filter(|piece| !piece.paired)
74befbc72bafatagen2026-10-25 11:18 UTC277
        .map(|piece| fold(&text[piece.from..piece.to]))
74befbc72bafatagen2026-10-25 11:18 UTC278
        .filter(|folded| !folded.is_empty())
74befbc72bafatagen2026-10-25 11:18 UTC279
        .collect();
74befbc72bafatagen2026-10-25 11:18 UTC280
    terms.dedup();
74befbc72bafatagen2026-10-25 11:18 UTC281
    match terms.len() {
74befbc72bafatagen2026-10-25 11:18 UTC282
        0 => None,
74befbc72bafatagen2026-10-25 11:18 UTC283
        1 => Some((term(field, &terms[0]), terms)),
74befbc72bafatagen2026-10-25 11:18 UTC284
        _ => {
74befbc72bafatagen2026-10-25 11:18 UTC285
            let musts = terms
74befbc72bafatagen2026-10-25 11:18 UTC286
                .iter()
74befbc72bafatagen2026-10-25 11:18 UTC287
                .map(|text| (Occur::Must, term(field, text)))
74befbc72bafatagen2026-10-25 11:18 UTC288
                .collect();
74befbc72bafatagen2026-10-25 11:18 UTC289
            Some((Box::new(BooleanQuery::new(musts)), terms))
74befbc72bafatagen2026-10-25 11:18 UTC290
        }
74befbc72bafatagen2026-10-25 11:18 UTC291
    }
74befbc72bafatagen2026-10-25 11:18 UTC292
}
74befbc72bafatagen2026-10-25 11:18 UTC293
74befbc72bafatagen2026-10-25 11:18 UTC294
/// the query and the terms to highlight. one with nothing left to find is `Invalid`,
74befbc72bafatagen2026-10-25 11:18 UTC295
/// rather than everything in scope
74befbc72bafatagen2026-10-25 11:18 UTC296
pub(crate) fn build_query(
74befbc72bafatagen2026-10-25 11:18 UTC297
    field: Field,
74befbc72bafatagen2026-10-25 11:18 UTC298
    query: &str,
74befbc72bafatagen2026-10-25 11:18 UTC299
) -> Result<(Box<dyn Query>, Vec<String>), Error> {
74befbc72bafatagen2026-10-25 11:18 UTC300
    let mut subqueries = Vec::new();
74befbc72bafatagen2026-10-25 11:18 UTC301
    let mut highlights = Vec::new();
74befbc72bafatagen2026-10-25 11:18 UTC302
    let mut positive = false;
74befbc72bafatagen2026-10-25 11:18 UTC303
    for (excluded, clause) in clauses(query) {
74befbc72bafatagen2026-10-25 11:18 UTC304
        let Some((subquery, terms)) = clause_query(field, &clause) else {
74befbc72bafatagen2026-10-25 11:18 UTC305
            continue;
74befbc72bafatagen2026-10-25 11:18 UTC306
        };
74befbc72bafatagen2026-10-25 11:18 UTC307
        if excluded {
74befbc72bafatagen2026-10-25 11:18 UTC308
            subqueries.push((Occur::MustNot, subquery));
74befbc72bafatagen2026-10-25 11:18 UTC309
        } else {
74befbc72bafatagen2026-10-25 11:18 UTC310
            positive = true;
74befbc72bafatagen2026-10-25 11:18 UTC311
            highlights.extend(terms);
74befbc72bafatagen2026-10-25 11:18 UTC312
            subqueries.push((Occur::Must, subquery));
74befbc72bafatagen2026-10-25 11:18 UTC313
        }
74befbc72bafatagen2026-10-25 11:18 UTC314
    }
74befbc72bafatagen2026-10-25 11:18 UTC315
    if !positive {
74befbc72bafatagen2026-10-25 11:18 UTC316
        return Err(Error::new(
74befbc72bafatagen2026-10-25 11:18 UTC317
            ErrorKind::Invalid,
74befbc72bafatagen2026-10-25 11:18 UTC318
            "search needs at least one word that isn't excluded",
74befbc72bafatagen2026-10-25 11:18 UTC319
        ));
74befbc72bafatagen2026-10-25 11:18 UTC320
    }
74befbc72bafatagen2026-10-25 11:18 UTC321
    highlights.sort();
74befbc72bafatagen2026-10-25 11:18 UTC322
    highlights.dedup();
74befbc72bafatagen2026-10-25 11:18 UTC323
    Ok((Box::new(BooleanQuery::new(subqueries)), highlights))
74befbc72bafatagen2026-10-25 11:18 UTC324
}
74befbc72bafatagen2026-10-25 11:18 UTC325
74befbc72bafatagen2026-10-25 11:18 UTC326
#[cfg(test)]
74befbc72bafatagen2026-10-25 11:18 UTC327
#[path = "text_tests.rs"]
74befbc72bafatagen2026-10-25 11:18 UTC328
mod tests;