Skip to main content

tantivy/query/phrase_query/
regex_phrase_query.rs

1use super::regex_phrase_weight::RegexPhraseWeight;
2use crate::query::bm25::Bm25Weight;
3use crate::query::{EnableScoring, Query, Weight};
4use crate::schema::{Field, IndexRecordOption, Term, Type};
5
6/// `RegexPhraseQuery` matches a specific sequence of regex queries.
7///
8/// For instance, the phrase query for `"pa.* time"` will match
9/// the sentence:
10///
11/// **Alan just got a part time job.**
12///
13/// On the other hand it will not match the sentence.
14///
15/// **This is my favorite part of the job.**
16///
17/// [Slop](RegexPhraseQuery::set_slop) allows leniency in term proximity
18/// for some performance trade-off.
19///
20/// Using a `RegexPhraseQuery` on a field requires positions
21/// to be indexed for this field.
22#[derive(Clone, Debug)]
23pub struct RegexPhraseQuery {
24    field: Field,
25    phrase_terms: Vec<(usize, String)>,
26    slop: u32,
27    max_expansions: u32,
28}
29
30/// Transform a wildcard query to a regex string.
31///
32/// `AB*CD` for example is converted to `AB.*CD`
33///
34/// All other chars are regex escaped.
35pub fn wildcard_query_to_regex_str(term: &str) -> String {
36    regex::escape(term).replace(r"\*", ".*")
37}
38
39impl RegexPhraseQuery {
40    /// Creates a new `RegexPhraseQuery` given a list of terms.
41    ///
42    /// There must be at least two terms, and all terms
43    /// must belong to the same field.
44    ///
45    /// Offset for each term will be same as index in the Vector
46    pub fn new(field: Field, terms: Vec<String>) -> RegexPhraseQuery {
47        let terms_with_offset = terms.into_iter().enumerate().collect();
48        RegexPhraseQuery::new_with_offset(field, terms_with_offset)
49    }
50
51    /// Creates a new `RegexPhraseQuery` given a list of terms and their offsets.
52    ///
53    /// Can be used to provide custom offset for each term.
54    pub fn new_with_offset(field: Field, terms: Vec<(usize, String)>) -> RegexPhraseQuery {
55        RegexPhraseQuery::new_with_offset_and_slop(field, terms, 0)
56    }
57
58    /// Creates a new `RegexPhraseQuery` given a list of terms, their offsets and a slop
59    pub fn new_with_offset_and_slop(
60        field: Field,
61        mut terms: Vec<(usize, String)>,
62        slop: u32,
63    ) -> RegexPhraseQuery {
64        assert!(
65            terms.len() > 1,
66            "A phrase query is required to have strictly more than one term."
67        );
68        terms.sort_by_key(|&(offset, _)| offset);
69        RegexPhraseQuery {
70            field,
71            phrase_terms: terms,
72            slop,
73            max_expansions: 1 << 14,
74        }
75    }
76
77    /// Slop allowed for the phrase.
78    ///
79    /// The query will match if its terms are separated by `slop` terms at most.
80    /// The slop can be considered a budget between all terms.
81    /// E.g. "A B C" with slop 1 allows "A X B C", "A B X C", but not "A X B X C".
82    ///
83    /// Transposition costs 2, e.g. "A B" with slop 1 will not match "B A" but it would with slop 2
84    /// Transposition is not a special case, in the example above A is moved 1 position and B is
85    /// moved 1 position, so the slop is 2.
86    ///
87    /// As a result slop works in both directions, so the order of the terms may changed as long as
88    /// they respect the slop.
89    ///
90    /// By default the slop is 0 meaning query terms need to be adjacent.
91    pub fn set_slop(&mut self, value: u32) {
92        self.slop = value;
93    }
94
95    /// Sets the max expansions a regex term can match. The limit will be over all terms.
96    /// After the limit is hit an error will be returned.
97    pub fn set_max_expansions(&mut self, value: u32) {
98        self.max_expansions = value;
99    }
100
101    /// The [`Field`] this `RegexPhraseQuery` is targeting.
102    pub fn field(&self) -> Field {
103        self.field
104    }
105
106    /// `Term`s in the phrase without the associated offsets.
107    pub fn phrase_terms(&self) -> Vec<Term> {
108        self.phrase_terms
109            .iter()
110            .map(|(_, term)| Term::from_field_text(self.field, term))
111            .collect::<Vec<Term>>()
112    }
113
114    /// Returns the [`RegexPhraseWeight`] for the given phrase query given a specific `searcher`.
115    ///
116    /// This function is the same as [`Query::weight()`] except it returns
117    /// a specialized type [`RegexPhraseWeight`] instead of a Boxed trait.
118    pub(crate) fn regex_phrase_weight(
119        &self,
120        enable_scoring: EnableScoring<'_>,
121    ) -> crate::Result<RegexPhraseWeight> {
122        let schema = enable_scoring.schema();
123        let field_type = schema.get_field_entry(self.field).field_type().value_type();
124        if field_type != Type::Str {
125            return Err(crate::TantivyError::SchemaError(format!(
126                "RegexPhraseQuery can only be used with a field of type text currently, but got \
127                 {field_type:?}"
128            )));
129        }
130
131        let field_entry = schema.get_field_entry(self.field);
132        let has_positions = field_entry
133            .field_type()
134            .get_index_record_option()
135            .map(IndexRecordOption::has_positions)
136            .unwrap_or(false);
137        if !has_positions {
138            let field_name = field_entry.name();
139            return Err(crate::TantivyError::SchemaError(format!(
140                "Applied phrase query on field {field_name:?}, which does not have positions \
141                 indexed"
142            )));
143        }
144        let terms = self.phrase_terms();
145        let bm25_weight_opt = match enable_scoring {
146            EnableScoring::Enabled {
147                statistics_provider,
148                ..
149            } => Some(Bm25Weight::for_terms(statistics_provider, &terms)?),
150            EnableScoring::Disabled { .. } => None,
151        };
152        let weight = RegexPhraseWeight::new(
153            self.field,
154            self.phrase_terms.clone(),
155            bm25_weight_opt,
156            self.max_expansions,
157            self.slop,
158        );
159        Ok(weight)
160    }
161}
162
163impl Query for RegexPhraseQuery {
164    /// Create the weight associated with a query.
165    ///
166    /// See [`Weight`].
167    fn weight(&self, enable_scoring: EnableScoring<'_>) -> crate::Result<Box<dyn Weight>> {
168        let phrase_weight = self.regex_phrase_weight(enable_scoring)?;
169        Ok(Box::new(phrase_weight))
170    }
171}