From 637803de99e9d87cd0ff3e66de3ee207c74e73c2 Mon Sep 17 00:00:00 2001 From: palmoni5 Date: Tue, 29 Sep 2026 12:45:00 +0300 Subject: [PATCH] Add RegexPhraseQuery::from_regexes (#3138) RegexQuery can be built from a compiled Regex, but RegexPhraseQuery always compiled its patterns with Regex::new and its default DFA state limit. from_regexes takes the compiled regexes directly, so a caller can build them with its own limits, or reuse ones it already compiled. --- src/query/phrase_query/regex_phrase_query.rs | 30 ++++++++++++++++++ src/query/phrase_query/regex_phrase_weight.rs | 31 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/query/phrase_query/regex_phrase_query.rs b/src/query/phrase_query/regex_phrase_query.rs index 1310871ca..67ca03499 100644 --- a/src/query/phrase_query/regex_phrase_query.rs +++ b/src/query/phrase_query/regex_phrase_query.rs @@ -96,6 +96,36 @@ impl RegexPhraseQuery { } } + /// Creates a new `RegexPhraseQuery` from already compiled regexes, e.g. built with a + /// non-default state limit. + /// + /// Each term is `(offset, pattern, regex)`, where `regex` is the compilation of + /// `pattern`; the pattern is what [`RegexPhraseQuery::phrase_terms`] returns. + pub fn from_regexes( + field: Field, + mut terms: Vec<(usize, String, Arc)>, + slop: u32, + ) -> RegexPhraseQuery { + assert!( + terms.len() > 1, + "A phrase query is required to have strictly more than one term." + ); + terms.sort_by_key(|&(offset, _, _)| offset); + let (phrase_terms, regexes): (Vec<_>, Vec<_>) = terms + .into_iter() + .map(|(offset, pattern, regex)| ((offset, pattern), regex)) + .unzip(); + let compiled = OnceCell::new(); + let _ = compiled.set(regexes); + RegexPhraseQuery { + field, + phrase_terms, + slop, + max_expansions: 1 << 14, + regexes: CompiledRegexes(compiled), + } + } + /// Slop allowed for the phrase. /// /// The query will match if its terms are separated by `slop` terms at most. diff --git a/src/query/phrase_query/regex_phrase_weight.rs b/src/query/phrase_query/regex_phrase_weight.rs index 42018f832..bbaddbc79 100644 --- a/src/query/phrase_query/regex_phrase_weight.rs +++ b/src/query/phrase_query/regex_phrase_weight.rs @@ -382,6 +382,37 @@ mod tests { Ok(()) } + #[test] + pub fn test_phrase_from_regexes_uses_the_given_automata() -> crate::Result<()> { + use std::sync::Arc; + + use tantivy_fst::Regex; + + use crate::collector::Count; + + let index = create_index(&["a b", "aa b", "b a", "a c"])?; + let text_field = index.schema().get_field("text").unwrap(); + let searcher = index.reader()?.searcher(); + let regex_a = Arc::new(Regex::new("a.*").unwrap()); + let regex_b = Arc::new(Regex::new("b").unwrap()); + let from_regexes = RegexPhraseQuery::from_regexes( + text_field, + vec![ + (1, "b".into(), regex_b.clone()), + (0, "a.*".into(), regex_a.clone()), + ], + 0, + ); + let regexes = from_regexes.regexes()?; + assert!(Arc::ptr_eq(®exes[0], ®ex_a)); + assert!(Arc::ptr_eq(®exes[1], ®ex_b)); + + let from_patterns = RegexPhraseQuery::new(text_field, vec!["a.*".into(), "b".into()]); + assert_eq!(searcher.search(&from_regexes, &Count)?, 2); + assert_eq!(searcher.search(&from_patterns, &Count)?, 2); + Ok(()) + } + #[test] pub fn test_phrase_count() -> crate::Result<()> { let index = create_index(&["a c", "a a b d a b c", " a b"])?;