Skip to content

Commit 912d5c5

Browse files
Add support for bool query operators in Query DSL for object initializer syntax (#7595) (#7596)
* Initial OR operator support * Add other operators and tests * Fix BOM Co-authored-by: Steve Gordon <[email protected]>
1 parent d1592f8 commit 912d5c5

File tree

86 files changed

+2320
-97
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+2320
-97
lines changed

src/Elastic.Clients.Elasticsearch/Core/Query/Query.cs

-85
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Elastic.Clients.Elasticsearch.QueryDsl;
6+
7+
public partial class BoolQuery
8+
{
9+
internal bool Locked => !QueryName.IsNullOrEmpty() || Boost.HasValue || MinimumShouldMatch is not null;
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
8+
namespace Elastic.Clients.Elasticsearch.QueryDsl;
9+
10+
internal static class BoolQueryAndExtensions
11+
{
12+
internal static Query CombineAsMust(this Query leftContainer, Query rightContainer)
13+
{
14+
var hasLeftBool = leftContainer.TryGet<BoolQuery>(out var leftBool);
15+
var hasRightBool = rightContainer.TryGet<BoolQuery>(out var rightBool);
16+
17+
//neither side is a bool, no special handling needed wrap in a bool must
18+
if (!hasLeftBool && !hasRightBool)
19+
return CreateMustContainer(new List<Query> { leftContainer, rightContainer });
20+
21+
else if (TryHandleBoolsWithOnlyShouldClauses(leftContainer, rightContainer, leftBool, rightBool, out var query))
22+
return query;
23+
24+
else if (TryHandleUnmergableBools(leftContainer, rightContainer, leftBool, rightBool, out query))
25+
return query;
26+
27+
//neither side is unmergable so neither is a bool with should clauses
28+
29+
var mustNotClauses = OrphanMustNots(leftContainer).EagerConcat(OrphanMustNots(rightContainer));
30+
var filterClauses = OrphanFilters(leftContainer).EagerConcat(OrphanFilters(rightContainer));
31+
var mustClauses = OrphanMusts(leftContainer).EagerConcat(OrphanMusts(rightContainer));
32+
33+
var queryContainer = CreateMustContainer(mustClauses, mustNotClauses, filterClauses);
34+
35+
return queryContainer;
36+
}
37+
38+
/// <summary>
39+
/// Handles cases where either side is a bool which indicates it can't be merged yet the other side is mergable.
40+
/// A side is considered unmergable if its locked (has important metadata) or has should clauses.
41+
/// Instead of always wrapping these cases in another bool we merge to unmergable side into to others must clause therefor flattening the
42+
/// generated graph
43+
/// </summary>
44+
private static bool TryHandleUnmergableBools(Query leftContainer, Query rightContainer, BoolQuery leftBool, BoolQuery rightBool, out Query query)
45+
{
46+
query = null;
47+
var leftCantMergeAnd = leftBool != null && !leftBool.CanMergeAnd();
48+
var rightCantMergeAnd = rightBool != null && !rightBool.CanMergeAnd();
49+
if (!leftCantMergeAnd && !rightCantMergeAnd)
50+
return false;
51+
52+
if (leftCantMergeAnd && rightCantMergeAnd)
53+
query = CreateMustContainer(leftContainer, rightContainer);
54+
55+
//right can't merge but left can and is a bool so we add left to the must clause of right
56+
else if (!leftCantMergeAnd && leftBool != null && rightCantMergeAnd)
57+
{
58+
leftBool.Must = leftBool.Must.AddIfNotNull(rightContainer).ToArray();
59+
query = leftContainer;
60+
}
61+
62+
//right can't merge and left is not a bool, we forcefully create a wrapped must container
63+
else if (!leftCantMergeAnd && leftBool == null && rightCantMergeAnd)
64+
query = CreateMustContainer(leftContainer, rightContainer);
65+
66+
//left can't merge but right can and is a bool so we add left to the must clause of right
67+
else if (leftCantMergeAnd && !rightCantMergeAnd && rightBool != null)
68+
{
69+
rightBool.Must = rightBool.Must.AddIfNotNull(leftContainer).ToArray();
70+
query = rightContainer;
71+
}
72+
73+
//left can't merge and right is not a bool, we forcefully create a wrapped must container
74+
else if (leftCantMergeAnd && !rightCantMergeAnd && rightBool == null)
75+
query = CreateMustContainer(new List<Query> { leftContainer, rightContainer });
76+
77+
return query != null;
78+
}
79+
80+
/// <summary>
81+
/// Both Sides are bools, but one of them has only should clauses so we should wrap into a new container.
82+
/// Unless we know one of the sides is a bool with only a must who's clauses are all bools with only should clauses.
83+
/// This is a piece of metadata we set at the bools creation time so we do not have to iterate the clauses on each combination
84+
/// In this case we can optimize the generated graph by merging and preventing stack overflows
85+
/// </summary>
86+
private static bool TryHandleBoolsWithOnlyShouldClauses(Query leftContainer, Query rightContainer, BoolQuery leftBool, BoolQuery rightBool, out Query query)
87+
{
88+
query = null;
89+
var leftHasOnlyShoulds = leftBool.HasOnlyShouldClauses();
90+
var rightHasOnlyShoulds = rightBool.HasOnlyShouldClauses();
91+
if (!leftHasOnlyShoulds && !rightHasOnlyShoulds)
92+
return false;
93+
94+
if (leftContainer.HoldsOnlyShouldMusts && rightHasOnlyShoulds)
95+
{
96+
leftBool.Must = leftBool.Must.AddIfNotNull(rightContainer).ToArray();
97+
query = leftContainer;
98+
}
99+
else if (rightContainer.HoldsOnlyShouldMusts && leftHasOnlyShoulds)
100+
{
101+
rightBool.Must = rightBool.Must.AddIfNotNull(leftContainer).ToArray();
102+
query = rightContainer;
103+
}
104+
else
105+
{
106+
query = CreateMustContainer(new List<Query> { leftContainer, rightContainer });
107+
query.HoldsOnlyShouldMusts = rightHasOnlyShoulds && leftHasOnlyShoulds;
108+
}
109+
return true;
110+
}
111+
112+
private static Query CreateMustContainer(Query left, Query right) =>
113+
CreateMustContainer(new List<Query> { left, right });
114+
115+
private static Query CreateMustContainer(List<Query> mustClauses) =>
116+
new Query(new BoolQuery() { Must = mustClauses.ToListOrNullIfEmpty() });
117+
118+
private static Query CreateMustContainer(
119+
List<Query> mustClauses,
120+
List<Query> mustNotClauses,
121+
List<Query> filters
122+
) => new Query(new BoolQuery
123+
{
124+
Must = mustClauses.ToListOrNullIfEmpty(),
125+
MustNot = mustNotClauses.ToListOrNullIfEmpty(),
126+
Filter = filters.ToListOrNullIfEmpty()
127+
});
128+
129+
private static bool CanMergeAnd(this BoolQuery boolQuery) =>
130+
boolQuery != null && !boolQuery.Locked && !boolQuery.Should.HasAny();
131+
132+
private static IEnumerable<Query> OrphanMusts(Query container)
133+
{
134+
if (!container.TryGet<BoolQuery>(out var lBoolQuery))
135+
return new[] { container };
136+
137+
return lBoolQuery.Must?.AsInstanceOrToListOrNull();
138+
}
139+
140+
private static IEnumerable<Query> OrphanMustNots(Query container) =>
141+
!container.TryGet<BoolQuery>(out var boolQuery) ? null : (IEnumerable<Query>)(boolQuery.MustNot?.AsInstanceOrToListOrNull());
142+
143+
private static IEnumerable<Query> OrphanFilters(Query container) =>
144+
!container.TryGet<BoolQuery>(out var boolQuery) ? null : (IEnumerable<Query>)(boolQuery.Filter?.AsInstanceOrToListOrNull());
145+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace Elastic.Clients.Elasticsearch.QueryDsl;
6+
7+
internal static class BoolQueryExtensions
8+
{
9+
internal static Query Self(this Query q) => q;
10+
11+
internal static bool HasOnlyShouldClauses(this BoolQuery boolQuery) =>
12+
boolQuery != null &&
13+
boolQuery.Should.HasAny() &&
14+
!boolQuery.Must.HasAny() &&
15+
!boolQuery.MustNot.HasAny() &&
16+
!boolQuery.Filter.HasAny();
17+
18+
internal static bool HasOnlyFilterClauses(this BoolQuery boolQuery) =>
19+
boolQuery != null &&
20+
!boolQuery.Should.HasAny() &&
21+
!boolQuery.Must.HasAny() &&
22+
!boolQuery.MustNot.HasAny() &&
23+
boolQuery.Filter.HasAny();
24+
25+
internal static bool HasOnlyMustNotClauses(this BoolQuery boolQuery) =>
26+
boolQuery != null &&
27+
!boolQuery.Should.HasAny() &&
28+
!boolQuery.Must.HasAny() &&
29+
boolQuery.MustNot.HasAny() &&
30+
!boolQuery.Filter.HasAny();
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
8+
namespace Elastic.Clients.Elasticsearch.QueryDsl;
9+
10+
internal static class BoolQueryOrExtensions
11+
{
12+
internal static Query CombineAsShould(this Query leftContainer, Query rightContainer)
13+
{
14+
var hasLeftBool = leftContainer.TryGet<BoolQuery>(out var leftBool);
15+
var hasRightBool = rightContainer.TryGet<BoolQuery>(out var rightBool);
16+
17+
if (TryFlattenShould(leftContainer, rightContainer, leftBool, rightBool, out var c))
18+
return c;
19+
20+
var lHasShouldQueries = hasLeftBool && leftBool.Should.HasAny();
21+
var rHasShouldQueries = hasRightBool && rightBool.Should.HasAny();
22+
23+
var lq = lHasShouldQueries ? leftBool.Should : new[] { leftContainer };
24+
var rq = rHasShouldQueries ? rightBool.Should : new[] { rightContainer };
25+
26+
var shouldClauses = lq.EagerConcat(rq);
27+
28+
return CreateShouldContainer(shouldClauses);
29+
}
30+
31+
private static bool TryFlattenShould(Query leftContainer, Query rightContainer, BoolQuery leftBool, BoolQuery rightBool, out Query query)
32+
{
33+
query = null;
34+
35+
var leftCanMerge = leftContainer.CanMergeShould();
36+
var rightCanMerge = rightContainer.CanMergeShould();
37+
38+
if (!leftCanMerge && !rightCanMerge)
39+
query = CreateShouldContainer(new List<Query> { leftContainer, rightContainer });
40+
41+
// Left can merge but right's bool can not. instead of wrapping into a new bool we inject the whole bool into left
42+
43+
else if (leftCanMerge && !rightCanMerge && rightBool is not null)
44+
{
45+
leftBool.Should = leftBool.Should.AddIfNotNull(rightContainer).ToArray();
46+
query = leftContainer;
47+
}
48+
else if (rightCanMerge && !leftCanMerge && leftBool is not null)
49+
{
50+
rightBool.Should = rightBool.Should.AddIfNotNull(leftContainer).ToArray();
51+
query = rightContainer;
52+
}
53+
54+
return query != null;
55+
}
56+
57+
private static bool CanMergeShould(this Query container) =>
58+
container.TryGet<BoolQuery>(out var boolQuery) && boolQuery.CanMergeShould();
59+
60+
private static bool CanMergeShould(this BoolQuery boolQuery) =>
61+
boolQuery is not null && !boolQuery.Locked && boolQuery.HasOnlyShouldClauses();
62+
63+
private static Query CreateShouldContainer(List<Query> shouldClauses) =>
64+
new BoolQuery
65+
{
66+
Should = shouldClauses.ToListOrNullIfEmpty()
67+
};
68+
}

0 commit comments

Comments
 (0)