lb_tantivy/query/boolean_query/
boolean_query.rs1use super::boolean_weight::BooleanWeight;
2use crate::query::{EnableScoring, Occur, Query, SumCombiner, TermQuery, Weight};
3use crate::schema::{IndexRecordOption, Term};
4
5#[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 pub fn new(subqueries: Vec<(Occur, Box<dyn Query>)>) -> BooleanQuery {
181 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 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 pub fn get_minimum_number_should_match(&self) -> usize {
210 self.minimum_number_should_match
211 }
212
213 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 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 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 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 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 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 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 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 let q3 = create_boolean_query_with_mr(["a", "b"], text, 3);
341 let docs = searcher.search(&q3, &DocSetCollector)?;
342 assert!(docs.is_empty());
343 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 assert!(doc_matches(
438 r#"cart.product_type:sneakers AND cart.attributes.color:white"#
439 ));
440 assert!(doc_matches(
442 r#"cart.product_type:sneakers AND cart.attributes.color:red"#
443 ));
444 assert!(!doc_matches(
446 r#"cart.product_type:sneakers AND cart.attributes.color:blues"#
447 ));
448 Ok(())
449 }
450}