Skip to content

Commit 47c5e29

Browse files
committed
Add support for the auto_date_histogram aggregation elastic/elasticsearch#28993 (#3521)
1 parent 000fc3d commit 47c5e29

File tree

8 files changed

+273
-0
lines changed

8 files changed

+273
-0
lines changed

src/Nest/Aggregations/AggregateDictionary.cs

+14
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,20 @@ public TermsAggregate<TKey> Terms<TKey>(string key)
177177

178178
public MultiBucketAggregate<DateHistogramBucket> DateHistogram(string key) => GetMultiBucketAggregate<DateHistogramBucket>(key);
179179

180+
public AutoDateHistogramAggregate AutoDateHistogram(string key)
181+
{
182+
var bucket = TryGet<BucketAggregate>(key);
183+
if (bucket == null) return null;
184+
185+
return new AutoDateHistogramAggregate
186+
{
187+
Buckets = bucket.Items.OfType<DateHistogramBucket>().ToList(),
188+
Meta = bucket.Meta,
189+
Interval = bucket.Interval
190+
};
191+
}
192+
193+
180194
public CompositeBucketAggregate Composite(string key)
181195
{
182196
var bucket = TryGet<BucketAggregate>(key);

src/Nest/Aggregations/AggregateJsonConverter.cs

+7
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,12 @@ private IAggregate GetMultiBucketAggregate(JsonReader reader, JsonSerializer ser
490490
} while (reader.TokenType != JsonToken.EndArray);
491491
bucket.Items = items;
492492
reader.Read();
493+
if (reader.TokenType == JsonToken.PropertyName && (string)reader.Value == Parser.Interval)
494+
{
495+
var interval = reader.ReadAsString();
496+
bucket.Interval = new Time(interval);
497+
}
498+
493499
return bucket;
494500
}
495501

@@ -755,6 +761,7 @@ private static class Parser
755761
public const string DocCountErrorUpperBound = "doc_count_error_upper_bound";
756762
public const string Fields = "fields";
757763
public const string From = "from";
764+
public const string Interval = "interval";
758765

759766
public const string FromAsString = "from_as_string";
760767
public const string Hits = "hits";

src/Nest/Aggregations/AggregationContainer.cs

+14
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ public interface IAggregationContainer
104104
[JsonProperty("date_histogram")]
105105
IDateHistogramAggregation DateHistogram { get; set; }
106106

107+
[JsonProperty("auto_date_histogram")]
108+
IAutoDateHistogramAggregation AutoDateHistogram { get; set; }
109+
107110
[JsonProperty("date_range")]
108111
IDateRangeAggregation DateRange { get; set; }
109112

@@ -253,11 +256,15 @@ public class AggregationContainer : IAggregationContainer
253256
public ICompositeAggregation Composite { get; set; }
254257

255258
public ICumulativeSumAggregation CumulativeSum { get; set; }
259+
256260
public IDateHistogramAggregation DateHistogram { get; set; }
257261

262+
public IAutoDateHistogramAggregation AutoDateHistogram { get; set; }
263+
258264
public IDateRangeAggregation DateRange { get; set; }
259265

260266
public IDerivativeAggregation Derivative { get; set; }
267+
261268
public IExtendedStatsAggregation ExtendedStats { get; set; }
262269

263270
public IExtendedStatsBucketAggregation ExtendedStatsBucket { get; set; }
@@ -386,6 +393,8 @@ public class AggregationContainerDescriptor<T> : DescriptorBase<AggregationConta
386393

387394
IDateHistogramAggregation IAggregationContainer.DateHistogram { get; set; }
388395

396+
IAutoDateHistogramAggregation IAggregationContainer.AutoDateHistogram { get; set; }
397+
389398
IDateRangeAggregation IAggregationContainer.DateRange { get; set; }
390399

391400
IDerivativeAggregation IAggregationContainer.Derivative { get; set; }
@@ -483,6 +492,11 @@ Func<DateHistogramAggregationDescriptor<T>, IDateHistogramAggregation> selector
483492
) =>
484493
_SetInnerAggregation(name, selector, (a, d) => a.DateHistogram = d);
485494

495+
public AggregationContainerDescriptor<T> AutoDateHistogram(string name,
496+
Func<AutoDateHistogramAggregationDescriptor<T>, IAutoDateHistogramAggregation> selector
497+
) =>
498+
_SetInnerAggregation(name, selector, (a, d) => a.AutoDateHistogram = d);
499+
486500
public AggregationContainerDescriptor<T> Percentiles(string name,
487501
Func<PercentilesAggregationDescriptor<T>, IPercentilesAggregation> selector
488502
) =>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq.Expressions;
4+
using Newtonsoft.Json;
5+
6+
namespace Nest
7+
{
8+
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
9+
[ContractJsonConverter(typeof(AggregationJsonConverter<AutoDateHistogramAggregation>))]
10+
public interface IAutoDateHistogramAggregation : IBucketAggregation
11+
{
12+
[JsonProperty("field")]
13+
Field Field { get; set; }
14+
15+
[JsonProperty("format")]
16+
string Format { get; set; }
17+
18+
[JsonProperty("missing")]
19+
DateTime? Missing { get; set; }
20+
21+
[JsonProperty("offset")]
22+
string Offset { get; set; }
23+
24+
[JsonProperty("params")]
25+
IDictionary<string, object> Params { get; set; }
26+
27+
[JsonProperty("script")]
28+
IScript Script { get; set; }
29+
30+
[JsonProperty("time_zone")]
31+
string TimeZone { get; set; }
32+
}
33+
34+
public class AutoDateHistogramAggregation : BucketAggregationBase, IAutoDateHistogramAggregation
35+
{
36+
private string _format;
37+
38+
internal AutoDateHistogramAggregation() { }
39+
40+
public AutoDateHistogramAggregation(string name) : base(name) { }
41+
42+
public Field Field { get; set; }
43+
44+
//see: https://github.com/elastic/elasticsearch/issues/9725
45+
public string Format
46+
{
47+
get => !string.IsNullOrEmpty(_format) &&
48+
!_format.Contains("date_optional_time") &&
49+
(Missing.HasValue)
50+
? _format + "||date_optional_time"
51+
: _format;
52+
set => _format = value;
53+
}
54+
55+
public DateTime? Missing { get; set; }
56+
public string Offset { get; set; }
57+
public IDictionary<string, object> Params { get; set; }
58+
public IScript Script { get; set; }
59+
public string TimeZone { get; set; }
60+
61+
internal override void WrapInContainer(AggregationContainer c) => c.AutoDateHistogram = this;
62+
}
63+
64+
public class AutoDateHistogramAggregationDescriptor<T>
65+
: BucketAggregationDescriptorBase<AutoDateHistogramAggregationDescriptor<T>, IAutoDateHistogramAggregation, T>
66+
, IAutoDateHistogramAggregation
67+
where T : class
68+
{
69+
private string _format;
70+
71+
Field IAutoDateHistogramAggregation.Field { get; set; }
72+
73+
//see: https://github.com/elastic/elasticsearch/issues/9725
74+
string IAutoDateHistogramAggregation.Format
75+
{
76+
get => !string.IsNullOrEmpty(_format) &&
77+
!_format.Contains("date_optional_time") &&
78+
(Self.Missing.HasValue)
79+
? _format + "||date_optional_time"
80+
: _format;
81+
set => _format = value;
82+
}
83+
84+
DateTime? IAutoDateHistogramAggregation.Missing { get; set; }
85+
86+
string IAutoDateHistogramAggregation.Offset { get; set; }
87+
88+
IDictionary<string, object> IAutoDateHistogramAggregation.Params { get; set; }
89+
90+
IScript IAutoDateHistogramAggregation.Script { get; set; }
91+
92+
string IAutoDateHistogramAggregation.TimeZone { get; set; }
93+
94+
public AutoDateHistogramAggregationDescriptor<T> Field(Field field) => Assign(a => a.Field = field);
95+
96+
public AutoDateHistogramAggregationDescriptor<T> Field(Expression<Func<T, object>> field) => Assign(a => a.Field = field);
97+
98+
public AutoDateHistogramAggregationDescriptor<T> Script(string script) => Assign(a => a.Script = (InlineScript)script);
99+
100+
public AutoDateHistogramAggregationDescriptor<T> Script(Func<ScriptDescriptor, IScript> scriptSelector) =>
101+
Assign(a => a.Script = scriptSelector?.Invoke(new ScriptDescriptor()));
102+
103+
public AutoDateHistogramAggregationDescriptor<T> Format(string format) => Assign(a => a.Format = format);
104+
105+
public AutoDateHistogramAggregationDescriptor<T> TimeZone(string timeZone) => Assign(a => a.TimeZone = timeZone);
106+
107+
public AutoDateHistogramAggregationDescriptor<T> Offset(string offset) => Assign(a => a.Offset = offset);
108+
109+
public AutoDateHistogramAggregationDescriptor<T> Missing(DateTime? missing) => Assign(a => a.Missing = missing);
110+
}
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Nest
2+
{
3+
public class AutoDateHistogramAggregate : MultiBucketAggregate<DateHistogramBucket>
4+
{
5+
public Time Interval { get; internal set; }
6+
}
7+
}

src/Nest/Aggregations/Bucket/BucketAggregate.cs

+1
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,6 @@ public class BucketAggregate : IAggregate
7070
public IReadOnlyCollection<IBucket> Items { get; set; } = EmptyReadOnly<IBucket>.Collection;
7171
public IReadOnlyDictionary<string, object> Meta { get; set; } = EmptyReadOnly<string, object>.Dictionary;
7272
public long? SumOtherDocCount { get; set; }
73+
public Time Interval { get; set; }
7374
}
7475
}

src/Nest/Aggregations/Bucket/DateHistogram/DateHistogramAggregation.cs

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public class DateHistogramAggregationDescriptor<T>
8787
ExtendedBounds<DateMath> IDateHistogramAggregation.ExtendedBounds { get; set; }
8888
Field IDateHistogramAggregation.Field { get; set; }
8989

90+
//see: https://github.com/elastic/elasticsearch/issues/9725
9091
string IDateHistogramAggregation.Format
9192
{
9293
get => !string.IsNullOrEmpty(_format) &&
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
using System;
2+
using System.Linq;
3+
using FluentAssertions;
4+
using Nest;
5+
using Tests.Core.Extensions;
6+
using Tests.Core.ManagedElasticsearch.Clusters;
7+
using Tests.Domain;
8+
using Tests.Framework.Integration;
9+
using static Nest.Infer;
10+
using static Tests.Domain.Helpers.TestValueHelper;
11+
12+
namespace Tests.Aggregations.Bucket.AutoDateHistogram
13+
{
14+
/**
15+
* A multi-bucket aggregation similar to the Date Histogram Aggregation except instead of providing an interval to
16+
* use as the width of each bucket, a target number of buckets is provided indicating the number of buckets needed
17+
* and the interval of the buckets is automatically chosen to best achieve that target. The number of buckets
18+
* returned will always be less than or equal to this target number.
19+
*
20+
* NOTE: When specifying a `format` **and** `extended_bounds` or `missing`, in order for Elasticsearch to be able to parse
21+
* the serialized `DateTime` of `extended_bounds` or `missing` correctly, the `date_optional_time` format is included
22+
* as part of the `format` value.
23+
*
24+
* Be sure to read the Elasticsearch documentation on {ref_current}/search-aggregations-bucket-autodatehistogram-aggregation.html[Auto Date Histogram Aggregation].
25+
*/
26+
public class AutoDateHistogramAggregationUsageTests : ProjectsOnlyAggregationUsageTestBase
27+
{
28+
public AutoDateHistogramAggregationUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { }
29+
30+
protected override object AggregationJson => new
31+
{
32+
projects_started_per_month = new
33+
{
34+
auto_date_histogram = new
35+
{
36+
field = "startedOn",
37+
format = "yyyy-MM-dd'T'HH:mm:ss||date_optional_time", //<1> Note the inclusion of `date_optional_time` to `format`
38+
missing = FixedDate
39+
},
40+
aggs = new
41+
{
42+
project_tags = new
43+
{
44+
nested = new
45+
{
46+
path = "tags"
47+
},
48+
aggs = new
49+
{
50+
tags = new
51+
{
52+
terms = new { field = "tags.name" }
53+
}
54+
}
55+
}
56+
}
57+
}
58+
};
59+
60+
protected override Func<AggregationContainerDescriptor<Project>, IAggregationContainer> FluentAggs => a => a
61+
.AutoDateHistogram("projects_started_per_month", date => date
62+
.Field(p => p.StartedOn)
63+
.Format("yyyy-MM-dd'T'HH:mm:ss")
64+
.Missing(FixedDate)
65+
.Aggregations(childAggs => childAggs
66+
.Nested("project_tags", n => n
67+
.Path(p => p.Tags)
68+
.Aggregations(nestedAggs => nestedAggs
69+
.Terms("tags", avg => avg.Field(p => p.Tags.First().Name))
70+
)
71+
)
72+
)
73+
);
74+
75+
protected override AggregationDictionary InitializerAggs =>
76+
new AutoDateHistogramAggregation("projects_started_per_month")
77+
{
78+
Field = Field<Project>(p => p.StartedOn),
79+
Format = "yyyy-MM-dd'T'HH:mm:ss",
80+
Missing = FixedDate,
81+
Aggregations = new NestedAggregation("project_tags")
82+
{
83+
Path = Field<Project>(p => p.Tags),
84+
Aggregations = new TermsAggregation("tags")
85+
{
86+
Field = Field<Project>(p => p.Tags.First().Name)
87+
}
88+
}
89+
};
90+
91+
protected override void ExpectResponse(ISearchResponse<Project> response)
92+
{
93+
/** ==== Handling responses
94+
* The `AggregateDictionary found on `.Aggregations` on `ISearchResponse<T>` has several helper methods
95+
* so we can fetch our aggregation results easily in the correct type.
96+
* <<handling-aggregate-response, Be sure to read more about these helper methods>>
97+
*/
98+
response.ShouldBeValid();
99+
100+
var dateHistogram = response.Aggregations.AutoDateHistogram("projects_started_per_month");
101+
dateHistogram.Should().NotBeNull();
102+
dateHistogram.Interval.Should().NotBeNull();
103+
dateHistogram.Buckets.Should().NotBeNull();
104+
dateHistogram.Buckets.Count.Should().BeGreaterThan(1);
105+
foreach (var item in dateHistogram.Buckets)
106+
{
107+
item.Date.Should().NotBe(default);
108+
item.DocCount.Should().BeGreaterThan(0);
109+
110+
var nested = item.Nested("project_tags");
111+
nested.Should().NotBeNull();
112+
113+
var nestedTerms = nested.Terms("tags");
114+
nestedTerms.Buckets.Count.Should().BeGreaterThan(0);
115+
}
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)