/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.index.query;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.ConstantScoreQuery;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Query;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.xcontent.XContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.search.internal.SearchContext;
import org.elasticsearch.test.AbstractQueryTestCase;
import org.hamcrest.Matchers;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
import static org.elasticsearch.index.query.QueryBuilders.constantScoreQuery;
import static org.elasticsearch.index.query.QueryBuilders.termQuery;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
public class BoolQueryBuilderTests extends AbstractQueryTestCase<BoolQueryBuilder> {
@Override
protected BoolQueryBuilder doCreateTestQueryBuilder() {
BoolQueryBuilder query = new BoolQueryBuilder();
if (randomBoolean()) {
query.adjustPureNegative(randomBoolean());
}
if (randomBoolean()) {
query.minimumShouldMatch(randomMinimumShouldMatch());
}
int mustClauses = randomIntBetween(0, 3);
for (int i = 0; i < mustClauses; i++) {
query.must(RandomQueryBuilder.createQuery(random()));
}
int mustNotClauses = randomIntBetween(0, 3);
for (int i = 0; i < mustNotClauses; i++) {
query.mustNot(RandomQueryBuilder.createQuery(random()));
}
int shouldClauses = randomIntBetween(0, 3);
for (int i = 0; i < shouldClauses; i++) {
query.should(RandomQueryBuilder.createQuery(random()));
}
int filterClauses = randomIntBetween(0, 3);
for (int i = 0; i < filterClauses; i++) {
query.filter(RandomQueryBuilder.createQuery(random()));
}
return query;
}
@Override
protected void doAssertLuceneQuery(BoolQueryBuilder queryBuilder, Query query, SearchContext searchContext) throws IOException {
if (!queryBuilder.hasClauses()) {
assertThat(query, instanceOf(MatchAllDocsQuery.class));
} else {
QueryShardContext context = searchContext.getQueryShardContext();
List<BooleanClause> clauses = new ArrayList<>();
clauses.addAll(getBooleanClauses(queryBuilder.must(), BooleanClause.Occur.MUST, context));
clauses.addAll(getBooleanClauses(queryBuilder.mustNot(), BooleanClause.Occur.MUST_NOT, context));
clauses.addAll(getBooleanClauses(queryBuilder.should(), BooleanClause.Occur.SHOULD, context));
clauses.addAll(getBooleanClauses(queryBuilder.filter(), BooleanClause.Occur.FILTER, context));
if (clauses.isEmpty()) {
assertThat(query, instanceOf(MatchAllDocsQuery.class));
} else {
assertThat(query, instanceOf(BooleanQuery.class));
BooleanQuery booleanQuery = (BooleanQuery) query;
if (queryBuilder.adjustPureNegative()) {
boolean isNegative = true;
for (BooleanClause clause : clauses) {
if (clause.isProhibited() == false) {
isNegative = false;
break;
}
}
if (isNegative) {
clauses.add(new BooleanClause(new MatchAllDocsQuery(), BooleanClause.Occur.MUST));
}
}
assertThat(booleanQuery.clauses().size(), equalTo(clauses.size()));
Iterator<BooleanClause> clauseIterator = clauses.iterator();
for (BooleanClause booleanClause : booleanQuery.clauses()) {
assertThat(booleanClause, instanceOf(clauseIterator.next().getClass()));
}
}
}
}
private static List<BooleanClause> getBooleanClauses(List<QueryBuilder> queryBuilders, BooleanClause.Occur occur, QueryShardContext context) throws IOException {
List<BooleanClause> clauses = new ArrayList<>();
for (QueryBuilder query : queryBuilders) {
Query innerQuery = query.toQuery(context);
if (innerQuery != null) {
clauses.add(new BooleanClause(innerQuery, occur));
}
}
return clauses;
}
@Override
protected Map<String, BoolQueryBuilder> getAlternateVersions() {
Map<String, BoolQueryBuilder> alternateVersions = new HashMap<>();
BoolQueryBuilder tempQueryBuilder = createTestQueryBuilder();
BoolQueryBuilder expectedQuery = new BoolQueryBuilder();
String contentString = "{\n" +
" \"bool\" : {\n";
if (tempQueryBuilder.must().size() > 0) {
QueryBuilder must = tempQueryBuilder.must().get(0);
contentString += "\"must\": " + must.toString() + ",";
expectedQuery.must(must);
}
if (tempQueryBuilder.mustNot().size() > 0) {
QueryBuilder mustNot = tempQueryBuilder.mustNot().get(0);
contentString += (randomBoolean() ? "\"must_not\": " : "\"mustNot\": ") + mustNot.toString() + ",";
expectedQuery.mustNot(mustNot);
}
if (tempQueryBuilder.should().size() > 0) {
QueryBuilder should = tempQueryBuilder.should().get(0);
contentString += "\"should\": " + should.toString() + ",";
expectedQuery.should(should);
}
if (tempQueryBuilder.filter().size() > 0) {
QueryBuilder filter = tempQueryBuilder.filter().get(0);
contentString += "\"filter\": " + filter.toString() + ",";
expectedQuery.filter(filter);
}
contentString = contentString.substring(0, contentString.length() - 1);
contentString += " } \n" + "}";
alternateVersions.put(contentString, expectedQuery);
return alternateVersions;
}
public void testIllegalArguments() {
BoolQueryBuilder booleanQuery = new BoolQueryBuilder();
expectThrows(IllegalArgumentException.class, () -> booleanQuery.must(null));
expectThrows(IllegalArgumentException.class, () -> booleanQuery.mustNot(null));
expectThrows(IllegalArgumentException.class, () -> booleanQuery.filter(null));
expectThrows(IllegalArgumentException.class, () -> booleanQuery.should(null));
}
// https://github.com/elastic/elasticsearch/issues/7240
public void testEmptyBooleanQuery() throws Exception {
XContentBuilder contentBuilder = XContentFactory.contentBuilder(randomFrom(XContentType.values()));
contentBuilder.startObject().startObject("bool").endObject().endObject();
Query parsedQuery = parseQuery(createParser(contentBuilder)).toQuery(createShardContext());
assertThat(parsedQuery, Matchers.instanceOf(MatchAllDocsQuery.class));
}
public void testDefaultMinShouldMatch() throws Exception {
// Queries have a minShouldMatch of 0
BooleanQuery bq = (BooleanQuery) parseQuery(boolQuery().must(termQuery("foo", "bar"))).toQuery(createShardContext());
assertEquals(0, bq.getMinimumNumberShouldMatch());
bq = (BooleanQuery) parseQuery(boolQuery().should(termQuery("foo", "bar"))).toQuery(createShardContext());
assertEquals(0, bq.getMinimumNumberShouldMatch());
// Filters have a minShouldMatch of 0/1
ConstantScoreQuery csq = (ConstantScoreQuery) parseQuery(constantScoreQuery(boolQuery().must(termQuery("foo", "bar")))).toQuery(createShardContext());
bq = (BooleanQuery) csq.getQuery();
assertEquals(0, bq.getMinimumNumberShouldMatch());
csq = (ConstantScoreQuery) parseQuery(constantScoreQuery(boolQuery().should(termQuery("foo", "bar")))).toQuery(createShardContext());
bq = (BooleanQuery) csq.getQuery();
assertEquals(1, bq.getMinimumNumberShouldMatch());
}
public void testMinShouldMatchFilterWithoutShouldClauses() throws Exception {
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
boolQueryBuilder.filter(new BoolQueryBuilder().must(new MatchAllQueryBuilder()));
Query query = boolQueryBuilder.toQuery(createShardContext());
assertThat(query, instanceOf(BooleanQuery.class));
BooleanQuery booleanQuery = (BooleanQuery) query;
assertThat(booleanQuery.getMinimumNumberShouldMatch(), equalTo(0));
assertThat(booleanQuery.clauses().size(), equalTo(1));
BooleanClause booleanClause = booleanQuery.clauses().get(0);
assertThat(booleanClause.getOccur(), equalTo(BooleanClause.Occur.FILTER));
assertThat(booleanClause.getQuery(), instanceOf(BooleanQuery.class));
BooleanQuery innerBooleanQuery = (BooleanQuery) booleanClause.getQuery();
//we didn't set minimum should match initially, there are no should clauses so it should be 0
assertThat(innerBooleanQuery.getMinimumNumberShouldMatch(), equalTo(0));
assertThat(innerBooleanQuery.clauses().size(), equalTo(1));
BooleanClause innerBooleanClause = innerBooleanQuery.clauses().get(0);
assertThat(innerBooleanClause.getOccur(), equalTo(BooleanClause.Occur.MUST));
assertThat(innerBooleanClause.getQuery(), instanceOf(MatchAllDocsQuery.class));
}
public void testMinShouldMatchFilterWithShouldClauses() throws Exception {
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
boolQueryBuilder.filter(new BoolQueryBuilder().must(new MatchAllQueryBuilder()).should(new MatchAllQueryBuilder()));
Query query = boolQueryBuilder.toQuery(createShardContext());
assertThat(query, instanceOf(BooleanQuery.class));
BooleanQuery booleanQuery = (BooleanQuery) query;
assertThat(booleanQuery.getMinimumNumberShouldMatch(), equalTo(0));
assertThat(booleanQuery.clauses().size(), equalTo(1));
BooleanClause booleanClause = booleanQuery.clauses().get(0);
assertThat(booleanClause.getOccur(), equalTo(BooleanClause.Occur.FILTER));
assertThat(booleanClause.getQuery(), instanceOf(BooleanQuery.class));
BooleanQuery innerBooleanQuery = (BooleanQuery) booleanClause.getQuery();
//we didn't set minimum should match initially, but there are should clauses so it should be 1
assertThat(innerBooleanQuery.getMinimumNumberShouldMatch(), equalTo(1));
assertThat(innerBooleanQuery.clauses().size(), equalTo(2));
BooleanClause innerBooleanClause1 = innerBooleanQuery.clauses().get(0);
assertThat(innerBooleanClause1.getOccur(), equalTo(BooleanClause.Occur.MUST));
assertThat(innerBooleanClause1.getQuery(), instanceOf(MatchAllDocsQuery.class));
BooleanClause innerBooleanClause2 = innerBooleanQuery.clauses().get(1);
assertThat(innerBooleanClause2.getOccur(), equalTo(BooleanClause.Occur.SHOULD));
assertThat(innerBooleanClause2.getQuery(), instanceOf(MatchAllDocsQuery.class));
}
public void testMinShouldMatchBiggerThanNumberOfShouldClauses() throws Exception {
BooleanQuery bq = (BooleanQuery) parseQuery(
boolQuery()
.should(termQuery("foo", "bar"))
.should(termQuery("foo2", "bar2"))
.minimumShouldMatch("3")).toQuery(createShardContext());
assertEquals(3, bq.getMinimumNumberShouldMatch());
bq = (BooleanQuery) parseQuery(
boolQuery()
.should(termQuery("foo", "bar"))
.should(termQuery("foo2", "bar2"))
.minimumShouldMatch(3)).toQuery(createShardContext());
assertEquals(3, bq.getMinimumNumberShouldMatch());
}
public void testMinShouldMatchDisableCoord() throws Exception {
BooleanQuery bq = (BooleanQuery) parseQuery(
boolQuery()
.should(termQuery("foo", "bar"))
.should(termQuery("foo2", "bar2"))
.minimumShouldMatch("3")).toQuery(createShardContext());
assertEquals(3, bq.getMinimumNumberShouldMatch());
}
public void testFromJson() throws IOException {
String query =
"{" +
"\"bool\" : {" +
" \"must\" : [ {" +
" \"term\" : {" +
" \"user\" : {" +
" \"value\" : \"kimchy\"," +
" \"boost\" : 1.0" +
" }" +
" }" +
" } ]," +
" \"filter\" : [ {" +
" \"term\" : {" +
" \"tag\" : {" +
" \"value\" : \"tech\"," +
" \"boost\" : 1.0" +
" }" +
" }" +
" } ]," +
" \"must_not\" : [ {" +
" \"range\" : {" +
" \"age\" : {" +
" \"from\" : 10," +
" \"to\" : 20," +
" \"include_lower\" : true," +
" \"include_upper\" : true," +
" \"boost\" : 1.0" +
" }" +
" }" +
" } ]," +
" \"should\" : [ {" +
" \"term\" : {" +
" \"tag\" : {" +
" \"value\" : \"wow\"," +
" \"boost\" : 1.0" +
" }" +
" }" +
" }, {" +
" \"term\" : {" +
" \"tag\" : {" +
" \"value\" : \"elasticsearch\"," +
" \"boost\" : 1.0" +
" }" +
" }" +
" } ]," +
" \"adjust_pure_negative\" : true," +
" \"minimum_should_match\" : \"23\"," +
" \"boost\" : 42.0" +
"}" +
"}";
BoolQueryBuilder queryBuilder = (BoolQueryBuilder) parseQuery(query);
checkGeneratedJson(query, queryBuilder);
assertEquals(query, 42, queryBuilder.boost, 0.00001);
assertEquals(query, "23", queryBuilder.minimumShouldMatch());
assertEquals(query, "kimchy", ((TermQueryBuilder)queryBuilder.must().get(0)).value());
}
/**
* test that unknown query names in the clauses throw an error
*/
public void testUnknownQueryName() throws IOException {
String query = "{\"bool\" : {\"must\" : { \"unknown_query\" : { } } } }";
ParsingException ex = expectThrows(ParsingException.class, () -> parseQuery(query));
assertEquals("no [query] registered for [unknown_query]", ex.getMessage());
}
/**
* test that two queries in object throws error
*/
public void testTooManyQueriesInObject() throws IOException {
assumeFalse("Test only makes sense if XContent parser doesn't have strict duplicate checks enabled",
XContent.isStrictDuplicateDetectionEnabled());
String clauseType = randomFrom("must", "should", "must_not", "filter");
// should also throw error if invalid query is preceded by a valid one
String query = "{\n" +
" \"bool\": {\n" +
" \"" + clauseType + "\": {\n" +
" \"match\": {\n" +
" \"foo\": \"bar\"\n" +
" },\n" +
" \"match\": {\n" +
" \"baz\": \"buzz\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
ParsingException ex = expectThrows(ParsingException.class, () -> parseQuery(query));
assertEquals("[match] malformed query, expected [END_OBJECT] but found [FIELD_NAME]", ex.getMessage());
}
public void testRewrite() throws IOException {
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
boolean mustRewrite = false;
if (randomBoolean()) {
mustRewrite = true;
boolQueryBuilder.must(new WrapperQueryBuilder(new TermsQueryBuilder("foo", "must").toString()));
}
if (randomBoolean()) {
mustRewrite = true;
boolQueryBuilder.should(new WrapperQueryBuilder(new TermsQueryBuilder("foo", "should").toString()));
}
if (randomBoolean()) {
mustRewrite = true;
boolQueryBuilder.filter(new WrapperQueryBuilder(new TermsQueryBuilder("foo", "filter").toString()));
}
if (randomBoolean()) {
mustRewrite = true;
boolQueryBuilder.mustNot(new WrapperQueryBuilder(new TermsQueryBuilder("foo", "must_not").toString()));
}
if (mustRewrite == false && randomBoolean()) {
boolQueryBuilder.must(new TermsQueryBuilder("foo", "no_rewrite"));
}
QueryBuilder rewritten = boolQueryBuilder.rewrite(createShardContext());
if (mustRewrite == false && boolQueryBuilder.must().isEmpty()) {
// if it's empty we rewrite to match all
assertEquals(rewritten, new MatchAllQueryBuilder());
} else {
BoolQueryBuilder rewrite = (BoolQueryBuilder) rewritten;
if (mustRewrite) {
assertNotSame(rewrite, boolQueryBuilder);
if (boolQueryBuilder.must().isEmpty() == false) {
assertEquals(new TermsQueryBuilder("foo", "must"), rewrite.must().get(0));
}
if (boolQueryBuilder.should().isEmpty() == false) {
assertEquals(new TermsQueryBuilder("foo", "should"), rewrite.should().get(0));
}
if (boolQueryBuilder.mustNot().isEmpty() == false) {
assertEquals(new TermsQueryBuilder("foo", "must_not"), rewrite.mustNot().get(0));
}
if (boolQueryBuilder.filter().isEmpty() == false) {
assertEquals(new TermsQueryBuilder("foo", "filter"), rewrite.filter().get(0));
}
} else {
assertSame(rewrite, boolQueryBuilder);
if (boolQueryBuilder.must().isEmpty() == false) {
assertSame(boolQueryBuilder.must().get(0), rewrite.must().get(0));
}
}
}
}
public void testRewriteMultipleTimes() throws IOException {
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
boolQueryBuilder.must(new WrapperQueryBuilder(new WrapperQueryBuilder(new MatchAllQueryBuilder().toString()).toString()));
QueryBuilder rewritten = boolQueryBuilder.rewrite(createShardContext());
BoolQueryBuilder expected = new BoolQueryBuilder();
expected.must(new WrapperQueryBuilder(new MatchAllQueryBuilder().toString()));
assertEquals(expected, rewritten);
expected = new BoolQueryBuilder();
expected.must(new MatchAllQueryBuilder());
QueryBuilder rewrittenAgain = rewritten.rewrite(createShardContext());
assertEquals(rewrittenAgain, expected);
assertEquals(QueryBuilder.rewriteQuery(boolQueryBuilder, createShardContext()), expected);
}
}