From 764efd1f8791ae70a1ef8d72ac83992c706db4b6 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 18 Jul 2025 16:41:07 -0500 Subject: [PATCH 1/6] Adds nested query and aggregation support Adds support for nested queries and aggregations. This change allows the parser to automatically wrap nested fields in nested queries and aggregations, simplifying the query and aggregation building process. Adds multiple tests to ensure that the nested queries and aggregations are built correctly. --- .../Visitors/CombineAggregationsVisitor.cs | 1 + .../ElasticQueryParserTests.cs | 444 ++++++++++++++++++ 2 files changed, 445 insertions(+) diff --git a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs index ba7985d4..081ab423 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineAggregationsVisitor.cs @@ -30,6 +30,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 if (termNode.Field == "@exclude") { diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs index a4e08be1..b5e75ff0 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs @@ -960,6 +960,450 @@ await Client.IndexManyAsync([ Assert.Equal(expectedResponse.Total, actualResponse.Total); } + [Fact] + public async Task NestedIndividualFieldQuery_WithSingleNestedField_WrapsInNestedQuery() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1)) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType + { + Field1 = "parent1", + Nested = { new MyType { Field1 = "child1", Field4 = 5 } } + }, + new MyNestedType + { + Field1 = "parent2", + Nested = { new MyType { Field1 = "child2", Field4 = 3 } } + } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildQueryAsync("nested.field4:5", new ElasticQueryVisitorContext { UseScoring = true }); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Query(q => q.Nested(n => n + .Path(p => p.Nested) + .Query(q2 => q2.Term("nested.field4", "5"))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + Assert.Equal(1, actualResponse.Total); + } + + [Fact] + public async Task NestedIndividualFieldQuery_WithMultipleNestedFieldsOrCondition_CombinesIntoSingleNestedQuery() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1)) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType + { + Field1 = "parent1", + Nested = { new MyType { Field1 = "target", Field4 = 5 } } + }, + new MyNestedType + { + Field1 = "parent2", + Nested = { new MyType { Field1 = "other", Field4 = 10 } } + } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildQueryAsync("nested.field1:target OR nested.field4:10", new ElasticQueryVisitorContext { UseScoring = true }); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Query(q => q.Nested(n => n + .Path(p => p.Nested) + .Query(q2 => q2.Match(m => m.Field("nested.field1").Query("target")) + || q2.Term("nested.field4", "10"))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + Assert.Equal(2, actualResponse.Total); + } + + [Fact] + public async Task NestedIndividualFieldQuery_WithRangeQuery_WrapsInNestedQuery() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field4 = 5 } } }, + new MyNestedType { Nested = { new MyType { Field4 = 15 } } }, + new MyNestedType { Nested = { new MyType { Field4 = 25 } } } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildQueryAsync("nested.field4:[10 TO 20]", new ElasticQueryVisitorContext { UseScoring = true }); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Query(q => q.Nested(n => n + .Path(p => p.Nested) + .Query(q2 => q2.TermRange(r => r.Field("nested.field4").GreaterThanOrEquals("10").LessThanOrEquals("20")))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + Assert.Equal(1, actualResponse.Total); + } + + [Fact] + public async Task NestedAggregation_WithSingleNestedField_AutomaticallyWrapsInNestedAggregation() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field4 = 5 } } }, + new MyNestedType { Nested = { new MyType { Field4 = 10 } } }, + new MyNestedType { Nested = { new MyType { Field4 = 5 } } } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildAggregationsAsync("terms:nested.field4"); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Aggregations(a => a + .Nested("nested_nested", n => n + .Path("nested") + .Aggregations(na => na + .Terms("terms_nested.field4", t => t + .Field("nested.field4") + .Meta(m => m.Add("@field_type", "integer"))))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + } + + [Fact] + public async Task NestedAggregation_WithMultipleNestedFields_CombinesIntoSingleNestedAggregation() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1)) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field1 = "test", Field4 = 5 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "other", Field4 = 10 } } } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildAggregationsAsync("terms:nested.field1 terms:nested.field4 max:nested.field4"); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Aggregations(a => a + .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"))) + .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"))))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + } + + [Fact] + public async Task NestedAggregation_WithIncludeCommaSeparatedValues_FiltersCorrectly() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1).Fields(f => f.Keyword(k => k.Name("keyword")))) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field1 = "apple", Field4 = 1 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "banana", Field4 = 2 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "cherry", Field4 = 3 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "date", Field4 = 4 } } } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildAggregationsAsync("terms:(nested.field1~@include:apple,banana,cherry nested.field4~@include:1,2,3)"); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Aggregations(a => a + .Nested("nested_nested", n => n + .Path("nested") + .Aggregations(na => na + .Terms("terms_nested.field1", t => t + .Field("nested.field1.keyword") + .Include(new string[] { "apple", "banana", "cherry" }) + .Meta(m => m.Add("@field_type", "text"))) + .Terms("terms_nested.field4", t => t + .Field("nested.field4") + .Include(new long[] { 1, 2, 3 }) + .Meta(m => m.Add("@field_type", "integer"))))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + } + + [Fact] + public async Task NestedAggregation_WithExcludeCommaSeparatedValues_FiltersCorrectly() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1).Fields(f => f.Keyword(k => k.Name("keyword")))) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field1 = "apple", Field4 = 1 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "banana", Field4 = 2 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "cherry", Field4 = 3 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "date", Field4 = 4 } } } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildAggregationsAsync("terms:(nested.field1~@exclude:date nested.field4~@exclude:4)"); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Aggregations(a => a + .Nested("nested_nested", n => n + .Path("nested") + .Aggregations(na => na + .Terms("terms_nested.field1", t => t + .Field("nested.field1.keyword") + .Exclude(new string[] { "date" }) + .Meta(m => m.Add("@field_type", "text"))) + .Terms("terms_nested.field4", t => t + .Field("nested.field4") + .Exclude(new long[] { 4 }) + .Meta(m => m.Add("@field_type", "integer"))))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + } + + [Fact] + public async Task NestedDefaultSearch_WithNestedFieldInDefaultFields_SearchesNestedFields() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType + { + Field1 = "parent", + Nested = { new MyType { Field1 = "special_value" } } + }, + new MyNestedType + { + Field1 = "other_parent", + Nested = { new MyType { Field1 = "normal_value" } } + } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c + .SetLoggerFactory(Log) + .UseMappings(Client) + .UseNested() + .SetDefaultFields(new[] { "field1", "nested.field1" })); + + // Act + var result = await processor.BuildQueryAsync("special_value", new ElasticQueryVisitorContext().UseSearchMode()); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); + string actualRequest = actualResponse.GetRequest(); + _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")))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + Assert.Equal(1, actualResponse.Total); + } + + [Fact] + public async Task NestedMixedOperations_WithQueryAndAggregation_HandlesNestedContextCorrectly() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1).Fields(f => f.Keyword(k => k.Name("keyword")))) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field1 = "high", Field4 = 10 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "medium", Field4 = 5 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "low", Field4 = 1 } } } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act - Query with nested field filter + var queryResult = await processor.BuildQueryAsync("nested.field4:>=5", new ElasticQueryVisitorContext { UseScoring = true }); + + // Act - Aggregation on nested fields + var aggResult = await processor.BuildAggregationsAsync("terms:nested.field1 max:nested.field4"); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Query(_ => queryResult).Aggregations(aggResult)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Query(q => q.Nested(n => n + .Path("nested") + .Query(q2 => q2.TermRange(r => r.Field("nested.field4").GreaterThanOrEquals("5"))))) + .Aggregations(a => a + .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"))))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + Assert.Equal(2, actualResponse.Total); // Should match high and medium + } + [Fact] public async Task CanGenerateMatchQuery() { From 73a4f6e05641b84654162d0b071c504da47520af Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 21 Jul 2025 10:15:44 -0500 Subject: [PATCH 2/6] Apply suggestion from @niemyjski --- .../ElasticQueryParserTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs index b5e75ff0..8c419aaf 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs @@ -1268,7 +1268,7 @@ await Client.IndexManyAsync([ var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); // Act - var result = await processor.BuildAggregationsAsync("terms:(nested.field1~@exclude:date nested.field4~@exclude:4)"); + var result = await processor.BuildAggregationsAsync("terms:(nested.field1 @exclude:myexclude @include:myinclude @include:otherinclude @missing:mymissing @exclude:otherexclude @min:1)"); // Assert var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); From 75b02e8f7699aeaac545b0158b51ce5f8212303b Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 21 Jul 2025 13:32:50 -0500 Subject: [PATCH 3/6] Fixes query parser include/exclude handling Updates query parser to correctly handle include/exclude lists for nested fields. - Corrects an issue where nested field includes were not properly parsed. - Modifies the expected type for include/exclude values to string for consistency. - Converts relevant tests to async and updates assertion logic to match expected behavior. --- .../ElasticQueryParserTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs index 8c419aaf..7ea8d47a 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs @@ -225,7 +225,7 @@ public async Task ShouldHandleMultipleTermsForAnalyzedFields() } [Fact] - public void CanGetMappingsFromCode() + public async Task CanGetMappingsFromCode() { TypeMappingDescriptor GetCodeMappings(TypeMappingDescriptor d) => d.Dynamic() @@ -238,8 +238,8 @@ TypeMappingDescriptor GetCodeMappings(TypeMappingDescriptor d) = .GeoPoint(g => g.Name(f => f.Field3)) .Keyword(e => e.Name(m => m.Field2)))); - var res = Client.Index(new MyType { Field1 = "value1", Field2 = "value2", Field4 = 1, Field5 = DateTime.Now }, i => i.Index(index)); - Client.Indices.Refresh(index); + await Client.IndexAsync(new MyType { Field1 = "value1", Field2 = "value2", Field4 = 1, Field5 = DateTime.Now }, i => i.Index(index)); + await Client.Indices.RefreshAsync(index); var parser = new ElasticQueryParser(c => c.SetDefaultFields(["field1"]).UseMappings(GetCodeMappings, Client, index)); @@ -1217,7 +1217,7 @@ 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 nested.field4~@include:1,2,3)"); + var result = await processor.BuildAggregationsAsync("terms:(nested.field1 @include:apple,banana,cherry @include:1,2,3)"); // Assert var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); @@ -1235,7 +1235,7 @@ await Client.IndexManyAsync([ .Meta(m => m.Add("@field_type", "text"))) .Terms("terms_nested.field4", t => t .Field("nested.field4") - .Include(new long[] { 1, 2, 3 }) + .Include(new string[] { "1", "2", "3" }) .Meta(m => m.Add("@field_type", "integer"))))))); string expectedRequest = expectedResponse.GetRequest(); @@ -1286,7 +1286,7 @@ await Client.IndexManyAsync([ .Meta(m => m.Add("@field_type", "text"))) .Terms("terms_nested.field4", t => t .Field("nested.field4") - .Exclude(new long[] { 4 }) + .Exclude(new string[] { "4" }) .Meta(m => m.Add("@field_type", "integer"))))))); string expectedRequest = expectedResponse.GetRequest(); From 9f4afa81123391d975fed18f022a015c9451641c Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 21 Jul 2025 13:41:40 -0500 Subject: [PATCH 4/6] Moved nested tests to it's own file --- .../ElasticNestedQueryParserTests.cs | 584 ++++++++++++++++++ .../ElasticQueryParserTests.cs | 566 ----------------- 2 files changed, 584 insertions(+), 566 deletions(-) create mode 100644 tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs new file mode 100644 index 00000000..966fd4ed --- /dev/null +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs @@ -0,0 +1,584 @@ +using System.Linq; +using System.Threading.Tasks; +using Foundatio.Parsers.ElasticQueries.Extensions; +using Foundatio.Parsers.ElasticQueries.Visitors; +using Foundatio.Parsers.LuceneQueries.Visitors; +using Microsoft.Extensions.Logging; +using Nest; +using Xunit; +using Xunit.Abstractions; + +namespace Foundatio.Parsers.ElasticQueries.Tests; + +public class ElasticNestedQueryParserTests : ElasticsearchTestBase +{ + public ElasticNestedQueryParserTests(ITestOutputHelper output, ElasticsearchFixture fixture) : base(output, fixture) + { + Log.DefaultLogLevel = Microsoft.Extensions.Logging.LogLevel.Trace; + } + + [Fact] + public async Task NestedFilterProcessor() + { + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1).Index()) + .Text(e => e.Name(n => n.Field2).Index()) + .Text(e => e.Name(n => n.Field3).Index()) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1).Index()) + .Text(e => e.Name(n => n.Field2).Index()) + .Text(e => e.Name(n => n.Field3).Index()) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + await Client.IndexManyAsync([ + new MyNestedType + { + Field1 = "value1", + Field2 = "value2", + Nested = { new MyType { Field1 = "value1", Field4 = 4 } } + }, + new MyNestedType { Field1 = "value2", Field2 = "value2" }, + new MyNestedType { Field1 = "value1", Field2 = "value4" } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseFieldMap(new FieldMap { { "blah", "nested" } }).UseMappings(Client).UseNested()); + var result = await processor.BuildQueryAsync("field1:value1 blah:(blah.field1:value1)", new ElasticQueryVisitorContext().UseScoring()); + + var actualResponse = Client.Search(d => d.Query(_ => result)); + string actualRequest = actualResponse.GetRequest(); + _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")))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + + result = await processor.BuildQueryAsync("field1:value1 blah:(blah.field1:value1 blah.field4:4)", new ElasticQueryVisitorContext().UseScoring()); + + actualResponse = Client.Search(d => d.Query(_ => result)); + actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + 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")) + && q2.Term("nested.field4", "4"))))); + + expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + } + + [Fact] + public async Task NestedFilterProcessor2() + { + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1).Index()) + .Text(e => e.Name(n => n.Field2).Index()) + .Text(e => e.Name(n => n.Field3).Index()) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1).Index()) + .Text(e => e.Name(n => n.Field2).Index()) + .Text(e => e.Name(n => n.Field3).Index()) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType + { + Field1 = "value1", + Field2 = "value2", + Nested = { new MyType { Field1 = "value1", Field4 = 4 } } + }, + new MyNestedType { Field1 = "value2", Field2 = "value2" }, + new MyNestedType { Field1 = "value1", Field2 = "value4", Field3 = "value3" } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + var result = await processor.BuildQueryAsync("field1:value1 nested:(nested.field1:value1 nested.field4:4 nested.field3:value3)", + new ElasticQueryVisitorContext { UseScoring = true }); + + var actualResponse = Client.Search(d => d.Query(_ => result)); + string actualRequest = actualResponse.GetRequest(); + _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")) + && q2.Term("nested.field4", "4") + && q2.Match(m => m.Field("nested.field3").Query("value3")))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + } + + [Fact] + public async Task NestedIndividualFieldQuery_WithSingleNestedField_WrapsInNestedQuery() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1)) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType + { + Field1 = "parent1", + Nested = { new MyType { Field1 = "child1", Field4 = 5 } } + }, + new MyNestedType + { + Field1 = "parent2", + Nested = { new MyType { Field1 = "child2", Field4 = 3 } } + } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildQueryAsync("nested.field4:5", new ElasticQueryVisitorContext { UseScoring = true }); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Query(q => q.Nested(n => n + .Path(p => p.Nested) + .Query(q2 => q2.Term("nested.field4", "5"))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + Assert.Equal(1, actualResponse.Total); + } + + [Fact] + public async Task NestedIndividualFieldQuery_WithMultipleNestedFieldsOrCondition_CombinesIntoSingleNestedQuery() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1)) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType + { + Field1 = "parent1", + Nested = { new MyType { Field1 = "target", Field4 = 5 } } + }, + new MyNestedType + { + Field1 = "parent2", + Nested = { new MyType { Field1 = "other", Field4 = 10 } } + } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildQueryAsync("nested.field1:target OR nested.field4:10", new ElasticQueryVisitorContext { UseScoring = true }); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Query(q => q.Nested(n => n + .Path(p => p.Nested) + .Query(q2 => q2.Match(m => m.Field("nested.field1").Query("target")) + || q2.Term("nested.field4", "10"))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + Assert.Equal(2, actualResponse.Total); + } + + [Fact] + public async Task NestedIndividualFieldQuery_WithRangeQuery_WrapsInNestedQuery() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field4 = 5 } } }, + new MyNestedType { Nested = { new MyType { Field4 = 15 } } }, + new MyNestedType { Nested = { new MyType { Field4 = 25 } } } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildQueryAsync("nested.field4:[10 TO 20]", new ElasticQueryVisitorContext { UseScoring = true }); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Query(q => q.Nested(n => n + .Path(p => p.Nested) + .Query(q2 => q2.TermRange(r => r.Field("nested.field4").GreaterThanOrEquals("10").LessThanOrEquals("20")))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + Assert.Equal(1, actualResponse.Total); + } + + [Fact] + public async Task NestedAggregation_WithSingleNestedField_AutomaticallyWrapsInNestedAggregation() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field4 = 5 } } }, + new MyNestedType { Nested = { new MyType { Field4 = 10 } } }, + new MyNestedType { Nested = { new MyType { Field4 = 5 } } } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildAggregationsAsync("terms:nested.field4"); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Aggregations(a => a + .Nested("nested_nested", n => n + .Path("nested") + .Aggregations(na => na + .Terms("terms_nested.field4", t => t + .Field("nested.field4") + .Meta(m => m.Add("@field_type", "integer"))))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + } + + [Fact] + public async Task NestedAggregation_WithMultipleNestedFields_CombinesIntoSingleNestedAggregation() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1)) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field1 = "test", Field4 = 5 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "other", Field4 = 10 } } } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act + var result = await processor.BuildAggregationsAsync("terms:nested.field1 terms:nested.field4 max:nested.field4"); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Aggregations(a => a + .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"))) + .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"))))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + } + + [Fact] + public async Task NestedAggregation_WithIncludeCommaSeparatedValues_FiltersCorrectly() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1).Fields(f => f.Keyword(k => k.Name("keyword")))) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field1 = "apple", Field4 = 1 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "banana", Field4 = 2 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "cherry", Field4 = 3 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "date", Field4 = 4 } } } + ]); + await Client.Indices.RefreshAsync(index); + + 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)"); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Aggregations(a => a + .Nested("nested_nested", n => n + .Path("nested") + .Aggregations(na => na + .Terms("terms_nested.field1", t => t + .Field("nested.field1.keyword") + .Include(new string[] { "apple", "banana", "cherry" }) + .Meta(m => m.Add("@field_type", "text"))) + .Terms("terms_nested.field4", t => t + .Field("nested.field4") + .Include(new string[] { "1", "2", "3" }) + .Meta(m => m.Add("@field_type", "integer"))))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + } + + [Fact] + public async Task NestedAggregation_WithExcludeCommaSeparatedValues_FiltersCorrectly() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1).Fields(f => f.Keyword(k => k.Name("keyword")))) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field1 = "apple", Field4 = 1 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "banana", Field4 = 2 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "cherry", Field4 = 3 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "date", Field4 = 4 } } } + ]); + await Client.Indices.RefreshAsync(index); + + 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)"); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Aggregations(a => a + .Nested("nested_nested", n => n + .Path("nested") + .Aggregations(na => na + .Terms("terms_nested.field1", t => t + .Field("nested.field1.keyword") + .Exclude(new string[] { "date" }) + .Meta(m => m.Add("@field_type", "text"))) + .Terms("terms_nested.field4", t => t + .Field("nested.field4") + .Exclude(new string[] { "4" }) + .Meta(m => m.Add("@field_type", "integer"))))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + } + + [Fact] + public async Task NestedDefaultSearch_WithNestedFieldInDefaultFields_SearchesNestedFields() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType + { + Field1 = "parent", + Nested = { new MyType { Field1 = "special_value" } } + }, + new MyNestedType + { + Field1 = "other_parent", + Nested = { new MyType { Field1 = "normal_value" } } + } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c + .SetLoggerFactory(Log) + .UseMappings(Client) + .UseNested() + .SetDefaultFields(new[] { "field1", "nested.field1" })); + + // Act + var result = await processor.BuildQueryAsync("special_value", new ElasticQueryVisitorContext().UseSearchMode()); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); + string actualRequest = actualResponse.GetRequest(); + _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")))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + Assert.Equal(1, actualResponse.Total); + } + + [Fact] + public async Task NestedMixedOperations_WithQueryAndAggregation_HandlesNestedContextCorrectly() + { + // Arrange + string index = CreateRandomIndex(d => d.Properties(p => p + .Text(e => e.Name(n => n.Field1)) + .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 + .Text(e => e.Name(n => n.Field1).Fields(f => f.Keyword(k => k.Name("keyword")))) + .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) + )) + )); + + await Client.IndexManyAsync([ + new MyNestedType { Nested = { new MyType { Field1 = "high", Field4 = 10 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "medium", Field4 = 5 } } }, + new MyNestedType { Nested = { new MyType { Field1 = "low", Field4 = 1 } } } + ]); + await Client.Indices.RefreshAsync(index); + + var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); + + // Act - Query with nested field filter + var queryResult = await processor.BuildQueryAsync("nested.field4:>=5", new ElasticQueryVisitorContext { UseScoring = true }); + + // Act - Aggregation on nested fields + var aggResult = await processor.BuildAggregationsAsync("terms:nested.field1 max:nested.field4"); + + // Assert + var actualResponse = Client.Search(d => d.Index(index).Query(_ => queryResult).Aggregations(aggResult)); + string actualRequest = actualResponse.GetRequest(); + _logger.LogInformation("Actual: {Request}", actualRequest); + + var expectedResponse = Client.Search(d => d.Index(index) + .Query(q => q.Nested(n => n + .Path("nested") + .Query(q2 => q2.TermRange(r => r.Field("nested.field4").GreaterThanOrEquals("5"))))) + .Aggregations(a => a + .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"))))))); + + string expectedRequest = expectedResponse.GetRequest(); + _logger.LogInformation("Expected: {Request}", expectedRequest); + + Assert.Equal(expectedRequest, actualRequest); + Assert.Equal(expectedResponse.Total, actualResponse.Total); + Assert.Equal(2, actualResponse.Total); // Should match high and medium + } +} diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs index 7ea8d47a..75b6c9eb 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Foundatio.Parsers.ElasticQueries.Extensions; using Foundatio.Parsers.ElasticQueries.Visitors; @@ -839,571 +838,6 @@ await Client.IndexManyAsync([ Assert.Equal(expectedResponse.Total, actualResponse.Total); } - [Fact] - public async Task NestedFilterProcessor() - { - string index = CreateRandomIndex(d => d.Properties(p => p - .Text(e => e.Name(n => n.Field1).Index()) - .Text(e => e.Name(n => n.Field2).Index()) - .Text(e => e.Name(n => n.Field3).Index()) - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 - .Text(e => e.Name(n => n.Field1).Index()) - .Text(e => e.Name(n => n.Field2).Index()) - .Text(e => e.Name(n => n.Field3).Index()) - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - )) - )); - await Client.IndexManyAsync([ - new MyNestedType - { - Field1 = "value1", - Field2 = "value2", - Nested = { new MyType { Field1 = "value1", Field4 = 4 } } - }, - new MyNestedType { Field1 = "value2", Field2 = "value2" }, - new MyNestedType { Field1 = "value1", Field2 = "value4" } - ]); - await Client.Indices.RefreshAsync(index); - - var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseFieldMap(new FieldMap { { "blah", "nested" } }).UseMappings(Client).UseNested()); - var result = await processor.BuildQueryAsync("field1:value1 blah:(blah.field1:value1)", new ElasticQueryVisitorContext().UseScoring()); - - var actualResponse = Client.Search(d => d.Query(_ => result)); - string actualRequest = actualResponse.GetRequest(); - _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")))))); - - string expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - - result = await processor.BuildQueryAsync("field1:value1 blah:(blah.field1:value1 blah.field4:4)", new ElasticQueryVisitorContext().UseScoring()); - - actualResponse = Client.Search(d => d.Query(_ => result)); - actualRequest = actualResponse.GetRequest(); - _logger.LogInformation("Actual: {Request}", actualRequest); - - 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")) - && q2.Term("nested.field4", "4"))))); - - expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - } - - [Fact] - public async Task NestedFilterProcessor2() - { - string index = CreateRandomIndex(d => d.Properties(p => p - .Text(e => e.Name(n => n.Field1).Index()) - .Text(e => e.Name(n => n.Field2).Index()) - .Text(e => e.Name(n => n.Field3).Index()) - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 - .Text(e => e.Name(n => n.Field1).Index()) - .Text(e => e.Name(n => n.Field2).Index()) - .Text(e => e.Name(n => n.Field3).Index()) - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - )) - )); - - await Client.IndexManyAsync([ - new MyNestedType - { - Field1 = "value1", - Field2 = "value2", - Nested = { new MyType { Field1 = "value1", Field4 = 4 } } - }, - new MyNestedType { Field1 = "value2", Field2 = "value2" }, - new MyNestedType { Field1 = "value1", Field2 = "value4", Field3 = "value3" } - ]); - await Client.Indices.RefreshAsync(index); - - var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); - var result = await processor.BuildQueryAsync("field1:value1 nested:(nested.field1:value1 nested.field4:4 nested.field3:value3)", - new ElasticQueryVisitorContext { UseScoring = true }); - - var actualResponse = Client.Search(d => d.Query(_ => result)); - string actualRequest = actualResponse.GetRequest(); - _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")) - && q2.Term("nested.field4", "4") - && q2.Match(m => m.Field("nested.field3").Query("value3")))))); - - string expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - } - - [Fact] - public async Task NestedIndividualFieldQuery_WithSingleNestedField_WrapsInNestedQuery() - { - // Arrange - string index = CreateRandomIndex(d => d.Properties(p => p - .Text(e => e.Name(n => n.Field1)) - .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 - .Text(e => e.Name(n => n.Field1)) - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - )) - )); - - await Client.IndexManyAsync([ - new MyNestedType - { - Field1 = "parent1", - Nested = { new MyType { Field1 = "child1", Field4 = 5 } } - }, - new MyNestedType - { - Field1 = "parent2", - Nested = { new MyType { Field1 = "child2", Field4 = 3 } } - } - ]); - await Client.Indices.RefreshAsync(index); - - var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); - - // Act - var result = await processor.BuildQueryAsync("nested.field4:5", new ElasticQueryVisitorContext { UseScoring = true }); - - // Assert - var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); - string actualRequest = actualResponse.GetRequest(); - _logger.LogInformation("Actual: {Request}", actualRequest); - - var expectedResponse = Client.Search(d => d.Index(index) - .Query(q => q.Nested(n => n - .Path(p => p.Nested) - .Query(q2 => q2.Term("nested.field4", "5"))))); - - string expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - Assert.Equal(1, actualResponse.Total); - } - - [Fact] - public async Task NestedIndividualFieldQuery_WithMultipleNestedFieldsOrCondition_CombinesIntoSingleNestedQuery() - { - // Arrange - string index = CreateRandomIndex(d => d.Properties(p => p - .Text(e => e.Name(n => n.Field1)) - .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 - .Text(e => e.Name(n => n.Field1)) - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - )) - )); - - await Client.IndexManyAsync([ - new MyNestedType - { - Field1 = "parent1", - Nested = { new MyType { Field1 = "target", Field4 = 5 } } - }, - new MyNestedType - { - Field1 = "parent2", - Nested = { new MyType { Field1 = "other", Field4 = 10 } } - } - ]); - await Client.Indices.RefreshAsync(index); - - var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); - - // Act - var result = await processor.BuildQueryAsync("nested.field1:target OR nested.field4:10", new ElasticQueryVisitorContext { UseScoring = true }); - - // Assert - var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); - string actualRequest = actualResponse.GetRequest(); - _logger.LogInformation("Actual: {Request}", actualRequest); - - var expectedResponse = Client.Search(d => d.Index(index) - .Query(q => q.Nested(n => n - .Path(p => p.Nested) - .Query(q2 => q2.Match(m => m.Field("nested.field1").Query("target")) - || q2.Term("nested.field4", "10"))))); - - string expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - Assert.Equal(2, actualResponse.Total); - } - - [Fact] - public async Task NestedIndividualFieldQuery_WithRangeQuery_WrapsInNestedQuery() - { - // Arrange - string index = CreateRandomIndex(d => d.Properties(p => p - .Text(e => e.Name(n => n.Field1)) - .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - )) - )); - - await Client.IndexManyAsync([ - new MyNestedType { Nested = { new MyType { Field4 = 5 } } }, - new MyNestedType { Nested = { new MyType { Field4 = 15 } } }, - new MyNestedType { Nested = { new MyType { Field4 = 25 } } } - ]); - await Client.Indices.RefreshAsync(index); - - var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); - - // Act - var result = await processor.BuildQueryAsync("nested.field4:[10 TO 20]", new ElasticQueryVisitorContext { UseScoring = true }); - - // Assert - var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); - string actualRequest = actualResponse.GetRequest(); - _logger.LogInformation("Actual: {Request}", actualRequest); - - var expectedResponse = Client.Search(d => d.Index(index) - .Query(q => q.Nested(n => n - .Path(p => p.Nested) - .Query(q2 => q2.TermRange(r => r.Field("nested.field4").GreaterThanOrEquals("10").LessThanOrEquals("20")))))); - - string expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - Assert.Equal(1, actualResponse.Total); - } - - [Fact] - public async Task NestedAggregation_WithSingleNestedField_AutomaticallyWrapsInNestedAggregation() - { - // Arrange - string index = CreateRandomIndex(d => d.Properties(p => p - .Text(e => e.Name(n => n.Field1)) - .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - )) - )); - - await Client.IndexManyAsync([ - new MyNestedType { Nested = { new MyType { Field4 = 5 } } }, - new MyNestedType { Nested = { new MyType { Field4 = 10 } } }, - new MyNestedType { Nested = { new MyType { Field4 = 5 } } } - ]); - await Client.Indices.RefreshAsync(index); - - var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); - - // Act - var result = await processor.BuildAggregationsAsync("terms:nested.field4"); - - // Assert - var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); - string actualRequest = actualResponse.GetRequest(); - _logger.LogInformation("Actual: {Request}", actualRequest); - - var expectedResponse = Client.Search(d => d.Index(index) - .Aggregations(a => a - .Nested("nested_nested", n => n - .Path("nested") - .Aggregations(na => na - .Terms("terms_nested.field4", t => t - .Field("nested.field4") - .Meta(m => m.Add("@field_type", "integer"))))))); - - string expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - } - - [Fact] - public async Task NestedAggregation_WithMultipleNestedFields_CombinesIntoSingleNestedAggregation() - { - // Arrange - string index = CreateRandomIndex(d => d.Properties(p => p - .Text(e => e.Name(n => n.Field1)) - .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 - .Text(e => e.Name(n => n.Field1)) - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - )) - )); - - await Client.IndexManyAsync([ - new MyNestedType { Nested = { new MyType { Field1 = "test", Field4 = 5 } } }, - new MyNestedType { Nested = { new MyType { Field1 = "other", Field4 = 10 } } } - ]); - await Client.Indices.RefreshAsync(index); - - var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); - - // Act - var result = await processor.BuildAggregationsAsync("terms:nested.field1 terms:nested.field4 max:nested.field4"); - - // Assert - var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); - string actualRequest = actualResponse.GetRequest(); - _logger.LogInformation("Actual: {Request}", actualRequest); - - var expectedResponse = Client.Search(d => d.Index(index) - .Aggregations(a => a - .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"))) - .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"))))))); - - string expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - } - - [Fact] - public async Task NestedAggregation_WithIncludeCommaSeparatedValues_FiltersCorrectly() - { - // Arrange - string index = CreateRandomIndex(d => d.Properties(p => p - .Text(e => e.Name(n => n.Field1)) - .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 - .Text(e => e.Name(n => n.Field1).Fields(f => f.Keyword(k => k.Name("keyword")))) - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - )) - )); - - await Client.IndexManyAsync([ - new MyNestedType { Nested = { new MyType { Field1 = "apple", Field4 = 1 } } }, - new MyNestedType { Nested = { new MyType { Field1 = "banana", Field4 = 2 } } }, - new MyNestedType { Nested = { new MyType { Field1 = "cherry", Field4 = 3 } } }, - new MyNestedType { Nested = { new MyType { Field1 = "date", Field4 = 4 } } } - ]); - await Client.Indices.RefreshAsync(index); - - 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)"); - - // Assert - var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); - string actualRequest = actualResponse.GetRequest(); - _logger.LogInformation("Actual: {Request}", actualRequest); - - var expectedResponse = Client.Search(d => d.Index(index) - .Aggregations(a => a - .Nested("nested_nested", n => n - .Path("nested") - .Aggregations(na => na - .Terms("terms_nested.field1", t => t - .Field("nested.field1.keyword") - .Include(new string[] { "apple", "banana", "cherry" }) - .Meta(m => m.Add("@field_type", "text"))) - .Terms("terms_nested.field4", t => t - .Field("nested.field4") - .Include(new string[] { "1", "2", "3" }) - .Meta(m => m.Add("@field_type", "integer"))))))); - - string expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - } - - [Fact] - public async Task NestedAggregation_WithExcludeCommaSeparatedValues_FiltersCorrectly() - { - // Arrange - string index = CreateRandomIndex(d => d.Properties(p => p - .Text(e => e.Name(n => n.Field1)) - .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 - .Text(e => e.Name(n => n.Field1).Fields(f => f.Keyword(k => k.Name("keyword")))) - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - )) - )); - - await Client.IndexManyAsync([ - new MyNestedType { Nested = { new MyType { Field1 = "apple", Field4 = 1 } } }, - new MyNestedType { Nested = { new MyType { Field1 = "banana", Field4 = 2 } } }, - new MyNestedType { Nested = { new MyType { Field1 = "cherry", Field4 = 3 } } }, - new MyNestedType { Nested = { new MyType { Field1 = "date", Field4 = 4 } } } - ]); - await Client.Indices.RefreshAsync(index); - - 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)"); - - // Assert - var actualResponse = Client.Search(d => d.Index(index).Aggregations(result)); - string actualRequest = actualResponse.GetRequest(); - _logger.LogInformation("Actual: {Request}", actualRequest); - - var expectedResponse = Client.Search(d => d.Index(index) - .Aggregations(a => a - .Nested("nested_nested", n => n - .Path("nested") - .Aggregations(na => na - .Terms("terms_nested.field1", t => t - .Field("nested.field1.keyword") - .Exclude(new string[] { "date" }) - .Meta(m => m.Add("@field_type", "text"))) - .Terms("terms_nested.field4", t => t - .Field("nested.field4") - .Exclude(new string[] { "4" }) - .Meta(m => m.Add("@field_type", "integer"))))))); - - string expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - } - - [Fact] - public async Task NestedDefaultSearch_WithNestedFieldInDefaultFields_SearchesNestedFields() - { - // Arrange - string index = CreateRandomIndex(d => d.Properties(p => p - .Text(e => e.Name(n => n.Field1)) - .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 - .Text(e => e.Name(n => n.Field1)) - )) - )); - - await Client.IndexManyAsync([ - new MyNestedType - { - Field1 = "parent", - Nested = { new MyType { Field1 = "special_value" } } - }, - new MyNestedType - { - Field1 = "other_parent", - Nested = { new MyType { Field1 = "normal_value" } } - } - ]); - await Client.Indices.RefreshAsync(index); - - var processor = new ElasticQueryParser(c => c - .SetLoggerFactory(Log) - .UseMappings(Client) - .UseNested() - .SetDefaultFields(new[] { "field1", "nested.field1" })); - - // Act - var result = await processor.BuildQueryAsync("special_value", new ElasticQueryVisitorContext().UseSearchMode()); - - // Assert - var actualResponse = Client.Search(d => d.Index(index).Query(_ => result)); - string actualRequest = actualResponse.GetRequest(); - _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")))))); - - string expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - Assert.Equal(1, actualResponse.Total); - } - - [Fact] - public async Task NestedMixedOperations_WithQueryAndAggregation_HandlesNestedContextCorrectly() - { - // Arrange - string index = CreateRandomIndex(d => d.Properties(p => p - .Text(e => e.Name(n => n.Field1)) - .Nested(r => r.Name(n => n.Nested.First()).Properties(p1 => p1 - .Text(e => e.Name(n => n.Field1).Fields(f => f.Keyword(k => k.Name("keyword")))) - .Number(e => e.Name(n => n.Field4).Type(NumberType.Integer)) - )) - )); - - await Client.IndexManyAsync([ - new MyNestedType { Nested = { new MyType { Field1 = "high", Field4 = 10 } } }, - new MyNestedType { Nested = { new MyType { Field1 = "medium", Field4 = 5 } } }, - new MyNestedType { Nested = { new MyType { Field1 = "low", Field4 = 1 } } } - ]); - await Client.Indices.RefreshAsync(index); - - var processor = new ElasticQueryParser(c => c.SetLoggerFactory(Log).UseMappings(Client).UseNested()); - - // Act - Query with nested field filter - var queryResult = await processor.BuildQueryAsync("nested.field4:>=5", new ElasticQueryVisitorContext { UseScoring = true }); - - // Act - Aggregation on nested fields - var aggResult = await processor.BuildAggregationsAsync("terms:nested.field1 max:nested.field4"); - - // Assert - var actualResponse = Client.Search(d => d.Index(index).Query(_ => queryResult).Aggregations(aggResult)); - string actualRequest = actualResponse.GetRequest(); - _logger.LogInformation("Actual: {Request}", actualRequest); - - var expectedResponse = Client.Search(d => d.Index(index) - .Query(q => q.Nested(n => n - .Path("nested") - .Query(q2 => q2.TermRange(r => r.Field("nested.field4").GreaterThanOrEquals("5"))))) - .Aggregations(a => a - .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"))))))); - - string expectedRequest = expectedResponse.GetRequest(); - _logger.LogInformation("Expected: {Request}", expectedRequest); - - Assert.Equal(expectedRequest, actualRequest); - Assert.Equal(expectedResponse.Total, actualResponse.Total); - Assert.Equal(2, actualResponse.Total); // Should match high and medium - } - [Fact] public async Task CanGenerateMatchQuery() { From 9a4f487b8133d7b1fc57dfc202ddb5bc772a067a Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Mon, 21 Jul 2025 13:43:44 -0500 Subject: [PATCH 5/6] Moves MyNestedType to ElasticNestedQueryParserTests Moves the MyNestedType class definition from ElasticQueryParserTests to ElasticNestedQueryParserTests. This change centralizes the type definition where it is primarily used and avoids code duplication, improving code organization and maintainability. --- .../ElasticNestedQueryParserTests.cs | 13 ++++++++++++ .../ElasticQueryParserTests.cs | 20 ++++--------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs index 966fd4ed..85264b71 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Foundatio.Parsers.ElasticQueries.Extensions; @@ -581,4 +582,16 @@ await Client.IndexManyAsync([ Assert.Equal(expectedResponse.Total, actualResponse.Total); Assert.Equal(2, actualResponse.Total); // Should match high and medium } + + + public class MyNestedType + { + public string Field1 { get; set; } + public string Field2 { get; set; } + public string Field3 { get; set; } + public int Field4 { get; set; } + public string Field5 { get; set; } + public string Payload { get; set; } + public IList Nested { get; set; } = new List(); + } } diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs index 75b6c9eb..75f9172a 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticQueryParserTests.cs @@ -1237,10 +1237,10 @@ public async Task CanParseSort() [Fact] public async Task CanHandleSpacedFields() { - string index = CreateRandomIndex(); + string index = CreateRandomIndex(); await Client.IndexManyAsync([ - new MyNestedType + new ElasticNestedQueryParserTests.MyNestedType { Field1 = "value1", Field2 = "value2", @@ -1257,8 +1257,8 @@ await Client.IndexManyAsync([ } ] }, - new MyNestedType { Field1 = "value2", Field2 = "value2" }, - new MyNestedType { Field1 = "value1", Field2 = "value4" } + new ElasticNestedQueryParserTests.MyNestedType { Field1 = "value2", Field2 = "value2" }, + new ElasticNestedQueryParserTests.MyNestedType { Field1 = "value1", Field2 = "value4" } ], index); await Client.Indices.RefreshAsync(index); @@ -1415,20 +1415,8 @@ public class MyType public Dictionary Data { get; set; } = new Dictionary(); } -public class MyNestedType -{ - public string Field1 { get; set; } - public string Field2 { get; set; } - public string Field3 { get; set; } - public int Field4 { get; set; } - public string Field5 { get; set; } - public string Payload { get; set; } - public IList Nested { get; set; } = new List(); -} - public class UpdateFixedTermFieldToDateFixedExistsQueryVisitor : ChainableQueryVisitor { - public override void Visit(TermNode node, IQueryVisitorContext context) { if (!String.Equals(node.Field, "fixed", StringComparison.OrdinalIgnoreCase)) From 6cf99c8ba9d313b496b090091b4cbc41b87491e9 Mon Sep 17 00:00:00 2001 From: Blake Niemyjski Date: Fri, 25 Jul 2025 09:57:37 -0500 Subject: [PATCH 6/6] Fixes ElasticMappingResolver tests and nested query parsing Updates the tests and nested query parsing logic to correctly reference the `MyNestedType` class, resolving type resolution issues. Additionally, renames one of the nested query parser tests for clarity. --- .../Visitors/CombineQueriesVisitor.cs | 1 - .../ElasticMappingResolverTests.cs | 28 +++++++++---------- .../ElasticNestedQueryParserTests.cs | 14 +++++----- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs index 390b5609..5f2ddd1b 100644 --- a/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs +++ b/src/Foundatio.Parsers.ElasticQueries/Visitors/CombineQueriesVisitor.cs @@ -11,7 +11,6 @@ namespace Foundatio.Parsers.ElasticQueries.Visitors; public class CombineQueriesVisitor : ChainableQueryVisitor { - public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context) { await base.VisitAsync(node, context).ConfigureAwait(false); diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticMappingResolverTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticMappingResolverTests.cs index f3b19b25..e63c4655 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticMappingResolverTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticMappingResolverTests.cs @@ -13,10 +13,10 @@ public ElasticMappingResolverTests(ITestOutputHelper output, ElasticsearchFixtur Log.DefaultLogLevel = Microsoft.Extensions.Logging.LogLevel.Trace; } - private ITypeMapping MapMyNestedType(TypeMappingDescriptor m) + private ITypeMapping MapMyNestedType(TypeMappingDescriptor m) { return m - .AutoMap() + .AutoMap() .Dynamic() .DynamicTemplates(t => t.DynamicTemplate("idx_text", t => t.Match("text*").Mapping(m => m.Text(mp => mp.AddKeywordAndSortFields())))) .Properties(p => p @@ -30,10 +30,10 @@ private ITypeMapping MapMyNestedType(TypeMappingDescriptor m) [Fact] public void CanResolveCodedProperty() { - string index = CreateRandomIndex(MapMyNestedType); + string index = CreateRandomIndex(MapMyNestedType); Client.IndexMany([ - new MyNestedType + new ElasticNestedQueryParserTests.MyNestedType { Field1 = "value1", Field2 = "value2", @@ -50,12 +50,12 @@ public void CanResolveCodedProperty() } ] }, - new MyNestedType { Field1 = "value2", Field2 = "value2" }, - new MyNestedType { Field1 = "value1", Field2 = "value4" } + new ElasticNestedQueryParserTests.MyNestedType { Field1 = "value2", Field2 = "value2" }, + new ElasticNestedQueryParserTests.MyNestedType { Field1 = "value1", Field2 = "value4" } ], index); Client.Indices.Refresh(index); - var resolver = ElasticMappingResolver.Create(MapMyNestedType, Client, index, _logger); + var resolver = ElasticMappingResolver.Create(MapMyNestedType, Client, index, _logger); var payloadProperty = resolver.GetMappingProperty("payload"); Assert.IsType(payloadProperty); @@ -65,10 +65,10 @@ public void CanResolveCodedProperty() [Fact] public void CanResolveProperties() { - string index = CreateRandomIndex(MapMyNestedType); + string index = CreateRandomIndex(MapMyNestedType); Client.IndexMany([ - new MyNestedType + new ElasticNestedQueryParserTests.MyNestedType { Field1 = "value1", Field2 = "value2", @@ -85,12 +85,12 @@ public void CanResolveProperties() } ] }, - new MyNestedType { Field1 = "value2", Field2 = "value2" }, - new MyNestedType { Field1 = "value1", Field2 = "value4" } + new ElasticNestedQueryParserTests.MyNestedType { Field1 = "value2", Field2 = "value2" }, + new ElasticNestedQueryParserTests.MyNestedType { Field1 = "value1", Field2 = "value4" } ], index); Client.Indices.Refresh(index); - var resolver = ElasticMappingResolver.Create(MapMyNestedType, Client, index, _logger); + var resolver = ElasticMappingResolver.Create(MapMyNestedType, Client, index, _logger); string dynamicTextAggregation = resolver.GetAggregationsFieldName("nested.data.text-0001"); Assert.Equal("nested.data.text-0001.keyword", dynamicTextAggregation); @@ -131,7 +131,7 @@ public void CanResolveProperties() var field4Property = resolver.GetMappingProperty("Field4"); Assert.IsType(field4Property); - var field4ReflectionProperty = resolver.GetMappingProperty(new Field(typeof(MyNestedType).GetProperty("Field4"))); + var field4ReflectionProperty = resolver.GetMappingProperty(new Field(typeof(ElasticNestedQueryParserTests.MyNestedType).GetProperty("Field4"))); Assert.IsType(field4ReflectionProperty); var field4ExpressionProperty = resolver.GetMappingProperty(new Field(GetObjectPath(p => p.Field4))); @@ -172,7 +172,7 @@ public void CanResolveProperties() Assert.IsType(nestedDataProperty); } - private static Expression GetObjectPath(Expression> objectPath) + private static Expression GetObjectPath(Expression> objectPath) { return objectPath; } diff --git a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs index 85264b71..39cadd8b 100644 --- a/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs @@ -19,7 +19,7 @@ public ElasticNestedQueryParserTests(ITestOutputHelper output, ElasticsearchFixt } [Fact] - public async Task NestedFilterProcessor() + public async Task NestedFilterProcessorWithFieldMapAsync() { string index = CreateRandomIndex(d => d.Properties(p => p .Text(e => e.Name(n => n.Field1).Index()) @@ -91,7 +91,7 @@ await Client.IndexManyAsync([ } [Fact] - public async Task NestedFilterProcessor2() + public async Task NestedFilterProcessor() { string index = CreateRandomIndex(d => d.Properties(p => p .Text(e => e.Name(n => n.Field1).Index()) @@ -410,11 +410,11 @@ await Client.IndexManyAsync([ .Aggregations(na => na .Terms("terms_nested.field1", t => t .Field("nested.field1.keyword") - .Include(new string[] { "apple", "banana", "cherry" }) + .Include(["apple", "banana", "cherry"]) .Meta(m => m.Add("@field_type", "text"))) .Terms("terms_nested.field4", t => t .Field("nested.field4") - .Include(new string[] { "1", "2", "3" }) + .Include(["1", "2", "3"]) .Meta(m => m.Add("@field_type", "integer"))))))); string expectedRequest = expectedResponse.GetRequest(); @@ -461,11 +461,11 @@ await Client.IndexManyAsync([ .Aggregations(na => na .Terms("terms_nested.field1", t => t .Field("nested.field1.keyword") - .Exclude(new string[] { "date" }) + .Exclude(["date"]) .Meta(m => m.Add("@field_type", "text"))) .Terms("terms_nested.field4", t => t .Field("nested.field4") - .Exclude(new string[] { "4" }) + .Exclude(["4"]) .Meta(m => m.Add("@field_type", "integer"))))))); string expectedRequest = expectedResponse.GetRequest(); @@ -504,7 +504,7 @@ await Client.IndexManyAsync([ .SetLoggerFactory(Log) .UseMappings(Client) .UseNested() - .SetDefaultFields(new[] { "field1", "nested.field1" })); + .SetDefaultFields(["field1", "nested.field1"])); // Act var result = await processor.BuildQueryAsync("special_value", new ElasticQueryVisitorContext().UseSearchMode());