Skip to content
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

Add support for bool query operators in Query DSL for object initializer syntax #7595

Merged
merged 3 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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