diff --git a/docs/changelog/110079.yaml b/docs/changelog/110079.yaml new file mode 100644 index 0000000000000..168e99fbfb5ab --- /dev/null +++ b/docs/changelog/110079.yaml @@ -0,0 +1,6 @@ +pr: 110079 +summary: "feat: Add new option omit_zero_term_query to bool query" +area: Search +type: feature +issues: + - 110080 diff --git a/docs/reference/query-dsl/bool-query.asciidoc b/docs/reference/query-dsl/bool-query.asciidoc index c24135a370914..273f1e5032cc9 100644 --- a/docs/reference/query-dsl/bool-query.asciidoc +++ b/docs/reference/query-dsl/bool-query.asciidoc @@ -54,6 +54,7 @@ POST _search { "term" : { "tags" : "deployed" } } ], "minimum_should_match" : 1, + "omit_zero_term_query" : false "boost" : 1.0 } } @@ -206,3 +207,9 @@ response. Typically, this adds a small overhead to a request. However, using computationally expensive named queries on a large number of hits may add significant overhead. For example, named queries in combination with a `top_hits` aggregation on many buckets may lead to longer response times. + +[[bool-omit-zero-term-query]] +==== Using `omit_zero_term_query` + +(Optional, Boolean) If set to `true`, the query will be omitted if the analyzer +removes all tokens using a stop filter or similar. Defaults to `false`. diff --git a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java index 4b4727bca4198..7209477bf1b61 100644 --- a/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/BoolQueryBuilder.java @@ -12,6 +12,7 @@ import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.MatchNoDocsQuery; import org.apache.lucene.search.Query; import org.elasticsearch.TransportVersion; import org.elasticsearch.TransportVersions; @@ -41,6 +42,7 @@ public class BoolQueryBuilder extends AbstractQueryBuilder { public static final String NAME = "bool"; public static final boolean ADJUST_PURE_NEGATIVE_DEFAULT = true; + public static final boolean OMIT_ZERO_TERM_QUERY_DEFAULT = false; private static final ParseField MUST_NOT = new ParseField("must_not").withDeprecation("mustNot"); private static final ParseField FILTER = new ParseField("filter"); @@ -48,6 +50,7 @@ public class BoolQueryBuilder extends AbstractQueryBuilder { private static final ParseField MUST = new ParseField("must"); 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 static final ParseField OMIT_ZERO_TERM_QUERY = new ParseField("omit_zero_term_query"); private final List mustClauses = new ArrayList<>(); @@ -61,6 +64,8 @@ public class BoolQueryBuilder extends AbstractQueryBuilder { private String minimumShouldMatch; + private boolean omitZeroTermQuery = OMIT_ZERO_TERM_QUERY_DEFAULT; + /** * Build an empty bool query. */ @@ -77,6 +82,7 @@ public BoolQueryBuilder(StreamInput in) throws IOException { filterClauses.addAll(readQueries(in)); adjustPureNegative = in.readBoolean(); minimumShouldMatch = in.readOptionalString(); + omitZeroTermQuery = in.readBoolean(); } @Override @@ -87,6 +93,7 @@ protected void doWriteTo(StreamOutput out) throws IOException { writeQueries(out, filterClauses); out.writeBoolean(adjustPureNegative); out.writeOptionalString(minimumShouldMatch); + out.writeBoolean(omitZeroTermQuery); } /** @@ -230,6 +237,22 @@ public boolean adjustPureNegative() { return this.adjustPureNegative; } + /** + * Decide whether to omit query to use in case no query terms are available, e.g. after analysis removed them. + * The default is true. + */ + public BoolQueryBuilder omitZeroTermQuery(boolean omitZeroTermQuery) { + this.omitZeroTermQuery = omitZeroTermQuery; + return this; + } + + /** + * @return the setting for the omit_zero_term_query setting in this query + */ + public boolean omitZeroTermQuery() { + return this.omitZeroTermQuery; + } + @Override protected void doXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(NAME); @@ -245,6 +268,7 @@ protected void doXContent(XContentBuilder builder, Params params) throws IOExcep if (minimumShouldMatch != null) { builder.field(MINIMUM_SHOULD_MATCH.getPreferredName(), minimumShouldMatch); } + builder.field(OMIT_ZERO_TERM_QUERY.getPreferredName(), omitZeroTermQuery); printBoostAndQueryName(builder); builder.endObject(); } @@ -286,6 +310,7 @@ private static void doXArrayContent(ParseField field, List clauses MINIMUM_SHOULD_MATCH, ObjectParser.ValueType.VALUE ); + PARSER.declareBoolean(BoolQueryBuilder::omitZeroTermQuery, OMIT_ZERO_TERM_QUERY); PARSER.declareString(BoolQueryBuilder::queryName, NAME_FIELD); PARSER.declareFloat(BoolQueryBuilder::boost, BOOST_FIELD); } @@ -308,14 +333,19 @@ protected Query doToQuery(SearchExecutionContext context) throws IOException { addBooleanClauses(context, booleanQueryBuilder, filterClauses, BooleanClause.Occur.FILTER); BooleanQuery booleanQuery = booleanQueryBuilder.build(); if (booleanQuery.clauses().isEmpty()) { - return new MatchAllDocsQuery(); + if (omitZeroTermQuery) { + return new MatchNoDocsQuery(); + } else { + return new MatchAllDocsQuery(); + } + } Query query = Queries.applyMinimumShouldMatch(booleanQuery, minimumShouldMatch); return adjustPureNegative ? fixNegativeQueryIfNeeded(query) : query; } - private static void addBooleanClauses( + private void addBooleanClauses( SearchExecutionContext context, BooleanQuery.Builder booleanQueryBuilder, List clauses, @@ -323,18 +353,30 @@ private static void addBooleanClauses( ) throws IOException { for (QueryBuilder query : clauses) { Query luceneQuery = query.toQuery(context); + if (luceneQuery instanceof MatchNoDocsQuery && omitZeroTermQuery) { + continue; + } booleanQueryBuilder.add(new BooleanClause(luceneQuery, occurs)); } } @Override protected int doHashCode() { - return Objects.hash(adjustPureNegative, minimumShouldMatch, mustClauses, shouldClauses, mustNotClauses, filterClauses); + return Objects.hash( + omitZeroTermQuery, + adjustPureNegative, + minimumShouldMatch, + mustClauses, + shouldClauses, + mustNotClauses, + filterClauses + ); } @Override protected boolean doEquals(BoolQueryBuilder other) { - return Objects.equals(adjustPureNegative, other.adjustPureNegative) + return Objects.equals(omitZeroTermQuery, other.omitZeroTermQuery) + && Objects.equals(adjustPureNegative, other.adjustPureNegative) && Objects.equals(minimumShouldMatch, other.minimumShouldMatch) && Objects.equals(mustClauses, other.mustClauses) && Objects.equals(shouldClauses, other.shouldClauses) @@ -370,6 +412,7 @@ protected QueryBuilder doRewrite(QueryRewriteContext queryRewriteContext) throws } if (changed) { + newBuilder.omitZeroTermQuery = omitZeroTermQuery; newBuilder.adjustPureNegative = adjustPureNegative; newBuilder.minimumShouldMatch = minimumShouldMatch; newBuilder.boost(boost()); diff --git a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java index c29957f04c515..c9e19c4068805 100644 --- a/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java +++ b/server/src/test/java/org/elasticsearch/index/query/BoolQueryBuilderTests.java @@ -274,6 +274,7 @@ public void testFromJson() throws IOException { + " }" + " } ]," + " \"minimum_should_match\" : \"23\"," + + " \"omit_zero_term_query\" : false," + " \"boost\" : 42.0" + "}" + "}"; @@ -463,4 +464,22 @@ public void testMustRewrite() throws IOException { IllegalStateException e = expectThrows(IllegalStateException.class, () -> boolQuery.toQuery(context)); assertEquals("Rewrite first", e.getMessage()); } + + public void testStopWordAndOmitZeroTermQueryIsTrue() throws Exception { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder().omitZeroTermQuery(true); + boolQueryBuilder.must(new MultiMatchQueryBuilder("The").field(TEXT_FIELD_NAME).analyzer("stop")); + Query query = boolQueryBuilder.toQuery(createSearchExecutionContext()); + assertThat(query, instanceOf(MatchNoDocsQuery.class)); + } + + public void testMultipleTokensAndOmitZeroTermQueryIsTrue() throws Exception { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder().omitZeroTermQuery(true); + boolQueryBuilder.must(new MultiMatchQueryBuilder("The").field(TEXT_FIELD_NAME).analyzer("stop")); + boolQueryBuilder.must(new MultiMatchQueryBuilder("brown").field(TEXT_FIELD_NAME)); + boolQueryBuilder.must(new MultiMatchQueryBuilder("fox").field(TEXT_FIELD_NAME)); + Query query = boolQueryBuilder.toQuery(createSearchExecutionContext()); + assertThat(query, instanceOf(BooleanQuery.class)); + BooleanQuery booleanQuery = (BooleanQuery) query; + assertThat(booleanQuery.clauses().size(), equalTo(2)); + } }