Skip to content

Commit b160d4a

Browse files
committed
Adds nested field support in tests
Introduces nested field support and related tests. This commit addresses the need for testing nested fields. It includes a new test file, updates configurations, and adds utilities for generating test data with nested properties. A nested field is also added to the default search fields.
1 parent 5477051 commit b160d4a

File tree

4 files changed

+287
-44
lines changed

4 files changed

+287
-44
lines changed

tests/Foundatio.Repositories.Elasticsearch.Tests/AggregationQueryTests.cs

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Linq;
55
using System.Threading.Tasks;
66
using Exceptionless.DateTimeExtensions;
7-
using Foundatio.Repositories.Elasticsearch.Extensions;
87
using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models;
98
using Foundatio.Repositories.Models;
109
using Microsoft.Extensions.Time.Testing;
@@ -106,47 +105,6 @@ await _employeeRepository.AddAsync(new Employee
106105
Assert.Equal(1, result.Aggregations.Cardinality("cardinality_twitter").Value);
107106
}
108107

109-
[Fact]
110-
public async Task GetNestedAggregationsAsync()
111-
{
112-
var utcToday = new DateTimeOffset(DateTime.UtcNow.Year, 1, 1, 12, 0, 0, TimeSpan.FromHours(5));
113-
var employees = new List<Employee> {
114-
EmployeeGenerator.Generate(nextReview: utcToday.SubtractDays(2)),
115-
EmployeeGenerator.Generate(nextReview: utcToday.SubtractDays(1))
116-
};
117-
employees[0].Id = "employee1";
118-
employees[0].Id = "employee2";
119-
employees[0].PeerReviews = new PeerReview[] { new PeerReview { ReviewerEmployeeId = employees[1].Id, Rating = 4 } };
120-
employees[1].PeerReviews = new PeerReview[] { new PeerReview { ReviewerEmployeeId = employees[0].Id, Rating = 5 } };
121-
122-
await _employeeRepository.AddAsync(employees, o => o.ImmediateConsistency());
123-
124-
var nestedAggQuery = _client.Search<Employee>(d => d.Index("employees").Aggregations(a => a
125-
.Nested("nested_reviewRating", h => h.Path("peerReviews")
126-
.Aggregations(a1 => a1.Terms("terms_rating", t => t.Field("peerReviews.rating").Meta(m => m.Add("@field_type", "integer")))))
127-
));
128-
129-
var result = nestedAggQuery.Aggregations.ToAggregations();
130-
Assert.Single(result);
131-
Assert.Equal(2, ((result["nested_reviewRating"] as Foundatio.Repositories.Models.SingleBucketAggregate).Aggregations["terms_rating"] as Foundatio.Repositories.Models.BucketAggregate).Items.Count);
132-
133-
var nestedAggQueryWithFilter = _client.Search<Employee>(d => d.Index("employees").Aggregations(a => a
134-
.Nested("nested_reviewRating", h => h.Path("peerReviews")
135-
.Aggregations(a1 => a1
136-
.Filter("user_" + employees[0].Id, f => f.Filter(q => q.Term(t => t.Field("peerReviews.reviewerEmployeeId").Value(employees[0].Id)))
137-
.Aggregations(a2 => a2.Terms("terms_rating", t => t.Field("peerReviews.rating").Meta(m => m.Add("@field_type", "integer")))))
138-
))));
139-
140-
result = nestedAggQueryWithFilter.Aggregations.ToAggregations();
141-
Assert.Single(result);
142-
143-
var filteredAgg = ((result["nested_reviewRating"] as Foundatio.Repositories.Models.SingleBucketAggregate).Aggregations["user_" + employees[0].Id] as Foundatio.Repositories.Models.SingleBucketAggregate);
144-
Assert.NotNull(filteredAgg);
145-
Assert.Single(filteredAgg.Aggregations.Terms("terms_rating").Buckets);
146-
Assert.Equal("5", filteredAgg.Aggregations.Terms("terms_rating").Buckets.First().Key);
147-
Assert.Equal(1, filteredAgg.Aggregations.Terms("terms_rating").Buckets.First().Total);
148-
}
149-
150108
[Fact]
151109
public async Task GetAliasedNumberAggregationThatCausesMappingAsync()
152110
{
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Exceptionless.DateTimeExtensions;
6+
using Foundatio.Repositories.Elasticsearch.Extensions;
7+
using Foundatio.Repositories.Elasticsearch.Tests.Repositories.Models;
8+
using Foundatio.Repositories.Models;
9+
using Newtonsoft.Json;
10+
using Xunit;
11+
using Xunit.Abstractions;
12+
13+
namespace Foundatio.Repositories.Elasticsearch.Tests;
14+
15+
public sealed class NestedFieldTests : ElasticRepositoryTestBase
16+
{
17+
private readonly IEmployeeRepository _employeeRepository;
18+
19+
public NestedFieldTests(ITestOutputHelper output) : base(output)
20+
{
21+
_employeeRepository = new EmployeeRepository(_configuration);
22+
}
23+
24+
public override async Task InitializeAsync()
25+
{
26+
await base.InitializeAsync();
27+
await RemoveDataAsync();
28+
}
29+
30+
[Fact]
31+
public async Task FindAsync_WithNestedPeerReviewOrCondition_ReturnsMatchingEmployees()
32+
{
33+
// Arrange
34+
List<Employee> employees = [
35+
EmployeeGenerator.Generate("alice_123", "Alice", peerReviews: [
36+
new PeerReview { ReviewerEmployeeId = "bob_456", Rating = 5 }
37+
]),
38+
EmployeeGenerator.Generate("bob_456", "Bob", peerReviews: [
39+
new PeerReview { ReviewerEmployeeId = "alice_123", Rating = 4 }
40+
]),
41+
EmployeeGenerator.Generate("charlie_789", "Charlie", peerReviews: [
42+
new PeerReview { ReviewerEmployeeId = "alice_123", Rating = 2 }
43+
])
44+
];
45+
46+
await _employeeRepository.AddAsync(employees, o => o.ImmediateConsistency());
47+
var searchRepository = (ISearchableReadOnlyRepository<Employee>)_employeeRepository;
48+
49+
// Act
50+
var results = await searchRepository.FindAsync(q => q.FilterExpression("peerReviews.rating:>=4 OR peerReviews.reviewerEmployeeId:bob_456"));
51+
52+
// Assert
53+
Assert.Equal(2, results.Documents.Count);
54+
Assert.Contains(results.Documents, e => e.Name == "Alice");
55+
Assert.Contains(results.Documents, e => e.Name == "Bob");
56+
}
57+
58+
[Fact]
59+
public async Task CountAsync_WithNestedPeerReviewAggregation_ReturnsAggregationData()
60+
{
61+
// Arrange
62+
List<Employee> employees = [
63+
EmployeeGenerator.Generate("alice_123", "Alice", peerReviews: [
64+
new PeerReview { ReviewerEmployeeId = "bob_456", Rating = 5 },
65+
new PeerReview { ReviewerEmployeeId = "charlie_789", Rating = 4 }
66+
]),
67+
EmployeeGenerator.Generate("bob_456", "Bob", peerReviews: [
68+
new PeerReview { ReviewerEmployeeId = "alice_123", Rating = 3 },
69+
new PeerReview { ReviewerEmployeeId = "charlie_789", Rating = 5 }
70+
]),
71+
EmployeeGenerator.Generate("charlie_789", "Charlie", peerReviews: [
72+
new PeerReview { ReviewerEmployeeId = "alice_123", Rating = 4 },
73+
new PeerReview { ReviewerEmployeeId = "bob_456", Rating = 2 }
74+
])
75+
];
76+
77+
await _employeeRepository.AddAsync(employees, o => o.ImmediateConsistency());
78+
79+
// Act
80+
var result = await _employeeRepository.CountAsync(q => q.AggregationsExpression("terms:peerReviews.rating"));
81+
82+
// Assert
83+
Assert.Equal(3, result.Total);
84+
Assert.Single(result.Aggregations);
85+
Assert.NotEmpty(result.Aggregations);
86+
87+
var nestedPeerReviewsAgg = result.Aggregations["nested_peerReviews"] as SingleBucketAggregate;
88+
Assert.NotNull(nestedPeerReviewsAgg);
89+
Assert.NotEmpty(nestedPeerReviewsAgg.Aggregations);
90+
}
91+
92+
[Fact]
93+
public async Task FindAsync_WithNestedFieldInDefaultSearch_ReturnsMatchingEmployee()
94+
{
95+
// Arrange
96+
const string specialReviewerId = "special_reviewer_123";
97+
List<Employee> employees =
98+
[
99+
EmployeeGenerator.Generate("alice_123", "Alice", peerReviews: [
100+
new PeerReview { ReviewerEmployeeId = specialReviewerId, Rating = 5 }
101+
]),
102+
EmployeeGenerator.Generate("bob_456", "Bob", peerReviews: [
103+
new PeerReview { ReviewerEmployeeId = "alice_123", Rating = 4 }
104+
]),
105+
EmployeeGenerator.Generate("charlie_789", "Charlie", peerReviews: [
106+
new PeerReview { ReviewerEmployeeId = "bob_456", Rating = 3 }
107+
])
108+
];
109+
110+
await _employeeRepository.AddAsync(employees, o => o.ImmediateConsistency());
111+
var searchRepository = (ISearchableReadOnlyRepository<Employee>)_employeeRepository;
112+
113+
// Act
114+
var results = await searchRepository.FindAsync(q => q.SearchExpression(specialReviewerId));
115+
116+
// Assert
117+
Assert.Single(results.Documents);
118+
Assert.Equal("Alice", results.Documents.Single().Name);
119+
}
120+
121+
[Fact]
122+
public async Task SearchAsync_WithNestedAggregations_ReturnsCorrectBuckets()
123+
{
124+
// Arrange
125+
var utcToday = new DateTimeOffset(DateTime.UtcNow.Year, 1, 1, 12, 0, 0, TimeSpan.FromHours(5));
126+
List<Employee> employees = [
127+
EmployeeGenerator.Generate("employee1", nextReview: utcToday.SubtractDays(2), peerReviews: [
128+
new PeerReview { ReviewerEmployeeId = "employee2", Rating = 4 }
129+
]),
130+
EmployeeGenerator.Generate("employee2", nextReview: utcToday.SubtractDays(1), peerReviews: [
131+
new PeerReview { ReviewerEmployeeId = "employee1", Rating = 5 }
132+
])
133+
];
134+
135+
await _employeeRepository.AddAsync(employees, o => o.ImmediateConsistency());
136+
137+
// Act
138+
var nestedAggQuery = _client.Search<Employee>(d => d.Index("employees").Aggregations(a => a
139+
.Nested("nested_reviewRating", h => h.Path("peerReviews")
140+
.Aggregations(a1 => a1.Terms("terms_rating", t => t.Field("peerReviews.rating").Meta(m => m.Add("@field_type", "integer")))))
141+
));
142+
143+
// Assert
144+
var result = nestedAggQuery.Aggregations.ToAggregations();
145+
Assert.Single(result);
146+
147+
var nestedReviewRatingAgg = result["nested_reviewRating"] as SingleBucketAggregate;
148+
var termsRatingAgg = nestedReviewRatingAgg.Aggregations["terms_rating"] as BucketAggregate;
149+
Assert.Equal(2, termsRatingAgg.Items.Count);
150+
151+
// Act - Test nested aggregation with filter
152+
var nestedAggQueryWithFilter = _client.Search<Employee>(d => d.Index("employees").Aggregations(a => a
153+
.Nested("nested_reviewRating", h => h.Path("peerReviews")
154+
.Aggregations(a1 => a1
155+
.Filter("user_" + employees[0].Id, f => f.Filter(q => q.Term(t => t.Field("peerReviews.reviewerEmployeeId").Value(employees[0].Id)))
156+
.Aggregations(a2 => a2.Terms("terms_rating", t => t.Field("peerReviews.rating").Meta(m => m.Add("@field_type", "integer")))))
157+
))));
158+
159+
// Assert - Verify filtered aggregation
160+
result = nestedAggQueryWithFilter.Aggregations.ToAggregations();
161+
Assert.Single(result);
162+
163+
var nestedReviewRatingFilteredAgg = result["nested_reviewRating"] as SingleBucketAggregate;
164+
var userFilteredAgg = nestedReviewRatingFilteredAgg.Aggregations["user_" + employees[0].Id] as SingleBucketAggregate;
165+
Assert.NotNull(userFilteredAgg);
166+
Assert.Single(userFilteredAgg.Aggregations.Terms("terms_rating").Buckets);
167+
Assert.Equal("5", userFilteredAgg.Aggregations.Terms("terms_rating").Buckets.First().Key);
168+
Assert.Equal(1, userFilteredAgg.Aggregations.Terms("terms_rating").Buckets.First().Total);
169+
}
170+
171+
[Fact]
172+
public async Task CountAsync_WithNestedLuceneBasedAggregations_ReturnsCorrectMetrics()
173+
{
174+
// Arrange
175+
var utcToday = new DateTimeOffset(DateTime.UtcNow.Year, 1, 1, 12, 0, 0, TimeSpan.FromHours(5));
176+
List<Employee> employees = [
177+
EmployeeGenerator.Generate("employee1", nextReview: utcToday.SubtractDays(2), peerReviews: [
178+
new PeerReview { ReviewerEmployeeId = "employee2", Rating = 4 },
179+
new PeerReview { ReviewerEmployeeId = "employee3", Rating = 5 }
180+
]),
181+
EmployeeGenerator.Generate("employee2", nextReview: utcToday.SubtractDays(1), peerReviews: [
182+
new PeerReview { ReviewerEmployeeId = "employee1", Rating = 5 },
183+
new PeerReview { ReviewerEmployeeId = "employee3", Rating = 3 }
184+
]),
185+
EmployeeGenerator.Generate("employee3", nextReview: utcToday.SubtractDays(3), peerReviews: [
186+
new PeerReview { ReviewerEmployeeId = "employee1", Rating = 4 },
187+
new PeerReview { ReviewerEmployeeId = "employee2", Rating = 5 }
188+
])
189+
];
190+
191+
await _employeeRepository.AddAsync(employees, o => o.ImmediateConsistency());
192+
const string aggregations = "terms:(peerReviews.reviewerEmployeeId peerReviews.rating) max:peerReviews.rating min:peerReviews.rating cardinality:peerReviews.reviewerEmployeeId";
193+
194+
// Act
195+
var result = await _employeeRepository.CountAsync(q => q.AggregationsExpression(aggregations));
196+
197+
// Assert
198+
Assert.Equal(3, result.Total);
199+
Assert.Equal(2, result.Aggregations.Count);
200+
201+
var nestedPeerReviewsAgg = result.Aggregations["nested_peerReviews"] as SingleBucketAggregate;
202+
Assert.NotNull(nestedPeerReviewsAgg);
203+
204+
var reviewerTermsAgg = nestedPeerReviewsAgg.Aggregations.Terms<string>("terms_peerReviews.reviewerEmployeeId");
205+
Assert.Equal(3, reviewerTermsAgg.Buckets.Count);
206+
207+
var ratingTermsAgg = nestedPeerReviewsAgg.Aggregations.Terms<int>("terms_peerReviews.rating");
208+
Assert.Equal(3, ratingTermsAgg.Buckets.Count);
209+
210+
Assert.Equal(3, nestedPeerReviewsAgg.Aggregations.Min("min_peerReviews.rating").Value);
211+
Assert.Equal(5, nestedPeerReviewsAgg.Aggregations.Max("max_peerReviews.rating").Value);
212+
Assert.Equal(3, nestedPeerReviewsAgg.Aggregations.Cardinality("cardinality_peerReviews.reviewerEmployeeId").Value);
213+
}
214+
215+
[Fact]
216+
public async Task CountAsync_WithNestedAggregationsSerialization_CanRoundtripBothSerializers()
217+
{
218+
// Arrange
219+
List<Employee> employees = [
220+
EmployeeGenerator.Generate("alice_123", "Alice", peerReviews: [
221+
new PeerReview { ReviewerEmployeeId = "bob_456", Rating = 5 },
222+
new PeerReview { ReviewerEmployeeId = "charlie_789", Rating = 4 }
223+
]),
224+
EmployeeGenerator.Generate("bob_456", "Bob", peerReviews: [
225+
new PeerReview { ReviewerEmployeeId = "alice_123", Rating = 3 },
226+
new PeerReview { ReviewerEmployeeId = "charlie_789", Rating = 5 }
227+
]),
228+
EmployeeGenerator.Generate("charlie_789", "Charlie", peerReviews: [
229+
new PeerReview { ReviewerEmployeeId = "alice_123", Rating = 4 },
230+
new PeerReview { ReviewerEmployeeId = "bob_456", Rating = 2 }
231+
])
232+
];
233+
234+
await _employeeRepository.AddAsync(employees, o => o.ImmediateConsistency());
235+
236+
// Act
237+
var result = await _employeeRepository.CountAsync(q => q.AggregationsExpression("terms:peerReviews.rating max:peerReviews.rating min:peerReviews.rating"));
238+
239+
// Assert
240+
Assert.Equal(3, result.Total);
241+
Assert.Single(result.Aggregations);
242+
243+
var nestedPeerReviewsAgg = result.Aggregations["nested_peerReviews"] as SingleBucketAggregate;
244+
Assert.NotNull(nestedPeerReviewsAgg);
245+
246+
var ratingTermsAgg = nestedPeerReviewsAgg.Aggregations.Terms<int>("terms_peerReviews.rating");
247+
Assert.Equal(4, ratingTermsAgg.Buckets.Count);
248+
var bucket = ratingTermsAgg.Buckets.First(f => f.Key == 5);
249+
Assert.Equal(2, bucket.Total);
250+
251+
// Test Newtonsoft.Json serialization
252+
string json = JsonConvert.SerializeObject(result);
253+
var roundTripped = JsonConvert.DeserializeObject<CountResult>(json);
254+
Assert.Equal(3, roundTripped.Total);
255+
Assert.Single(roundTripped.Aggregations);
256+
257+
var roundTrippedNestedAgg = roundTripped.Aggregations["nested_peerReviews"] as SingleBucketAggregate;
258+
Assert.NotNull(roundTrippedNestedAgg);
259+
260+
var roundTrippedRatingTermsAgg = roundTrippedNestedAgg.Aggregations.Terms<int>("terms_peerReviews.rating");
261+
Assert.Equal(4, roundTrippedRatingTermsAgg.Buckets.Count);
262+
bucket = roundTrippedRatingTermsAgg.Buckets.First(f => f.Key == 5);
263+
Assert.Equal(2, bucket.Total);
264+
265+
// Test System.Text.Json serialization
266+
string systemTextJson = System.Text.Json.JsonSerializer.Serialize(result);
267+
Assert.Equal(json, systemTextJson);
268+
roundTripped = System.Text.Json.JsonSerializer.Deserialize<CountResult>(systemTextJson);
269+
Assert.Equal(3, roundTripped.Total);
270+
Assert.Single(roundTripped.Aggregations);
271+
272+
roundTrippedNestedAgg = roundTripped.Aggregations["nested_peerReviews"] as SingleBucketAggregate;
273+
Assert.NotNull(roundTrippedNestedAgg);
274+
275+
roundTrippedRatingTermsAgg = roundTrippedNestedAgg.Aggregations.Terms<int>("terms_peerReviews.rating");
276+
Assert.Equal(4, roundTrippedRatingTermsAgg.Buckets.Count);
277+
bucket = roundTrippedRatingTermsAgg.Buckets.First(f => f.Key == 5);
278+
Assert.Equal(2, bucket.Total);
279+
}
280+
}

tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Configuration/Indexes/EmployeeIndex.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ protected override void ConfigureQueryParser(ElasticQueryParserConfiguration con
8282
{
8383
base.ConfigureQueryParser(config);
8484
config.UseIncludes(ResolveIncludeAsync).UseOptInRuntimeFieldResolver(ResolveRuntimeFieldAsync);
85+
config.SetDefaultFields([
86+
nameof(Employee.Id).ToLower(),
87+
"peerReviews.reviewerEmployeeId"
88+
]);
8589
}
8690

8791
private async Task<string> ResolveIncludeAsync(string name)

tests/Foundatio.Repositories.Elasticsearch.Tests/Repositories/Models/Employee.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public static class EmployeeGenerator
150150
EmploymentType = EmploymentType.FullTime
151151
};
152152

153-
public static Employee Generate(string id = null, string name = null, int? age = null, int? yearsEmployed = null, string companyName = null, string companyId = null, string location = null, DateTime? lastReview = null, DateTimeOffset? nextReview = null, DateTime? createdUtc = null, DateTime? updatedUtc = null, EmploymentType? employmentType = null)
153+
public static Employee Generate(string id = null, string name = null, int? age = null, int? yearsEmployed = null, string companyName = null, string companyId = null, string location = null, DateTime? lastReview = null, DateTimeOffset? nextReview = null, DateTime? createdUtc = null, DateTime? updatedUtc = null, EmploymentType? employmentType = null, PeerReview[] peerReviews = null)
154154
{
155155
return new Employee
156156
{
@@ -165,7 +165,8 @@ public static Employee Generate(string id = null, string name = null, int? age =
165165
NextReview = nextReview.GetValueOrDefault(),
166166
CreatedUtc = createdUtc.GetValueOrDefault(),
167167
UpdatedUtc = updatedUtc.GetValueOrDefault(),
168-
Location = location ?? RandomData.GetCoordinate()
168+
Location = location ?? RandomData.GetCoordinate(),
169+
PeerReviews = peerReviews
169170
};
170171
}
171172

0 commit comments

Comments
 (0)