Skip to content

Commit d6eee91

Browse files
committed
Fix #3317 and #3322 add after_key support to composite aggregation result and allow format to be set for date histogram composite source
1 parent 67eb409 commit d6eee91

File tree

6 files changed

+177
-6
lines changed

6 files changed

+177
-6
lines changed

src/Nest/Aggregations/AggregateDictionary.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,17 @@ public TermsAggregate<TKey> Terms<TKey>(string key)
175175

176176
public MultiBucketAggregate<DateHistogramBucket> DateHistogram(string key) => GetMultiBucketAggregate<DateHistogramBucket>(key);
177177

178-
public MultiBucketAggregate<CompositeBucket> Composite(string key) => GetMultiBucketAggregate<CompositeBucket>(key);
178+
public CompositeBucketAggregate Composite(string key)
179+
{
180+
var bucket = this.TryGet<BucketAggregate>(key);
181+
if (bucket == null) return null;
182+
return new CompositeBucketAggregate()
183+
{
184+
Buckets = bucket.Items.OfType<CompositeBucket>().ToList(),
185+
Meta = bucket.Meta,
186+
AfterKey = bucket.AfterKey
187+
};
188+
}
179189

180190
public MatrixStatsAggregate MatrixStats(string key) => this.TryGet<MatrixStatsAggregate>(key);
181191

src/Nest/Aggregations/AggregateJsonConverter.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ private static class Parser
3636
public const string Hits = "hits";
3737
public const string Location = "location";
3838
public const string Fields = "fields";
39+
public const string AfterKey = "after_key";
3940

4041
public const string Key = "key";
4142
public const string From = "from";
@@ -126,6 +127,16 @@ private IAggregate ReadAggregate(JsonReader reader, JsonSerializer serializer)
126127
case Parser.Value:
127128
aggregate = GetValueAggregate(reader, serializer);
128129
break;
130+
case Parser.AfterKey:
131+
reader.Read();
132+
var afterKeys = serializer.Deserialize<Dictionary<string, object>>(reader);
133+
reader.Read();
134+
var bucketAggregate = reader.Value.ToString() == Parser.Buckets
135+
? this.GetMultiBucketAggregate(reader, serializer) as BucketAggregate ?? new BucketAggregate()
136+
: new BucketAggregate();
137+
bucketAggregate.AfterKey = afterKeys;
138+
aggregate = bucketAggregate;
139+
break;
129140
case Parser.Buckets:
130141
case Parser.DocCountErrorUpperBound:
131142
aggregate = GetMultiBucketAggregate(reader, serializer);

src/Nest/Aggregations/Bucket/BucketAggregate.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ public class MultiBucketAggregate<TBucket> : IAggregate
3434
public IReadOnlyCollection<TBucket> Buckets { get; set; } = EmptyReadOnly<TBucket>.Collection;
3535
}
3636

37+
public class CompositeBucketAggregate : IAggregate
38+
{
39+
public IReadOnlyDictionary<string, object> Meta { get; set; }
40+
41+
public IReadOnlyCollection<CompositeBucket> Buckets { get; set; } = EmptyReadOnly<CompositeBucket>.Collection;
42+
43+
/// <summary>
44+
/// The after_key is equals to the last bucket returned in the response before any filtering that could be done by Pipeline aggregations.
45+
/// If all buckets are filtered/removed by a pipeline aggregation, the after_key will contain the last bucket before filtering.
46+
/// </summary>
47+
/// <remarks> Valid for Elasticsearch 6.3.0+ </remarks>
48+
public IReadOnlyDictionary<string, object> AfterKey { get; set; } = EmptyReadOnly<string, object>.Dictionary;
49+
}
50+
3751

3852
// Intermediate object used for deserialization
3953
public class BucketAggregate : IAggregate
@@ -44,5 +58,6 @@ public class BucketAggregate : IAggregate
4458
public IReadOnlyDictionary<string, object> Meta { get; set; } = EmptyReadOnly<string, object>.Dictionary;
4559
public long DocCount { get; set; }
4660
public long BgCount { get; set; }
61+
public IReadOnlyDictionary<string, object> AfterKey { get; set; } = EmptyReadOnly<string, object>.Dictionary;
4762
}
4863
}

src/Nest/Aggregations/Bucket/Composite/DateHistogramCompositeAggregationSource.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ public interface IDateHistogramCompositeAggregationSource : ICompositeAggregatio
2222
/// </summary>
2323
[JsonProperty("time_zone")]
2424
string Timezone { get; set; }
25+
26+
/// <summary>
27+
/// Return a formatted date string as the key instead an epoch long
28+
/// </summary>
29+
/// <remarks> Valid for Elasticsearch 6.3.0+ </remarks>
30+
[JsonProperty("format")]
31+
string Format { get; set; }
2532
}
2633

2734
/// <inheritdoc cref="IDateHistogramCompositeAggregationSource"/>
@@ -35,6 +42,9 @@ public DateHistogramCompositeAggregationSource(string name) : base(name) {}
3542
/// <inheritdoc />
3643
public string Timezone { get; set; }
3744

45+
/// <inheritdoc />
46+
public string Format { get; set; }
47+
3848
/// <inheritdoc />
3949
protected override string SourceType => "date_histogram";
4050
}
@@ -46,6 +56,7 @@ public class DateHistogramCompositeAggregationSourceDescriptor<T>
4656
{
4757
Union<DateInterval?,Time> IDateHistogramCompositeAggregationSource.Interval { get; set; }
4858
string IDateHistogramCompositeAggregationSource.Timezone { get; set; }
59+
string IDateHistogramCompositeAggregationSource.Format { get; set; }
4960

5061
public DateHistogramCompositeAggregationSourceDescriptor(string name) : base(name, "date_histogram") {}
5162

@@ -58,7 +69,9 @@ public DateHistogramCompositeAggregationSourceDescriptor<T> Interval(Time interv
5869
Assign(a => a.Interval = interval);
5970

6071
/// <inheritdoc cref="IDateHistogramCompositeAggregationSource.Timezone"/>
61-
public DateHistogramCompositeAggregationSourceDescriptor<T> Timezone(string timezone) =>
62-
Assign(a => a.Timezone = timezone);
72+
public DateHistogramCompositeAggregationSourceDescriptor<T> Timezone(string timezone) => Assign(a => a.Timezone = timezone);
73+
74+
/// <inheritdoc cref="IDateHistogramCompositeAggregationSource.Timezone"/>
75+
public DateHistogramCompositeAggregationSourceDescriptor<T> Format(string format) => Assign(a => a.Format = format);
6376
}
6477
}

src/Tests/Tests/Aggregations/Bucket/Composite/CompositeAggregationUsageTests.cs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Threading.Tasks;
45
using Elastic.Xunit.XunitPlumbing;
56
using FluentAssertions;
67
using Nest;
78
using Newtonsoft.Json;
9+
using Tests.Configuration;
810
using Tests.Core.Extensions;
911
using Tests.Core.ManagedElasticsearch.Clusters;
1012
using Tests.Domain;
@@ -163,6 +165,12 @@ protected override void ExpectResponse(ISearchResponse<Project> response)
163165
var composite = response.Aggregations.Composite("my_buckets");
164166
composite.Should().NotBeNull();
165167
composite.Buckets.Should().NotBeNullOrEmpty();
168+
composite.AfterKey.Should().NotBeNull();
169+
if (TestConfiguration.Instance.InRange(">=6.3.0"))
170+
{
171+
composite.AfterKey.Should().HaveCount(3)
172+
.And.ContainKeys("branches", "started", "branch_count");
173+
}
166174
foreach (var item in composite.Buckets)
167175
{
168176
var key = item.Key;
@@ -187,4 +195,118 @@ protected override void ExpectResponse(ISearchResponse<Project> response)
187195
}
188196
}
189197
}
198+
199+
200+
//hide
201+
[SkipVersion("<6.3.0", "Date histogram source only supports format starting from Elasticsearch 6.3.0+")]
202+
public class DateFormatCompositeAggregationUsageTests : ProjectsOnlyAggregationUsageTestBase
203+
{
204+
public DateFormatCompositeAggregationUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { }
205+
206+
protected override object AggregationJson => new
207+
{
208+
my_buckets = new
209+
{
210+
composite = new
211+
{
212+
sources = new object[]
213+
{
214+
new
215+
{
216+
started = new
217+
{
218+
date_histogram = new
219+
{
220+
field = "startedOn",
221+
interval = "month",
222+
format = "yyyy-MM-dd"
223+
}
224+
}
225+
},
226+
}
227+
},
228+
aggs = new
229+
{
230+
project_tags = new
231+
{
232+
nested = new
233+
{
234+
path = "tags"
235+
},
236+
aggs = new
237+
{
238+
tags = new
239+
{
240+
terms = new {field = "tags.name"}
241+
}
242+
}
243+
}
244+
}
245+
}
246+
};
247+
248+
protected override Func<AggregationContainerDescriptor<Project>, IAggregationContainer> FluentAggs => a => a
249+
.Composite("my_buckets", date => date
250+
.Sources(s => s
251+
.DateHistogram("started", d => d
252+
.Field(f => f.StartedOn)
253+
.Interval(DateInterval.Month)
254+
.Format("yyyy-MM-dd")
255+
)
256+
)
257+
.Aggregations(childAggs => childAggs
258+
.Nested("project_tags", n => n
259+
.Path(p => p.Tags)
260+
.Aggregations(nestedAggs => nestedAggs
261+
.Terms("tags", avg => avg.Field(p => p.Tags.First().Name))
262+
)
263+
)
264+
)
265+
);
266+
267+
protected override AggregationDictionary InitializerAggs =>
268+
new CompositeAggregation("my_buckets")
269+
{
270+
Sources = new List<ICompositeAggregationSource>
271+
{
272+
new DateHistogramCompositeAggregationSource("started")
273+
{
274+
Field = Infer.Field<Project>(f => f.StartedOn),
275+
Interval = DateInterval.Month,
276+
Format = "yyyy-MM-dd"
277+
},
278+
},
279+
Aggregations = new NestedAggregation("project_tags")
280+
{
281+
Path = Field<Project>(p => p.Tags),
282+
Aggregations = new TermsAggregation("tags")
283+
{
284+
Field = Field<Project>(p => p.Tags.First().Name)
285+
}
286+
}
287+
};
288+
289+
/**==== Handling Responses
290+
* Each Composite aggregation bucket key is an `CompositeKey`, a specialized
291+
* `IReadOnlyDictionary<string, object>` type with methods to convert values to supported types
292+
*/
293+
protected override void ExpectResponse(ISearchResponse<Project> response)
294+
{
295+
response.ShouldBeValid();
296+
297+
var composite = response.Aggregations.Composite("my_buckets");
298+
composite.Should().NotBeNull();
299+
composite.Buckets.Should().NotBeNullOrEmpty();
300+
composite.AfterKey.Should().NotBeNull();
301+
composite.AfterKey.Should().HaveCount(1).And.ContainKeys("started");
302+
foreach (var item in composite.Buckets)
303+
{
304+
var key = item.Key;
305+
key.Should().NotBeNull();
306+
307+
key.TryGetValue("started", out string startedString).Should().BeTrue();
308+
startedString.Should().NotBeNullOrWhiteSpace();
309+
}
310+
}
311+
}
190312
}

src/Tests/Tests/Framework/EndpointTests/ApiIntegrationTestBase.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ protected ApiIntegrationTestBase(TCluster cluster, EndpointUsage usage) : base(c
3232
public override IElasticClient Client => this.Cluster.Client;
3333
protected override TInitializer Initializer => Activator.CreateInstance<TInitializer>();
3434

35-
[I] public async Task ReturnsExpectedStatusCode() =>
35+
[I] public virtual async Task ReturnsExpectedStatusCode() =>
3636
await this.AssertOnAllResponses(r => r.ApiCall.HttpStatusCode.Should().Be(this.ExpectStatusCode));
3737

38-
[I] public async Task ReturnsExpectedIsValid() =>
38+
[I] public virtual async Task ReturnsExpectedIsValid() =>
3939
await this.AssertOnAllResponses(r => r.ShouldHaveExpectedIsValid(this.ExpectIsValid));
4040

41-
[I] public async Task ReturnsExpectedResponse() => await this.AssertOnAllResponses(ExpectResponse);
41+
[I] public virtual async Task ReturnsExpectedResponse() => await this.AssertOnAllResponses(ExpectResponse);
4242

4343
protected override Task AssertOnAllResponses(Action<TResponse> assert)
4444
{

0 commit comments

Comments
 (0)