From c7fac15de231bdcb424db7d3027a7bc468cef46b Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Thu, 6 Apr 2023 18:50:33 +0100 Subject: [PATCH] Add support for bool query operators in Query DSL for object initializer syntax (#7595) * Initial OR operator support * Add other operators and tests * Fix BOM --- .../Core/Query/Query.cs | 85 ----- .../Types/QueryDsl/BoolQuery.cs | 10 + .../Types/QueryDsl/BoolQueryAndExtensions.cs | 145 ++++++++ .../Types/QueryDsl/BoolQueryExtensions.cs | 31 ++ .../Types/QueryDsl/BoolQueryOrExtensions.cs | 68 ++++ .../Types/QueryDsl/Query.cs | 62 +++- .../Types/QueryDsl/QueryDescriptor.cs | 20 +- .../Types/QueryDsl/RangeQuery.cs | 4 +- .../Types/QueryDsl/RawJsonQuery.cs | 2 + .../Types/QueryDsl/SearchQuery.cs | 65 ++++ .../_Generated/Types/QueryDsl/BoolQuery.g.cs | 2 + .../Types/QueryDsl/BoostingQuery.g.cs | 2 + .../Types/QueryDsl/CombinedFieldsQuery.g.cs | 2 + .../Types/QueryDsl/ConstantScoreQuery.g.cs | 2 + .../Types/QueryDsl/DateRangeQuery.g.cs | 2 + .../Types/QueryDsl/DisMaxQuery.g.cs | 2 + .../Types/QueryDsl/ExistsQuery.g.cs | 2 + .../Types/QueryDsl/FunctionScoreQuery.g.cs | 2 + .../_Generated/Types/QueryDsl/FuzzyQuery.g.cs | 2 + .../Types/QueryDsl/GeoBoundingBoxQuery.g.cs | 2 + .../Types/QueryDsl/GeoDistanceQuery.g.cs | 2 + .../Types/QueryDsl/GeoPolygonQuery.g.cs | 2 + .../Types/QueryDsl/HasChildQuery.g.cs | 2 + .../Types/QueryDsl/HasParentQuery.g.cs | 2 + .../_Generated/Types/QueryDsl/IdsQuery.g.cs | 2 + .../Types/QueryDsl/IntervalsQuery.g.cs | 2 + .../Types/QueryDsl/MatchAllQuery.g.cs | 2 + .../Types/QueryDsl/MatchBoolPrefixQuery.g.cs | 2 + .../Types/QueryDsl/MatchNoneQuery.g.cs | 2 + .../QueryDsl/MatchPhrasePrefixQuery.g.cs | 2 + .../Types/QueryDsl/MatchPhraseQuery.g.cs | 2 + .../_Generated/Types/QueryDsl/MatchQuery.g.cs | 2 + .../Types/QueryDsl/MoreLikeThisQuery.g.cs | 2 + .../Types/QueryDsl/MultiMatchQuery.g.cs | 2 + .../Types/QueryDsl/NestedQuery.g.cs | 2 + .../Types/QueryDsl/NumberRangeQuery.g.cs | 2 + .../Types/QueryDsl/ParentIdQuery.g.cs | 2 + .../Types/QueryDsl/PercolateQuery.g.cs | 2 + .../Types/QueryDsl/PinnedQuery.g.cs | 2 + .../Types/QueryDsl/PrefixQuery.g.cs | 2 + .../_Generated/Types/QueryDsl/Query.g.cs | 12 +- .../Types/QueryDsl/QueryStringQuery.g.cs | 2 + .../Types/QueryDsl/RankFeatureQuery.g.cs | 2 + .../Types/QueryDsl/RegexpQuery.g.cs | 2 + .../Types/QueryDsl/ScriptQuery.g.cs | 2 + .../Types/QueryDsl/ScriptScoreQuery.g.cs | 2 + .../QueryDsl/SimpleQueryStringQuery.g.cs | 2 + .../Types/QueryDsl/SpanContainingQuery.g.cs | 2 + .../Types/QueryDsl/SpanFieldMaskingQuery.g.cs | 2 + .../Types/QueryDsl/SpanFirstQuery.g.cs | 2 + .../Types/QueryDsl/SpanMultiTermQuery.g.cs | 2 + .../Types/QueryDsl/SpanNearQuery.g.cs | 2 + .../Types/QueryDsl/SpanNotQuery.g.cs | 2 + .../Types/QueryDsl/SpanOrQuery.g.cs | 2 + .../_Generated/Types/QueryDsl/SpanQuery.g.cs | 2 +- .../Types/QueryDsl/SpanTermQuery.g.cs | 2 + .../Types/QueryDsl/SpanWithinQuery.g.cs | 2 + .../_Generated/Types/QueryDsl/TermQuery.g.cs | 2 + .../_Generated/Types/QueryDsl/TermsQuery.g.cs | 2 + .../Types/QueryDsl/TermsSetQuery.g.cs | 2 + .../Types/QueryDsl/WildcardQuery.g.cs | 2 + .../Types/QueryDsl/WrapperQuery.g.cs | 2 + .../QueryDsl/BoolDsl/AndOperatorUsageTests.cs | 351 ++++++++++++++++++ tests/Tests/QueryDsl/BoolDsl/BoolApiTests.cs | 115 ++++++ .../QueryDsl/BoolDsl/CombinationUsageTests.cs | 91 +++++ .../QueryDsl/BoolDsl/NotOperatorUsageTests.cs | 87 +++++ .../QueryDsl/BoolDsl/OperatorUsageBase.cs | 35 ++ .../QueryDsl/BoolDsl/OrOperatorUsageTests.cs | 330 ++++++++++++++++ .../QueryOperatorSerializationTests.cs | 281 ++++++++++++++ .../BoolDsl/UnaryAddOperatorUsageTests.cs | 87 +++++ ...dNotBeViralAnyWayYouComposeIt.verified.txt | 1 + ...ts.DoNotCombineLeftLockedBool.verified.txt | 27 ++ ...Tests.DoNotCombineLockedBools.verified.txt | 32 ++ ...s.DoNotCombineRightLockedBool.verified.txt | 27 ++ ...or_SerializeAsManyMustClauses.verified.txt | 29 ++ ...erator_SerializeAsMustClauses.verified.txt | 22 ++ ...ator_SerializeAsShouldClauses.verified.txt | 22 ++ ...ldClauses_SerializesCorrectly.verified.txt | 42 +++ ...ouldMatch_SerializesCorrectly.verified.txt | 61 +++ ...xAndMatch_SerializesCorrectly.verified.txt | 36 ++ ...AndFilter_SerializesCorrectly.verified.txt | 36 ++ ...ndMustNot_SerializesCorrectly.verified.txt | 29 ++ ...edWithAnd_SerializesCorrectly.verified.txt | 22 ++ ...rator_SerializeAsFilterClause.verified.txt | 13 + ...edWithAnd_SerializesCorrectly.verified.txt | 22 ++ ...ator_SerializeAsMustNotClause.verified.txt | 13 + 86 files changed, 2320 insertions(+), 97 deletions(-) delete mode 100644 src/Elastic.Clients.Elasticsearch/Core/Query/Query.cs create mode 100644 src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQuery.cs create mode 100644 src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryAndExtensions.cs create mode 100644 src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryExtensions.cs create mode 100644 src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryOrExtensions.cs create mode 100644 src/Elastic.Clients.Elasticsearch/Types/QueryDsl/SearchQuery.cs create mode 100644 tests/Tests/QueryDsl/BoolDsl/AndOperatorUsageTests.cs create mode 100644 tests/Tests/QueryDsl/BoolDsl/BoolApiTests.cs create mode 100644 tests/Tests/QueryDsl/BoolDsl/CombinationUsageTests.cs create mode 100644 tests/Tests/QueryDsl/BoolDsl/NotOperatorUsageTests.cs create mode 100644 tests/Tests/QueryDsl/BoolDsl/OperatorUsageBase.cs create mode 100644 tests/Tests/QueryDsl/BoolDsl/OrOperatorUsageTests.cs create mode 100644 tests/Tests/QueryDsl/BoolDsl/QueryOperatorSerializationTests.cs create mode 100644 tests/Tests/QueryDsl/BoolDsl/UnaryAddOperatorUsageTests.cs create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.AndShouldNotBeViralAnyWayYouComposeIt.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineLeftLockedBool.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineLockedBools.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineRightLockedBool.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.MultipleSearchQueriesCombinedWithAndOperator_SerializeAsManyMustClauses.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueriesCombinedWithAndOperator_SerializeAsMustClauses.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueriesCombinedWithOrOperator_SerializeAsShouldClauses.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryJoinsWithShouldClauses_SerializesCorrectly.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryMixAndMatchMinimumShouldMatch_SerializesCorrectly.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryMixAndMatch_SerializesCorrectly.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithMustAndMustNotAndFilter_SerializesCorrectly.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithMustAndMustNot_SerializesCorrectly.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryAddOperator_CombinedWithAnd_SerializesCorrectly.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryAddOperator_SerializeAsFilterClause.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryNegationOperator_CombinedWithAnd_SerializesCorrectly.verified.txt create mode 100644 tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryNegationOperator_SerializeAsMustNotClause.verified.txt diff --git a/src/Elastic.Clients.Elasticsearch/Core/Query/Query.cs b/src/Elastic.Clients.Elasticsearch/Core/Query/Query.cs deleted file mode 100644 index 7a1ad4cdd44..00000000000 --- a/src/Elastic.Clients.Elasticsearch/Core/Query/Query.cs +++ /dev/null @@ -1,85 +0,0 @@ -// 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 abstract partial class SearchQuery -{ - //[JsonIgnore] - //public bool IsWritable => throw new NotImplementedException(); - - ////protected abstract bool Conditionless { get; } - //[JsonIgnore] - //public bool IsStrict { get; set; } - - //[JsonIgnore] - //public bool IsVerbatim { get; set; } - - //[JsonIgnore] - //public bool IsWritable => true; //IsVerbatim || !Conditionless; - - //bool IQuery.Conditionless => Conditionless; - - //always evaluate to false so that each side of && equation is evaluated - public static bool operator false(SearchQuery a) => false; - - //always evaluate to false so that each side of && equation is evaluated - public static bool operator true(SearchQuery a) => false; - - //public static QueryBase operator &(QueryBase leftQuery, QueryBase rightQuery) => Combine(leftQuery, rightQuery, (l, r) => l && r); - - //public static QueryBase operator |(QueryBase leftQuery, QueryBase rightQuery) => Combine(leftQuery, rightQuery, (l, r) => l || r); - - //public static QueryBase operator !(QueryBase query) => query == null || !query.IsWritable - // ? null - // : new BoolQuery { MustNot = new Query[] { query } }; - - //public static QueryBase operator +(QueryBase query) => query == null || !query.IsWritable - // ? null - // : new BoolQuery { Filter = new Query[] { query } }; - - //private static QueryBase Combine(QueryBase leftQuery, QueryBase rightQuery, Func combine) - //{ - // if (IfEitherIsEmptyReturnTheOtherOrEmpty(leftQuery, rightQuery, out var q)) - // return q; - - // IQuery container = combine(leftQuery, rightQuery); - // var query = container.Bool; - // return new BoolQuery - // { - // Must = query.Must, - // MustNot = query.MustNot, - // Should = query.Should, - // Filter = query.Filter, - // }; - //} - - //private static bool IfEitherIsEmptyReturnTheOtherOrEmpty(QueryBase leftQuery, QueryBase rightQuery, - // out QueryBase query) - //{ - // query = null; - // if (leftQuery == null && rightQuery == null) - // return true; - - // var leftWritable = leftQuery?.IsWritable ?? false; - // var rightWritable = rightQuery?.IsWritable ?? false; - // if (leftWritable && rightWritable) - // return false; - // if (!leftWritable && !rightWritable) - // return true; - - // query = leftWritable ? leftQuery : rightQuery; - // return true; - //} - - //public static implicit operator Query(QueryBase query) => - // query == null ? null : new Query(query); - - //internal void WrapInContainer(IQuery container) => InternalWrapInContainer(container); - - ////container.IsVerbatim = IsVerbatim; - ////container.IsStrict = IsStrict; - - //internal abstract void InternalWrapInContainer(IQuery container); -} diff --git a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQuery.cs b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQuery.cs new file mode 100644 index 00000000000..2efca3b70a3 --- /dev/null +++ b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQuery.cs @@ -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; +} diff --git a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryAndExtensions.cs b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryAndExtensions.cs new file mode 100644 index 00000000000..b3eacd47ad2 --- /dev/null +++ b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryAndExtensions.cs @@ -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(out var leftBool); + var hasRightBool = rightContainer.TryGet(out var rightBool); + + //neither side is a bool, no special handling needed wrap in a bool must + if (!hasLeftBool && !hasRightBool) + return CreateMustContainer(new List { 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; + } + + /// + /// 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 + /// + 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 { leftContainer, rightContainer }); + + return query != null; + } + + /// + /// 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 + /// + 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 { leftContainer, rightContainer }); + query.HoldsOnlyShouldMusts = rightHasOnlyShoulds && leftHasOnlyShoulds; + } + return true; + } + + private static Query CreateMustContainer(Query left, Query right) => + CreateMustContainer(new List { left, right }); + + private static Query CreateMustContainer(List mustClauses) => + new Query(new BoolQuery() { Must = mustClauses.ToListOrNullIfEmpty() }); + + private static Query CreateMustContainer( + List mustClauses, + List mustNotClauses, + List 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 OrphanMusts(Query container) + { + if (!container.TryGet(out var lBoolQuery)) + return new[] { container }; + + return lBoolQuery.Must?.AsInstanceOrToListOrNull(); + } + + private static IEnumerable OrphanMustNots(Query container) => + !container.TryGet(out var boolQuery) ? null : (IEnumerable)(boolQuery.MustNot?.AsInstanceOrToListOrNull()); + + private static IEnumerable OrphanFilters(Query container) => + !container.TryGet(out var boolQuery) ? null : (IEnumerable)(boolQuery.Filter?.AsInstanceOrToListOrNull()); +} diff --git a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryExtensions.cs b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryExtensions.cs new file mode 100644 index 00000000000..38af2755d39 --- /dev/null +++ b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryExtensions.cs @@ -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(); +} diff --git a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryOrExtensions.cs b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryOrExtensions.cs new file mode 100644 index 00000000000..3be41d8d08d --- /dev/null +++ b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/BoolQueryOrExtensions.cs @@ -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(out var leftBool); + var hasRightBool = rightContainer.TryGet(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 { 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(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 shouldClauses) => + new BoolQuery + { + Should = shouldClauses.ToListOrNullIfEmpty() + }; +} diff --git a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/Query.cs b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/Query.cs index ff63ed9070f..ebdf046a127 100644 --- a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/Query.cs +++ b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/Query.cs @@ -2,15 +2,24 @@ // 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; using System.Diagnostics.CodeAnalysis; namespace Elastic.Clients.Elasticsearch.QueryDsl; public partial class Query { + internal Query(SearchQuery query) + { + if (query is null) + return; + + query.WrapInContainer(this); + } + public bool TryGet([NotNullWhen(true)]out T? query) { - query = default(T); + query = default; if (Variant is T variant) { @@ -20,5 +29,54 @@ public bool TryGet([NotNullWhen(true)]out T? query) return false; } -} + internal bool HoldsOnlyShouldMusts { get; set; } + + public static bool operator false(Query _) => false; + public static bool operator true(Query _) => false; + + public static Query operator &(Query leftContainer, Query rightContainer) => + And(leftContainer, rightContainer); + + internal static Query And(Query leftContainer, Query rightContainer) + { + if (leftContainer is null && rightContainer is null) + { + throw new ArgumentException("Queries to combine should not both be null."); + } + + if (rightContainer is null) + return leftContainer; + + if (leftContainer is null) + return rightContainer; + + return leftContainer.CombineAsMust(rightContainer); + } + + public static Query operator |(Query leftContainer, Query rightContainer) => Or(leftContainer, rightContainer); + + internal static Query Or(Query leftContainer, Query rightContainer) + { + if (leftContainer is null && rightContainer is null) + { + throw new ArgumentException("Queries to combine should not both be null."); + } + + if (rightContainer is null) + return leftContainer; + + if (leftContainer is null) + return rightContainer; + + return leftContainer.CombineAsShould(rightContainer); + } + + public static Query operator !(Query queryContainer) => queryContainer is null + ? null + : new Query(new BoolQuery { MustNot = new[] { queryContainer } }); + + public static Query operator +(Query queryContainer) => queryContainer is null + ? null + : new Query(new BoolQuery { Filter = new[] { queryContainer } }); +} diff --git a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/QueryDescriptor.cs b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/QueryDescriptor.cs index 6d54f0390bd..8525ed407af 100644 --- a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/QueryDescriptor.cs +++ b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/QueryDescriptor.cs @@ -14,11 +14,16 @@ public sealed partial class QueryDescriptor /// public QueryDescriptor RawJson(string rawJson) => Set(new RawJsonQuery(rawJson), "raw_json"); - public void MatchAll() => + public QueryDescriptor MatchAll() => Set(_ => { }, "match_all"); - public void Term(Expression> field, object value, float? boost = null) => + public QueryDescriptor Term(Expression> field, object value, float? boost = null) => Term(t => t.Field(field).Value(FieldValue.Composite(value)).Boost(boost)); + + /// + /// Used purely for testing (for now). + /// + internal bool TestHasVariant => ContainsVariant; } public sealed partial class QueryDescriptor @@ -28,9 +33,14 @@ public sealed partial class QueryDescriptor /// public QueryDescriptor RawJson(string rawJson) => Set(new RawJsonQuery(rawJson), "raw_json"); - public void MatchAll() => + public QueryDescriptor MatchAll() => Set(_ => { }, "match_all"); - public void Term(Expression> field, object value, float? boost = null) => - Term(t => t.Field(field).Value(FieldValue.Composite(value)).Boost(boost)); + public QueryDescriptor Term(Expression> field, object value, float? boost = null) => + Term(t => t.Field(field).Value(FieldValue.Composite(value)).Boost(boost)); + + /// + /// Used purely for testing (for now). + /// + internal bool TestHasVariant => ContainsVariant; } diff --git a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/RangeQuery.cs b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/RangeQuery.cs index 12e47622f7d..6264da9eb77 100644 --- a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/RangeQuery.cs +++ b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/RangeQuery.cs @@ -15,7 +15,9 @@ public class RangeQuery : SearchQuery { internal RangeQuery() { } - public static implicit operator Query(RangeQuery rangeQuery) => QueryDsl.Query.Range(rangeQuery); + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("range", this); + + public static implicit operator Query(RangeQuery rangeQuery) => Query.Range(rangeQuery); } internal sealed class RangeQueryConverter : JsonConverter diff --git a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/RawJsonQuery.cs b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/RawJsonQuery.cs index 6161fdade14..181c4d5f868 100644 --- a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/RawJsonQuery.cs +++ b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/RawJsonQuery.cs @@ -20,6 +20,8 @@ public sealed class RawJsonQuery : SearchQuery /// public string Raw { get; } + internal override void InternalWrapInContainer(Query container) => new Query("raw_json", this); + public static implicit operator Query(RawJsonQuery rawJsonQuery) => Query.RawJson(rawJsonQuery); } diff --git a/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/SearchQuery.cs b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/SearchQuery.cs new file mode 100644 index 00000000000..55ce44031ba --- /dev/null +++ b/src/Elastic.Clients.Elasticsearch/Types/QueryDsl/SearchQuery.cs @@ -0,0 +1,65 @@ +// 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; + +namespace Elastic.Clients.Elasticsearch.QueryDsl; + +/// +/// A base type for all query variants. +/// +public abstract partial class SearchQuery +{ + //always evaluate to false so that each side of && equation is evaluated + public static bool operator false(SearchQuery _) => false; + + //always evaluate to false so that each side of && equation is evaluated + public static bool operator true(SearchQuery _) => false; + + public static SearchQuery operator &(SearchQuery leftQuery, SearchQuery rightQuery) => Combine(leftQuery, rightQuery, (l, r) => l && r); + + public static SearchQuery? operator |(SearchQuery leftQuery, SearchQuery rightQuery) => Combine(leftQuery, rightQuery, (l, r) => l || r); + + public static SearchQuery? operator !(SearchQuery query) => query is null + ? null + : new BoolQuery { MustNot = new Query[] { query } }; + + public static SearchQuery? operator +(SearchQuery query) => query is null + ? null + : new BoolQuery { Filter = new Query[] { query } }; + + private static SearchQuery Combine(SearchQuery leftQuery, SearchQuery rightQuery, Func combine) + { + if (leftQuery is null && rightQuery is null) + return null; + + if (leftQuery is null) + return rightQuery; + + if (rightQuery is null) + return leftQuery; + + var container = combine(leftQuery, rightQuery); + + if (container.TryGet(out var query)) + { + return new BoolQuery + { + Must = query.Must, + MustNot = query.MustNot, + Should = query.Should, + Filter = query.Filter, + }; + } + + throw new Exception("Unable to combine queries."); + } + + public static implicit operator Query?(SearchQuery query) => query is null ? null : new Query(query); + + // We no longer (currently) support verbatim/strict query containers so this may be simplified to a direct abstract method in the future. + internal void WrapInContainer(Query container) => InternalWrapInContainer(container); + + internal abstract void InternalWrapInContainer(Query container); +} diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/BoolQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/BoolQuery.g.cs index 6ed8e499ff5..de171d22f7b 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/BoolQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/BoolQuery.g.cs @@ -45,6 +45,8 @@ public sealed partial class BoolQuery : SearchQuery public ICollection? Should { get; set; } public static implicit operator Query(BoolQuery boolQuery) => QueryDsl.Query.Bool(boolQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("bool", this); } public sealed partial class BoolQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/BoostingQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/BoostingQuery.g.cs index 49e0ea9bbef..296cc2edf93 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/BoostingQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/BoostingQuery.g.cs @@ -41,6 +41,8 @@ public sealed partial class BoostingQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.Query Positive { get; set; } public static implicit operator Query(BoostingQuery boostingQuery) => QueryDsl.Query.Boosting(boostingQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("boosting", this); } public sealed partial class BoostingQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/CombinedFieldsQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/CombinedFieldsQuery.g.cs index e5962e1d278..048e0550ff5 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/CombinedFieldsQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/CombinedFieldsQuery.g.cs @@ -47,6 +47,8 @@ public sealed partial class CombinedFieldsQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.CombinedFieldsZeroTerms? ZeroTermsQuery { get; set; } public static implicit operator Query(CombinedFieldsQuery combinedFieldsQuery) => QueryDsl.Query.CombinedFields(combinedFieldsQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("combined_fields", this); } public sealed partial class CombinedFieldsQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ConstantScoreQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ConstantScoreQuery.g.cs index fbed7ca01c9..54c1e093e4e 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ConstantScoreQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ConstantScoreQuery.g.cs @@ -37,6 +37,8 @@ public sealed partial class ConstantScoreQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.Query Filter { get; set; } public static implicit operator Query(ConstantScoreQuery constantScoreQuery) => QueryDsl.Query.ConstantScore(constantScoreQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("constant_score", this); } public sealed partial class ConstantScoreQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/DateRangeQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/DateRangeQuery.g.cs index 640838ce7ce..9099daa274a 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/DateRangeQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/DateRangeQuery.g.cs @@ -222,6 +222,8 @@ public DateRangeQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(DateRangeQuery dateRangeQuery) => QueryDsl.Query.Range(dateRangeQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("range", this); } public sealed partial class DateRangeQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/DisMaxQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/DisMaxQuery.g.cs index 80320adec1e..6e704ffb7fa 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/DisMaxQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/DisMaxQuery.g.cs @@ -39,6 +39,8 @@ public sealed partial class DisMaxQuery : SearchQuery public double? TieBreaker { get; set; } public static implicit operator Query(DisMaxQuery disMaxQuery) => QueryDsl.Query.DisMax(disMaxQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("dis_max", this); } public sealed partial class DisMaxQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ExistsQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ExistsQuery.g.cs index 1ba3171d000..8a1ab57e9c2 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ExistsQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ExistsQuery.g.cs @@ -37,6 +37,8 @@ public sealed partial class ExistsQuery : SearchQuery public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(ExistsQuery existsQuery) => QueryDsl.Query.Exists(existsQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("exists", this); } public sealed partial class ExistsQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/FunctionScoreQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/FunctionScoreQuery.g.cs index 8ca090e4320..f302fae4b1f 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/FunctionScoreQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/FunctionScoreQuery.g.cs @@ -47,6 +47,8 @@ public sealed partial class FunctionScoreQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.FunctionScoreMode? ScoreMode { get; set; } public static implicit operator Query(FunctionScoreQuery functionScoreQuery) => QueryDsl.Query.FunctionScore(functionScoreQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("function_score", this); } public sealed partial class FunctionScoreQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/FuzzyQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/FuzzyQuery.g.cs index e23fe8a5a63..b136b2a690a 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/FuzzyQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/FuzzyQuery.g.cs @@ -179,6 +179,8 @@ public FuzzyQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(FuzzyQuery fuzzyQuery) => QueryDsl.Query.Fuzzy(fuzzyQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("fuzzy", this); } public sealed partial class FuzzyQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoBoundingBoxQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoBoundingBoxQuery.g.cs index 4bd839f5bde..797ff271083 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoBoundingBoxQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoBoundingBoxQuery.g.cs @@ -126,6 +126,8 @@ public sealed partial class GeoBoundingBoxQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.GeoValidationMethod? ValidationMethod { get; set; } public static implicit operator Query(GeoBoundingBoxQuery geoBoundingBoxQuery) => QueryDsl.Query.GeoBoundingBox(geoBoundingBoxQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("geo_bounding_box", this); } public sealed partial class GeoBoundingBoxQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoDistanceQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoDistanceQuery.g.cs index 684962732cb..f2672c0d6d0 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoDistanceQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoDistanceQuery.g.cs @@ -139,6 +139,8 @@ public sealed partial class GeoDistanceQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.GeoValidationMethod? ValidationMethod { get; set; } public static implicit operator Query(GeoDistanceQuery geoDistanceQuery) => QueryDsl.Query.GeoDistance(geoDistanceQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("geo_distance", this); } public sealed partial class GeoDistanceQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoPolygonQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoPolygonQuery.g.cs index f73c52efb30..f756906ad13 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoPolygonQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/GeoPolygonQuery.g.cs @@ -126,6 +126,8 @@ public sealed partial class GeoPolygonQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.GeoValidationMethod? ValidationMethod { get; set; } public static implicit operator Query(GeoPolygonQuery geoPolygonQuery) => QueryDsl.Query.GeoPolygon(geoPolygonQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("geo_polygon", this); } public sealed partial class GeoPolygonQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/HasChildQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/HasChildQuery.g.cs index 68ebe40cb44..8c963542ee6 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/HasChildQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/HasChildQuery.g.cs @@ -49,6 +49,8 @@ public sealed partial class HasChildQuery : SearchQuery public string Type { get; set; } public static implicit operator Query(HasChildQuery hasChildQuery) => QueryDsl.Query.HasChild(hasChildQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("has_child", this); } public sealed partial class HasChildQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/HasParentQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/HasParentQuery.g.cs index 5c2e5492e4e..4424beb1aca 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/HasParentQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/HasParentQuery.g.cs @@ -45,6 +45,8 @@ public sealed partial class HasParentQuery : SearchQuery public bool? Score { get; set; } public static implicit operator Query(HasParentQuery hasParentQuery) => QueryDsl.Query.HasParent(hasParentQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("has_parent", this); } public sealed partial class HasParentQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/IdsQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/IdsQuery.g.cs index f5c656ee2c4..45b6d125b35 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/IdsQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/IdsQuery.g.cs @@ -37,6 +37,8 @@ public sealed partial class IdsQuery : SearchQuery public Elastic.Clients.Elasticsearch.Ids? Values { get; set; } public static implicit operator Query(IdsQuery idsQuery) => QueryDsl.Query.Ids(idsQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("ids", this); } public sealed partial class IdsQueryDescriptor : SerializableDescriptor diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/IntervalsQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/IntervalsQuery.g.cs index fc52f05db03..daaf66c1f42 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/IntervalsQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/IntervalsQuery.g.cs @@ -64,6 +64,8 @@ internal IntervalsQuery(Field field, string variantName, object variant) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(IntervalsQuery intervalsQuery) => QueryDsl.Query.Intervals(intervalsQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("intervals", this); } internal sealed partial class IntervalsQueryConverter : JsonConverter diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchAllQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchAllQuery.g.cs index 4126a014f00..c312715eaea 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchAllQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchAllQuery.g.cs @@ -35,6 +35,8 @@ public sealed partial class MatchAllQuery : SearchQuery public float? Boost { get; set; } public static implicit operator Query(MatchAllQuery matchAllQuery) => QueryDsl.Query.MatchAll(matchAllQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("match_all", this); } public sealed partial class MatchAllQueryDescriptor : SerializableDescriptor diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchBoolPrefixQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchBoolPrefixQuery.g.cs index 653a88a20dd..be101535f53 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchBoolPrefixQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchBoolPrefixQuery.g.cs @@ -218,6 +218,8 @@ public MatchBoolPrefixQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(MatchBoolPrefixQuery matchBoolPrefixQuery) => QueryDsl.Query.MatchBoolPrefix(matchBoolPrefixQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("match_bool_prefix", this); } public sealed partial class MatchBoolPrefixQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchNoneQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchNoneQuery.g.cs index a8c0fc38a8c..22d40401af0 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchNoneQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchNoneQuery.g.cs @@ -35,6 +35,8 @@ public sealed partial class MatchNoneQuery : SearchQuery public float? Boost { get; set; } public static implicit operator Query(MatchNoneQuery matchNoneQuery) => QueryDsl.Query.MatchNone(matchNoneQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("match_none", this); } public sealed partial class MatchNoneQueryDescriptor : SerializableDescriptor diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchPhrasePrefixQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchPhrasePrefixQuery.g.cs index 0c74258d258..812396cb38d 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchPhrasePrefixQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchPhrasePrefixQuery.g.cs @@ -166,6 +166,8 @@ public MatchPhrasePrefixQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(MatchPhrasePrefixQuery matchPhrasePrefixQuery) => QueryDsl.Query.MatchPhrasePrefix(matchPhrasePrefixQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("match_phrase_prefix", this); } public sealed partial class MatchPhrasePrefixQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchPhraseQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchPhraseQuery.g.cs index 9630b972ce8..e52fa0cd14f 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchPhraseQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchPhraseQuery.g.cs @@ -153,6 +153,8 @@ public MatchPhraseQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(MatchPhraseQuery matchPhraseQuery) => QueryDsl.Query.MatchPhrase(matchPhraseQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("match_phrase", this); } public sealed partial class MatchPhraseQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchQuery.g.cs index 1bf4b85c287..4334d164f6e 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MatchQuery.g.cs @@ -257,6 +257,8 @@ public MatchQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(MatchQuery matchQuery) => QueryDsl.Query.Match(matchQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("match", this); } public sealed partial class MatchQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MoreLikeThisQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MoreLikeThisQuery.g.cs index 75b684bbf14..ff88b0b19d5 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MoreLikeThisQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MoreLikeThisQuery.g.cs @@ -74,6 +74,8 @@ public sealed partial class MoreLikeThisQuery : SearchQuery public Elastic.Clients.Elasticsearch.VersionType? VersionType { get; set; } public static implicit operator Query(MoreLikeThisQuery moreLikeThisQuery) => QueryDsl.Query.MoreLikeThis(moreLikeThisQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("more_like_this", this); } public sealed partial class MoreLikeThisQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MultiMatchQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MultiMatchQuery.g.cs index da7ea968722..5d5e24caeb3 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MultiMatchQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/MultiMatchQuery.g.cs @@ -67,6 +67,8 @@ public sealed partial class MultiMatchQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.ZeroTermsQuery? ZeroTermsQuery { get; set; } public static implicit operator Query(MultiMatchQuery multiMatchQuery) => QueryDsl.Query.MultiMatch(multiMatchQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("multi_match", this); } public sealed partial class MultiMatchQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/NestedQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/NestedQuery.g.cs index 5b83cc38ea1..2c88e1b3ba3 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/NestedQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/NestedQuery.g.cs @@ -45,6 +45,8 @@ public sealed partial class NestedQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.ChildScoreMode? ScoreMode { get; set; } public static implicit operator Query(NestedQuery nestedQuery) => QueryDsl.Query.Nested(nestedQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("nested", this); } public sealed partial class NestedQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/NumberRangeQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/NumberRangeQuery.g.cs index 21510da8d69..a5ca062b67a 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/NumberRangeQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/NumberRangeQuery.g.cs @@ -196,6 +196,8 @@ public NumberRangeQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(NumberRangeQuery numberRangeQuery) => QueryDsl.Query.Range(numberRangeQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("range", this); } public sealed partial class NumberRangeQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ParentIdQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ParentIdQuery.g.cs index dfd39b0994e..7a7a6bac75d 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ParentIdQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ParentIdQuery.g.cs @@ -41,6 +41,8 @@ public sealed partial class ParentIdQuery : SearchQuery public string? Type { get; set; } public static implicit operator Query(ParentIdQuery parentIdQuery) => QueryDsl.Query.ParentId(parentIdQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("parent_id", this); } public sealed partial class ParentIdQueryDescriptor : SerializableDescriptor diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PercolateQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PercolateQuery.g.cs index 5fd27a12eda..8405c0bd1ce 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PercolateQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PercolateQuery.g.cs @@ -53,6 +53,8 @@ public sealed partial class PercolateQuery : SearchQuery public long? Version { get; set; } public static implicit operator Query(PercolateQuery percolateQuery) => QueryDsl.Query.Percolate(percolateQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("percolate", this); } public sealed partial class PercolateQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PinnedQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PinnedQuery.g.cs index 11aa01d3e47..6cc8ef9b470 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PinnedQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PinnedQuery.g.cs @@ -56,6 +56,8 @@ internal PinnedQuery(string variantName, object variant) public Elastic.Clients.Elasticsearch.QueryDsl.Query Organic { get; set; } public static implicit operator Query(PinnedQuery pinnedQuery) => QueryDsl.Query.Pinned(pinnedQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("pinned", this); } internal sealed partial class PinnedQueryConverter : JsonConverter diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PrefixQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PrefixQuery.g.cs index 26134706ec1..fcf9b00d4da 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PrefixQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/PrefixQuery.g.cs @@ -140,6 +140,8 @@ public PrefixQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(PrefixQuery prefixQuery) => QueryDsl.Query.Prefix(prefixQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("prefix", this); } public sealed partial class PrefixQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/Query.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/Query.g.cs index 99fc0055b3d..7c2ae07304a 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/Query.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/Query.g.cs @@ -30,7 +30,7 @@ namespace Elastic.Clients.Elasticsearch.QueryDsl; [JsonConverter(typeof(QueryConverter))] public sealed partial class Query { - internal Query(string variantName, object variant) + internal Query(string variantName, SearchQuery variant) { if (variantName is null) throw new ArgumentNullException(nameof(variantName)); @@ -42,8 +42,14 @@ internal Query(string variantName, object variant) Variant = variant; } - internal object Variant { get; } - internal string VariantName { get; } + internal object Variant { get; private set; } + internal string VariantName { get; private set; } + + internal void WrapVariant(string variantName, SearchQuery variant) + { + VariantName = variantName; + Variant = variant; + } public static Query Bool(Elastic.Clients.Elasticsearch.QueryDsl.BoolQuery boolQuery) => new Query("bool", boolQuery); public static Query Boosting(Elastic.Clients.Elasticsearch.QueryDsl.BoostingQuery boostingQuery) => new Query("boosting", boostingQuery); diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/QueryStringQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/QueryStringQuery.g.cs index 8ff2a37e588..4d880f23f43 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/QueryStringQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/QueryStringQuery.g.cs @@ -85,6 +85,8 @@ public sealed partial class QueryStringQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.TextQueryType? Type { get; set; } public static implicit operator Query(QueryStringQuery queryStringQuery) => QueryDsl.Query.QueryString(queryStringQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("query_string", this); } public sealed partial class QueryStringQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/RankFeatureQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/RankFeatureQuery.g.cs index 1a7e8847152..3af3c3703fc 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/RankFeatureQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/RankFeatureQuery.g.cs @@ -45,6 +45,8 @@ public sealed partial class RankFeatureQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.RankFeatureFunctionSigmoid? Sigmoid { get; set; } public static implicit operator Query(RankFeatureQuery rankFeatureQuery) => QueryDsl.Query.RankFeature(rankFeatureQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("rank_feature", this); } public sealed partial class RankFeatureQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/RegexpQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/RegexpQuery.g.cs index 750db9bc878..a12af3134b6 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/RegexpQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/RegexpQuery.g.cs @@ -166,6 +166,8 @@ public RegexpQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(RegexpQuery regexpQuery) => QueryDsl.Query.Regexp(regexpQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("regexp", this); } public sealed partial class RegexpQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ScriptQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ScriptQuery.g.cs index 04a7f94197b..ab82f3c8f05 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ScriptQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ScriptQuery.g.cs @@ -37,6 +37,8 @@ public sealed partial class ScriptQuery : SearchQuery public Elastic.Clients.Elasticsearch.Script Script { get; set; } public static implicit operator Query(ScriptQuery scriptQuery) => QueryDsl.Query.Script(scriptQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("script", this); } public sealed partial class ScriptQueryDescriptor : SerializableDescriptor diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ScriptScoreQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ScriptScoreQuery.g.cs index de3e570abb8..8be0131e620 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ScriptScoreQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/ScriptScoreQuery.g.cs @@ -41,6 +41,8 @@ public sealed partial class ScriptScoreQuery : SearchQuery public Elastic.Clients.Elasticsearch.Script Script { get; set; } public static implicit operator Query(ScriptScoreQuery scriptScoreQuery) => QueryDsl.Query.ScriptScore(scriptScoreQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("script_score", this); } public sealed partial class ScriptScoreQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SimpleQueryStringQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SimpleQueryStringQuery.g.cs index 93742be03bc..26479f4cd70 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SimpleQueryStringQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SimpleQueryStringQuery.g.cs @@ -59,6 +59,8 @@ public sealed partial class SimpleQueryStringQuery : SearchQuery public string? QuoteFieldSuffix { get; set; } public static implicit operator Query(SimpleQueryStringQuery simpleQueryStringQuery) => QueryDsl.Query.SimpleQueryString(simpleQueryStringQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("simple_query_string", this); } public sealed partial class SimpleQueryStringQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanContainingQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanContainingQuery.g.cs index a220106e57e..5667eacc824 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanContainingQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanContainingQuery.g.cs @@ -37,6 +37,8 @@ public sealed partial class SpanContainingQuery : SearchQuery public float? Boost { get; set; } [JsonInclude, JsonPropertyName("little")] public Elastic.Clients.Elasticsearch.QueryDsl.SpanQuery Little { get; set; } + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("span_containing", this); } public sealed partial class SpanContainingQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanFieldMaskingQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanFieldMaskingQuery.g.cs index 993bdb17b19..5f1c8d038e1 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanFieldMaskingQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanFieldMaskingQuery.g.cs @@ -37,6 +37,8 @@ public sealed partial class SpanFieldMaskingQuery : SearchQuery public Elastic.Clients.Elasticsearch.Field Field { get; set; } [JsonInclude, JsonPropertyName("query")] public Elastic.Clients.Elasticsearch.QueryDsl.SpanQuery Query { get; set; } + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("field_masking_span", this); } public sealed partial class SpanFieldMaskingQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanFirstQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanFirstQuery.g.cs index 8e299a31c99..8d24b55461e 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanFirstQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanFirstQuery.g.cs @@ -37,6 +37,8 @@ public sealed partial class SpanFirstQuery : SearchQuery public int End { get; set; } [JsonInclude, JsonPropertyName("match")] public Elastic.Clients.Elasticsearch.QueryDsl.SpanQuery Match { get; set; } + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("span_first", this); } public sealed partial class SpanFirstQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanMultiTermQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanMultiTermQuery.g.cs index cce64064e3e..640680611d9 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanMultiTermQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanMultiTermQuery.g.cs @@ -39,6 +39,8 @@ public sealed partial class SpanMultiTermQuery : SearchQuery /// [JsonInclude, JsonPropertyName("match")] public Elastic.Clients.Elasticsearch.QueryDsl.Query Match { get; set; } + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("span_multi", this); } public sealed partial class SpanMultiTermQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanNearQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanNearQuery.g.cs index 99c856fb061..5f4230a430c 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanNearQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanNearQuery.g.cs @@ -39,6 +39,8 @@ public sealed partial class SpanNearQuery : SearchQuery public bool? InOrder { get; set; } [JsonInclude, JsonPropertyName("slop")] public int? Slop { get; set; } + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("span_near", this); } public sealed partial class SpanNearQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanNotQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanNotQuery.g.cs index 9bded3c5647..f8045b9e584 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanNotQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanNotQuery.g.cs @@ -43,6 +43,8 @@ public sealed partial class SpanNotQuery : SearchQuery public int? Post { get; set; } [JsonInclude, JsonPropertyName("pre")] public int? Pre { get; set; } + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("span_not", this); } public sealed partial class SpanNotQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanOrQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanOrQuery.g.cs index 0c7a5d7f134..9161e7ee8c5 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanOrQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanOrQuery.g.cs @@ -35,6 +35,8 @@ public sealed partial class SpanOrQuery : SearchQuery public float? Boost { get; set; } [JsonInclude, JsonPropertyName("clauses")] public ICollection Clauses { get; set; } + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("span_or", this); } public sealed partial class SpanOrQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanQuery.g.cs index 9068a7e28b2..98ed6913ae5 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanQuery.g.cs @@ -28,7 +28,7 @@ namespace Elastic.Clients.Elasticsearch.QueryDsl; [JsonConverter(typeof(SpanQueryConverter))] -public sealed partial class SpanQuery : SearchQuery +public sealed partial class SpanQuery { internal SpanQuery(string variantName, object variant) { diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanTermQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanTermQuery.g.cs index 98d7910acd9..79d53fdfb5e 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanTermQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanTermQuery.g.cs @@ -112,6 +112,8 @@ public SpanTermQuery(Field field) public float? Boost { get; set; } public string Value { get; set; } public Elastic.Clients.Elasticsearch.Field Field { get; set; } + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("span_term", this); } public sealed partial class SpanTermQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanWithinQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanWithinQuery.g.cs index 9bb8c30fd6c..ab62419f9f5 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanWithinQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/SpanWithinQuery.g.cs @@ -37,6 +37,8 @@ public sealed partial class SpanWithinQuery : SearchQuery public float? Boost { get; set; } [JsonInclude, JsonPropertyName("little")] public Elastic.Clients.Elasticsearch.QueryDsl.SpanQuery Little { get; set; } + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("span_within", this); } public sealed partial class SpanWithinQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermQuery.g.cs index df0fa6d17ee..d3e6f1ea747 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermQuery.g.cs @@ -127,6 +127,8 @@ public TermQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(TermQuery termQuery) => QueryDsl.Query.Term(termQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("term", this); } public sealed partial class TermQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermsQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermsQuery.g.cs index 658b2cb5e42..55970412e9d 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermsQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermsQuery.g.cs @@ -100,6 +100,8 @@ public sealed partial class TermsQuery : SearchQuery public Elastic.Clients.Elasticsearch.QueryDsl.TermsQueryField Terms { get; set; } public static implicit operator Query(TermsQuery termsQuery) => QueryDsl.Query.Terms(termsQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("terms", this); } public sealed partial class TermsQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermsSetQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermsSetQuery.g.cs index c754c79c48a..6c42320ff48 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermsSetQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/TermsSetQuery.g.cs @@ -140,6 +140,8 @@ public TermsSetQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(TermsSetQuery termsSetQuery) => QueryDsl.Query.TermsSet(termsSetQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("terms_set", this); } public sealed partial class TermsSetQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/WildcardQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/WildcardQuery.g.cs index 6856ceef3de..a13fd1f26e0 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/WildcardQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/WildcardQuery.g.cs @@ -173,6 +173,8 @@ public WildcardQuery(Field field) public Elastic.Clients.Elasticsearch.Field Field { get; set; } public static implicit operator Query(WildcardQuery wildcardQuery) => QueryDsl.Query.Wildcard(wildcardQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("wildcard", this); } public sealed partial class WildcardQueryDescriptor : SerializableDescriptor> diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/WrapperQuery.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/WrapperQuery.g.cs index dfbf622fe98..92e922a1104 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/WrapperQuery.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/QueryDsl/WrapperQuery.g.cs @@ -41,6 +41,8 @@ public sealed partial class WrapperQuery : SearchQuery public string Query { get; set; } public static implicit operator Query(WrapperQuery wrapperQuery) => QueryDsl.Query.Wrapper(wrapperQuery); + + internal override void InternalWrapInContainer(Query container) => container.WrapVariant("wrapper", this); } public sealed partial class WrapperQueryDescriptor : SerializableDescriptor diff --git a/tests/Tests/QueryDsl/BoolDsl/AndOperatorUsageTests.cs b/tests/Tests/QueryDsl/BoolDsl/AndOperatorUsageTests.cs new file mode 100644 index 00000000000..dd03c4701e3 --- /dev/null +++ b/tests/Tests/QueryDsl/BoolDsl/AndOperatorUsageTests.cs @@ -0,0 +1,351 @@ +// 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; +using System.Linq; +using Elastic.Clients.Elasticsearch.QueryDsl; +using Tests.Domain; + +namespace Tests.QueryDsl.BoolDsl; + +public class AndOperatorUsageTests : OperatorUsageBase +{ + private static readonly int Iterations = 10000; + + [U] + public void AndShouldNotBeViralAnyWayYouComposeIt() + { + Query andQuery = new TermQuery("a") { Value = "a" }; + andQuery &= new TermQuery("b") { Value = "b" }; + + var original = andQuery; + + original.TryGet(out var originalBoolQuery).Should().BeTrue(); + originalBoolQuery.Must.Should().HaveCount(2); + + var result = andQuery && new TermQuery("c") { Value = "c" }; + + result.TryGet(out var resultBoolQuery).Should().BeTrue(); + resultBoolQuery.Must.Should().HaveCount(3); + + original.TryGet(out originalBoolQuery).Should().BeTrue(); + originalBoolQuery.Must.Should().HaveCount(2); + } + + [U] + public void ReturnsBoolWithMust_CombiningTwoQueries() => + ReturnsBool(TermQuery && TermQuery, b => + { + b.Must.Should().NotBeEmpty().And.HaveCount(2); + b.Should.Should().BeNull(); + b.MustNot.Should().BeNull(); + b.Filter.Should().BeNull(); + }); + + [U] + public void ReturnsOriginalTermQuery_WhenOtherQueryIsNull() + { + ReturnsSingleQuery(TermQuery && NullQuery, + c => c.Value.Should().NotBeNull()); + + ReturnsSingleQuery(NullQuery && TermQuery, + c => c.Value.Should().NotBeNull()); + } + + [U] + public void ReturnsOriginalTermQuery_WhenOtherQueriesAreNull() + { + ReturnsSingleQuery(TermQuery && NullQuery && NullQuery, + c => c.Value.Should().NotBeNull()); + + ReturnsSingleQuery(NullQuery && TermQuery && NullQuery && NullQuery, + c => c.Value.Should().NotBeNull()); + } + + [U] + public void ReturnsNull_WhenBothQueriesAreNull() => ReturnsNull(NullQuery && NullQuery); + + [U] + public void CombiningManyUsingAggregate() + { + var lotsOfOrs = Enumerable.Range(0, Iterations).Aggregate((SearchQuery)NullQuery, (q, c) => q && TermQuery, q => q); + AssertLotsOfAnds(lotsOfOrs); + } + + [U] + public void CombiningManyUsingForEachInitializingWithNull() + { + Query container = null; + + foreach (var i in Enumerable.Range(0, Iterations)) + container &= TermQuery; + + AssertLotsOfAnds(container); + } + + [U] + public void CombiningTwoBools() + { + var queries = new Query[] { TermQuery }; + + ReturnsBool( + new BoolQuery { Must = queries, Should = queries } + && new BoolQuery { MustNot = queries, Should = queries } + , b => + { + b.Must.Should().NotBeEmpty().And.HaveCount(2); + + var first = b.Must.First(); + var last = b.Must.Last(); + + first.TryGet(out var firstBool).Should().BeTrue(); + last.TryGet(out var lastBool).Should().BeTrue(); + + firstBool.Should.Should().NotBeEmpty().And.HaveCount(1); + firstBool.Must.Should().NotBeEmpty().And.HaveCount(1); + + lastBool.Should.Should().NotBeEmpty().And.HaveCount(1); + lastBool.MustNot.Should().NotBeEmpty().And.HaveCount(1); + }); + } + + [U] + public void AndIntoBoolWithMustAndShould() + { + var queries = new Query[] { TermQuery }; + + CombineBothWays( + new BoolQuery { Must = queries, Should = queries }, TermQuery + , l => l.TryGet(out _).Should().BeTrue() + , r => r.TryGet(out _).Should().BeTrue() + , b => b.Must.Should().NotBeEmpty().And.HaveCount(2) + ); + } + + [U] + public void AndIntoBoolWithMustAndMustNot() + { + var queries = new Query[] { TermQuery }; + CombineBothWays( + new BoolQuery { Must = queries, MustNot = queries }, TermQuery + , l => l.TryGet(out _).Should().BeTrue() + , r => r.TryGet(out _).Should().BeTrue() + , b => + { + b.Must.Should().NotBeEmpty().And.HaveCount(2); + b.MustNot.Should().NotBeEmpty().And.HaveCount(1); + } + ); + } + + [U] + public void AndIntoBoolWithMust() + { + var queries = new Query[] { TermQuery }; + + CombineBothWays( + new BoolQuery { Must = queries }, TermQuery + , l => l.TryGet(out _).Should().BeTrue() + , r => r.TryGet(out _).Should().BeTrue() + , b => { b.Must.Should().NotBeEmpty().And.HaveCount(2); } + ); + } + + [U] + public void AndIntoBoolWithShould() + { + var queries = new Query[] { TermQuery }; + + CombineBothWays( + new BoolQuery { Should = queries }, TermQuery + , l => + { + l.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should.Should().NotBeNullOrEmpty(); + }, r => r.TryGet(out _).Should().BeTrue() + , b => b.Must.Should().NotBeEmpty().And.HaveCount(2) + ); + } + + [U] + public void AndIntoNamedBool() + { + var queries = new Query[] { TermQuery }; + + CombineBothWays( + new BoolQuery { Should = queries, QueryName = "name" }, TermQuery + , l => + { + l.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should.Should().NotBeNullOrEmpty(); + boolQuery.QueryName.Should().Be("name"); + } + , r => r.TryGet(out _).Should().BeTrue() + , b => b.Must.Should().NotBeEmpty().And.HaveCount(2) + ); + } + + [U] + public void AndAssigningManyBoolShouldQueries() + { + var query = Query.Bool(new BoolQuery + { + Should = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = AndAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Must.Should().OnlyContain(s => s.VariantName == "bool" && (BoolQuery)s.Variant != null && ((BoolQuery)s.Variant).Should != null); + } + + [U] + public void AndAssigningManyBoolMustNotQueries() + { + var query = Query.Bool(new BoolQuery + { + MustNot = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = AndAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.MustNot.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.MustNot.Should().OnlyContain(s => s.VariantName == "term" && (TermQuery)s.Variant != null); + } + + [U] + public void AndAssigningBoolMustQueries() + { + var query = Query.Bool(new BoolQuery + { + Must = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = AndAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Must.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Must.Should().OnlyContain(s => s.VariantName == "term" && (TermQuery)s.Variant != null); + } + + [U] + public void AndAssigningBoolShouldQueriesWithMustClauses() + { + var query = Query.Bool(new BoolQuery + { + Should = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } }, + Must = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = AndAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Must.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Must.Should().OnlyContain(s => s.VariantName == "bool" && + (BoolQuery)s.Variant != null && + ((BoolQuery)s.Variant).Must != null && + ((BoolQuery)s.Variant).Should != null); + } + + [U] + public void AndAssigningBoolShouldQueriesWithMustNotClauses() + { + var query = Query.Bool(new BoolQuery + { + Should = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } }, + MustNot = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = AndAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Must.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Must.Should().OnlyContain(s => s.VariantName == "bool" && + (BoolQuery)s.Variant != null && + ((BoolQuery)s.Variant).MustNot != null && + ((BoolQuery)s.Variant).Should != null); + } + + [U] + public void AndAssigningBoolMustQueriesWithMustNotClauses() + { + var query = Query.Bool(new BoolQuery + { + Must = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } }, + MustNot = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = AndAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Must.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Must.Should().OnlyContain(s => s.VariantName == "term" && (TermQuery)s.Variant != null); + } + + [U] + public void AndAssigningNamedBoolShouldQueries() + { + var query = Query.Bool(new BoolQuery + { + Should = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } }, + QueryName = "name" + }); + + var container = AndAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Must.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Must.Should().OnlyContain(s => s.VariantName == "bool" && + (BoolQuery)s.Variant != null && + ((BoolQuery)s.Variant).Should != null && + ((BoolQuery)s.Variant).QueryName == "name"); + } + + private static void AssertLotsOfAnds(Query lotsOfAnds) + { + lotsOfAnds.Should().NotBeNull(); + lotsOfAnds.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Must.Should().NotBeEmpty().And.HaveCount(Iterations); + } + + protected static void CombineBothWays( + Query ois1, + Query ois2, + Action assertLeft, + Action assertRight, + Action assertContainer = null + ) + { + var oisLeft = ois1 && ois2; + + ReturnsBool(oisLeft, b => + { + var left = b.Must.First(); + var right = b.Must.Last(); + assertLeft(left); + assertRight(right); + assertContainer?.Invoke(b); + }); + + var oisRight = ois2 && ois1; + + ReturnsBool(oisRight, b => + { + var left = b.Must.First(); + var right = b.Must.Last(); + assertRight(left); + assertLeft(right); + assertContainer?.Invoke(b); + }); + } + + private static Query AndAssignManyBoolQueries(Query query) + { + Query container = null; + + Action act = () => + { + for (var i = 0; i < Iterations; i++) + container &= query; + }; + + act.Should().NotThrow(); + + return container; + } +} diff --git a/tests/Tests/QueryDsl/BoolDsl/BoolApiTests.cs b/tests/Tests/QueryDsl/BoolDsl/BoolApiTests.cs new file mode 100644 index 00000000000..6fe3b5a586e --- /dev/null +++ b/tests/Tests/QueryDsl/BoolDsl/BoolApiTests.cs @@ -0,0 +1,115 @@ +// 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; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Elastic.Clients.Elasticsearch.QueryDsl; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Core.Extensions; + +namespace Tests.QueryDsl.BoolDsl; + +public class BoolCluster : ClientTestClusterBase +{ + public enum E + { + Option1, + Option2 + } + + protected override void SeedNode() + { + var client = Client; + var index = client.Indices.Create(Infer.Index(), i => i + .Mappings(m => m + .Properties(props => props + .Keyword(p => p.Option) + ) + ) + ); + + var bulkResponse = client.Bulk(b => b.IndexMany(A.Documents)); + + if (!bulkResponse.IsValidResponse) + throw new Exception("Could not bootstrap bool cluster, bulk was invalid."); + + client.Indices.Refresh(Infer.Indices()); + } + + public class A + { + private static readonly E[] Options = new[] { E.Option1, E.Option2 }; + public static IList Documents => Enumerable.Range(0, 20).Select(i => new A { Id = i + 1, Option = Options[i % 2] }).ToList(); + public int Id { get; set; } + public E Option { get; set; } + } +} +public class BoolsInPractice : IClusterFixture +{ + private readonly BoolCluster _cluster; + + public BoolsInPractice(BoolCluster cluster) => _cluster = cluster; + + private async Task BoolAsync( + Func programmatic, + Query initializerQuery, + int expectedCount + ) + { + var documents = BoolCluster.A.Documents.Where(programmatic).ToList(); + documents.Count.Should().Be(expectedCount, " filtering the documents in memory did not yield the expected count"); + + var client = _cluster.Client; + + var initializer = client.Search(new SearchRequest { Query = initializerQuery }); + var initializerAsync = await client.SearchAsync(new SearchRequest { Query = initializerQuery }); + + var responses = new[] { initializer, initializerAsync }; + foreach (var response in responses) + { + response.ShouldBeValid(); + response.Total.Should().Be(expectedCount); + } + } + + private static TermQuery Id(int id) => new("id") { Value = id }; + + private static TermQuery Option(BoolCluster.E option) => new("option") { Value = FieldValue.Composite(option) }; + + [I] + public async Task CompareBoolQueryTranslationsToRealBooleanLogic() + { + await BoolAsync( + a => a.Id == 1 && a.Option == BoolCluster.E.Option1, + Id(1) && Option(BoolCluster.E.Option1), + 1 + ); + + await BoolAsync( + a => a.Id == 1 || a.Id == 2 || a.Id == 3 || a.Id == 4, + +Id(1) || +Id(2) || +Id(3) || +Id(4), + 4 + ); + + await BoolAsync( + a => a.Id == 1 || a.Id == 2 || a.Id == 3 || a.Id == 4 && (a.Option != BoolCluster.E.Option1 || a.Option == BoolCluster.E.Option2), + +Id(1) || +Id(2) || +Id(3) || +Id(4) && (!Option(BoolCluster.E.Option1) || Option(BoolCluster.E.Option2)), + 4 + ); + + await BoolAsync( + a => a.Id == 1 || a.Id == 2 || a.Id == 3 || a.Id == 4 && a.Option != BoolCluster.E.Option1 || a.Option == BoolCluster.E.Option2, + +Id(1) || +Id(2) || +Id(3) || +Id(4) && !Option(BoolCluster.E.Option1) || Option(BoolCluster.E.Option2), + 12 + ); + + await BoolAsync( + a => a.Option != BoolCluster.E.Option1 && a.Id != 2 && a.Id != 3, + !Option(BoolCluster.E.Option1) && !Id(2) && !+Id(3), + 9 + ); + } +} diff --git a/tests/Tests/QueryDsl/BoolDsl/CombinationUsageTests.cs b/tests/Tests/QueryDsl/BoolDsl/CombinationUsageTests.cs new file mode 100644 index 00000000000..08c539fe45e --- /dev/null +++ b/tests/Tests/QueryDsl/BoolDsl/CombinationUsageTests.cs @@ -0,0 +1,91 @@ +// 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 Tests.QueryDsl.BoolDsl; + +public class CombinationUsageTests : OperatorUsageBase +{ + [U] + public void DoesNotJoinTwoShouldsUsingAnd() => + ReturnsBool( + (TermQuery || TermQuery) && (TermQuery || TermQuery), + b => + { + b.Must.Should().NotBeEmpty().And.HaveCount(2); + b.Should.Should().BeNull(); + b.MustNot.Should().BeNull(); + b.Filter.Should().BeNull(); + }); + [U] + public void DoesJoinTwoShouldsUsingOr() => + ReturnsBool( + TermQuery || TermQuery || (TermQuery || TermQuery), + b => + { + b.Should.Should().NotBeEmpty().And.HaveCount(4); + b.Must.Should().BeNull(); + b.MustNot.Should().BeNull(); + b.Filter.Should().BeNull(); + }); + + [U] + public void DoesNotJoinTwoMustsUsingOr() => + ReturnsBool( + TermQuery && TermQuery || TermQuery && TermQuery, + b => + { + b.Should.Should().NotBeEmpty().And.HaveCount(2); + b.Must.Should().BeNull(); + b.MustNot.Should().BeNull(); + b.Filter.Should().BeNull(); + }); + + [U] + public void DoesJoinTwoMustsUsingAnd() => + ReturnsBool( + TermQuery && TermQuery && (TermQuery && TermQuery), + b => + { + b.Must.Should().NotBeEmpty().And.HaveCount(4); + b.Should.Should().BeNull(); + b.MustNot.Should().BeNull(); + b.Filter.Should().BeNull(); + }); + + [U] + public void AndJoinsMustNot() => + ReturnsBool( + TermQuery && !TermQuery, + b => + { + b.Must.Should().NotBeEmpty().And.HaveCount(1); + b.MustNot.Should().NotBeEmpty().And.HaveCount(1); + }); + + [U] + public void OrDoesNotJoinMustNot() => + ReturnsBool( + TermQuery || !TermQuery, + b => { b.Should.Should().NotBeEmpty().And.HaveCount(2); }); + + [U] + public void OrDoesNotJoinFilter() => + ReturnsBool( + TermQuery || !TermQuery, + b => + { + b.Should.Should().NotBeEmpty().And.HaveCount(2); + b.Filter.Should().BeNull(); + }); + + [U] + public void AndJoinsFilter() => + ReturnsBool( + TermQuery && +TermQuery, + b => + { + b.Must.Should().NotBeEmpty().And.HaveCount(1); + b.Filter.Should().NotBeEmpty().And.HaveCount(1); + }); +} diff --git a/tests/Tests/QueryDsl/BoolDsl/NotOperatorUsageTests.cs b/tests/Tests/QueryDsl/BoolDsl/NotOperatorUsageTests.cs new file mode 100644 index 00000000000..201dc6ed35b --- /dev/null +++ b/tests/Tests/QueryDsl/BoolDsl/NotOperatorUsageTests.cs @@ -0,0 +1,87 @@ +// 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.Linq; +using Elastic.Clients.Elasticsearch.QueryDsl; + +namespace Tests.QueryDsl.BoolDsl; + +public class NotOperatorUsageTests : OperatorUsageBase +{ + private static readonly int Iterations = 10000; + + [U] + public void ReturnsBoolWithMustNot_CombiningTwoQueries() => + ReturnsBool(!TermQuery && !TermQuery, b => + { + b.MustNot.Should().NotBeEmpty().And.HaveCount(2); + b.Must.Should().BeNull(); + b.Should.Should().BeNull(); + b.Filter.Should().BeNull(); + }); + + [U] + public void ReturnsBoolWithShould_CombiningTwoQueries() => + ReturnsBool(!TermQuery || !TermQuery, b => + { + b.Should.Should().NotBeEmpty().And.HaveCount(2); + b.Must.Should().BeNull(); + b.MustNot.Should().BeNull(); + b.Filter.Should().BeNull(); + + foreach (var query in b.Should) + { + query.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.MustNot.Should().NotBeEmpty().And.HaveCount(1); + } + }); + + [U] + public void ReturnsBoolQuery_WhenOtherQueryIsNull() + { + ReturnsSingleQuery(!TermQuery || !NullQuery, + c => c.MustNot.Should().NotBeNull().And.HaveCount(1)); + + ReturnsSingleQuery(!NullQuery || !TermQuery, + c => c.MustNot.Should().NotBeNull().And.HaveCount(1)); + } + + [U] + public void ReturnsBoolQuery_WhenOtherQueriesAreNull() + { + ReturnsSingleQuery(!TermQuery || !NullQuery || !NullQuery, + c => c.MustNot.Should().NotBeNull().And.HaveCount(1)); + + ReturnsSingleQuery(!NullQuery || !TermQuery || !NullQuery || !NullQuery, + c => c.MustNot.Should().NotBeNull().And.HaveCount(1)); + } + + [U] + public void ReturnsNull_WhenBothQueriesAreNull() => ReturnsNull(!NullQuery && !NullQuery); + + [U] + public void CombiningManyUsingAggregate() + { + var lotsOfNots = Enumerable.Range(0, Iterations).Aggregate(!NullQuery, (q, c) => q && !TermQuery, q => q); + AssertLotsOfNots(lotsOfNots); + } + + [U] + public void CombiningManyUsingForEachInitializingWithNull() + { + Query container = null; + + foreach (var i in Enumerable.Range(0, Iterations)) + container &= !TermQuery; + + AssertLotsOfNots(container); + } + + private static void AssertLotsOfNots(Query lotsOfNots) + { + lotsOfNots.Should().NotBeNull(); + lotsOfNots.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.MustNot.Should().NotBeEmpty().And.HaveCount(Iterations); + } +} diff --git a/tests/Tests/QueryDsl/BoolDsl/OperatorUsageBase.cs b/tests/Tests/QueryDsl/BoolDsl/OperatorUsageBase.cs new file mode 100644 index 00000000000..34c25690561 --- /dev/null +++ b/tests/Tests/QueryDsl/BoolDsl/OperatorUsageBase.cs @@ -0,0 +1,35 @@ +// 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; +using Elastic.Clients.Elasticsearch.QueryDsl; + +namespace Tests.QueryDsl.BoolDsl; + +public abstract class OperatorUsageBase +{ + protected static readonly TermQuery NullQuery = null; + protected static readonly TermQuery TermQuery = new("x") { Value = "y" }; + + protected static void ReturnsNull(Query combined) + { + combined.Should().BeNull(); + } + + protected static void ReturnsBool(Query combined, Action boolQueryAssert) + { + combined.Should().NotBeNull(); + combined.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should().NotBeNull(); + boolQueryAssert(boolQuery); + } + + protected static void ReturnsSingleQuery(Query combined, Action queryAssert) where T : SearchQuery + { + combined.Should().NotBeNull(); + combined.TryGet(out var query).Should().BeTrue(); + query.Should().NotBeNull(); + queryAssert(query); + } +} diff --git a/tests/Tests/QueryDsl/BoolDsl/OrOperatorUsageTests.cs b/tests/Tests/QueryDsl/BoolDsl/OrOperatorUsageTests.cs new file mode 100644 index 00000000000..7da81465bbc --- /dev/null +++ b/tests/Tests/QueryDsl/BoolDsl/OrOperatorUsageTests.cs @@ -0,0 +1,330 @@ +// 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; +using System.Linq; +using Elastic.Clients.Elasticsearch.QueryDsl; +using Tests.Domain; + +namespace Tests.QueryDsl.BoolDsl; + +public class OrOperatorUsageTests : OperatorUsageBase +{ + private static readonly int Iterations = 10000; + + [U] + public void ReturnsBoolWithShould_CombiningTwoQueries() => ReturnsBool(TermQuery || TermQuery, b => + { + b.Should.Should().NotBeEmpty().And.HaveCount(2); + b.Must.Should().BeNull(); + b.MustNot.Should().BeNull(); + b.Filter.Should().BeNull(); + }); + + [U] + public void ReturnsOriginalTermQuery_WhenOtherQueryIsNull() + { + ReturnsSingleQuery(TermQuery || NullQuery, + c => c.Value.Should().NotBeNull()); + + ReturnsSingleQuery(NullQuery || TermQuery, + c => c.Value.Should().NotBeNull()); + } + + [U] + public void ReturnsOriginalTermQuery_WhenOtherQueriesAreNull() + { + ReturnsSingleQuery(TermQuery || NullQuery || NullQuery, + c => c.Value.Should().NotBeNull()); + + ReturnsSingleQuery(NullQuery || TermQuery || NullQuery || NullQuery, + c => c.Value.Should().NotBeNull()); + } + + [U] + public void ReturnsNull_WhenBothQueriesAreNull() => ReturnsNull(NullQuery || NullQuery); + + [U] + public void CombiningManyUsingAggregate() + { + var lotsOfOrs = Enumerable.Range(0, Iterations).Aggregate((SearchQuery)NullQuery, (q, c) => q || TermQuery, q => q); + AssertLotsOfOrs(lotsOfOrs); + } + + [U] + public void CombiningManyUsingForEachInitializingWithNull() + { + Query container = null; + + foreach (var i in Enumerable.Range(0, Iterations)) + container |= TermQuery; + + AssertLotsOfOrs(container); + } + + [U] + public void CombiningTwoBools() + { + var queries = new Query[] { TermQuery }; + + ReturnsBool( + new BoolQuery { Must = queries, Should = queries } + || new BoolQuery { MustNot = queries, Should = queries } + , b => + { + b.Should.Should().NotBeEmpty().And.HaveCount(2); + + var first = b.Should.First(); + var last = b.Should.Last(); + + first.TryGet(out var firstBool).Should().BeTrue(); + last.TryGet(out var lastBool).Should().BeTrue(); + + firstBool.Should.Should().NotBeEmpty().And.HaveCount(1); + firstBool.Must.Should().NotBeEmpty().And.HaveCount(1); + + lastBool.Should.Should().NotBeEmpty().And.HaveCount(1); + lastBool.MustNot.Should().NotBeEmpty().And.HaveCount(1); + }); + } + + [U] + public void OrIntoBoolWithMustAndShould() + { + var queries = new Query[] { TermQuery }; + + CombineBothWays( + new BoolQuery { Must = queries, Should = queries }, TermQuery + , l => l.TryGet(out _).Should().BeTrue() + , r => r.TryGet(out _).Should().BeTrue() + , b => b.Should.Should().NotBeEmpty().And.HaveCount(2) + ); + } + + [U] + public void OrIntoBoolWithMustAndMustNot() + { + var queries = new Query[] { TermQuery }; + CombineBothWays( + new BoolQuery { Must = queries, MustNot = queries }, TermQuery + , l => l.TryGet(out _).Should().BeTrue() + , r => r.TryGet(out _).Should().BeTrue() + , b => { b.Should.Should().NotBeEmpty().And.HaveCount(2); } + ); + } + + [U] + public void OrIntoBoolWithMust() + { + var queries = new Query[] { TermQuery }; + + CombineBothWays( + new BoolQuery { Must = queries }, TermQuery + , l => l.TryGet(out _).Should().BeTrue() + , r => r.TryGet(out _).Should().BeTrue() + , b => { b.Should.Should().NotBeEmpty().And.HaveCount(2); } + ); + } + + [U] + public void OrIntoBoolWithShould() + { + var queries = new Query[] { TermQuery }; + + CombineBothWays( + new BoolQuery { Should = queries }, TermQuery + , l => l.TryGet(out _).Should().BeTrue() + , r => r.TryGet(out _).Should().BeTrue() + , b => b.Should.Should().NotBeEmpty().And.HaveCount(2) + ); + } + + [U] + public void OrIntoNamedBool() + { + var queries = new Query[] { TermQuery }; + + CombineBothWays( + new BoolQuery { Should = queries, QueryName = "name" }, TermQuery + , l => + { + l.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should.Should().NotBeNullOrEmpty(); + boolQuery.QueryName.Should().Be("name"); + } + , r => r.TryGet(out _).Should().BeTrue() + , b => b.Should.Should().NotBeEmpty().And.HaveCount(2) + ); + } + + [U] + public void OrAssigningManyBoolMustQueries() + { + var query = Query.Bool(new BoolQuery + { + Must = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = OrAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Should.Should().OnlyContain(s => s.VariantName == "bool" && (BoolQuery)s.Variant != null && ((BoolQuery)s.Variant).Must != null); + } + + [U] + public void OrAssigningManyBoolMustNotQueries() + { + var query = Query.Bool(new BoolQuery + { + MustNot = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = OrAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Should.Should().OnlyContain(s => s.VariantName == "bool" && (BoolQuery)s.Variant != null && ((BoolQuery)s.Variant).MustNot != null); + } + + [U] + public void OrAssigningBoolShouldQueries() + { + // |= assigning many bool queries with only should clauses flattens even further to a single bool with only term queries in the should + + var query = Query.Bool(new BoolQuery + { + Should = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = OrAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Should.Should().OnlyContain(s => s.VariantName == "term" && (TermQuery)s.Variant != null); + } + + [U] + public void OrAssigningBoolShouldQueriesWithMustClauses() + { + var query = Query.Bool(new BoolQuery + { + Should = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } }, + Must = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = OrAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Should.Should().OnlyContain(s => s.VariantName == "bool" && + (BoolQuery)s.Variant != null && + ((BoolQuery)s.Variant).Must != null && + ((BoolQuery)s.Variant).Should != null); + } + + [U] + public void OrAssigningBoolShouldQueriesWithMustNotClauses() + { + var query = Query.Bool(new BoolQuery + { + Should = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } }, + MustNot = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = OrAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Should.Should().OnlyContain(s => s.VariantName == "bool" && + (BoolQuery)s.Variant != null && + ((BoolQuery)s.Variant).MustNot != null && + ((BoolQuery)s.Variant).Should != null); + } + + [U] + public void OrAssigningBoolMustQueriesWithMustNotClauses() + { + var query = Query.Bool(new BoolQuery + { + Must = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } }, + MustNot = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } } + }); + + var container = OrAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Should.Should().OnlyContain(s => s.VariantName == "bool" && + (BoolQuery)s.Variant != null && + ((BoolQuery)s.Variant).MustNot != null && + ((BoolQuery)s.Variant).Must != null); + } + + [U] + public void OrAssigningNamedBoolShouldQueries() + { + var query = Query.Bool(new BoolQuery + { + Should = new Query[] { new TermQuery(Infer.Field(f => f.Name)) { Value = "foo" } }, + QueryName = "name" + }); + + var container = OrAssignManyBoolQueries(query); + container.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should.Should().NotBeNullOrEmpty().And.HaveCount(Iterations); + boolQuery.Should.Should().OnlyContain(s => s.VariantName == "bool" && + (BoolQuery)s.Variant != null && + ((BoolQuery)s.Variant).Should != null && + ((BoolQuery)s.Variant).QueryName == "name"); + } + + private static Query OrAssignManyBoolQueries(Query query) + { + Query container = null; + + Action act = () => + { + for (var i = 0; i < Iterations; i++) + container |= query; + }; + + act.Should().NotThrow(); + AssertLotsOfOrs(container); + + return container; + } + + private static void AssertLotsOfOrs(Query lotsOfOrs) + { + lotsOfOrs.Should().NotBeNull(); + lotsOfOrs.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Should.Should().NotBeEmpty().And.HaveCount(Iterations); + } + + protected static void CombineBothWays( + Query ois1, + Query ois2, + Action assertLeft, + Action assertRight, + Action assertContainer = null + ) + { + var oisLeft = ois1 || ois2; + + ReturnsBool(oisLeft, b => + { + var left = b.Should.First(); + var right = b.Should.Last(); + assertLeft(left); + assertRight(right); + assertContainer?.Invoke(b); + }); + + var oisRight = ois2 || ois1; + + ReturnsBool(oisRight, b => + { + var left = b.Should.First(); + var right = b.Should.Last(); + assertRight(left); + assertLeft(right); + assertContainer?.Invoke(b); + }); + } +} diff --git a/tests/Tests/QueryDsl/BoolDsl/QueryOperatorSerializationTests.cs b/tests/Tests/QueryDsl/BoolDsl/QueryOperatorSerializationTests.cs new file mode 100644 index 00000000000..4462582679d --- /dev/null +++ b/tests/Tests/QueryDsl/BoolDsl/QueryOperatorSerializationTests.cs @@ -0,0 +1,281 @@ +// 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.Threading.Tasks; +using Elastic.Clients.Elasticsearch.QueryDsl; +using Tests.Domain; +using Tests.Serialization; +using VerifyXunit; + +namespace Tests.QueryDsl.BoolDsl; + +[UsesVerify] +public class QueryOperatorSerializationTests : SerializerTestBase +{ + [U] + public async Task SearchQueriesCombinedWithOrOperator_SerializeAsShouldClauses() + { + var search = new SearchRequest + { + Query = new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } || + new TermQuery(Infer.Field(p => p.Name)) { Value = "y" } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task SearchQueriesCombinedWithAndOperator_SerializeAsMustClauses() + { + var search = new SearchRequest + { + Query = new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } && + new TermQuery(Infer.Field(p => p.Name)) { Value = "y" } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task MultipleSearchQueriesCombinedWithAndOperator_SerializeAsManyMustClauses() + { + var search = new SearchRequest + { + Query = new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } && + new TermQuery(Infer.Field(p => p.Name)) { Value = "y" } && + new TermQuery(Infer.Field(p => p.Name)) { Value = "z" } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task SearchQueryWithUnaryNegationOperator_SerializeAsMustNotClause() + { + var search = new SearchRequest + { + Query = !new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task SearchQueryWithUnaryNegationOperator_CombinedWithAnd_SerializesCorrectly() + { + var search = new SearchRequest + { + Query = !new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } && + !new TermQuery(Infer.Field(p => p.Name)) { Value = "y" } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task SearchQueryWithUnaryAddOperator_SerializeAsFilterClause() + { + var search = new SearchRequest + { + Query = +new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task SearchQueryWithUnaryAddOperator_CombinedWithAnd_SerializesCorrectly() + { + var search = new SearchRequest + { + Query = +new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } && + +new TermQuery(Infer.Field(p => p.Name)) { Value = "y" } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task SearchQueryWithMustAndMustNot_SerializesCorrectly() + { + var search = new SearchRequest + { + Query = new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } && + new TermQuery(Infer.Field(p => p.Name)) { Value = "y" } && + !new TermQuery(Infer.Field(p => p.Name)) { Value = "z" } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task SearchQueryWithMustAndMustNotAndFilter_SerializesCorrectly() + { + var search = new SearchRequest + { + Query = new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } && + new TermQuery(Infer.Field(p => p.Name)) { Value = "y" } && + !new TermQuery(Infer.Field(p => p.Name)) { Value = "z" } && + +new TermQuery(Infer.Field(p => p.Name)) { Value = "a" } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task SearchQueryMixAndMatch_SerializesCorrectly() + { + var search = new SearchRequest + { + Query = new BoolQuery { Must = new Query[] + { + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" }, + new TermQuery(Infer.Field(p => p.Name)) { Value = "y" }, + new TermQuery(Infer.Field(p => p.Name)) { Value = "z" } } } && + !new TermQuery(Infer.Field(p => p.Name)) { Value = "a" } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task SearchQueryJoinsWithShouldClauses_SerializesCorrectly() + { + var search = new SearchRequest + { + Query = new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } && + ( + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } || + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } || + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } + ) + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task SearchQueryMixAndMatchMinimumShouldMatch_SerializesCorrectly() + { + var search = new SearchRequest + { + Query = new BoolQuery + { + Should = new Query[] + { + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" }, + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" }, + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" }, + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } + }, + MinimumShouldMatch = 2 + } || !new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } || new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task DoNotCombineLockedBools() + { + var search = new SearchRequest + { + Query = new BoolQuery + { + Should = new Query[] + { + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } + }, + QueryName = "leftBool" + } || new BoolQuery + { + Should = new Query[] + { + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } + }, + QueryName = "rightBool" + } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task DoNotCombineRightLockedBool() + { + var search = new SearchRequest + { + Query = new BoolQuery + { + Should = new Query[] + { + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } + } + } || new BoolQuery + { + Should = new Query[] + { + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } + }, + QueryName = "rightBool" + } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } + + [U] + public async Task DoNotCombineLeftLockedBool() + { + var search = new SearchRequest + { + Query = new BoolQuery + { + Should = new Query[] + { + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } + }, + QueryName = "leftBool" + } || new BoolQuery + { + Should = new Query[] + { + new TermQuery(Infer.Field(p => p.Name)) { Value = "x" } + } + } + }; + + var serialisedJson = await SerializeAndGetJsonStringAsync(search); + + await Verifier.VerifyJson(serialisedJson); + } +} diff --git a/tests/Tests/QueryDsl/BoolDsl/UnaryAddOperatorUsageTests.cs b/tests/Tests/QueryDsl/BoolDsl/UnaryAddOperatorUsageTests.cs new file mode 100644 index 00000000000..f36ce6eb26f --- /dev/null +++ b/tests/Tests/QueryDsl/BoolDsl/UnaryAddOperatorUsageTests.cs @@ -0,0 +1,87 @@ +// 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.Linq; +using Elastic.Clients.Elasticsearch.QueryDsl; + +namespace Tests.QueryDsl.BoolDsl; + +public class UnaryAddOperatorUsageTests : OperatorUsageBase +{ + private static readonly int Iterations = 10000; + + [U] + public void ReturnsBoolWithFilter_CombiningTwoQueries() => + ReturnsBool(+TermQuery && +TermQuery, b => + { + b.Filter.Should().NotBeEmpty().And.HaveCount(2); + b.Must.Should().BeNull(); + b.Should.Should().BeNull(); + b.MustNot.Should().BeNull(); + }); + + [U] + public void ReturnsBoolWithShould_CombiningTwoQueries() => + ReturnsBool(+TermQuery || +TermQuery, b => + { + b.Should.Should().NotBeEmpty().And.HaveCount(2); + b.Must.Should().BeNull(); + b.MustNot.Should().BeNull(); + b.Filter.Should().BeNull(); + + foreach (var query in b.Should) + { + query.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Filter.Should().NotBeEmpty().And.HaveCount(1); + } + }); + + [U] + public void ReturnsBoolQuery_WhenOtherQueryIsNull() + { + ReturnsSingleQuery(+TermQuery || +NullQuery, + c => c.Filter.Should().NotBeNull().And.HaveCount(1)); + + ReturnsSingleQuery(+NullQuery || +TermQuery, + c => c.Filter.Should().NotBeNull().And.HaveCount(1)); + } + + [U] + public void ReturnsBoolQuery_WhenOtherQueriesAreNull() + { + ReturnsSingleQuery(+TermQuery || +NullQuery || +NullQuery, + c => c.Filter.Should().NotBeNull().And.HaveCount(1)); + + ReturnsSingleQuery(+NullQuery || +TermQuery || +NullQuery || +NullQuery, + c => c.Filter.Should().NotBeNull().And.HaveCount(1)); + } + + [U] + public void ReturnsNull_WhenBothQueriesAreNull() => ReturnsNull(+NullQuery && +NullQuery); + + [U] + public void CombiningManyUsingAggregate() + { + var lotsOfUnaryAdds = Enumerable.Range(0, Iterations).Aggregate(!NullQuery, (q, c) => q && +TermQuery, q => q); + AssertLotsOfUnaryAdds(lotsOfUnaryAdds); + } + + [U] + public void CombiningManyUsingForEachInitializingWithNull() + { + Query container = null; + + foreach (var i in Enumerable.Range(0, Iterations)) + container &= +TermQuery; + + AssertLotsOfUnaryAdds(container); + } + + private static void AssertLotsOfUnaryAdds(Query lotsOfNots) + { + lotsOfNots.Should().NotBeNull(); + lotsOfNots.TryGet(out var boolQuery).Should().BeTrue(); + boolQuery.Filter.Should().NotBeEmpty().And.HaveCount(Iterations); + } +} diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.AndShouldNotBeViralAnyWayYouComposeIt.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.AndShouldNotBeViralAnyWayYouComposeIt.verified.txt new file mode 100644 index 00000000000..5f282702bb0 --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.AndShouldNotBeViralAnyWayYouComposeIt.verified.txt @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineLeftLockedBool.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineLeftLockedBool.verified.txt new file mode 100644 index 00000000000..c322ec98bf4 --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineLeftLockedBool.verified.txt @@ -0,0 +1,27 @@ +{ + query: { + bool: { + should: [ + { + term: { + name: { + value: x + } + } + }, + { + bool: { + should: { + term: { + name: { + value: x + } + } + }, + _name: leftBool + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineLockedBools.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineLockedBools.verified.txt new file mode 100644 index 00000000000..3c76fa2b45e --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineLockedBools.verified.txt @@ -0,0 +1,32 @@ +{ + query: { + bool: { + should: [ + { + bool: { + should: { + term: { + name: { + value: x + } + } + }, + _name: leftBool + } + }, + { + bool: { + should: { + term: { + name: { + value: x + } + } + }, + _name: rightBool + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineRightLockedBool.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineRightLockedBool.verified.txt new file mode 100644 index 00000000000..d96523e833f --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.DoNotCombineRightLockedBool.verified.txt @@ -0,0 +1,27 @@ +{ + query: { + bool: { + should: [ + { + term: { + name: { + value: x + } + } + }, + { + bool: { + should: { + term: { + name: { + value: x + } + } + }, + _name: rightBool + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.MultipleSearchQueriesCombinedWithAndOperator_SerializeAsManyMustClauses.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.MultipleSearchQueriesCombinedWithAndOperator_SerializeAsManyMustClauses.verified.txt new file mode 100644 index 00000000000..f1461c1ae6f --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.MultipleSearchQueriesCombinedWithAndOperator_SerializeAsManyMustClauses.verified.txt @@ -0,0 +1,29 @@ +{ + query: { + bool: { + must: [ + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: y + } + } + }, + { + term: { + name: { + value: z + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueriesCombinedWithAndOperator_SerializeAsMustClauses.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueriesCombinedWithAndOperator_SerializeAsMustClauses.verified.txt new file mode 100644 index 00000000000..e5932cb0a31 --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueriesCombinedWithAndOperator_SerializeAsMustClauses.verified.txt @@ -0,0 +1,22 @@ +{ + query: { + bool: { + must: [ + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: y + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueriesCombinedWithOrOperator_SerializeAsShouldClauses.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueriesCombinedWithOrOperator_SerializeAsShouldClauses.verified.txt new file mode 100644 index 00000000000..7e451a1ff05 --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueriesCombinedWithOrOperator_SerializeAsShouldClauses.verified.txt @@ -0,0 +1,22 @@ +{ + query: { + bool: { + should: [ + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: y + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryJoinsWithShouldClauses_SerializesCorrectly.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryJoinsWithShouldClauses_SerializesCorrectly.verified.txt new file mode 100644 index 00000000000..c9a64329a86 --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryJoinsWithShouldClauses_SerializesCorrectly.verified.txt @@ -0,0 +1,42 @@ +{ + query: { + bool: { + must: [ + { + term: { + name: { + value: x + } + } + }, + { + bool: { + should: [ + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: x + } + } + } + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryMixAndMatchMinimumShouldMatch_SerializesCorrectly.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryMixAndMatchMinimumShouldMatch_SerializesCorrectly.verified.txt new file mode 100644 index 00000000000..555dc6dfc7b --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryMixAndMatchMinimumShouldMatch_SerializesCorrectly.verified.txt @@ -0,0 +1,61 @@ +{ + query: { + bool: { + should: [ + { + bool: { + minimum_should_match: 2, + should: [ + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: x + } + } + } + ] + } + }, + { + bool: { + must_not: { + term: { + name: { + value: x + } + } + } + } + }, + { + term: { + name: { + value: x + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryMixAndMatch_SerializesCorrectly.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryMixAndMatch_SerializesCorrectly.verified.txt new file mode 100644 index 00000000000..eb9ad937485 --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryMixAndMatch_SerializesCorrectly.verified.txt @@ -0,0 +1,36 @@ +{ + query: { + bool: { + must: [ + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: y + } + } + }, + { + term: { + name: { + value: z + } + } + } + ], + must_not: { + term: { + name: { + value: a + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithMustAndMustNotAndFilter_SerializesCorrectly.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithMustAndMustNotAndFilter_SerializesCorrectly.verified.txt new file mode 100644 index 00000000000..6a39a35ad3d --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithMustAndMustNotAndFilter_SerializesCorrectly.verified.txt @@ -0,0 +1,36 @@ +{ + query: { + bool: { + filter: { + term: { + name: { + value: a + } + } + }, + must: [ + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: y + } + } + } + ], + must_not: { + term: { + name: { + value: z + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithMustAndMustNot_SerializesCorrectly.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithMustAndMustNot_SerializesCorrectly.verified.txt new file mode 100644 index 00000000000..30069c62d83 --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithMustAndMustNot_SerializesCorrectly.verified.txt @@ -0,0 +1,29 @@ +{ + query: { + bool: { + must: [ + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: y + } + } + } + ], + must_not: { + term: { + name: { + value: z + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryAddOperator_CombinedWithAnd_SerializesCorrectly.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryAddOperator_CombinedWithAnd_SerializesCorrectly.verified.txt new file mode 100644 index 00000000000..aca05218f9a --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryAddOperator_CombinedWithAnd_SerializesCorrectly.verified.txt @@ -0,0 +1,22 @@ +{ + query: { + bool: { + filter: [ + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: y + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryAddOperator_SerializeAsFilterClause.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryAddOperator_SerializeAsFilterClause.verified.txt new file mode 100644 index 00000000000..058fed4828c --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryAddOperator_SerializeAsFilterClause.verified.txt @@ -0,0 +1,13 @@ +{ + query: { + bool: { + filter: { + term: { + name: { + value: x + } + } + } + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryNegationOperator_CombinedWithAnd_SerializesCorrectly.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryNegationOperator_CombinedWithAnd_SerializesCorrectly.verified.txt new file mode 100644 index 00000000000..3f24d72863f --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryNegationOperator_CombinedWithAnd_SerializesCorrectly.verified.txt @@ -0,0 +1,22 @@ +{ + query: { + bool: { + must_not: [ + { + term: { + name: { + value: x + } + } + }, + { + term: { + name: { + value: y + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryNegationOperator_SerializeAsMustNotClause.verified.txt b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryNegationOperator_SerializeAsMustNotClause.verified.txt new file mode 100644 index 00000000000..d894e3b26ae --- /dev/null +++ b/tests/Tests/_VerifySnapshots/QueryOperatorSerializationTests.SearchQueryWithUnaryNegationOperator_SerializeAsMustNotClause.verified.txt @@ -0,0 +1,13 @@ +{ + query: { + bool: { + must_not: { + term: { + name: { + value: x + } + } + } + } + } +} \ No newline at end of file