Skip to content

Commit

Permalink
Add support for bool query operators in Query DSL for object initiali…
Browse files Browse the repository at this point in the history
…zer syntax (#7595) (#7601)

* Initial OR operator support

* Add other operators and tests

* Fix BOM

Co-authored-by: Steve Gordon <sgordon@hotmail.co.uk>
  • Loading branch information
github-actions[bot] and stevejgordon committed Apr 6, 2023
1 parent ca20a25 commit 1a98093
Show file tree
Hide file tree
Showing 86 changed files with 2,320 additions and 97 deletions.
85 changes: 0 additions & 85 deletions src/Elastic.Clients.Elasticsearch/Core/Query/Query.cs

This file was deleted.

10 changes: 10 additions & 0 deletions src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace Elastic.Clients.Elasticsearch.QueryDsl;

public partial class BoolQuery
{
internal bool Locked => !QueryName.IsNullOrEmpty() || Boost.HasValue || MinimumShouldMatch is not null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Linq;

namespace Elastic.Clients.Elasticsearch.QueryDsl;

internal static class BoolQueryAndExtensions
{
internal static Query CombineAsMust(this Query leftContainer, Query rightContainer)
{
var hasLeftBool = leftContainer.TryGet<BoolQuery>(out var leftBool);
var hasRightBool = rightContainer.TryGet<BoolQuery>(out var rightBool);

//neither side is a bool, no special handling needed wrap in a bool must
if (!hasLeftBool && !hasRightBool)
return CreateMustContainer(new List<Query> { leftContainer, rightContainer });

else if (TryHandleBoolsWithOnlyShouldClauses(leftContainer, rightContainer, leftBool, rightBool, out var query))
return query;

else if (TryHandleUnmergableBools(leftContainer, rightContainer, leftBool, rightBool, out query))
return query;

//neither side is unmergable so neither is a bool with should clauses

var mustNotClauses = OrphanMustNots(leftContainer).EagerConcat(OrphanMustNots(rightContainer));
var filterClauses = OrphanFilters(leftContainer).EagerConcat(OrphanFilters(rightContainer));
var mustClauses = OrphanMusts(leftContainer).EagerConcat(OrphanMusts(rightContainer));

var queryContainer = CreateMustContainer(mustClauses, mustNotClauses, filterClauses);

return queryContainer;
}

/// <summary>
/// Handles cases where either side is a bool which indicates it can't be merged yet the other side is mergable.
/// A side is considered unmergable if its locked (has important metadata) or has should clauses.
/// Instead of always wrapping these cases in another bool we merge to unmergable side into to others must clause therefor flattening the
/// generated graph
/// </summary>
private static bool TryHandleUnmergableBools(Query leftContainer, Query rightContainer, BoolQuery leftBool, BoolQuery rightBool, out Query query)
{
query = null;
var leftCantMergeAnd = leftBool != null && !leftBool.CanMergeAnd();
var rightCantMergeAnd = rightBool != null && !rightBool.CanMergeAnd();
if (!leftCantMergeAnd && !rightCantMergeAnd)
return false;

if (leftCantMergeAnd && rightCantMergeAnd)
query = CreateMustContainer(leftContainer, rightContainer);

//right can't merge but left can and is a bool so we add left to the must clause of right
else if (!leftCantMergeAnd && leftBool != null && rightCantMergeAnd)
{
leftBool.Must = leftBool.Must.AddIfNotNull(rightContainer).ToArray();
query = leftContainer;
}

//right can't merge and left is not a bool, we forcefully create a wrapped must container
else if (!leftCantMergeAnd && leftBool == null && rightCantMergeAnd)
query = CreateMustContainer(leftContainer, rightContainer);

//left can't merge but right can and is a bool so we add left to the must clause of right
else if (leftCantMergeAnd && !rightCantMergeAnd && rightBool != null)
{
rightBool.Must = rightBool.Must.AddIfNotNull(leftContainer).ToArray();
query = rightContainer;
}

//left can't merge and right is not a bool, we forcefully create a wrapped must container
else if (leftCantMergeAnd && !rightCantMergeAnd && rightBool == null)
query = CreateMustContainer(new List<Query> { leftContainer, rightContainer });

return query != null;
}

/// <summary>
/// Both Sides are bools, but one of them has only should clauses so we should wrap into a new container.
/// Unless we know one of the sides is a bool with only a must who's clauses are all bools with only should clauses.
/// 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
/// In this case we can optimize the generated graph by merging and preventing stack overflows
/// </summary>
private static bool TryHandleBoolsWithOnlyShouldClauses(Query leftContainer, Query rightContainer, BoolQuery leftBool, BoolQuery rightBool, out Query query)
{
query = null;
var leftHasOnlyShoulds = leftBool.HasOnlyShouldClauses();
var rightHasOnlyShoulds = rightBool.HasOnlyShouldClauses();
if (!leftHasOnlyShoulds && !rightHasOnlyShoulds)
return false;

if (leftContainer.HoldsOnlyShouldMusts && rightHasOnlyShoulds)
{
leftBool.Must = leftBool.Must.AddIfNotNull(rightContainer).ToArray();
query = leftContainer;
}
else if (rightContainer.HoldsOnlyShouldMusts && leftHasOnlyShoulds)
{
rightBool.Must = rightBool.Must.AddIfNotNull(leftContainer).ToArray();
query = rightContainer;
}
else
{
query = CreateMustContainer(new List<Query> { leftContainer, rightContainer });
query.HoldsOnlyShouldMusts = rightHasOnlyShoulds && leftHasOnlyShoulds;
}
return true;
}

private static Query CreateMustContainer(Query left, Query right) =>
CreateMustContainer(new List<Query> { left, right });

private static Query CreateMustContainer(List<Query> mustClauses) =>
new Query(new BoolQuery() { Must = mustClauses.ToListOrNullIfEmpty() });

private static Query CreateMustContainer(
List<Query> mustClauses,
List<Query> mustNotClauses,
List<Query> filters
) => new Query(new BoolQuery
{
Must = mustClauses.ToListOrNullIfEmpty(),
MustNot = mustNotClauses.ToListOrNullIfEmpty(),
Filter = filters.ToListOrNullIfEmpty()
});

private static bool CanMergeAnd(this BoolQuery boolQuery) =>
boolQuery != null && !boolQuery.Locked && !boolQuery.Should.HasAny();

private static IEnumerable<Query> OrphanMusts(Query container)
{
if (!container.TryGet<BoolQuery>(out var lBoolQuery))
return new[] { container };

return lBoolQuery.Must?.AsInstanceOrToListOrNull();
}

private static IEnumerable<Query> OrphanMustNots(Query container) =>
!container.TryGet<BoolQuery>(out var boolQuery) ? null : (IEnumerable<Query>)(boolQuery.MustNot?.AsInstanceOrToListOrNull());

private static IEnumerable<Query> OrphanFilters(Query container) =>
!container.TryGet<BoolQuery>(out var boolQuery) ? null : (IEnumerable<Query>)(boolQuery.Filter?.AsInstanceOrToListOrNull());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

namespace Elastic.Clients.Elasticsearch.QueryDsl;

internal static class BoolQueryExtensions
{
internal static Query Self(this Query q) => q;

internal static bool HasOnlyShouldClauses(this BoolQuery boolQuery) =>
boolQuery != null &&
boolQuery.Should.HasAny() &&
!boolQuery.Must.HasAny() &&
!boolQuery.MustNot.HasAny() &&
!boolQuery.Filter.HasAny();

internal static bool HasOnlyFilterClauses(this BoolQuery boolQuery) =>
boolQuery != null &&
!boolQuery.Should.HasAny() &&
!boolQuery.Must.HasAny() &&
!boolQuery.MustNot.HasAny() &&
boolQuery.Filter.HasAny();

internal static bool HasOnlyMustNotClauses(this BoolQuery boolQuery) =>
boolQuery != null &&
!boolQuery.Should.HasAny() &&
!boolQuery.Must.HasAny() &&
boolQuery.MustNot.HasAny() &&
!boolQuery.Filter.HasAny();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Linq;

namespace Elastic.Clients.Elasticsearch.QueryDsl;

internal static class BoolQueryOrExtensions
{
internal static Query CombineAsShould(this Query leftContainer, Query rightContainer)
{
var hasLeftBool = leftContainer.TryGet<BoolQuery>(out var leftBool);
var hasRightBool = rightContainer.TryGet<BoolQuery>(out var rightBool);

if (TryFlattenShould(leftContainer, rightContainer, leftBool, rightBool, out var c))
return c;

var lHasShouldQueries = hasLeftBool && leftBool.Should.HasAny();
var rHasShouldQueries = hasRightBool && rightBool.Should.HasAny();

var lq = lHasShouldQueries ? leftBool.Should : new[] { leftContainer };
var rq = rHasShouldQueries ? rightBool.Should : new[] { rightContainer };

var shouldClauses = lq.EagerConcat(rq);

return CreateShouldContainer(shouldClauses);
}

private static bool TryFlattenShould(Query leftContainer, Query rightContainer, BoolQuery leftBool, BoolQuery rightBool, out Query query)
{
query = null;

var leftCanMerge = leftContainer.CanMergeShould();
var rightCanMerge = rightContainer.CanMergeShould();

if (!leftCanMerge && !rightCanMerge)
query = CreateShouldContainer(new List<Query> { leftContainer, rightContainer });

// Left can merge but right's bool can not. instead of wrapping into a new bool we inject the whole bool into left

else if (leftCanMerge && !rightCanMerge && rightBool is not null)
{
leftBool.Should = leftBool.Should.AddIfNotNull(rightContainer).ToArray();
query = leftContainer;
}
else if (rightCanMerge && !leftCanMerge && leftBool is not null)
{
rightBool.Should = rightBool.Should.AddIfNotNull(leftContainer).ToArray();
query = rightContainer;
}

return query != null;
}

private static bool CanMergeShould(this Query container) =>
container.TryGet<BoolQuery>(out var boolQuery) && boolQuery.CanMergeShould();

private static bool CanMergeShould(this BoolQuery boolQuery) =>
boolQuery is not null && !boolQuery.Locked && boolQuery.HasOnlyShouldClauses();

private static Query CreateShouldContainer(List<Query> shouldClauses) =>
new BoolQuery
{
Should = shouldClauses.ToListOrNullIfEmpty()
};
}
Loading

0 comments on commit 1a98093

Please sign in to comment.