Skip to main content

lb_tantivy/query/boolean_query/
boolean_query.rs

1use super::boolean_weight::BooleanWeight;
2use crate::query::{EnableScoring, Occur, Query, SumCombiner, TermQuery, Weight};
3use crate::schema::{IndexRecordOption, Term};
4
5/// The boolean query returns a set of documents
6/// that matches the Boolean combination of constituent subqueries.
7///
8/// The documents matched by the boolean query are those which
9/// - match all of the sub queries associated with the `Must` occurrence
10/// - match none of the sub queries associated with the `MustNot` occurrence.
11/// - match at least one of the sub queries associated with the `Must` or `Should` occurrence.
12///
13/// You can combine other query types and their `Occur`ances into one `BooleanQuery`
14///
15/// ```rust
16/// use tantivy::collector::Count;
17/// use tantivy::doc;
18/// use tantivy::query::{BooleanQuery, Occur, PhraseQuery, Query, TermQuery};
19/// use tantivy::schema::{IndexRecordOption, Schema, TEXT};
20/// use tantivy::Term;
21/// use tantivy::Index;
22/// use tantivy::IndexWriter;
23///
24/// fn main() -> tantivy::Result<()> {
25///    let mut schema_builder = Schema::builder();
26///    let title = schema_builder.add_text_field("title", TEXT);
27///    let body = schema_builder.add_text_field("body", TEXT);
28///    let schema = schema_builder.build();
29///    let index = Index::create_in_ram(schema);
30///    {
31///        let mut index_writer: IndexWriter = index.writer(15_000_000)?;
32///        index_writer.add_document(doc!(
33///            title => "The Name of the Wind",
34///        ))?;
35///        index_writer.add_document(doc!(
36///            title => "The Diary of Muadib",
37///        ))?;
38///        index_writer.add_document(doc!(
39///            title => "A Dairy Cow",
40///            body => "hidden",
41///        ))?;
42///        index_writer.add_document(doc!(
43///            title => "A Dairy Cow",
44///            body => "found",
45///        ))?;
46///        index_writer.add_document(doc!(
47///            title => "The Diary of a Young Girl",
48///        ))?;
49///        index_writer.commit()?;
50///    }
51///
52///    let reader = index.reader()?;
53///    let searcher = reader.searcher();
54///
55///    // Make TermQuery's for "girl" and "diary" in the title
56///    let girl_term_query: Box<dyn Query> = Box::new(TermQuery::new(
57///        Term::from_field_text(title, "girl"),
58///        IndexRecordOption::Basic,
59///    ));
60///    let diary_term_query: Box<dyn Query> = Box::new(TermQuery::new(
61///        Term::from_field_text(title, "diary"),
62///        IndexRecordOption::Basic,
63///    ));
64///    let cow_term_query: Box<dyn Query> = Box::new(TermQuery::new(
65///        Term::from_field_text(title, "cow"),
66///        IndexRecordOption::Basic
67///    ));
68///    // A TermQuery with "found" in the body
69///    let body_term_query: Box<dyn Query> = Box::new(TermQuery::new(
70///        Term::from_field_text(body, "found"),
71///        IndexRecordOption::Basic,
72///    ));
73///    // TermQuery "diary" must and "girl" must not be present
74///    let queries_with_occurs1 = vec![
75///        (Occur::Must, diary_term_query.box_clone()),
76///        (Occur::MustNot, girl_term_query.box_clone()),
77///    ];
78///    // Make a BooleanQuery equivalent to
79///    // title:+diary title:-girl
80///    let diary_must_and_girl_mustnot = BooleanQuery::new(queries_with_occurs1);
81///    let count1 = searcher.search(&diary_must_and_girl_mustnot, &Count)?;
82///    assert_eq!(count1, 1);
83///
84///    // "title:diary OR title:cow"
85///    let title_diary_or_cow = BooleanQuery::new(vec![
86///        (Occur::Should, diary_term_query.box_clone()),
87///        (Occur::Should, cow_term_query.box_clone()),
88///    ]);
89///    let count2 = searcher.search(&title_diary_or_cow, &Count)?;
90///    assert_eq!(count2, 4);
91///
92///    // Make a `PhraseQuery` from a vector of `Term`s
93///    let phrase_query: Box<dyn Query> = Box::new(PhraseQuery::new(vec![
94///        Term::from_field_text(title, "dairy"),
95///        Term::from_field_text(title, "cow"),
96///    ]));
97///    // You can combine subqueries of different types into 1 BooleanQuery:
98///    // `TermQuery` and `PhraseQuery`
99///    // "title:diary OR "dairy cow"
100///    let term_of_phrase_query = BooleanQuery::new(vec![
101///        (Occur::Should, diary_term_query.box_clone()),
102///        (Occur::Should, phrase_query.box_clone()),
103///    ]);
104///    let count3 = searcher.search(&term_of_phrase_query, &Count)?;
105///    assert_eq!(count3, 4);
106///
107///    // You can nest one BooleanQuery inside another
108///    // body:found AND ("title:diary OR "dairy cow")
109///    let nested_query = BooleanQuery::new(vec![
110///        (Occur::Must, body_term_query),
111///        (Occur::Must, Box::new(term_of_phrase_query))
112///    ]);
113///    let count4 = searcher.search(&nested_query, &Count)?;
114///    assert_eq!(count4, 1);
115///
116///    // You may call `with_minimum_required_clauses` to
117///    // specify the number of should clauses the returned documents must match.
118///    let minimum_required_query = BooleanQuery::with_minimum_required_clauses(vec![
119///         (Occur::Should, cow_term_query.box_clone()),
120///         (Occur::Should, girl_term_query.box_clone()),
121///         (Occur::Should, diary_term_query.box_clone()),
122///    ], 2);
123///    // Return documents contains "Diary Cow", "Diary Girl" or "Cow Girl"
124///    // Notice: "Diary" isn't "Dairy". ;-)
125///    let count5 = searcher.search(&minimum_required_query, &Count)?;
126///    assert_eq!(count5, 1);
127///    Ok(())
128/// }
129/// ```
130#[derive(Debug)]
131pub struct BooleanQuery {
132    subqueries: Vec<(Occur, Box<dyn Query>)>,
133    minimum_number_should_match: usize,
134}
135
136impl Clone for BooleanQuery {
137    fn clone(&self) -> Self {
138        let subqueries = self
139            .subqueries
140            .iter()
141            .map(|(occur, subquery)| (*occur, subquery.box_clone()))
142            .collect::<Vec<_>>();
143        Self {
144            subqueries,
145            minimum_number_should_match: self.minimum_number_should_match,
146        }
147    }
148}
149
150impl From<Vec<(Occur, Box<dyn Query>)>> for BooleanQuery {
151    fn from(subqueries: Vec<(Occur, Box<dyn Query>)>) -> BooleanQuery {
152        BooleanQuery::new(subqueries)
153    }
154}
155
156impl Query for BooleanQuery {
157    fn weight(&self, enable_scoring: EnableScoring<'_>) -> crate::Result<Box<dyn Weight>> {
158        let sub_weights = self
159            .subqueries
160            .iter()
161            .map(|(occur, subquery)| Ok((*occur, subquery.weight(enable_scoring)?)))
162            .collect::<crate::Result<_>>()?;
163        Ok(Box::new(BooleanWeight::with_minimum_number_should_match(
164            sub_weights,
165            self.minimum_number_should_match,
166            enable_scoring.is_scoring_enabled(),
167            Box::new(SumCombiner::default),
168        )))
169    }
170
171    fn query_terms<'a>(&'a self, visitor: &mut dyn FnMut(&'a Term, bool)) {
172        for (_occur, subquery) in &self.subqueries {
173            subquery.query_terms(visitor);
174        }
175    }
176}
177
178impl BooleanQuery {
179    /// Creates a new boolean query.
180    pub fn new(subqueries: Vec<(Occur, Box<dyn Query>)>) -> BooleanQuery {
181        // If the bool query includes at least one should clause
182        // and no Must or MustNot clauses, the default value is 1. Otherwise, the default value is
183        // 0. Keep compatible with Elasticsearch.
184        let mut minimum_required = 0;
185        for (occur, _) in &subqueries {
186            match occur {
187                Occur::Should => minimum_required = 1,
188                Occur::Must | Occur::MustNot => {
189                    minimum_required = 0;
190                    break;
191                }
192            }
193        }
194        Self::with_minimum_required_clauses(subqueries, minimum_required)
195    }
196
197    /// Create a new boolean query with minimum number of required should clauses specified.
198    pub fn with_minimum_required_clauses(
199        subqueries: Vec<(Occur, Box<dyn Query>)>,
200        minimum_number_should_match: usize,
201    ) -> BooleanQuery {
202        BooleanQuery {
203            subqueries,
204            minimum_number_should_match,
205        }
206    }
207
208    /// Getter for `minimum_number_should_match`
209    pub fn get_minimum_number_should_match(&self) -> usize {
210        self.minimum_number_should_match
211    }
212
213    /// Setter for `minimum_number_should_match`
214    pub fn set_minimum_number_should_match(&mut self, minimum_number_should_match: usize) {
215        self.minimum_number_should_match = minimum_number_should_match;
216    }
217
218    /// Returns the intersection of the queries.
219    pub fn intersection(queries: Vec<Box<dyn Query>>) -> BooleanQuery {
220        let subqueries = queries.into_iter().map(|s| (Occur::Must, s)).collect();
221        BooleanQuery::new(subqueries)
222    }
223
224    /// Returns the union of the queries.
225    pub fn union(queries: Vec<Box<dyn Query>>) -> BooleanQuery {
226        let subqueries = queries.into_iter().map(|s| (Occur::Should, s)).collect();
227        BooleanQuery::new(subqueries)
228    }
229
230    /// Returns the union of the queries with minimum required clause.
231    pub fn union_with_minimum_required_clauses(
232        queries: Vec<Box<dyn Query>>,
233        minimum_required_clauses: usize,
234    ) -> BooleanQuery {
235        let subqueries = queries
236            .into_iter()
237            .map(|sub_query| (Occur::Should, sub_query))
238            .collect();
239        BooleanQuery::with_minimum_required_clauses(subqueries, minimum_required_clauses)
240    }
241
242    /// Helper method to create a boolean query matching a given list of terms.
243    /// The resulting query is a disjunction of the terms.
244    pub fn new_multiterms_query(terms: Vec<Term>) -> BooleanQuery {
245        let occur_term_queries: Vec<(Occur, Box<dyn Query>)> = terms
246            .into_iter()
247            .map(|term| {
248                let term_query: Box<dyn Query> =
249                    Box::new(TermQuery::new(term, IndexRecordOption::WithFreqs));
250                (Occur::Should, term_query)
251            })
252            .collect();
253        BooleanQuery::new(occur_term_queries)
254    }
255
256    /// Deconstructed view of the clauses making up this query.
257    pub fn clauses(&self) -> &[(Occur, Box<dyn Query>)] {
258        &self.subqueries[..]
259    }
260}
261
262#[cfg(test)]
263mod tests {
264    use std::collections::HashSet;
265
266    use super::BooleanQuery;
267    use crate::collector::{Count, DocSetCollector};
268    use crate::query::{Query, QueryClone, QueryParser, TermQuery};
269    use crate::schema::{Field, IndexRecordOption, Schema, TEXT};
270    use crate::{DocAddress, DocId, Index, Term};
271
272    fn create_test_index() -> crate::Result<Index> {
273        let mut schema_builder = Schema::builder();
274        let text = schema_builder.add_text_field("text", TEXT);
275        let schema = schema_builder.build();
276        let index = Index::create_in_ram(schema);
277        let mut writer = index.writer_for_tests()?;
278        writer.add_document(doc!(text=>"b c"))?;
279        writer.add_document(doc!(text=>"a c"))?;
280        writer.add_document(doc!(text=>"a b"))?;
281        writer.add_document(doc!(text=>"a d"))?;
282        writer.commit()?;
283        Ok(index)
284    }
285
286    #[test]
287    fn test_minimum_required() -> crate::Result<()> {
288        fn create_test_index_with<T: IntoIterator<Item = &'static str>>(
289            docs: T,
290        ) -> crate::Result<Index> {
291            let mut schema_builder = Schema::builder();
292            let text = schema_builder.add_text_field("text", TEXT);
293            let schema = schema_builder.build();
294            let index = Index::create_in_ram(schema);
295            let mut writer = index.writer_for_tests()?;
296            for doc in docs {
297                writer.add_document(doc!(text => doc))?;
298            }
299            writer.commit()?;
300            Ok(index)
301        }
302        fn create_boolean_query_with_mr<T: IntoIterator<Item = &'static str>>(
303            queries: T,
304            field: Field,
305            mr: usize,
306        ) -> BooleanQuery {
307            let terms = queries
308                .into_iter()
309                .map(|t| Term::from_field_text(field, t))
310                .map(|t| TermQuery::new(t, IndexRecordOption::Basic))
311                .map(|q| -> Box<dyn Query> { Box::new(q) })
312                .collect();
313            BooleanQuery::union_with_minimum_required_clauses(terms, mr)
314        }
315        fn check_doc_id<T: IntoIterator<Item = DocId>>(
316            expected: T,
317            actually: HashSet<DocAddress>,
318            seg: u32,
319        ) {
320            assert_eq!(
321                actually,
322                expected
323                    .into_iter()
324                    .map(|id| DocAddress::new(seg, id))
325                    .collect()
326            );
327        }
328        let index = create_test_index_with(["a b c", "a c e", "d f g", "z z z", "c i b"])?;
329        let searcher = index.reader()?.searcher();
330        let text = index.schema().get_field("text").unwrap();
331        // Documents contains 'a c' 'a z' 'a i' 'c z' 'c i' or 'z i' shall be return.
332        let q1 = create_boolean_query_with_mr(["a", "c", "z", "i"], text, 2);
333        let docs = searcher.search(&q1, &DocSetCollector)?;
334        check_doc_id([0, 1, 4], docs, 0);
335        // Documents contains 'a b c', 'a b e', 'a c e' or 'b c e' shall be return.
336        let q2 = create_boolean_query_with_mr(["a", "b", "c", "e"], text, 3);
337        let docs = searcher.search(&q2, &DocSetCollector)?;
338        check_doc_id([0, 1], docs, 0);
339        // Nothing queried since minimum_required is too large.
340        let q3 = create_boolean_query_with_mr(["a", "b"], text, 3);
341        let docs = searcher.search(&q3, &DocSetCollector)?;
342        assert!(docs.is_empty());
343        // When mr is set to zero or one, there are no difference with `Boolean::Union`.
344        let q4 = create_boolean_query_with_mr(["a", "z"], text, 1);
345        let docs = searcher.search(&q4, &DocSetCollector)?;
346        check_doc_id([0, 1, 3], docs, 0);
347        let q5 = create_boolean_query_with_mr(["a", "b"], text, 0);
348        let docs = searcher.search(&q5, &DocSetCollector)?;
349        check_doc_id([0, 1, 4], docs, 0);
350        Ok(())
351    }
352
353    #[test]
354    fn test_union() -> crate::Result<()> {
355        let index = create_test_index()?;
356        let searcher = index.reader()?.searcher();
357        let text = index.schema().get_field("text").unwrap();
358        let term_a = TermQuery::new(Term::from_field_text(text, "a"), IndexRecordOption::Basic);
359        let term_d = TermQuery::new(Term::from_field_text(text, "d"), IndexRecordOption::Basic);
360        let union_ad = BooleanQuery::union(vec![term_a.box_clone(), term_d.box_clone()]);
361        let docs = searcher.search(&union_ad, &DocSetCollector)?;
362        assert_eq!(
363            docs,
364            vec![
365                DocAddress::new(0u32, 1u32),
366                DocAddress::new(0u32, 2u32),
367                DocAddress::new(0u32, 3u32)
368            ]
369            .into_iter()
370            .collect()
371        );
372        Ok(())
373    }
374
375    #[test]
376    fn test_intersection() -> crate::Result<()> {
377        let index = create_test_index()?;
378        let searcher = index.reader()?.searcher();
379        let text = index.schema().get_field("text").unwrap();
380        let term_a = TermQuery::new(Term::from_field_text(text, "a"), IndexRecordOption::Basic);
381        let term_b = TermQuery::new(Term::from_field_text(text, "b"), IndexRecordOption::Basic);
382        let term_c = TermQuery::new(Term::from_field_text(text, "c"), IndexRecordOption::Basic);
383        let intersection_ab =
384            BooleanQuery::intersection(vec![term_a.box_clone(), term_b.box_clone()]);
385        let intersection_ac =
386            BooleanQuery::intersection(vec![term_a.box_clone(), term_c.box_clone()]);
387        let intersection_bc =
388            BooleanQuery::intersection(vec![term_b.box_clone(), term_c.box_clone()]);
389        {
390            let docs = searcher.search(&intersection_ab, &DocSetCollector)?;
391            assert_eq!(
392                docs,
393                vec![DocAddress::new(0u32, 2u32)].into_iter().collect()
394            );
395        }
396        {
397            let docs = searcher.search(&intersection_ac, &DocSetCollector)?;
398            assert_eq!(
399                docs,
400                vec![DocAddress::new(0u32, 1u32)].into_iter().collect()
401            );
402        }
403        {
404            let docs = searcher.search(&intersection_bc, &DocSetCollector)?;
405            assert_eq!(
406                docs,
407                vec![DocAddress::new(0u32, 0u32)].into_iter().collect()
408            );
409        }
410        Ok(())
411    }
412
413    #[test]
414    pub fn test_json_array_pitfall_bag_of_terms() -> crate::Result<()> {
415        let mut schema_builder = Schema::builder();
416        let json_field = schema_builder.add_json_field("json", TEXT);
417        let schema = schema_builder.build();
418        let index = Index::create_in_ram(schema);
419        {
420            let mut index_writer = index.writer_for_tests()?;
421            index_writer.add_document(doc!(json_field=>json!({
422                "cart": [
423                    {"product_type": "sneakers", "attributes": {"color": "white"}},
424                    {"product_type": "t-shirt", "attributes": {"color": "red"}},
425                    {"product_type": "cd", "attributes": {"genre": "blues"}},
426                ]
427            })))?;
428            index_writer.commit()?;
429        }
430        let searcher = index.reader()?.searcher();
431        let doc_matches = |query: &str| {
432            let query_parser = QueryParser::for_index(&index, vec![json_field]);
433            let query = query_parser.parse_query(query).unwrap();
434            searcher.search(&query, &Count).unwrap() == 1
435        };
436        // As expected
437        assert!(doc_matches(
438            r#"cart.product_type:sneakers AND cart.attributes.color:white"#
439        ));
440        // Unexpected match, due to the fact that array do not act as nested docs.
441        assert!(doc_matches(
442            r#"cart.product_type:sneakers AND cart.attributes.color:red"#
443        ));
444        // However, bviously this works...
445        assert!(!doc_matches(
446            r#"cart.product_type:sneakers AND cart.attributes.color:blues"#
447        ));
448        Ok(())
449    }
450}