org.elasticsearch.index.query.BoolQueryBuilder Maven / Gradle / Ivy
Show all versions of elasticsearch Show documentation
/*
* 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.logging.log4j.LogManager;
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.Version;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;
import static org.elasticsearch.common.lucene.search.Queries.fixNegativeQueryIfNeeded;
/**
* A Query that matches documents matching boolean combinations of other queries.
*/
public class BoolQueryBuilder extends AbstractQueryBuilder {
public static final String NAME = "bool";
private static final DeprecationLogger deprecationLogger = new DeprecationLogger(
LogManager.getLogger(BoolQueryBuilder.class)
);
public static final boolean ADJUST_PURE_NEGATIVE_DEFAULT = true;
private static final String MUSTNOT = "mustNot";
private static final String MUST_NOT = "must_not";
private static final String FILTER = "filter";
private static final String SHOULD = "should";
private static final String MUST = "must";
private static final ParseField DISABLE_COORD_FIELD = new ParseField("disable_coord")
.withAllDeprecated("disable_coord has been removed");
private static final ParseField MINIMUM_SHOULD_MATCH = new ParseField("minimum_should_match");
private static final ParseField ADJUST_PURE_NEGATIVE = new ParseField("adjust_pure_negative");
private final List mustClauses = new ArrayList<>();
private final List mustNotClauses = new ArrayList<>();
private final List filterClauses = new ArrayList<>();
private final List shouldClauses = new ArrayList<>();
private boolean adjustPureNegative = ADJUST_PURE_NEGATIVE_DEFAULT;
private String minimumShouldMatch;
/**
* Build an empty bool query.
*/
public BoolQueryBuilder() {
}
/**
* Read from a stream.
*/
public BoolQueryBuilder(StreamInput in) throws IOException {
super(in);
mustClauses.addAll(readQueries(in));
mustNotClauses.addAll(readQueries(in));
shouldClauses.addAll(readQueries(in));
filterClauses.addAll(readQueries(in));
adjustPureNegative = in.readBoolean();
if (in.getVersion().before(Version.V_6_0_0_alpha1)) {
in.readBoolean(); // disable_coord
}
minimumShouldMatch = in.readOptionalString();
}
@Override
protected void doWriteTo(StreamOutput out) throws IOException {
writeQueries(out, mustClauses);
writeQueries(out, mustNotClauses);
writeQueries(out, shouldClauses);
writeQueries(out, filterClauses);
out.writeBoolean(adjustPureNegative);
if (out.getVersion().before(Version.V_6_0_0_alpha1)) {
out.writeBoolean(true); // disable_coord
}
out.writeOptionalString(minimumShouldMatch);
}
/**
* Adds a query that must appear in the matching documents and will
* contribute to scoring. No {@code null} value allowed.
*/
public BoolQueryBuilder must(QueryBuilder queryBuilder) {
if (queryBuilder == null) {
throw new IllegalArgumentException("inner bool query clause cannot be null");
}
mustClauses.add(queryBuilder);
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. No {@code null} value allowed.
*/
public BoolQueryBuilder filter(QueryBuilder queryBuilder) {
if (queryBuilder == null) {
throw new IllegalArgumentException("inner bool query clause cannot be null");
}
filterClauses.add(queryBuilder);
return this;
}
/**
* Gets the queries that must appear in the matching documents but don't contribute to scoring
*/
public List filter() {
return this.filterClauses;
}
/**
* Adds a query that must not appear in the matching documents.
* No {@code null} value allowed.
*/
public BoolQueryBuilder mustNot(QueryBuilder queryBuilder) {
if (queryBuilder == null) {
throw new IllegalArgumentException("inner bool query clause cannot be null");
}
mustNotClauses.add(queryBuilder);
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
* {@code MUST} clauses one or more SHOULD
clauses must match a document
* for the BooleanQuery to match. No {@code null} value allowed.
*
* @see #minimumShouldMatch(int)
*/
public BoolQueryBuilder should(QueryBuilder queryBuilder) {
if (queryBuilder == null) {
throw new IllegalArgumentException("inner bool query clause cannot be null");
}
shouldClauses.add(queryBuilder);
return this;
}
/**
* Gets the list of clauses that should be matched by the returned documents.
*
* @see #should(QueryBuilder)
* @see #minimumShouldMatch(int)
*/
public List should() {
return this.shouldClauses;
}
/**
* @return the string representation of the minimumShouldMatch settings for this query
*/
public String minimumShouldMatch() {
return this.minimumShouldMatch;
}
/**
* Sets the minimum should match parameter using the special syntax (for example, supporting percentage).
* @see BoolQueryBuilder#minimumShouldMatch(int)
*/
public BoolQueryBuilder minimumShouldMatch(String minimumShouldMatch) {
this.minimumShouldMatch = minimumShouldMatch;
return this;
}
/**
* Specifies a minimum number of the optional (should) boolean clauses which must be satisfied.
*
* By default no optional clauses are necessary for a match
* (unless there are no required clauses). If this method is used,
* then the specified number of clauses is required.
*
* Use of this method is totally independent of specifying that
* any specific clauses are required (or prohibited). This number will
* only be compared against the number of matching optional clauses.
*
* @param minimumShouldMatch the number of optional clauses that must match
*/
public BoolQueryBuilder minimumShouldMatch(int minimumShouldMatch) {
this.minimumShouldMatch = Integer.toString(minimumShouldMatch);
return this;
}
/**
* Returns true
iff this query builder has at least one should, must, must not or filter clause.
* Otherwise false
.
*/
public boolean hasClauses() {
return !(mustClauses.isEmpty() && shouldClauses.isEmpty() && mustNotClauses.isEmpty() && filterClauses.isEmpty());
}
/**
* If a boolean query contains only negative ("must not") clauses should the
* BooleanQuery be enhanced with a {@link MatchAllDocsQuery} in order to act
* as a pure exclude. The default is true
.
*/
public BoolQueryBuilder adjustPureNegative(boolean adjustPureNegative) {
this.adjustPureNegative = adjustPureNegative;
return this;
}
/**
* @return the setting for the adjust_pure_negative setting in this query
*/
public boolean adjustPureNegative() {
return this.adjustPureNegative;
}
@Override
protected void doXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(NAME);
doXArrayContent(MUST, mustClauses, builder, params);
doXArrayContent(FILTER, filterClauses, builder, params);
doXArrayContent(MUST_NOT, mustNotClauses, builder, params);
doXArrayContent(SHOULD, shouldClauses, builder, params);
builder.field(ADJUST_PURE_NEGATIVE.getPreferredName(), adjustPureNegative);
if (minimumShouldMatch != null) {
builder.field(MINIMUM_SHOULD_MATCH.getPreferredName(), minimumShouldMatch);
}
printBoostAndQueryName(builder);
builder.endObject();
}
private static void doXArrayContent(String field, List clauses, XContentBuilder builder, Params params)
throws IOException {
if (clauses.isEmpty()) {
return;
}
builder.startArray(field);
for (QueryBuilder clause : clauses) {
clause.toXContent(builder, params);
}
builder.endArray();
}
public static BoolQueryBuilder fromXContent(XContentParser parser) throws IOException, ParsingException {
boolean adjustPureNegative = BoolQueryBuilder.ADJUST_PURE_NEGATIVE_DEFAULT;
float boost = DEFAULT_BOOST;
String minimumShouldMatch = null;
final List mustClauses = new ArrayList<>();
final List mustNotClauses = new ArrayList<>();
final List shouldClauses = new ArrayList<>();
final List filterClauses = new ArrayList<>();
String queryName = null;
String currentFieldName = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
currentFieldName = parser.currentName();
} else if (token == XContentParser.Token.START_OBJECT) {
switch (currentFieldName) {
case MUST:
mustClauses.add(parseInnerQueryBuilder(parser));
break;
case SHOULD:
shouldClauses.add(parseInnerQueryBuilder(parser));
break;
case FILTER:
filterClauses.add(parseInnerQueryBuilder(parser));
break;
case MUST_NOT:
case MUSTNOT:
mustNotClauses.add(parseInnerQueryBuilder(parser));
break;
default:
throw new ParsingException(parser.getTokenLocation(), "[bool] query does not support [" + currentFieldName + "]");
}
} else if (token == XContentParser.Token.START_ARRAY) {
while ((token = parser.nextToken()) != XContentParser.Token.END_ARRAY) {
switch (currentFieldName) {
case MUST:
mustClauses.add(parseInnerQueryBuilder(parser));
break;
case SHOULD:
shouldClauses.add(parseInnerQueryBuilder(parser));
break;
case FILTER:
filterClauses.add(parseInnerQueryBuilder(parser));
break;
case MUST_NOT:
case MUSTNOT:
mustNotClauses.add(parseInnerQueryBuilder(parser));
break;
default:
throw new ParsingException(parser.getTokenLocation(), "bool query does not support [" + currentFieldName + "]");
}
}
} else if (token.isValue()) {
if (DISABLE_COORD_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
// ignore
} else if (MINIMUM_SHOULD_MATCH.match(currentFieldName, parser.getDeprecationHandler())) {
minimumShouldMatch = parser.textOrNull();
} else if (BOOST_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
boost = parser.floatValue();
} else if (ADJUST_PURE_NEGATIVE.match(currentFieldName, parser.getDeprecationHandler())) {
adjustPureNegative = parser.booleanValue();
} else if (NAME_FIELD.match(currentFieldName, parser.getDeprecationHandler())) {
queryName = parser.text();
} else {
throw new ParsingException(parser.getTokenLocation(), "[bool] query does not support [" + currentFieldName + "]");
}
}
}
BoolQueryBuilder boolQuery = new BoolQueryBuilder();
for (QueryBuilder queryBuilder : mustClauses) {
boolQuery.must(queryBuilder);
}
for (QueryBuilder queryBuilder : mustNotClauses) {
boolQuery.mustNot(queryBuilder);
}
for (QueryBuilder queryBuilder : shouldClauses) {
boolQuery.should(queryBuilder);
}
for (QueryBuilder queryBuilder : filterClauses) {
boolQuery.filter(queryBuilder);
}
boolQuery.boost(boost);
boolQuery.adjustPureNegative(adjustPureNegative);
boolQuery.minimumShouldMatch(minimumShouldMatch);
boolQuery.queryName(queryName);
return boolQuery;
}
@Override
public String getWriteableName() {
return NAME;
}
@Override
protected Query doToQuery(QueryShardContext context) throws IOException {
BooleanQuery.Builder booleanQueryBuilder = new BooleanQuery.Builder();
addBooleanClauses(context, booleanQueryBuilder, mustClauses, BooleanClause.Occur.MUST);
addBooleanClauses(context, booleanQueryBuilder, mustNotClauses, BooleanClause.Occur.MUST_NOT);
addBooleanClauses(context, booleanQueryBuilder, shouldClauses, BooleanClause.Occur.SHOULD);
addBooleanClauses(context, booleanQueryBuilder, filterClauses, BooleanClause.Occur.FILTER);
BooleanQuery booleanQuery = booleanQueryBuilder.build();
if (booleanQuery.clauses().isEmpty()) {
return new MatchAllDocsQuery();
}
final String minimumShouldMatch;
if (context.isFilter() && this.minimumShouldMatch == null && shouldClauses.size() > 0) {
if (mustClauses.size() > 0 || mustNotClauses.size() > 0 || filterClauses.size() > 0) {
deprecationLogger.deprecatedAndMaybeLog("filter_context_min_should_match",
"Should clauses in the filter context will no longer automatically set the minimum should " +
"match to 1 in the next major version. You should group them in a [filter] clause or explicitly set " +
"[minimum_should_match] to 1 to restore this behavior in the next major version." );
}
minimumShouldMatch = "1";
} else {
minimumShouldMatch = this.minimumShouldMatch;
}
Query query = Queries.applyMinimumShouldMatch(booleanQuery, minimumShouldMatch);
return adjustPureNegative ? fixNegativeQueryIfNeeded(query) : query;
}
private static void addBooleanClauses(QueryShardContext context, BooleanQuery.Builder booleanQueryBuilder,
List clauses, Occur occurs) throws IOException {
for (QueryBuilder query : clauses) {
Query luceneQuery = null;
switch (occurs) {
case MUST:
case SHOULD:
luceneQuery = query.toQuery(context);
break;
case FILTER:
case MUST_NOT:
luceneQuery = query.toFilter(context);
break;
}
booleanQueryBuilder.add(new BooleanClause(luceneQuery, occurs));
}
}
@Override
protected int doHashCode() {
return Objects.hash(adjustPureNegative,
minimumShouldMatch, mustClauses, shouldClauses, mustNotClauses, filterClauses);
}
@Override
protected boolean doEquals(BoolQueryBuilder other) {
return Objects.equals(adjustPureNegative, other.adjustPureNegative) &&
Objects.equals(minimumShouldMatch, other.minimumShouldMatch) &&
Objects.equals(mustClauses, other.mustClauses) &&
Objects.equals(shouldClauses, other.shouldClauses) &&
Objects.equals(mustNotClauses, other.mustNotClauses) &&
Objects.equals(filterClauses, other.filterClauses);
}
@Override
protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws IOException {
BoolQueryBuilder newBuilder = new BoolQueryBuilder();
boolean changed = false;
final int clauses = mustClauses.size() + mustNotClauses.size() + filterClauses.size() + shouldClauses.size();
if (clauses == 0) {
return new MatchAllQueryBuilder().boost(boost()).queryName(queryName());
}
changed |= rewriteClauses(queryRewriteContext, mustClauses, newBuilder::must);
changed |= rewriteClauses(queryRewriteContext, mustNotClauses, newBuilder::mustNot);
changed |= rewriteClauses(queryRewriteContext, filterClauses, newBuilder::filter);
changed |= rewriteClauses(queryRewriteContext, shouldClauses, newBuilder::should);
// lets do some early termination and prevent any kind of rewriting if we have a mandatory query that is a MatchNoneQueryBuilder
Optional any = Stream.concat(newBuilder.mustClauses.stream(), newBuilder.filterClauses.stream())
.filter(b -> b instanceof MatchNoneQueryBuilder).findAny();
if (any.isPresent()) {
return any.get();
}
if (changed) {
newBuilder.adjustPureNegative = adjustPureNegative;
newBuilder.minimumShouldMatch = minimumShouldMatch;
newBuilder.boost(boost());
newBuilder.queryName(queryName());
return newBuilder;
}
return this;
}
@Override
protected void extractInnerHitBuilders(Map innerHits) {
List clauses = new ArrayList<>(filter());
clauses.addAll(must());
clauses.addAll(should());
// no need to include must_not (since there will be no hits for it)
for (QueryBuilder clause : clauses) {
InnerHitContextBuilder.extractInnerHits(clause, innerHits);
}
}
private static boolean rewriteClauses(QueryRewriteContext queryRewriteContext, List builders,
Consumer consumer) throws IOException {
boolean changed = false;
for (QueryBuilder builder : builders) {
QueryBuilder result = builder.rewrite(queryRewriteContext);
if (result != builder) {
changed = true;
}
consumer.accept(result);
}
return changed;
}
}