From b4b8fa7e4e78a4b7dda81a6ecf95ff78df27a7fe Mon Sep 17 00:00:00 2001 From: veeraagandhi Date: Thu, 31 Jul 2025 17:25:05 +0530 Subject: [PATCH 1/7] Enhances aggregation handling in SearchDescriptorExtensions and CombineAggregationsVisitor; Fixed NestedAggregation_WithIncludeCommaSeparatedValues_FiltersCorrectly test of ElasticNestedQueryParserTests --- .../Extensions/SearchDescriptorExtensions.cs | 57 +++++++++++++-- .../Visitors/CombineAggregationsVisitor.cs | 73 +++++++++++++++---- .../ElasticNestedQueryParserTests.cs | 40 ++++++++-- 3 files changed, 145 insertions(+), 25 deletions(-) diff --git a/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs b/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs index f462b798..e63c95e6 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Nest; namespace Foundatio.Parsers.ElasticQueries.Extensions; @@ -7,13 +8,59 @@ public static class SearchDescriptorExtensions { public static SearchDescriptor Aggregations(this SearchDescriptor descriptor, AggregationContainer aggregations) where T : class { - descriptor.Aggregations(f => + return descriptor.Aggregations(f => CopyAggregations(f, aggregations.Aggregations)); + } + + public static AggregationContainerDescriptor CopyAggregations( + AggregationContainerDescriptor target, + IDictionary sourceAggregations + ) where T : class + { + foreach (var kvp in sourceAggregations) { - ((IAggregationContainer)f).Aggregations = aggregations.Aggregations; - return f; - }); + string name = kvp.Key; + var agg = kvp.Value; - return descriptor; + if (agg.Nested != null) + { + // Nested aggregation: copy nested path and inner aggregations recursively + target.Nested(name, n => n + .Path(agg.Nested.Path) + .Aggregations(a => CopyAggregations(a, agg.Nested.Aggregations))); + } + else if (agg.Terms != null) + { + target.Terms(name, t => + { + // Copy field + if (agg.Terms.Field != null) + t.Field(agg.Terms.Field); + + // Copy exclude + if (agg.Terms.Exclude != null) + { + if (agg.Terms.Exclude.Values != null && agg.Terms.Exclude.Values.Count() > 0) + { + t.Exclude([.. agg.Terms.Exclude.Values]); + } + } + + // Copy Meta if present + if (agg.Meta != null) + { + t.Meta(d => { + foreach (var meta in agg.Terms.Meta) + d.Add(meta.Key, meta.Value); + return d; + }); + } + + return t; + }); + } + } + + return target; } public static SearchDescriptor Sort(this SearchDescriptor descriptor, IEnumerable sorts) where T : class diff --git a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs index 081ab423..d66069a1 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs @@ -22,6 +22,9 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte var container = await GetParentContainerAsync(node, context); var termsAggregation = container as ITermsAggregation; + var parentBucket = container as BucketAggregationBase; + var childAggregations = new Dictionary(); + foreach (var child in node.Children.OfType()) { var aggregation = await child.GetAggregationAsync(() => child.GetDefaultAggregationAsync(context)); @@ -30,8 +33,7 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte var termNode = child as TermNode; if (termNode != null && termsAggregation != null) { - // Look into this... - // TODO: Move these to the default aggs method using a visitor to walk down the tree to gather them but not going into any sub groups + // Accumulate @exclude values as a list if (termNode.Field == "@exclude") { termsAggregation.Exclude = termsAggregation.Exclude.AddValue(termNode.UnescapedTerm); @@ -46,11 +48,8 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte } else if (termNode.Field == "@min") { - int? minCount = null; - if (!String.IsNullOrEmpty(termNode.Term) && Int32.TryParse(termNode.UnescapedTerm, out int parsedMinCount)) - minCount = parsedMinCount; - - termsAggregation.MinimumDocumentCount = minCount; + if (!string.IsNullOrEmpty(termNode.Term) && Int32.TryParse(termNode.UnescapedTerm, out int parsedMinCount)) + termsAggregation.MinimumDocumentCount = parsedMinCount; } } @@ -79,7 +78,7 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte if (termNode.Field == "@missing") { DateTime? missingValue = null; - if (!String.IsNullOrEmpty(termNode.Term) && DateTime.TryParse(termNode.Term, out var parsedMissingDate)) + if (!string.IsNullOrEmpty(termNode.Term) && DateTime.TryParse(termNode.Term, out var parsedMissingDate)) missingValue = parsedMissingDate; dateHistogramAggregation.Missing = missingValue; @@ -93,13 +92,7 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte continue; } - if (container is BucketAggregationBase bucketContainer) - { - if (bucketContainer.Aggregations == null) - bucketContainer.Aggregations = new AggregationDictionary(); - - bucketContainer.Aggregations[((IAggregation)aggregation).Name] = (AggregationContainer)aggregation; - } + childAggregations[((IAggregation)aggregation).Name] = (AggregationContainer)aggregation; if (termsAggregation != null && (child.Prefix == "-" || child.Prefix == "+")) { @@ -114,6 +107,56 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte } } + if (parentBucket != null) + { + bool containsNestedField = node.Children + .OfType() + .Any(c => !String.IsNullOrEmpty(c.Field) && c.Field.StartsWith("nested.", StringComparison.OrdinalIgnoreCase)); + + if (containsNestedField) + { + parentBucket.Aggregations ??= []; + + bool nestedExists = parentBucket.Aggregations.Any(kvp => kvp.Key == "nested_nested"); + + if (!nestedExists) + { + var nestedAggregation = new NestedAggregation("nested_nested") + { + Path = "nested", + Aggregations = [] + }; + + var nestedAggregationContainer = new AggregationContainer + { + Nested = nestedAggregation + }; + + parentBucket.Aggregations["nested_nested"] = nestedAggregationContainer; + } + + var nestedAggregationContainerFromDict = parentBucket.Aggregations["nested_nested"]; + var nestedAgg = nestedAggregationContainerFromDict.Nested; + + nestedAgg.Aggregations ??= []; + + foreach (var kvp in childAggregations) + { + nestedAgg.Aggregations[kvp.Key] = kvp.Value; + } + } + else + { + if (parentBucket.Aggregations == null) + parentBucket.Aggregations = []; + + foreach (var kvp in childAggregations) + { + parentBucket.Aggregations[kvp.Key] = kvp.Value; + } + } + } + if (node.Parent == null) node.SetAggregation(container); } diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs index 39cadd8b..90b96281 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs @@ -447,10 +447,14 @@ await Client.IndexManyAsync([ var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); // Act - var result = await processor.BuildAggregationsAsync("terms:(nested.field1 @exclude:myexclude @include:myinclude @include:otherinclude @missing:mymissing @exclude:otherexclude @min:1)"); + + var result = await processor.BuildAggregationsAsync( + "terms:(nested.field1 @exclude:cherry @exclude:date) " + + "terms:(nested.field4 @exclude:3 @exclude:4)" + ); // Assert - var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); + var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); string actualRequest = actualResponse.GetRequest(); _logger.LogInformation("Actual: {Request}", actualRequest); @@ -461,13 +465,39 @@ await Client.IndexManyAsync([ .Aggregations(na => na .Terms("terms_nested.field1", t => t .Field("nested.field1.keyword") - .Exclude(["date"]) - .Meta(m => m.Add("@field_type", "text"))) + .Exclude(["cherry","date"]) + .Meta(m => m.Add("@field_type", "keyword"))) .Terms("terms_nested.field4", t => t .Field("nested.field4") - .Exclude(["4"]) + .Exclude(["3","4"]) .Meta(m => m.Add("@field_type", "integer"))))))); + // expected response buckets + var expectedNestedAgg = expectedResponse.Aggregations.Nested("nested_nested"); + var expectedField1Terms = expectedNestedAgg.Terms("terms_nested.field1"); + var expectedField4Terms = expectedNestedAgg.Terms("terms_nested.field4"); + + // actual response buckets + var actualNestedAgg = actualResponse.Aggregations.Nested("nested_nested"); + var actualField1Terms = actualNestedAgg.Terms("terms_nested.field1"); + var actualField4Terms = actualNestedAgg.Terms("terms_nested.field4"); + + // Add assertions for bucket counts and contents + Assert.Equal(expectedField1Terms.Buckets.Count, actualField1Terms.Buckets.Count); + Assert.Equal(expectedField4Terms.Buckets.Count, actualField4Terms.Buckets.Count); + + // Verify field1 buckets (apple and banana should be included, cherry and date excluded) + Assert.Contains(actualField1Terms.Buckets, b => b.Key == "apple" && b.DocCount == 1); + Assert.Contains(actualField1Terms.Buckets, b => b.Key == "banana" && b.DocCount == 1); + Assert.DoesNotContain(actualField1Terms.Buckets, b => b.Key == "cherry"); + Assert.DoesNotContain(actualField1Terms.Buckets, b => b.Key == "date"); + + // Verify field4 buckets (1 and 2 should be included, 3 and 4 excluded) + Assert.Contains(actualField4Terms.Buckets, b => b.Key == "1" && b.DocCount == 1); + Assert.Contains(actualField4Terms.Buckets, b => b.Key == "2" && b.DocCount == 1); + Assert.DoesNotContain(actualField4Terms.Buckets, b => b.Key == "3"); + Assert.DoesNotContain(actualField4Terms.Buckets, b => b.Key == "4"); + string expectedRequest = expectedResponse.GetRequest(); _logger.LogInformation("Expected: {Request}", expectedRequest); From 3c03030ad186388264a9cb5ee5758ad2080b7b4c Mon Sep 17 00:00:00 2001 From: veeraagandhi Date: Fri, 1 Aug 2025 15:10:38 +0530 Subject: [PATCH 2/7] Fixed nested query parser test NestedMixedOperations_WithQueryAndAggregation_HandlesNestedContextCorrectly --- .../Extensions/SearchDescriptorExtensions.cs | 21 ++++ .../Visitors/CombineAggregationsVisitor.cs | 7 +- .../Visitors/CombineQueriesVisitor.cs | 96 ++++++++++++++++--- .../ElasticNestedQueryParserTests.cs | 9 +- 4 files changed, 117 insertions(+), 16 deletions(-) diff --git a/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs b/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs index e63c95e6..a6e9fac4 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs @@ -58,6 +58,27 @@ IDictionary sourceAggregations return t; }); } + else if (agg.Max != null) + { + target.Max(name, m => + { + // Copy field + if (agg.Max.Field != null) + m.Field(agg.Max.Field); + + // Copy Meta if present + if (agg.Max.Meta != null) + { + m.Meta(d => { + foreach (var meta in agg.Max.Meta) + d.Add(meta.Key, meta.Value); + return d; + }); + } + + return m; + }); + } } return target; diff --git a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs index d66069a1..f11b1f18 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs @@ -48,9 +48,14 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte } else if (termNode.Field == "@min") { - if (!string.IsNullOrEmpty(termNode.Term) && Int32.TryParse(termNode.UnescapedTerm, out int parsedMinCount)) + if (!String.IsNullOrEmpty(termNode.Term) && Int32.TryParse(termNode.UnescapedTerm, out int parsedMinCount)) termsAggregation.MinimumDocumentCount = parsedMinCount; } + else if (termNode.Field == "@max") + { + if (!String.IsNullOrEmpty(termNode.Term) && Int32.TryParse(termNode.UnescapedTerm, out int parsedMaxCount)) + termsAggregation.Size = parsedMaxCount; + } } if (termNode != null && container is ITopHitsAggregation topHitsAggregation) diff --git a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs index 5f2ddd1b..be63fef2 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Foundatio.Parsers.ElasticQueries.Extensions; @@ -23,42 +24,109 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte if (context is not IElasticQueryVisitorContext elasticContext) throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context)); - QueryBase query = await node.GetQueryAsync(() => node.GetDefaultQueryAsync(context)).ConfigureAwait(false); - QueryBase container = query; - var nested = query as NestedQuery; - if (nested != null && node.Parent != null) - container = null; + QueryBase defaultQuery = await node.GetQueryAsync(() => node.GetDefaultQueryAsync(context)).ConfigureAwait(false); + + // Will accumulate combined queries for non-nested fields + QueryBase container = null; + + // Dictionary to accumulate queries per nested path + var nestedQueries = new Dictionary(); foreach (var child in node.Children.OfType()) { var childQuery = await child.GetQueryAsync(() => child.GetDefaultQueryAsync(context)).ConfigureAwait(false); if (childQuery == null) continue; - var op = node.GetOperator(elasticContext); if (child.IsExcluded()) childQuery = !childQuery; + var op = node.GetOperator(elasticContext); if (op == GroupOperator.Or && node.IsRequired()) op = GroupOperator.And; - if (op == GroupOperator.And) + string fieldName = child.Field; + + // Check if field is nested (has a dot) - could be improved to check against valid nested paths + int dotIndex = fieldName.IndexOf('.'); + if (dotIndex > 0) + { + string nestedPath = fieldName.Substring(0, dotIndex); + + // Get or create NestedQuery for this path + if (!nestedQueries.TryGetValue(nestedPath, out NestedQuery nestedQuery)) + { + nestedQuery = new NestedQuery + { + Path = nestedPath, + Query = null + }; + nestedQueries[nestedPath] = nestedQuery; + } + + // Combine this child's query into the nested query's inner query + if (nestedQuery.Query == null) + { + nestedQuery.Query = childQuery; + } + else + { + if (op == GroupOperator.And) + nestedQuery.Query &= childQuery; + else if (op == GroupOperator.Or) + nestedQuery.Query |= childQuery; + } + } + else + { + // Non-nested field queries combined here + if (container == null) + { + container = childQuery; + } + else + { + if (op == GroupOperator.And) + container &= childQuery; + else if (op == GroupOperator.Or) + container |= childQuery; + } + } + } + + // Combine all nestedQueries with the container (non-nested) + QueryBase combinedNestedQueries = null; + foreach (var nestedQuery in nestedQueries.Values) + { + if (combinedNestedQueries == null) { - container &= childQuery; + combinedNestedQueries = nestedQuery; } - else if (op == GroupOperator.Or) + else { - container |= childQuery; + combinedNestedQueries &= nestedQuery; // Assuming AND combining nested groups; adjust if needed } } - if (nested != null) + QueryBase finalQuery = null; + if (combinedNestedQueries != null && container != null) + { + finalQuery = combinedNestedQueries & container; + } + else if (combinedNestedQueries != null) + { + finalQuery = combinedNestedQueries; + } + else if (container != null) { - nested.Query = container; - node.SetQuery(nested); + finalQuery = container; } else { - node.SetQuery(container); + // fallback to default query + finalQuery = defaultQuery; } + + node.SetQuery(finalQuery); } + } diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs index 90b96281..2a13824a 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs @@ -610,7 +610,14 @@ await Client.IndexManyAsync([ Assert.Equal(expectedRequest, actualRequest); Assert.Equal(expectedResponse.Total, actualResponse.Total); - Assert.Equal(2, actualResponse.Total); // Should match high and medium + Assert.Equal(2, actualResponse.Total); + var documents = actualResponse.Documents.ToList(); + var field1Values = documents.Select(d => d.Nested.First().Field1).ToList(); + + // Verify that we have both high and medium values in the results + Assert.Contains("high", field1Values); + Assert.Contains("medium", field1Values); + Assert.DoesNotContain("low", field1Values); } From 701e014e9d3e6ec6b6832badf730695068e47a2a Mon Sep 17 00:00:00 2001 From: veeraagandhi Date: Fri, 1 Aug 2025 16:16:33 +0530 Subject: [PATCH 3/7] Fixed NestedFieldParser tests NestedAggregation_WithIncludeCommaSeparatedValues_FiltersCorrectly --- .../Extensions/SearchDescriptorExtensions.cs | 13 ++++++++-- .../ElasticNestedQueryParserTests.cs | 26 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs b/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs index a6e9fac4..230b52c0 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs @@ -39,9 +39,18 @@ IDictionary sourceAggregations // Copy exclude if (agg.Terms.Exclude != null) { - if (agg.Terms.Exclude.Values != null && agg.Terms.Exclude.Values.Count() > 0) + if (agg.Terms.Exclude.Values != null && agg.Terms.Exclude.Values.Any()) { - t.Exclude([.. agg.Terms.Exclude.Values]); + t.Exclude([.. agg.Terms.Exclude.Values.OrderBy(v => v)]); + } + } + + // Copy include + if (agg.Terms.Include != null) + { + if (agg.Terms.Include.Values != null && agg.Terms.Include.Values.Any()) + { + t.Include([.. agg.Terms.Include.Values.OrderBy(v => v)]); } } diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs index 2a13824a..c4e79332 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs @@ -396,7 +396,10 @@ await Client.IndexManyAsync([ var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); // Act - var result = await processor.BuildAggregationsAsync("terms:(nested.field1 @include:apple,banana,cherry @include:1,2,3)"); + var result = await processor.BuildAggregationsAsync( + "terms:(nested.field1 @include:apple @include:banana @include:cherry)" + + "terms:(nested.field4 @include:1 @include:2 @include:3)" + ); // Assert var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); @@ -411,7 +414,7 @@ await Client.IndexManyAsync([ .Terms("terms_nested.field1", t => t .Field("nested.field1.keyword") .Include(["apple", "banana", "cherry"]) - .Meta(m => m.Add("@field_type", "text"))) + .Meta(m => m.Add("@field_type", "keyword"))) .Terms("terms_nested.field4", t => t .Field("nested.field4") .Include(["1", "2", "3"]) @@ -422,6 +425,25 @@ await Client.IndexManyAsync([ Assert.Equal(expectedRequest, actualRequest); Assert.Equal(expectedResponse.Total, actualResponse.Total); + + // Verify bucket contents + var actualNestedAgg = actualResponse.Aggregations.Nested("nested_nested"); + var actualField1Terms = actualNestedAgg.Terms("terms_nested.field1"); + var actualField4Terms = actualNestedAgg.Terms("terms_nested.field4"); + + // Verify field1 buckets have only the included values + Assert.Equal(3, actualField1Terms.Buckets.Count); + Assert.Contains(actualField1Terms.Buckets, b => b.Key == "apple" && b.DocCount == 1); + Assert.Contains(actualField1Terms.Buckets, b => b.Key == "banana" && b.DocCount == 1); + Assert.Contains(actualField1Terms.Buckets, b => b.Key == "cherry" && b.DocCount == 1); + Assert.DoesNotContain(actualField1Terms.Buckets, b => b.Key == "date"); + + // Verify field4 buckets have only the included values + Assert.Equal(3, actualField4Terms.Buckets.Count); + Assert.Contains(actualField4Terms.Buckets, b => b.Key == "1" && b.DocCount == 1); + Assert.Contains(actualField4Terms.Buckets, b => b.Key == "2" && b.DocCount == 1); + Assert.Contains(actualField4Terms.Buckets, b => b.Key == "3" && b.DocCount == 1); + Assert.DoesNotContain(actualField4Terms.Buckets, b => b.Key == "4"); } [Fact] From 869e511dca1cd211a8ae929980416e56706007d9 Mon Sep 17 00:00:00 2001 From: veeraagandhi Date: Fri, 1 Aug 2025 16:26:08 +0530 Subject: [PATCH 4/7] PR feedback --- .../Extensions/SearchDescriptorExtensions.cs | 2 +- .../Visitors/CombineAggregationsVisitor.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs b/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs index 230b52c0..ad29167d 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs @@ -55,7 +55,7 @@ IDictionary sourceAggregations } // Copy Meta if present - if (agg.Meta != null) + if (agg.Meta != null && agg.Terms.Meta !=null) { t.Meta(d => { foreach (var meta in agg.Terms.Meta) diff --git a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs index f11b1f18..fc1cc60a 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs @@ -12,6 +12,8 @@ namespace Foundatio.Parsers.ElasticQueries.Visitors; public class CombineAggregationsVisitor : ChainableQueryVisitor { + private const string NestedPrefix = "nested."; + public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context) { await base.VisitAsync(node, context).ConfigureAwait(false); @@ -116,7 +118,7 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte { bool containsNestedField = node.Children .OfType() - .Any(c => !String.IsNullOrEmpty(c.Field) && c.Field.StartsWith("nested.", StringComparison.OrdinalIgnoreCase)); + .Any(c => !String.IsNullOrEmpty(c.Field) && c.Field.StartsWith(NestedPrefix, StringComparison.OrdinalIgnoreCase)); if (containsNestedField) { From d1ba0dd199ccbdfc5ac4f2c913e1489ec9a6c7be Mon Sep 17 00:00:00 2001 From: veeraagandhi Date: Mon, 4 Aug 2025 20:08:38 +0530 Subject: [PATCH 5/7] updated defaultQuery, combine queriesvisitor and aggregationVisisotr to fix nested query parser tests --- .../Extensions/DefaultQueryNodeExtensions.cs | 113 ++++++++++++++---- .../Extensions/SearchDescriptorExtensions.cs | 27 ++++- .../Visitors/CombineAggregationsVisitor.cs | 91 ++++++++++---- .../Visitors/CombineQueriesVisitor.cs | 62 ++++++---- .../ElasticNestedQueryParserTests.cs | 29 +++-- 5 files changed, 240 insertions(+), 82 deletions(-) diff --git a/src/Foundatio.Parsers.ElasticQueries/Extensions/DefaultQueryNodeExtensions.cs b/src/Foundatio.Parsers.ElasticQueries/Extensions/DefaultQueryNodeExtensions.cs index 4c2aba27..141f10dc 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Extensions/DefaultQueryNodeExtensions.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Extensions/DefaultQueryNodeExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Foundatio.Parsers.ElasticQueries.Visitors; using Foundatio.Parsers.LuceneQueries.Extensions; @@ -40,8 +42,9 @@ public static QueryBase GetDefaultQuery(this TermNode node, IQueryVisitorContext if (elasticContext.MappingResolver.IsPropertyAnalyzed(field)) { - string[] fields = !String.IsNullOrEmpty(field) ? [field] : defaultFields; + string[] fields = !String.IsNullOrEmpty(field) ? new[] { field } : defaultFields; + // Handle wildcard query string case if (!node.IsQuotedTerm && node.UnescapedTerm.EndsWith("*")) { query = new QueryStringQuery @@ -51,42 +54,108 @@ public static QueryBase GetDefaultQuery(this TermNode node, IQueryVisitorContext AnalyzeWildcard = true, Query = node.UnescapedTerm }; + return query; } - else + + // If a single field, use single Match or MatchPhrase query + if (fields != null && fields.Length == 1) { - if (fields != null && fields.Length == 1) + if (node.IsQuotedTerm) { - if (node.IsQuotedTerm) - { - query = new MatchPhraseQuery - { - Field = fields[0], - Query = node.UnescapedTerm - }; - } - else + query = new MatchPhraseQuery { - query = new MatchQuery - { - Field = fields[0], - Query = node.UnescapedTerm - }; - } + Field = fields[0], + Query = node.UnescapedTerm + }; } else { - query = new MultiMatchQuery + query = new MatchQuery { - Fields = fields, + Field = fields[0], Query = node.UnescapedTerm }; - if (node.IsQuotedTerm) - ((MultiMatchQuery)query).Type = TextQueryType.Phrase; } + return query; + } + + // Multiple fields: split into nested groups and non-nested + var shouldQueries = new List(); + + // Group nested fields by prefix (before dot) + var nestedGroups = fields + .Where(f => !string.IsNullOrEmpty(f) && f.Contains('.')) + .GroupBy(f => f.Substring(0, f.IndexOf('.'))); + + // Non-nested fields (no dot) + var nonNestedFields = fields.Where(f => string.IsNullOrEmpty(f) || !f.Contains('.')).ToArray(); + + // Add non-nested query if any + if (nonNestedFields.Length == 1) + { + if (node.IsQuotedTerm) + { + shouldQueries.Add(new MatchPhraseQuery + { + Field = nonNestedFields[0], + Query = node.UnescapedTerm + }); + } + else + { + shouldQueries.Add(new MatchQuery + { + Field = nonNestedFields[0], + Query = node.UnescapedTerm + }); + } + } + else if (nonNestedFields.Length > 1) + { + var mmq = new MultiMatchQuery + { + Fields = nonNestedFields, + Query = node.UnescapedTerm + }; + if (node.IsQuotedTerm) + mmq.Type = TextQueryType.Phrase; + + shouldQueries.Add(mmq); } + + // Add nested queries per nested group + foreach (var group in nestedGroups) + { + string nestedPath = group.Key; + + var nestedShouldQueries = group.Select(f => (QueryBase)(node.IsQuotedTerm + ? new MatchPhraseQuery { Field = f, Query = node.UnescapedTerm } + : new MatchQuery { Field = f, Query = node.UnescapedTerm })).ToList(); + + QueryBase nestedInnerQuery; + if (nestedShouldQueries.Count == 1) + nestedInnerQuery = nestedShouldQueries[0]; + else + nestedInnerQuery = new BoolQuery { Should = nestedShouldQueries.Select(q => (QueryContainer)q), MinimumShouldMatch = 1 }; + + var nestedQuery = new NestedQuery + { + Path = nestedPath, + Query = nestedInnerQuery + }; + + shouldQueries.Add(nestedQuery); + } + + query = new BoolQuery + { + Should = shouldQueries.Select(q => (QueryContainer)q), + MinimumShouldMatch = 1 + }; } else { + // Non-analyzed field path: handle prefix and term queries if (!node.IsQuotedTerm && node.UnescapedTerm.EndsWith("*")) { query = new PrefixQuery diff --git a/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs b/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs index ad29167d..005fda96 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Extensions/SearchDescriptorExtensions.cs @@ -16,7 +16,7 @@ public static AggregationContainerDescriptor CopyAggregations( IDictionary sourceAggregations ) where T : class { - foreach (var kvp in sourceAggregations) + foreach (var kvp in sourceAggregations.OrderBy(x => x.Key)) { string name = kvp.Key; var agg = kvp.Value; @@ -34,7 +34,26 @@ IDictionary sourceAggregations { // Copy field if (agg.Terms.Field != null) - t.Field(agg.Terms.Field); + { + var fieldName = agg.Terms.Field.Name; + + // For text fields, use the keyword sub-field if it's not already specified + // This helps handle the common case where a text field needs to be aggregated + bool isTextFieldWithoutKeyword = agg.Meta != null && + agg.Meta.TryGetValue("@field_type", out var fieldType) && + fieldType?.ToString() == "text" && + !fieldName.EndsWith(".keyword"); + + if (isTextFieldWithoutKeyword) + { + // Use the keyword sub-field for text field aggregations + t.Field($"{fieldName}.keyword"); + } + else + { + t.Field(agg.Terms.Field); + } + } // Copy exclude if (agg.Terms.Exclude != null) @@ -58,7 +77,7 @@ IDictionary sourceAggregations if (agg.Meta != null && agg.Terms.Meta !=null) { t.Meta(d => { - foreach (var meta in agg.Terms.Meta) + foreach (var meta in agg.Terms.Meta.OrderBy(m => m.Key)) d.Add(meta.Key, meta.Value); return d; }); @@ -79,7 +98,7 @@ IDictionary sourceAggregations if (agg.Max.Meta != null) { m.Meta(d => { - foreach (var meta in agg.Max.Meta) + foreach (var meta in agg.Max.Meta.OrderBy(m => m.Key)) d.Add(meta.Key, meta.Value); return d; }); diff --git a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs index fc1cc60a..14642de8 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs @@ -12,8 +12,6 @@ namespace Foundatio.Parsers.ElasticQueries.Visitors; public class CombineAggregationsVisitor : ChainableQueryVisitor { - private const string NestedPrefix = "nested."; - public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context) { await base.VisitAsync(node, context).ConfigureAwait(false); @@ -116,49 +114,89 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte if (parentBucket != null) { - bool containsNestedField = node.Children - .OfType() - .Any(c => !String.IsNullOrEmpty(c.Field) && c.Field.StartsWith(NestedPrefix, StringComparison.OrdinalIgnoreCase)); - - if (containsNestedField) + // Map aggregation names to their originating field nodes to avoid invalid casts + var aggNameToFieldNode = new Dictionary(); + foreach (var child in node.Children.OfType()) { - parentBucket.Aggregations ??= []; + var aggregation = await child.GetAggregationAsync(() => child.GetDefaultAggregationAsync(context)); + if (aggregation != null) + { + string name = ((IAggregation)aggregation).Name; + aggNameToFieldNode[name] = child; + childAggregations[name] = (AggregationContainer)aggregation; + } + } + + // Get distinct nested paths from child fields + var nestedPaths = aggNameToFieldNode.Values + .Select(c => GetNestedPath(c.Field)) + .Where(np => !String.IsNullOrEmpty(np)) + .Distinct() + .ToList(); - bool nestedExists = parentBucket.Aggregations.Any(kvp => kvp.Key == "nested_nested"); + if (parentBucket.Aggregations == null) + parentBucket.Aggregations = new AggregationDictionary(); + + foreach (string nestedPath in nestedPaths) + { + // Create nested aggregation name based on the path + string nestedAggName = $"nested_{nestedPath}"; + + // Try to find existing nested aggregation container by name + bool nestedExists = parentBucket.Aggregations.Any(kvp => kvp.Key == nestedAggName); if (!nestedExists) { - var nestedAggregation = new NestedAggregation("nested_nested") + // Create new nested aggregation + var nestedAggregation = new NestedAggregation(nestedAggName) { - Path = "nested", + Path = nestedPath, Aggregations = [] }; - var nestedAggregationContainer = new AggregationContainer + var nestedAggContainer = new AggregationContainer { Nested = nestedAggregation }; - parentBucket.Aggregations["nested_nested"] = nestedAggregationContainer; + parentBucket.Aggregations[nestedAggName] = nestedAggContainer; } - var nestedAggregationContainerFromDict = parentBucket.Aggregations["nested_nested"]; - var nestedAgg = nestedAggregationContainerFromDict.Nested; - + var nestedAgg = parentBucket.Aggregations[nestedAggName].Nested; nestedAgg.Aggregations ??= []; + // Add child aggregations belonging to this nested path foreach (var kvp in childAggregations) { - nestedAgg.Aggregations[kvp.Key] = kvp.Value; + if (aggNameToFieldNode.TryGetValue(kvp.Key, out var fieldNode) && + !String.IsNullOrEmpty(fieldNode.Field) && + fieldNode.Field.StartsWith($"{nestedPath}.", StringComparison.OrdinalIgnoreCase)) + { + nestedAgg.Aggregations[kvp.Key] = kvp.Value; + } } } - else - { - if (parentBucket.Aggregations == null) - parentBucket.Aggregations = []; - foreach (var kvp in childAggregations) + // Add non-nested child aggregations directly under parentBucket + foreach (var kvp in childAggregations) + { + if (aggNameToFieldNode.TryGetValue(kvp.Key, out var fieldNode)) + { + bool isNested = false; + if (!String.IsNullOrEmpty(fieldNode.Field)) + { + string path = GetNestedPath(fieldNode.Field); + if (!String.IsNullOrEmpty(path) && nestedPaths.Contains(path)) + isNested = true; + } + if (!isNested) + { + parentBucket.Aggregations[kvp.Key] = kvp.Value; + } + } + else { + // If no field info, assume not nested and add parentBucket.Aggregations[kvp.Key] = kvp.Value; } } @@ -198,4 +236,13 @@ private async Task GetParentContainerAsync(IQueryNode node, IQu return container; } + + private static string GetNestedPath(string field) + { + if (String.IsNullOrEmpty(field)) + return null; + + int dotIndex = field.IndexOf('.'); + return dotIndex > 0 ? field.Substring(0, dotIndex) : null; + } } diff --git a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs index be63fef2..b9590f59 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs @@ -47,38 +47,56 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte string fieldName = child.Field; // Check if field is nested (has a dot) - could be improved to check against valid nested paths - int dotIndex = fieldName.IndexOf('.'); - if (dotIndex > 0) + if (!String.IsNullOrEmpty(fieldName)) { - string nestedPath = fieldName.Substring(0, dotIndex); - - // Get or create NestedQuery for this path - if (!nestedQueries.TryGetValue(nestedPath, out NestedQuery nestedQuery)) + int dotIndex = fieldName.IndexOf('.'); + if (dotIndex > 0) { - nestedQuery = new NestedQuery - { - Path = nestedPath, - Query = null - }; - nestedQueries[nestedPath] = nestedQuery; - } + string nestedPath = fieldName.Substring(0, dotIndex); - // Combine this child's query into the nested query's inner query - if (nestedQuery.Query == null) - { - nestedQuery.Query = childQuery; + // Get or create NestedQuery for this path + if (!nestedQueries.TryGetValue(nestedPath, out NestedQuery nestedQuery)) + { + nestedQuery = new NestedQuery + { + Path = nestedPath, + Query = null + }; + nestedQueries[nestedPath] = nestedQuery; + } + + // Combine this child's query into the nested query's inner query + if (nestedQuery.Query == null) + { + nestedQuery.Query = childQuery; + } + else + { + if (op == GroupOperator.And) + nestedQuery.Query &= childQuery; + else if (op == GroupOperator.Or) + nestedQuery.Query |= childQuery; + } } else { - if (op == GroupOperator.And) - nestedQuery.Query &= childQuery; - else if (op == GroupOperator.Or) - nestedQuery.Query |= childQuery; + // Non-nested field queries combined here + if (container == null) + { + container = childQuery; + } + else + { + if (op == GroupOperator.And) + container &= childQuery; + else if (op == GroupOperator.Or) + container |= childQuery; + } } } else { - // Non-nested field queries combined here + // Handle null field case - this is for default field searches if (container == null) { container = childQuery; diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs index c4e79332..3781f66f 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs @@ -356,15 +356,15 @@ await Client.IndexManyAsync([ .Nested("nested_nested", n => n .Path("nested") .Aggregations(na => na + .Max("max_nested.field4", m => m + .Field("nested.field4") + .Meta(m2 => m2.Add("@field_type", "integer"))) .Terms("terms_nested.field1", t => t .Field("nested.field1.keyword") .Meta(m => m.Add("@field_type", "text"))) .Terms("terms_nested.field4", t => t .Field("nested.field4") - .Meta(m => m.Add("@field_type", "integer"))) - .Max("max_nested.field4", m => m - .Field("nested.field4") - .Meta(m2 => m2.Add("@field_type", "integer"))))))); + .Meta(m => m.Add("@field_type", "integer"))))))); string expectedRequest = expectedResponse.GetRequest(); _logger.LogInformation("Expected: {Request}", expectedRequest); @@ -567,10 +567,15 @@ await Client.IndexManyAsync([ _logger.LogInformation("Actual: {Request}", actualRequest); var expectedResponse = Client.Search(d => d.Index(index) - .Query(q => q.Match(m => m.Field("field1").Query("special_value")) - || q.Nested(n => n - .Path("nested") - .Query(q2 => q2.Match(m => m.Field("nested.field1").Query("special_value")))))); + .Query(q => q.Bool(b => b + .MinimumShouldMatch(1) + .Should( + q1 => q1.Match(m => m.Field("field1").Query("special_value")), + q2 => q2.Nested(n => n + .Path("nested") + .Query(nq => nq.Match(m => m.Field("nested.field1").Query("special_value"))) + ) + )))); string expectedRequest = expectedResponse.GetRequest(); _logger.LogInformation("Expected: {Request}", expectedRequest); @@ -620,12 +625,12 @@ await Client.IndexManyAsync([ .Nested("nested_nested", n => n .Path("nested") .Aggregations(na => na - .Terms("terms_nested.field1", t => t - .Field("nested.field1.keyword") - .Meta(m => m.Add("@field_type", "text"))) .Max("max_nested.field4", m => m .Field("nested.field4") - .Meta(m2 => m2.Add("@field_type", "integer"))))))); + .Meta(m2 => m2.Add("@field_type", "integer"))) + .Terms("terms_nested.field1", t => t + .Field("nested.field1.keyword") + .Meta(m => m.Add("@field_type", "text"))))))); string expectedRequest = expectedResponse.GetRequest(); _logger.LogInformation("Expected: {Request}", expectedRequest); From e193a9df42c5ea3bf25639e0abea9545ed405676 Mon Sep 17 00:00:00 2001 From: veeraagandhi Date: Mon, 4 Aug 2025 20:35:16 +0530 Subject: [PATCH 6/7] fixed expectedResponse to fix failing test NestedFilterProcessor --- .../ElasticNestedQueryParserTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs index 3781f66f..d2ed5236 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs @@ -127,10 +127,9 @@ await Client.IndexManyAsync([ _logger.LogInformation("Actual: {Request}", actualRequest); var expectedResponse = Client.Search(d => d.Query(q => q.Match(m => m.Field(e => e.Field1).Query("value1")) + && q.Nested(n => n.Path(p => p.Nested).Query(q2 => q2.Match(m => m.Field("nested.field1").Query("value1")))) && q.Nested(n => n.Path(p => p.Nested).Query(q2 => - q2.Match(m => m.Field("nested.field1").Query("value1")) - && q2.Term("nested.field4", "4") - && q2.Match(m => m.Field("nested.field3").Query("value3")))))); + q2.Term("nested.field4", "4") && q2.Match(m => m.Field("nested.field3").Query("value3")))))); string expectedRequest = expectedResponse.GetRequest(); _logger.LogInformation("Expected: {Request}", expectedRequest); From 1d666897dad1a0c03d69ec8e99649661bdf5e456 Mon Sep 17 00:00:00 2001 From: veeraagandhi Date: Tue, 5 Aug 2025 19:25:45 +0530 Subject: [PATCH 7/7] Self reviewed Code cleanup --- .../Extensions/DefaultQueryNodeExtensions.cs | 6 +++--- .../Visitors/CombineAggregationsVisitor.cs | 3 +-- .../Visitors/CombineQueriesVisitor.cs | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Foundatio.Parsers.ElasticQueries/Extensions/DefaultQueryNodeExtensions.cs b/src/Foundatio.Parsers.ElasticQueries/Extensions/DefaultQueryNodeExtensions.cs index 141f10dc..c457cafa 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Extensions/DefaultQueryNodeExtensions.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Extensions/DefaultQueryNodeExtensions.cs @@ -42,7 +42,7 @@ public static QueryBase GetDefaultQuery(this TermNode node, IQueryVisitorContext if (elasticContext.MappingResolver.IsPropertyAnalyzed(field)) { - string[] fields = !String.IsNullOrEmpty(field) ? new[] { field } : defaultFields; + string[] fields = !String.IsNullOrEmpty(field) ? [field] : defaultFields; // Handle wildcard query string case if (!node.IsQuotedTerm && node.UnescapedTerm.EndsWith("*")) @@ -84,11 +84,11 @@ public static QueryBase GetDefaultQuery(this TermNode node, IQueryVisitorContext // Group nested fields by prefix (before dot) var nestedGroups = fields - .Where(f => !string.IsNullOrEmpty(f) && f.Contains('.')) + .Where(f => !String.IsNullOrEmpty(f) && f.Contains('.')) .GroupBy(f => f.Substring(0, f.IndexOf('.'))); // Non-nested fields (no dot) - var nonNestedFields = fields.Where(f => string.IsNullOrEmpty(f) || !f.Contains('.')).ToArray(); + string[] nonNestedFields = [.. fields.Where(f => String.IsNullOrEmpty(f) || !f.Contains('.'))]; // Add non-nested query if any if (nonNestedFields.Length == 1) diff --git a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs index 14642de8..0dd9a261 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs @@ -134,8 +134,7 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte .Distinct() .ToList(); - if (parentBucket.Aggregations == null) - parentBucket.Aggregations = new AggregationDictionary(); + parentBucket.Aggregations ??= []; foreach (string nestedPath in nestedPaths) { diff --git a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs index b9590f59..40425f2b 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs @@ -18,13 +18,13 @@ public override async Task VisitAsync(GroupNode node, IQueryVisitorContext conte // Only stop on scoped group nodes (parens). Gather all child queries (including scoped groups) and then combine them. // Combining only happens at the scoped group level though. - // Merge all non-field terms together into a single match or multi-match query + // For all non-field terms, processes default field searches, adds them to container and combines them with AND/OR operators. merging into match/multi-match queries happen in DefaultQueryNodeExtensions. // Merge all nested queries for the same nested field together if (context is not IElasticQueryVisitorContext elasticContext) throw new ArgumentException("Context must be of type IElasticQueryVisitorContext", nameof(context)); - QueryBase defaultQuery = await node.GetQueryAsync(() => node.GetDefaultQueryAsync(context)).ConfigureAwait(false); + var defaultQuery = await node.GetQueryAsync(() => node.GetDefaultQueryAsync(context)).ConfigureAwait(false); // Will accumulate combined queries for non-nested fields QueryBase container = null;