-
Notifications
You must be signed in to change notification settings - Fork 25.6k
QL: Reduce nesting of same bool queries #96265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| pr: 96265 | ||
| summary: Reduce nesting of same bool queries | ||
| area: Query Languages | ||
| type: enhancement | ||
| issues: | ||
| - 96236 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,9 +9,15 @@ | |
| import org.elasticsearch.index.query.BoolQueryBuilder; | ||
| import org.elasticsearch.index.query.QueryBuilder; | ||
| import org.elasticsearch.search.sort.NestedSortBuilder; | ||
| import org.elasticsearch.xpack.ql.QlIllegalArgumentException; | ||
| import org.elasticsearch.xpack.ql.tree.Source; | ||
| import org.elasticsearch.xpack.ql.util.CollectionUtils; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.StringJoiner; | ||
|
|
||
| import static org.elasticsearch.index.query.QueryBuilders.boolQuery; | ||
|
|
||
|
|
@@ -23,52 +29,59 @@ public class BoolQuery extends Query { | |
| * {@code true} for boolean {@code AND}, {@code false} for boolean {@code OR}. | ||
| */ | ||
| private final boolean isAnd; | ||
| private final Query left; | ||
| private final Query right; | ||
| private final List<Query> queries; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Imo, this change is a bit forced, conceptually speaking. A boolean query by definition is a boolean operation between two expressions, just like a math operation is. Having multiple expressions ALL linked together by either OR or AND seems like a specialized boolean query, MultiBoolQuery if you want. The BoolQueryBuilder in ES is built similarly, but is generalized: a list of must, a list of should, a list of filters etc. I would have expected a solution around "flattening" the bool statements, but not touching the BoolQuery in this way.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Balancing the tree would have been another alternative but I opted for the above because it simplifies the Lucene query which is what triggers the exception in the first place. |
||
|
|
||
| public BoolQuery(Source source, boolean isAnd, Query left, Query right) { | ||
| this(source, isAnd, Arrays.asList(left, right)); | ||
| } | ||
|
|
||
| public BoolQuery(Source source, boolean isAnd, List<Query> queries) { | ||
| super(source); | ||
| if (left == null) { | ||
| throw new IllegalArgumentException("left is required"); | ||
| } | ||
| if (right == null) { | ||
| throw new IllegalArgumentException("right is required"); | ||
| if (CollectionUtils.isEmpty(queries) || queries.size() < 2) { | ||
| throw new QlIllegalArgumentException("At least two queries required by bool query"); | ||
| } | ||
| this.isAnd = isAnd; | ||
| this.left = left; | ||
| this.right = right; | ||
| this.queries = queries; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean containsNestedField(String path, String field) { | ||
| return left.containsNestedField(path, field) || right.containsNestedField(path, field); | ||
| for (Query query : queries) { | ||
| if (query.containsNestedField(path, field)) { | ||
| return true; | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| @Override | ||
| public Query addNestedField(String path, String field, String format, boolean hasDocValues) { | ||
| Query rewrittenLeft = left.addNestedField(path, field, format, hasDocValues); | ||
| Query rewrittenRight = right.addNestedField(path, field, format, hasDocValues); | ||
| if (rewrittenLeft == left && rewrittenRight == right) { | ||
| return this; | ||
| boolean unchanged = true; | ||
| List<Query> rewritten = new ArrayList<>(queries.size()); | ||
| for (Query query : queries) { | ||
| var rewrittenQuery = query.addNestedField(path, field, format, hasDocValues); | ||
| unchanged &= rewrittenQuery == query; | ||
| rewritten.add(rewrittenQuery); | ||
| } | ||
| return new BoolQuery(source(), isAnd, rewrittenLeft, rewrittenRight); | ||
| return unchanged ? this : new BoolQuery(source(), isAnd, rewritten); | ||
| } | ||
|
|
||
| @Override | ||
| public void enrichNestedSort(NestedSortBuilder sort) { | ||
| left.enrichNestedSort(sort); | ||
| right.enrichNestedSort(sort); | ||
| for (Query query : queries) { | ||
| query.enrichNestedSort(sort); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public QueryBuilder asBuilder() { | ||
| BoolQueryBuilder boolQuery = boolQuery(); | ||
| if (isAnd) { | ||
| boolQuery.must(left.asBuilder()); | ||
| boolQuery.must(right.asBuilder()); | ||
| } else { | ||
| boolQuery.should(left.asBuilder()); | ||
| boolQuery.should(right.asBuilder()); | ||
| for (Query query : queries) { | ||
| if (isAnd) { | ||
| boolQuery.must(query.asBuilder()); | ||
| } else { | ||
| boolQuery.should(query.asBuilder()); | ||
| } | ||
| } | ||
| return boolQuery; | ||
| } | ||
|
|
@@ -77,17 +90,13 @@ public boolean isAnd() { | |
| return isAnd; | ||
| } | ||
|
|
||
| public Query left() { | ||
| return left; | ||
| } | ||
|
|
||
| public Query right() { | ||
| return right; | ||
| public List<Query> queries() { | ||
| return queries; | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hash(super.hashCode(), isAnd, left, right); | ||
| return Objects.hash(super.hashCode(), isAnd, queries); | ||
| } | ||
|
|
||
| @Override | ||
|
|
@@ -96,11 +105,15 @@ public boolean equals(Object obj) { | |
| return false; | ||
| } | ||
| BoolQuery other = (BoolQuery) obj; | ||
| return isAnd == other.isAnd && left.equals(other.left) && right.equals(other.right); | ||
| return isAnd == other.isAnd && queries.equals(other.queries); | ||
| } | ||
|
|
||
| @Override | ||
| protected String innerToString() { | ||
| return left + (isAnd ? " AND " : " OR ") + right; | ||
| StringJoiner sb = new StringJoiner(isAnd ? " AND " : " OR "); | ||
| for (Query query : queries) { | ||
| sb.add(query.toString()); | ||
| } | ||
| return sb.toString(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,8 @@ | |
|
|
||
| import static java.util.Collections.singletonMap; | ||
| import static org.elasticsearch.test.EqualsHashCodeTestUtils.checkEqualsAndHashCode; | ||
| import static org.hamcrest.Matchers.greaterThanOrEqualTo; | ||
| import static org.hamcrest.Matchers.hasSize; | ||
|
|
||
| public class BoolQueryTests extends ESTestCase { | ||
| static BoolQuery randomBoolQuery(int depth) { | ||
|
|
@@ -35,15 +37,15 @@ public void testEqualsAndHashCode() { | |
| } | ||
|
|
||
| private static BoolQuery copy(BoolQuery query) { | ||
| return new BoolQuery(query.source(), query.isAnd(), query.left(), query.right()); | ||
| return new BoolQuery(query.source(), query.isAnd(), query.queries()); | ||
| } | ||
|
|
||
| private static BoolQuery mutate(BoolQuery query) { | ||
| List<Function<BoolQuery, BoolQuery>> options = Arrays.asList( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test doesn't fully reflect the changes in this PR, but I'm ok with it.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed it nevertheless. |
||
| q -> new BoolQuery(SourceTests.mutate(q.source()), q.isAnd(), q.left(), q.right()), | ||
| q -> new BoolQuery(q.source(), false == q.isAnd(), q.left(), q.right()), | ||
| q -> new BoolQuery(q.source(), q.isAnd(), randomValueOtherThan(q.left(), () -> NestedQueryTests.randomQuery(5)), q.right()), | ||
| q -> new BoolQuery(q.source(), q.isAnd(), q.left(), randomValueOtherThan(q.right(), () -> NestedQueryTests.randomQuery(5))) | ||
| q -> new BoolQuery(SourceTests.mutate(q.source()), q.isAnd(), left(q), right(q)), | ||
| q -> new BoolQuery(q.source(), false == q.isAnd(), left(q), right(q)), | ||
| q -> new BoolQuery(q.source(), q.isAnd(), randomValueOtherThan(left(q), () -> NestedQueryTests.randomQuery(5)), right(q)), | ||
| q -> new BoolQuery(q.source(), q.isAnd(), left(q), randomValueOtherThan(right(q), () -> NestedQueryTests.randomQuery(5))) | ||
| ); | ||
| return randomFrom(options).apply(query); | ||
| } | ||
|
|
@@ -124,4 +126,19 @@ public void testToString() { | |
| ).toString() | ||
| ); | ||
| } | ||
|
|
||
| public static Query left(BoolQuery bool) { | ||
| return indexOf(bool, 0); | ||
| } | ||
|
|
||
| public static Query right(BoolQuery bool) { | ||
| return indexOf(bool, 1); | ||
| } | ||
|
|
||
| private static Query indexOf(BoolQuery bool, int index) { | ||
| List<Query> queries = bool.queries(); | ||
| assertThat(queries, hasSize(greaterThanOrEqualTo(2))); | ||
| return queries.get(index); | ||
| } | ||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if this change is not affecting the score of a query that uses
bools and returns theSCORE()as well.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Confirmed with the search team that the two types of queries are equivalent as Lucene normalizes them to the same query (the one created above) when
minimum_should_queryis the default (1 - which it is).