From 79b03328af4d5e3b52433b465907402945da9a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Thu, 11 Jun 2015 12:42:00 +0200 Subject: [PATCH] Refactors BoolQueryBuilder and Parser. Splits the parse(QueryParseContext ctx) method into a parsing and a query building part, adding NamedWriteable implementation for serialization and hashCode(), equals() for testing. This change also adds tests using fixed set of leaf queries (Terms, Ids, MatchAll) as nested Queries in test query setup. Also QueryParseContext is adapted to return QueryBuilder instances for inner filter parses instead of previously returning Query instances, keeping old methods in place but deprecating them. Relates to #10217 Closes #11427 --- .../common/io/stream/StreamInput.java | 14 + .../common/io/stream/StreamOutput.java | 11 + .../index/query/BoolQueryBuilder.java | 257 ++++++++++++++++-- .../index/query/BoolQueryParser.java | 81 +++--- .../index/query/QueryParseContext.java | 35 ++- .../index/query/BoolQueryBuilderTest.java | 115 ++++++++ .../index/query/RandomQueryBuilder.java | 49 ++++ 7 files changed, 496 insertions(+), 66 deletions(-) create mode 100644 core/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTest.java create mode 100644 core/src/test/java/org/elasticsearch/index/query/RandomQueryBuilder.java diff --git a/core/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java b/core/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java index 82a57a1652dd9..6ecf2231659f6 100644 --- a/core/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java +++ b/core/src/main/java/org/elasticsearch/common/io/stream/StreamInput.java @@ -509,6 +509,20 @@ public C readNamedWriteable() throws IOException { return namedWriteable.readFrom(this); } + /** + * Reads a list of {@link NamedWriteable} from the current stream, by first reading its size and then + * reading the individual objects using {@link #readNamedWriteable()}. + */ + public List readNamedWritableList() throws IOException { + List list = new ArrayList<>(); + int size = readInt(); + for (int i = 0; i < size; i++) { + C obj = readNamedWriteable(); + list.add(obj); + } + return list; + } + public static StreamInput wrap(BytesReference reference) { if (reference.hasArray() == false) { reference = reference.toBytesArray(); diff --git a/core/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java b/core/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java index 02f01e599e86e..e3ceb1028c9d2 100644 --- a/core/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java +++ b/core/src/main/java/org/elasticsearch/common/io/stream/StreamOutput.java @@ -457,4 +457,15 @@ public void writeNamedWriteable(NamedWriteable namedWriteable) throws IOExceptio writeString(namedWriteable.getName()); namedWriteable.writeTo(this); } + + /** + * Writes a list of {@link NamedWriteable} to the current stream, by first writing its size and then iterating over the objects + * in the list + */ + public void writeNamedWritableList(List list) throws IOException { + writeInt(list.size()); + for (NamedWriteable obj : list) { + writeNamedWriteable(obj); + } + } } diff --git a/core/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/core/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index 6977e100a4fe9..f86d3169033c9 100644 --- a/core/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/core/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -19,20 +19,36 @@ package org.elasticsearch.index.query; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanClause.Occur; +import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; + +import static org.elasticsearch.common.lucene.search.Queries.fixNegativeQueryIfNeeded; /** * A Query that matches documents matching boolean combinations of other queries. */ -public class BoolQueryBuilder extends QueryBuilder implements BoostableQueryBuilder { +public class BoolQueryBuilder extends QueryBuilder implements BoostableQueryBuilder { public static final String NAME = "bool"; + static final boolean ADJUST_PURE_NEGATIVE_DEFAULT = true; + + static final boolean DISABLE_COORD_DEFAULT = false; + + static final BoolQueryBuilder PROTOTYPE = new BoolQueryBuilder(); + private final List mustClauses = new ArrayList<>(); private final List mustNotClauses = new ArrayList<>(); @@ -41,18 +57,16 @@ public class BoolQueryBuilder extends QueryBuilder implements BoostableQueryBuil private final List shouldClauses = new ArrayList<>(); - private float boost = -1; + private float boost = 1.0f; - private Boolean disableCoord; + private boolean disableCoord = DISABLE_COORD_DEFAULT; - private String minimumShouldMatch; + private boolean adjustPureNegative = ADJUST_PURE_NEGATIVE_DEFAULT; - private Boolean adjustPureNegative; + private String minimumShouldMatch; private String queryName; - static final BoolQueryBuilder PROTOTYPE = new BoolQueryBuilder(); - /** * Adds a query that must appear in the matching documents and will * contribute to scoring. @@ -62,6 +76,22 @@ public BoolQueryBuilder must(QueryBuilder queryBuilder) { return this; } + /** + * Adds a list of queries that must appear in the matching documents and will + * contribute to scoring. + */ + public BoolQueryBuilder must(List queryBuilders) { + mustClauses.addAll(queryBuilders); + return this; + } + + /** + * Gets the queries that must appear in the matching documents. + */ + public List must() { + return this.mustClauses; + } + /** * Adds a query that must appear in the matching documents but will * not contribute to scoring. @@ -72,8 +102,23 @@ public BoolQueryBuilder filter(QueryBuilder queryBuilder) { } /** - * Adds a query that must not appear in the matching documents and - * will not contribute to scoring. + * Adds a list of queries that must appear in the matching documents but will + * not contribute to scoring. + */ + public BoolQueryBuilder filter(List queryBuilders) { + filterClauses.addAll(queryBuilders); + return this; + } + + /** + * Gets the queries that must appear in the matching documents but don't conntribute to scoring + */ + public List filter() { + return this.filterClauses; + } + + /** + * Adds a query that must not appear in the matching documents. */ public BoolQueryBuilder mustNot(QueryBuilder queryBuilder) { mustNotClauses.add(queryBuilder); @@ -81,7 +126,22 @@ public BoolQueryBuilder mustNot(QueryBuilder queryBuilder) { } /** - * Adds a query that should appear in the matching documents. For a boolean query with no + * Adds a list of queries that must not appear in the matching documents. + */ + public BoolQueryBuilder mustNot(List queryBuilders) { + mustNotClauses.addAll(queryBuilders); + return this; + } + + /** + * Gets the queries that must not appear in the matching documents. + */ + public List mustNot() { + return this.mustNotClauses; + } + + /** + * Adds a clause that should be matched by the returned documents. For a boolean query with no * MUST clauses one or more SHOULD clauses must match a document * for the BooleanQuery to match. * @@ -92,6 +152,28 @@ public BoolQueryBuilder should(QueryBuilder queryBuilder) { return this; } + /** + * Adds a list of clauses that should be matched by the returned documents. For a boolean query with no + * MUST clauses one or more SHOULD clauses must match a document + * for the BooleanQuery to match. + * + * @see #minimumNumberShouldMatch(int) + */ + public BoolQueryBuilder should(List queryBuilders) { + shouldClauses.addAll(queryBuilders); + return this; + } + + /** + * Gets the list of clauses that should be matched by the returned documents. + * + * @see #should(QueryBuilder) + * @see #minimumNumberShouldMatch(int) + */ + public List should() { + return this.shouldClauses; + } + /** * Sets the boost for this query. Documents matching this query will (in addition to the normal * weightings) have their score multiplied by the boost provided. @@ -102,6 +184,13 @@ public BoolQueryBuilder boost(float boost) { return this; } + /** + * Get the boost for this query. + */ + public float boost() { + return this.boost; + } + /** * Disables Similarity#coord(int,int) in scoring. Defaults to false. */ @@ -110,6 +199,13 @@ public BoolQueryBuilder disableCoord(boolean disableCoord) { return this; } + /** + * @return whether the Similarity#coord(int,int) in scoring are disabled. Defaults to false. + */ + public boolean disableCoord() { + return this.disableCoord; + } + /** * Specifies a minimum number of the optional (should) boolean clauses which must be satisfied. *

@@ -128,6 +224,23 @@ public BoolQueryBuilder minimumNumberShouldMatch(int minimumNumberShouldMatch) { return this; } + + /** + * Specifies a minimum number of the optional (should) boolean clauses which must be satisfied. + * @see BoolQueryBuilder#minimumNumberShouldMatch(int) + */ + public BoolQueryBuilder minimumNumberShouldMatch(String minimumNumberShouldMatch) { + this.minimumShouldMatch = minimumNumberShouldMatch; + return this; + } + + /** + * @return the string representation of the minimumShouldMatch settings for this query + */ + public String minimumNumberShouldMatch() { + return this.minimumShouldMatch; + } + /** * Sets the minimum should match using the special syntax (for example, supporting percentage). */ @@ -154,6 +267,13 @@ public BoolQueryBuilder adjustPureNegative(boolean adjustPureNegative) { return this; } + /** + * @return the setting for the adjust_pure_negative setting in this query + */ + public boolean adjustPureNegative() { + return this.adjustPureNegative; + } + /** * Sets the query name for the filter that can be used when searching for matched_filters per hit. */ @@ -162,6 +282,13 @@ public BoolQueryBuilder queryName(String queryName) { return this; } + /** + * Gets the query name for the filter that can be used when searching for matched_filters per hit. + */ + public String queryName() { + return this.queryName; + } + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(NAME); @@ -169,25 +296,19 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep doXArrayContent("filter", filterClauses, builder, params); doXArrayContent("must_not", mustNotClauses, builder, params); doXArrayContent("should", shouldClauses, builder, params); - if (boost != -1) { - builder.field("boost", boost); - } - if (disableCoord != null) { - builder.field("disable_coord", disableCoord); - } + builder.field("boost", boost); + builder.field("disable_coord", disableCoord); + builder.field("adjust_pure_negative", adjustPureNegative); if (minimumShouldMatch != null) { builder.field("minimum_should_match", minimumShouldMatch); } - if (adjustPureNegative != null) { - builder.field("adjust_pure_negative", adjustPureNegative); - } if (queryName != null) { builder.field("_name", queryName); } builder.endObject(); } - private void doXArrayContent(String field, List clauses, XContentBuilder builder, Params params) throws IOException { + private static void doXArrayContent(String field, List clauses, XContentBuilder builder, Params params) throws IOException { if (clauses.isEmpty()) { return; } @@ -207,4 +328,100 @@ private void doXArrayContent(String field, List clauses, XContentB public String queryId() { return NAME; } + + @Override + public Query toQuery(QueryParseContext parseContext) throws QueryParsingException, IOException { + if (!hasClauses()) { + return new MatchAllDocsQuery(); + } + + BooleanQuery booleanQuery = new BooleanQuery(this.disableCoord); + addBooleanClauses(parseContext, booleanQuery, this.mustClauses, BooleanClause.Occur.MUST); + addBooleanClauses(parseContext, booleanQuery, this.mustNotClauses, BooleanClause.Occur.MUST_NOT); + addBooleanClauses(parseContext, booleanQuery, this.shouldClauses, BooleanClause.Occur.SHOULD); + addBooleanClauses(parseContext, booleanQuery, this.filterClauses, BooleanClause.Occur.FILTER); + + booleanQuery.setBoost(this.boost); + Queries.applyMinimumShouldMatch(booleanQuery, this.minimumShouldMatch); + Query query = this.adjustPureNegative ? fixNegativeQueryIfNeeded(booleanQuery) : booleanQuery; + if (this.queryName != null) { + parseContext.addNamedQuery(this.queryName, query); + } + return query; + } + + @Override + public QueryValidationException validate() { + // nothing to validate, clauses are optional, see hasClauses(), other parameters have defaults + return null; + } + + private static void addBooleanClauses(QueryParseContext parseContext, BooleanQuery booleanQuery, List clauses, Occur occurs) + throws IOException { + for (QueryBuilder query : clauses) { + Query luceneQuery = query.toQuery(parseContext); + if (luceneQuery != null) { + booleanQuery.add(new BooleanClause(luceneQuery, occurs)); + } + } + } + + @Override + public int hashCode() { + return Objects.hash(boost, adjustPureNegative, disableCoord, + minimumShouldMatch, queryName, mustClauses, shouldClauses, mustNotClauses, filterClauses); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + BoolQueryBuilder other = (BoolQueryBuilder) obj; + return Objects.equals(boost, other.boost) && + Objects.equals(adjustPureNegative, other.adjustPureNegative) && + Objects.equals(disableCoord, other.disableCoord) && + Objects.equals(minimumShouldMatch, other.minimumShouldMatch) && + Objects.equals(queryName, other.queryName) && + Objects.equals(mustClauses, other.mustClauses) && + Objects.equals(shouldClauses, other.shouldClauses) && + Objects.equals(mustNotClauses, other.mustNotClauses) && + Objects.equals(filterClauses, other.filterClauses); + } + + @Override + public BoolQueryBuilder readFrom(StreamInput in) throws IOException { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + List queryBuilders = in.readNamedWritableList(); + boolQueryBuilder.must(queryBuilders); + queryBuilders = in.readNamedWritableList(); + boolQueryBuilder.mustNot(queryBuilders); + queryBuilders = in.readNamedWritableList(); + boolQueryBuilder.should(queryBuilders); + queryBuilders = in.readNamedWritableList(); + boolQueryBuilder.filter(queryBuilders); + boolQueryBuilder.boost = in.readFloat(); + boolQueryBuilder.adjustPureNegative = in.readBoolean(); + boolQueryBuilder.disableCoord = in.readBoolean(); + boolQueryBuilder.queryName = in.readOptionalString(); + boolQueryBuilder.minimumShouldMatch = in.readOptionalString(); + return boolQueryBuilder; + + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeNamedWritableList(this.mustClauses); + out.writeNamedWritableList(this.mustNotClauses); + out.writeNamedWritableList(this.shouldClauses); + out.writeNamedWritableList(this.filterClauses); + out.writeFloat(this.boost); + out.writeBoolean(this.adjustPureNegative); + out.writeBoolean(this.disableCoord); + out.writeOptionalString(queryName); + out.writeOptionalString(this.minimumShouldMatch); + } } diff --git a/core/src/main/java/org/elasticsearch/index/query/BoolQueryParser.java b/core/src/main/java/org/elasticsearch/index/query/BoolQueryParser.java index 34a947c302f86..68725c0f8881f 100644 --- a/core/src/main/java/org/elasticsearch/index/query/BoolQueryParser.java +++ b/core/src/main/java/org/elasticsearch/index/query/BoolQueryParser.java @@ -19,12 +19,8 @@ package org.elasticsearch.index.query; -import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; -import org.apache.lucene.search.MatchAllDocsQuery; -import org.apache.lucene.search.Query; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.lucene.search.Queries; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentParser; @@ -32,12 +28,11 @@ import java.util.List; import static com.google.common.collect.Lists.newArrayList; -import static org.elasticsearch.common.lucene.search.Queries.fixNegativeQueryIfNeeded; /** - * + * Parser for the {@link BoolQueryBuilder} */ -public class BoolQueryParser extends BaseQueryParserTemp { +public class BoolQueryParser extends BaseQueryParser { @Inject public BoolQueryParser(Settings settings) { @@ -50,19 +45,23 @@ public String[] names() { } @Override - public Query parse(QueryParseContext parseContext) throws IOException, QueryParsingException { + public QueryBuilder fromXContent(QueryParseContext parseContext) throws IOException, QueryParsingException { XContentParser parser = parseContext.parser(); - boolean disableCoord = false; + boolean disableCoord = BoolQueryBuilder.DISABLE_COORD_DEFAULT; + boolean adjustPureNegative = BoolQueryBuilder.ADJUST_PURE_NEGATIVE_DEFAULT; float boost = 1.0f; String minimumShouldMatch = null; - List clauses = newArrayList(); - boolean adjustPureNegative = true; + List mustClauses = newArrayList(); + List mustNotClauses = newArrayList(); + List shouldClauses = newArrayList(); + List filterClauses = newArrayList(); String queryName = null; String currentFieldName = null; XContentParser.Token token; + QueryBuilder query; while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { if (token == XContentParser.Token.FIELD_NAME) { currentFieldName = parser.currentName(); @@ -71,31 +70,31 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars } else if (token == XContentParser.Token.START_OBJECT) { switch (currentFieldName) { case "must": - Query query = parseContext.parseInnerQuery(); + query = parseContext.parseInnerQueryBuilder(); if (query != null) { - clauses.add(new BooleanClause(query, BooleanClause.Occur.MUST)); + mustClauses.add(query); } break; case "should": - query = parseContext.parseInnerQuery(); + query = parseContext.parseInnerQueryBuilder(); if (query != null) { - clauses.add(new BooleanClause(query, BooleanClause.Occur.SHOULD)); + shouldClauses.add(query); if (parseContext.isFilter() && minimumShouldMatch == null) { minimumShouldMatch = "1"; } } break; case "filter": - query = parseContext.parseInnerFilter(); + query = parseContext.parseInnerFilterToQueryBuilder(); if (query != null) { - clauses.add(new BooleanClause(query, BooleanClause.Occur.FILTER)); + filterClauses.add(query); } break; case "must_not": case "mustNot": - query = parseContext.parseInnerFilter(); + query = parseContext.parseInnerFilterToQueryBuilder(); if (query != null) { - clauses.add(new BooleanClause(query, BooleanClause.Occur.MUST_NOT)); + mustNotClauses.add(query); } break; default: @@ -105,31 +104,31 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) { switch (currentFieldName) { case "must": - Query query = parseContext.parseInnerQuery(); + query = parseContext.parseInnerQueryBuilder(); if (query != null) { - clauses.add(new BooleanClause(query, BooleanClause.Occur.MUST)); + mustClauses.add(query); } break; case "should": - query = parseContext.parseInnerQuery(); + query = parseContext.parseInnerQueryBuilder(); if (query != null) { - clauses.add(new BooleanClause(query, BooleanClause.Occur.SHOULD)); + shouldClauses.add(query); if (parseContext.isFilter() && minimumShouldMatch == null) { minimumShouldMatch = "1"; } } break; case "filter": - query = parseContext.parseInnerFilter(); + query = parseContext.parseInnerFilterToQueryBuilder(); if (query != null) { - clauses.add(new BooleanClause(query, BooleanClause.Occur.FILTER)); + filterClauses.add(query); } break; case "must_not": case "mustNot": - query = parseContext.parseInnerFilter(); + query = parseContext.parseInnerFilterToQueryBuilder(); if (query != null) { - clauses.add(new BooleanClause(query, BooleanClause.Occur.MUST_NOT)); + mustNotClauses.add(query); } break; default: @@ -154,22 +153,18 @@ public Query parse(QueryParseContext parseContext) throws IOException, QueryPars } } } - - if (clauses.isEmpty()) { - return new MatchAllDocsQuery(); - } - - BooleanQuery booleanQuery = new BooleanQuery(disableCoord); - for (BooleanClause clause : clauses) { - booleanQuery.add(clause); - } - booleanQuery.setBoost(boost); - Queries.applyMinimumShouldMatch(booleanQuery, minimumShouldMatch); - Query query = adjustPureNegative ? fixNegativeQueryIfNeeded(booleanQuery) : booleanQuery; - if (queryName != null) { - parseContext.addNamedQuery(queryName, query); - } - return query; + BoolQueryBuilder boolQuery = new BoolQueryBuilder(); + boolQuery.must(mustClauses); + boolQuery.mustNot(mustNotClauses); + boolQuery.should(shouldClauses); + boolQuery.filter(filterClauses); + boolQuery.boost(boost); + boolQuery.disableCoord(disableCoord); + boolQuery.adjustPureNegative(adjustPureNegative); + boolQuery.minimumNumberShouldMatch(minimumShouldMatch); + boolQuery.queryName(queryName); + boolQuery.validate(); + return boolQuery; } @Override diff --git a/core/src/main/java/org/elasticsearch/index/query/QueryParseContext.java b/core/src/main/java/org/elasticsearch/index/query/QueryParseContext.java index 40e00d4df5703..8e45743e43be0 100644 --- a/core/src/main/java/org/elasticsearch/index/query/QueryParseContext.java +++ b/core/src/main/java/org/elasticsearch/index/query/QueryParseContext.java @@ -267,18 +267,37 @@ public Query parseInnerQuery() throws IOException, QueryParsingException { return result; } + /** + * @deprecated replaced by calls to parseInnerFilterToQueryBuilder() for the resulting queries + */ @Nullable + @Deprecated public Query parseInnerFilter() throws QueryParsingException, IOException { + QueryBuilder builder = parseInnerFilterToQueryBuilder(); + Query result = null; + if (builder != null) { + result = builder.toQuery(this); + } + return result; + } + + /** + * @return + * @throws QueryParsingException + * @throws IOException + */ + @Nullable + public QueryBuilder parseInnerFilterToQueryBuilder() throws QueryParsingException, IOException { final boolean originalIsFilter = isFilter; try { isFilter = true; - return parseInnerQuery(); + return parseInnerQueryBuilder(); } finally { isFilter = originalIsFilter; } } - public Query parseInnerFilter(String queryName) throws IOException, QueryParsingException { + public QueryBuilder parseInnerFilterToQueryBuilder(String queryName) throws IOException, QueryParsingException { final boolean originalIsFilter = isFilter; try { isFilter = true; @@ -286,12 +305,22 @@ public Query parseInnerFilter(String queryName) throws IOException, QueryParsing if (queryParser == null) { throw new QueryParsingException(this, "No query registered for [" + queryName + "]"); } - return queryParser.parse(this); + return queryParser.fromXContent(this); } finally { isFilter = originalIsFilter; } } + /** + * @deprecated replaced by calls to parseInnerFilterToQueryBuilder(String queryName) for the resulting queries + */ + @Nullable + @Deprecated + public Query parseInnerFilter(String queryName) throws IOException, QueryParsingException { + QueryBuilder builder = parseInnerFilterToQueryBuilder(queryName); + return (builder != null) ? builder.toQuery(this) : null; + } + public Collection simpleMatchToIndexNames(String pattern) { return indexQueryParser.mapperService.simpleMatchToIndexNames(pattern, getTypes()); } diff --git a/core/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTest.java b/core/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTest.java new file mode 100644 index 0000000000000..d362a0d783a03 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTest.java @@ -0,0 +1,115 @@ +/* + * 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.MatchAllDocsQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.BooleanClause.Occur; +import org.elasticsearch.common.lucene.search.Queries; + +import java.io.IOException; +import java.util.List; + +import static org.hamcrest.Matchers.equalTo; +import static org.elasticsearch.common.lucene.search.Queries.fixNegativeQueryIfNeeded; + +public class BoolQueryBuilderTest extends BaseQueryTestCase { + + @Override + protected void assertLuceneQuery(BoolQueryBuilder queryBuilder, Query query, QueryParseContext context) { + if (queryBuilder.queryName() != null) { + Query namedQuery = context.copyNamedFilters().get(queryBuilder.queryName()); + if (queryBuilder.hasClauses()) { + assertThat(namedQuery, equalTo(query)); + } else { + assertNull(namedQuery); + } + } + } + + @Override + protected BoolQueryBuilder createTestQueryBuilder() { + BoolQueryBuilder query = new BoolQueryBuilder(); + if (randomBoolean()) { + query.boost(2.0f / randomIntBetween(1, 20)); + } + if (randomBoolean()) { + query.adjustPureNegative(randomBoolean()); + } + if (randomBoolean()) { + query.disableCoord(randomBoolean()); + } + if (randomBoolean()) { + query.minimumNumberShouldMatch(randomIntBetween(1, 10)); + } + int mustClauses = randomIntBetween(0, 3); + for (int i = 0; i < mustClauses; i++) { + query.must(RandomQueryBuilder.create(random())); + } + int mustNotClauses = randomIntBetween(0, 3); + for (int i = 0; i < mustNotClauses; i++) { + query.mustNot(RandomQueryBuilder.create(random())); + } + int shouldClauses = randomIntBetween(0, 3); + for (int i = 0; i < shouldClauses; i++) { + query.should(RandomQueryBuilder.create(random())); + } + int filterClauses = randomIntBetween(0, 3); + for (int i = 0; i < filterClauses; i++) { + query.filter(RandomQueryBuilder.create(random())); + } + if (randomBoolean()) { + query.queryName(randomUnicodeOfLengthBetween(3, 15)); + } + return query; + } + + @Override + protected BoolQueryBuilder createEmptyQueryBuilder() { + return new BoolQueryBuilder(); + } + + @Override + protected Query createExpectedQuery(BoolQueryBuilder queryBuilder, QueryParseContext context) throws IOException { + if (!queryBuilder.hasClauses()) { + return new MatchAllDocsQuery(); + } + + BooleanQuery boolQuery = new BooleanQuery(queryBuilder.disableCoord()); + boolQuery.setBoost(queryBuilder.boost()); + addBooleanClauses(context, boolQuery, queryBuilder.must(), BooleanClause.Occur.MUST); + addBooleanClauses(context, boolQuery, queryBuilder.mustNot(), BooleanClause.Occur.MUST_NOT); + addBooleanClauses(context, boolQuery, queryBuilder.should(), BooleanClause.Occur.SHOULD); + addBooleanClauses(context, boolQuery, queryBuilder.filter(), BooleanClause.Occur.FILTER); + + Queries.applyMinimumShouldMatch(boolQuery, queryBuilder.minimumNumberShouldMatch()); + Query returnedQuery = queryBuilder.adjustPureNegative() ? fixNegativeQueryIfNeeded(boolQuery) : boolQuery; + return returnedQuery; + } + + private static void addBooleanClauses(QueryParseContext parseContext, BooleanQuery booleanQuery, List clauses, Occur occurs) + throws IOException { + for (QueryBuilder query : clauses) { + booleanQuery.add(new BooleanClause(query.toQuery(parseContext), occurs)); + } + } +} diff --git a/core/src/test/java/org/elasticsearch/index/query/RandomQueryBuilder.java b/core/src/test/java/org/elasticsearch/index/query/RandomQueryBuilder.java new file mode 100644 index 0000000000000..ea24723bbd729 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/index/query/RandomQueryBuilder.java @@ -0,0 +1,49 @@ +/* + * 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 com.carrotsearch.randomizedtesting.generators.RandomInts; + +import java.util.Random; + +/** + * Utility class for creating random QueryBuilders. + * So far only leaf queries like {@link MatchAllQueryBuilder}, {@link TermQueryBuilder} or + * {@link IdsQueryBuilder} are returned. + */ +public class RandomQueryBuilder { + + /** + * @param r random seed + * @return a random {@link QueryBuilder} + */ + public static QueryBuilder create(Random r) { + QueryBuilder query = null; + switch (RandomInts.randomIntBetween(r, 0, 2)) { + case 0: + return new MatchAllQueryBuilderTest().createTestQueryBuilder(); + case 1: + return new TermQueryBuilderTest().createTestQueryBuilder(); + case 2: + return new IdsQueryBuilderTest().createTestQueryBuilder(); + } + return query; + } +}