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/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 new file mode 100644 index 00000000..39cadd8b --- /dev/null +++ b/tests/Foundatio.Parsers.ElasticQueries.Tests/ElasticNestedQueryParserTests.cs @@ -0,0 +1,597 @@ +using System.Collections.Generic; +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 NestedFilterProcessorWithFieldMapAsync() + { + 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 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", 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(["apple", "banana", "cherry"]) + .Meta(m => m.Add("@field_type", "text"))) + .Terms("terms_nested.field4", t => t + .Field("nested.field4") + .Include(["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(["date"]) + .Meta(m => m.Add("@field_type", "text"))) + .Terms("terms_nested.field4", t => t + .Field("nested.field4") + .Exclude(["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(["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 + } + + + 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 a4e08be1..75f9172a 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; @@ -225,7 +224,7 @@ public async Task ShouldHandleMultipleTermsForAnalyzedFields() } [Fact] - public void CanGetMappingsFromCode() + public async Task CanGetMappingsFromCode() { TypeMappingDescriptor GetCodeMappings(TypeMappingDescriptor d) => d.Dynamic() @@ -238,8 +237,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)); @@ -839,127 +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 CanGenerateMatchQuery() { @@ -1359,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", @@ -1379,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); @@ -1537,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))