diff --git a/src/Nest/Aggregations/Aggregate.cs b/src/Nest/Aggregations/Aggregate.cs index e326f95364f..bc63a1780f6 100644 --- a/src/Nest/Aggregations/Aggregate.cs +++ b/src/Nest/Aggregations/Aggregate.cs @@ -3,12 +3,15 @@ namespace Nest { /// - /// Represents the result of an aggregation on the response + /// Aggregation response for an aggregation request /// [ExactContractJsonConverter(typeof(AggregateJsonConverter))] public interface IAggregate { //TODO this public set is problematic + /// + /// Metadata for the aggregation + /// IReadOnlyDictionary Meta { get; set; } } } diff --git a/src/Nest/Aggregations/AggregateDictionary.cs b/src/Nest/Aggregations/AggregateDictionary.cs index fc5d3b59664..c59b27d8abf 100644 --- a/src/Nest/Aggregations/AggregateDictionary.cs +++ b/src/Nest/Aggregations/AggregateDictionary.cs @@ -175,7 +175,17 @@ public TermsAggregate Terms(string key) public MultiBucketAggregate DateHistogram(string key) => GetMultiBucketAggregate(key); - public MultiBucketAggregate Composite(string key) => GetMultiBucketAggregate(key); + public CompositeBucketAggregate Composite(string key) + { + var bucket = this.TryGet(key); + if (bucket == null) return null; + return new CompositeBucketAggregate + { + Buckets = bucket.Items.OfType().ToList(), + Meta = bucket.Meta, + AfterKey = new CompositeKey(bucket.AfterKey) + }; + } public MatrixStatsAggregate MatrixStats(string key) => this.TryGet(key); diff --git a/src/Nest/Aggregations/AggregateJsonConverter.cs b/src/Nest/Aggregations/AggregateJsonConverter.cs index b5366cd67b4..2bc12dc7733 100644 --- a/src/Nest/Aggregations/AggregateJsonConverter.cs +++ b/src/Nest/Aggregations/AggregateJsonConverter.cs @@ -36,6 +36,7 @@ private static class Parser public const string Hits = "hits"; public const string Location = "location"; public const string Fields = "fields"; + public const string AfterKey = "after_key"; public const string Key = "key"; public const string From = "from"; @@ -122,6 +123,16 @@ private IAggregate ReadAggregate(JsonReader reader, JsonSerializer serializer) case Parser.Value: aggregate = GetValueAggregate(reader, serializer); break; + case Parser.AfterKey: + reader.Read(); + var afterKeys = serializer.Deserialize>(reader); + reader.Read(); + var bucketAggregate = reader.Value.ToString() == Parser.Buckets + ? this.GetMultiBucketAggregate(reader, serializer) as BucketAggregate ?? new BucketAggregate() + : new BucketAggregate(); + bucketAggregate.AfterKey = afterKeys; + aggregate = bucketAggregate; + break; case Parser.Buckets: case Parser.DocCountErrorUpperBound: aggregate = GetMultiBucketAggregate(reader, serializer); diff --git a/src/Nest/Aggregations/Bucket/BucketAggregate.cs b/src/Nest/Aggregations/Bucket/BucketAggregate.cs index 1aa934ab695..40a4c0e256b 100644 --- a/src/Nest/Aggregations/Bucket/BucketAggregate.cs +++ b/src/Nest/Aggregations/Bucket/BucketAggregate.cs @@ -14,17 +14,42 @@ public class SingleBucketAggregate : BucketAggregateBase { public SingleBucketAggregate(IReadOnlyDictionary aggregations) : base(aggregations) { } + /// + /// Count of documents in the bucket + /// public long DocCount { get; internal set; } } + /// + /// Aggregation response for a bucket aggregation + /// + /// public class MultiBucketAggregate : IAggregate where TBucket : IBucket { + /// public IReadOnlyDictionary Meta { get; set; } + /// + /// The buckets into which results are grouped + /// public IReadOnlyCollection Buckets { get; set; } = EmptyReadOnly.Collection; } + /// + /// Aggregation response of + /// + public class CompositeBucketAggregate : MultiBucketAggregate + { + /// + /// The composite key of the last bucket returned + /// in the response before any filtering by pipeline aggregations. + /// If all buckets are filtered/removed by pipeline aggregations, + /// will contain the composite key of the last bucket before filtering. + /// + /// Valid for Elasticsearch 6.3.0+ + public CompositeKey AfterKey { get; set; } + } // Intermediate object used for deserialization public class BucketAggregate : IAggregate @@ -35,5 +60,6 @@ public class BucketAggregate : IAggregate public IReadOnlyDictionary Meta { get; set; } = EmptyReadOnly.Dictionary; public long DocCount { get; set; } public long BgCount { get; set; } + public IReadOnlyDictionary AfterKey { get; set; } = EmptyReadOnly.Dictionary; } } diff --git a/src/Nest/Aggregations/Bucket/Composite/DateHistogramCompositeAggregationSource.cs b/src/Nest/Aggregations/Bucket/Composite/DateHistogramCompositeAggregationSource.cs index 77fd4cd5f51..fc700b44987 100644 --- a/src/Nest/Aggregations/Bucket/Composite/DateHistogramCompositeAggregationSource.cs +++ b/src/Nest/Aggregations/Bucket/Composite/DateHistogramCompositeAggregationSource.cs @@ -22,6 +22,13 @@ public interface IDateHistogramCompositeAggregationSource : ICompositeAggregatio /// [JsonProperty("time_zone")] string Timezone { get; set; } + + /// + /// Return a formatted date string as the key instead an epoch long + /// + /// Valid for Elasticsearch 6.3.0+ + [JsonProperty("format")] + string Format { get; set; } } /// @@ -35,6 +42,9 @@ public DateHistogramCompositeAggregationSource(string name) : base(name) {} /// public string Timezone { get; set; } + /// + public string Format { get; set; } + /// protected override string SourceType => "date_histogram"; } @@ -46,6 +56,7 @@ public class DateHistogramCompositeAggregationSourceDescriptor { Union IDateHistogramCompositeAggregationSource.Interval { get; set; } string IDateHistogramCompositeAggregationSource.Timezone { get; set; } + string IDateHistogramCompositeAggregationSource.Format { get; set; } public DateHistogramCompositeAggregationSourceDescriptor(string name) : base(name, "date_histogram") {} @@ -58,7 +69,9 @@ public DateHistogramCompositeAggregationSourceDescriptor Interval(Time interv Assign(a => a.Interval = interval); /// - public DateHistogramCompositeAggregationSourceDescriptor Timezone(string timezone) => - Assign(a => a.Timezone = timezone); + public DateHistogramCompositeAggregationSourceDescriptor Timezone(string timezone) => Assign(a => a.Timezone = timezone); + + /// + public DateHistogramCompositeAggregationSourceDescriptor Format(string format) => Assign(a => a.Format = format); } } diff --git a/src/Tests/Tests/Aggregations/Bucket/Composite/CompositeAggregationUsageTests.cs b/src/Tests/Tests/Aggregations/Bucket/Composite/CompositeAggregationUsageTests.cs index cb00d3197b6..0f22d0ebcf7 100644 --- a/src/Tests/Tests/Aggregations/Bucket/Composite/CompositeAggregationUsageTests.cs +++ b/src/Tests/Tests/Aggregations/Bucket/Composite/CompositeAggregationUsageTests.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Elastic.Xunit.XunitPlumbing; using FluentAssertions; using Nest; using Newtonsoft.Json; +using Tests.Configuration; using Tests.Core.Extensions; using Tests.Core.ManagedElasticsearch.Clusters; using Tests.Domain; @@ -163,6 +165,12 @@ protected override void ExpectResponse(ISearchResponse response) var composite = response.Aggregations.Composite("my_buckets"); composite.Should().NotBeNull(); composite.Buckets.Should().NotBeNullOrEmpty(); + composite.AfterKey.Should().NotBeNull(); + if (TestConfiguration.Instance.InRange(">=6.3.0")) + { + composite.AfterKey.Should().HaveCount(3) + .And.ContainKeys("branches", "started", "branch_count"); + } foreach (var item in composite.Buckets) { var key = item.Key; @@ -187,4 +195,118 @@ protected override void ExpectResponse(ISearchResponse response) } } } + + + //hide + [SkipVersion("<6.3.0", "Date histogram source only supports format starting from Elasticsearch 6.3.0+")] + public class DateFormatCompositeAggregationUsageTests : ProjectsOnlyAggregationUsageTestBase + { + public DateFormatCompositeAggregationUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override object AggregationJson => new + { + my_buckets = new + { + composite = new + { + sources = new object[] + { + new + { + started = new + { + date_histogram = new + { + field = "startedOn", + interval = "month", + format = "yyyy-MM-dd" + } + } + }, + } + }, + aggs = new + { + project_tags = new + { + nested = new + { + path = "tags" + }, + aggs = new + { + tags = new + { + terms = new {field = "tags.name"} + } + } + } + } + } + }; + + protected override Func, IAggregationContainer> FluentAggs => a => a + .Composite("my_buckets", date => date + .Sources(s => s + .DateHistogram("started", d => d + .Field(f => f.StartedOn) + .Interval(DateInterval.Month) + .Format("yyyy-MM-dd") + ) + ) + .Aggregations(childAggs => childAggs + .Nested("project_tags", n => n + .Path(p => p.Tags) + .Aggregations(nestedAggs => nestedAggs + .Terms("tags", avg => avg.Field(p => p.Tags.First().Name)) + ) + ) + ) + ); + + protected override AggregationDictionary InitializerAggs => + new CompositeAggregation("my_buckets") + { + Sources = new List + { + new DateHistogramCompositeAggregationSource("started") + { + Field = Infer.Field(f => f.StartedOn), + Interval = DateInterval.Month, + Format = "yyyy-MM-dd" + }, + }, + Aggregations = new NestedAggregation("project_tags") + { + Path = Field(p => p.Tags), + Aggregations = new TermsAggregation("tags") + { + Field = Field(p => p.Tags.First().Name) + } + } + }; + + /**==== Handling Responses + * Each Composite aggregation bucket key is an `CompositeKey`, a specialized + * `IReadOnlyDictionary` type with methods to convert values to supported types + */ + protected override void ExpectResponse(ISearchResponse response) + { + response.ShouldBeValid(); + + var composite = response.Aggregations.Composite("my_buckets"); + composite.Should().NotBeNull(); + composite.Buckets.Should().NotBeNullOrEmpty(); + composite.AfterKey.Should().NotBeNull(); + composite.AfterKey.Should().HaveCount(1).And.ContainKeys("started"); + foreach (var item in composite.Buckets) + { + var key = item.Key; + key.Should().NotBeNull(); + + key.TryGetValue("started", out string startedString).Should().BeTrue(); + startedString.Should().NotBeNullOrWhiteSpace(); + } + } + } } diff --git a/src/Tests/Tests/Tests.csproj b/src/Tests/Tests/Tests.csproj index 9c8ea43f139..3c7b3eaab00 100644 --- a/src/Tests/Tests/Tests.csproj +++ b/src/Tests/Tests/Tests.csproj @@ -7,7 +7,7 @@ $(NoWarn);xUnit1013 - +