From 8d07509f0dae6a467a7fd6f5c3c0959962579ff1 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Thu, 9 Mar 2023 10:03:12 +0000 Subject: [PATCH] Support bucket sort pipeline aggregation --- .../AggregateDictionaryConverter.cs | 2 - .../Types/Aggregations/Aggregation.g.cs | 20 + .../Aggregations/BucketSortAggregation.g.cs | 464 ++++++++++++++++++ .../Aggregations/TopHitsAggregation.g.cs | 2 +- .../Aggregations/TopMetricsAggregation.g.cs | 2 +- .../BucketSortAggregationUsageTests.cs | 116 +++++ 6 files changed, 602 insertions(+), 4 deletions(-) create mode 100644 src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/BucketSortAggregation.g.cs create mode 100644 tests/Tests/Aggregations/Pipeline/BucketSortAggregationUsageTests.cs diff --git a/src/Elastic.Clients.Elasticsearch/Types/Aggregations/AggregateDictionaryConverter.cs b/src/Elastic.Clients.Elasticsearch/Types/Aggregations/AggregateDictionaryConverter.cs index 60657be4c7f..53861cab923 100644 --- a/src/Elastic.Clients.Elasticsearch/Types/Aggregations/AggregateDictionaryConverter.cs +++ b/src/Elastic.Clients.Elasticsearch/Types/Aggregations/AggregateDictionaryConverter.cs @@ -399,8 +399,6 @@ public static void ReadAggregate(ref Utf8JsonReader reader, JsonSerializerOption throw new Exception("The aggregate in response is not yet supported."); case "bucket_selector": throw new Exception("The aggregate in response is not yet supported."); - case "bucket_sort": - throw new Exception("The aggregate in response is not yet supported."); case "cumulative_cardinality": { diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/Aggregation.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/Aggregation.g.cs index f313f339c15..23747b215ee 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/Aggregation.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/Aggregation.g.cs @@ -64,6 +64,11 @@ public override Aggregation Read(ref Utf8JsonReader reader, Type typeToConvert, return AggregationSerializationHelper.ReadContainer("boxplot", ref reader, options); } + if (propertyName == "bucket_sort") + { + return AggregationSerializationHelper.ReadContainer("bucket_sort", ref reader, options); + } + if (propertyName == "cardinality") { return AggregationSerializationHelper.ReadContainer("cardinality", ref reader, options); @@ -316,6 +321,11 @@ public AggregationDescriptor Boxplot(string name, Action BucketSort(string name, Action> configure) + { + return SetContainer(name, Aggregation.CreateWithAction("bucket_sort", configure)); + } + public AggregationDescriptor Cardinality(string name, Action> configure) { return SetContainer(name, Aggregation.CreateWithAction("cardinality", configure)); @@ -564,6 +574,16 @@ public AggregationDescriptor Boxplot(string name, Action configure) + { + return SetContainer(name, Aggregation.CreateWithAction("bucket_sort", configure)); + } + + public AggregationDescriptor BucketSort(string name, Action> configure) + { + return SetContainer(name, Aggregation.CreateWithAction("bucket_sort", configure)); + } + public AggregationDescriptor Cardinality(string name, Action configure) { return SetContainer(name, Aggregation.CreateWithAction("cardinality", configure)); diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/BucketSortAggregation.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/BucketSortAggregation.g.cs new file mode 100644 index 00000000000..97044ecb22d --- /dev/null +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/BucketSortAggregation.g.cs @@ -0,0 +1,464 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. +// +// ███╗ ██╗ ██████╗ ████████╗██╗ ██████╗███████╗ +// ████╗ ██║██╔═══██╗╚══██╔══╝██║██╔════╝██╔════╝ +// ██╔██╗ ██║██║ ██║ ██║ ██║██║ █████╗ +// ██║╚██╗██║██║ ██║ ██║ ██║██║ ██╔══╝ +// ██║ ╚████║╚██████╔╝ ██║ ██║╚██████╗███████╗ +// ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝ +// ------------------------------------------------ +// +// This file is automatically generated. +// Please do not edit these files manually. +// +// ------------------------------------------------ + +using Elastic.Clients.Elasticsearch.Fluent; +using Elastic.Clients.Elasticsearch.Serialization; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text.Json; +using System.Text.Json.Serialization; + +#nullable restore +namespace Elastic.Clients.Elasticsearch.Aggregations; +internal sealed class BucketSortAggregationConverter : JsonConverter +{ + public override BucketSortAggregation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException("Unexpected JSON detected."); + reader.Read(); + var aggName = reader.GetString(); + if (aggName != "bucket_sort") + throw new JsonException("Unexpected JSON detected."); + var agg = new BucketSortAggregation(aggName); + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + if (reader.ValueTextEquals("from")) + { + reader.Read(); + var value = JsonSerializer.Deserialize(ref reader, options); + if (value is not null) + { + agg.From = value; + } + + continue; + } + + if (reader.ValueTextEquals("gap_policy")) + { + reader.Read(); + var value = JsonSerializer.Deserialize(ref reader, options); + if (value is not null) + { + agg.GapPolicy = value; + } + + continue; + } + + if (reader.ValueTextEquals("size")) + { + reader.Read(); + var value = JsonSerializer.Deserialize(ref reader, options); + if (value is not null) + { + agg.Size = value; + } + + continue; + } + + if (reader.ValueTextEquals("sort")) + { + reader.Read(); + var value = SingleOrManySerializationHelper.Deserialize(ref reader, options); + if (value is not null) + { + agg.Sort = value; + } + + continue; + } + } + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + if (reader.ValueTextEquals("meta")) + { + var value = JsonSerializer.Deserialize>(ref reader, options); + if (value is not null) + { + agg.Meta = value; + } + + continue; + } + } + } + + return agg; + } + + public override void Write(Utf8JsonWriter writer, BucketSortAggregation value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WritePropertyName("bucket_sort"); + writer.WriteStartObject(); + if (value.From.HasValue) + { + writer.WritePropertyName("from"); + writer.WriteNumberValue(value.From.Value); + } + + if (value.GapPolicy is not null) + { + writer.WritePropertyName("gap_policy"); + JsonSerializer.Serialize(writer, value.GapPolicy, options); + } + + if (value.Size.HasValue) + { + writer.WritePropertyName("size"); + writer.WriteNumberValue(value.Size.Value); + } + + if (value.Sort is not null) + { + writer.WritePropertyName("sort"); + SingleOrManySerializationHelper.Serialize(value.Sort, writer, options); + } + + writer.WriteEndObject(); + if (value.Meta is not null) + { + writer.WritePropertyName("meta"); + JsonSerializer.Serialize(writer, value.Meta, options); + } + + writer.WriteEndObject(); + } +} + +[JsonConverter(typeof(BucketSortAggregationConverter))] +public sealed partial class BucketSortAggregation : SearchAggregation +{ + public BucketSortAggregation(string name) => Name = name; + internal BucketSortAggregation() + { + } + + public int? From { get; set; } + + public Elastic.Clients.Elasticsearch.Aggregations.GapPolicy? GapPolicy { get; set; } + + public IDictionary? Meta { get; set; } + + public override string? Name { get; internal set; } + + public int? Size { get; set; } + + public ICollection? Sort { get; set; } +} + +public sealed partial class BucketSortAggregationDescriptor : SerializableDescriptor> +{ + internal BucketSortAggregationDescriptor(Action> configure) => configure.Invoke(this); + public BucketSortAggregationDescriptor() : base() + { + } + + private ICollection? SortValue { get; set; } + + private SortOptionsDescriptor SortDescriptor { get; set; } + + private Action> SortDescriptorAction { get; set; } + + private Action>[] SortDescriptorActions { get; set; } + + private int? FromValue { get; set; } + + private Elastic.Clients.Elasticsearch.Aggregations.GapPolicy? GapPolicyValue { get; set; } + + private IDictionary? MetaValue { get; set; } + + private int? SizeValue { get; set; } + + public BucketSortAggregationDescriptor Sort(ICollection? sort) + { + SortDescriptor = null; + SortDescriptorAction = null; + SortDescriptorActions = null; + SortValue = sort; + return Self; + } + + public BucketSortAggregationDescriptor Sort(SortOptionsDescriptor descriptor) + { + SortValue = null; + SortDescriptorAction = null; + SortDescriptorActions = null; + SortDescriptor = descriptor; + return Self; + } + + public BucketSortAggregationDescriptor Sort(Action> configure) + { + SortValue = null; + SortDescriptor = null; + SortDescriptorActions = null; + SortDescriptorAction = configure; + return Self; + } + + public BucketSortAggregationDescriptor Sort(params Action>[] configure) + { + SortValue = null; + SortDescriptor = null; + SortDescriptorAction = null; + SortDescriptorActions = configure; + return Self; + } + + public BucketSortAggregationDescriptor From(int? from) + { + FromValue = from; + return Self; + } + + public BucketSortAggregationDescriptor GapPolicy(Elastic.Clients.Elasticsearch.Aggregations.GapPolicy? gapPolicy) + { + GapPolicyValue = gapPolicy; + return Self; + } + + public BucketSortAggregationDescriptor Meta(Func, FluentDictionary> selector) + { + MetaValue = selector?.Invoke(new FluentDictionary()); + return Self; + } + + public BucketSortAggregationDescriptor Size(int? size) + { + SizeValue = size; + return Self; + } + + protected override void Serialize(Utf8JsonWriter writer, JsonSerializerOptions options, IElasticsearchClientSettings settings) + { + writer.WriteStartObject(); + writer.WritePropertyName("bucket_sort"); + writer.WriteStartObject(); + if (SortDescriptor is not null) + { + writer.WritePropertyName("sort"); + JsonSerializer.Serialize(writer, SortDescriptor, options); + } + else if (SortDescriptorAction is not null) + { + writer.WritePropertyName("sort"); + JsonSerializer.Serialize(writer, new SortOptionsDescriptor(SortDescriptorAction), options); + } + else if (SortDescriptorActions is not null) + { + writer.WritePropertyName("sort"); + if (SortDescriptorActions.Length > 1) + writer.WriteStartArray(); + foreach (var action in SortDescriptorActions) + { + JsonSerializer.Serialize(writer, new SortOptionsDescriptor(action), options); + } + + if (SortDescriptorActions.Length > 1) + writer.WriteEndArray(); + } + else if (SortValue is not null) + { + writer.WritePropertyName("sort"); + SingleOrManySerializationHelper.Serialize(SortValue, writer, options); + } + + if (FromValue.HasValue) + { + writer.WritePropertyName("from"); + writer.WriteNumberValue(FromValue.Value); + } + + if (GapPolicyValue is not null) + { + writer.WritePropertyName("gap_policy"); + JsonSerializer.Serialize(writer, GapPolicyValue, options); + } + + if (SizeValue.HasValue) + { + writer.WritePropertyName("size"); + writer.WriteNumberValue(SizeValue.Value); + } + + writer.WriteEndObject(); + if (MetaValue is not null) + { + writer.WritePropertyName("meta"); + JsonSerializer.Serialize(writer, MetaValue, options); + } + + writer.WriteEndObject(); + } +} + +public sealed partial class BucketSortAggregationDescriptor : SerializableDescriptor +{ + internal BucketSortAggregationDescriptor(Action configure) => configure.Invoke(this); + public BucketSortAggregationDescriptor() : base() + { + } + + private ICollection? SortValue { get; set; } + + private SortOptionsDescriptor SortDescriptor { get; set; } + + private Action SortDescriptorAction { get; set; } + + private Action[] SortDescriptorActions { get; set; } + + private int? FromValue { get; set; } + + private Elastic.Clients.Elasticsearch.Aggregations.GapPolicy? GapPolicyValue { get; set; } + + private IDictionary? MetaValue { get; set; } + + private int? SizeValue { get; set; } + + public BucketSortAggregationDescriptor Sort(ICollection? sort) + { + SortDescriptor = null; + SortDescriptorAction = null; + SortDescriptorActions = null; + SortValue = sort; + return Self; + } + + public BucketSortAggregationDescriptor Sort(SortOptionsDescriptor descriptor) + { + SortValue = null; + SortDescriptorAction = null; + SortDescriptorActions = null; + SortDescriptor = descriptor; + return Self; + } + + public BucketSortAggregationDescriptor Sort(Action configure) + { + SortValue = null; + SortDescriptor = null; + SortDescriptorActions = null; + SortDescriptorAction = configure; + return Self; + } + + public BucketSortAggregationDescriptor Sort(params Action[] configure) + { + SortValue = null; + SortDescriptor = null; + SortDescriptorAction = null; + SortDescriptorActions = configure; + return Self; + } + + public BucketSortAggregationDescriptor From(int? from) + { + FromValue = from; + return Self; + } + + public BucketSortAggregationDescriptor GapPolicy(Elastic.Clients.Elasticsearch.Aggregations.GapPolicy? gapPolicy) + { + GapPolicyValue = gapPolicy; + return Self; + } + + public BucketSortAggregationDescriptor Meta(Func, FluentDictionary> selector) + { + MetaValue = selector?.Invoke(new FluentDictionary()); + return Self; + } + + public BucketSortAggregationDescriptor Size(int? size) + { + SizeValue = size; + return Self; + } + + protected override void Serialize(Utf8JsonWriter writer, JsonSerializerOptions options, IElasticsearchClientSettings settings) + { + writer.WriteStartObject(); + writer.WritePropertyName("bucket_sort"); + writer.WriteStartObject(); + if (SortDescriptor is not null) + { + writer.WritePropertyName("sort"); + JsonSerializer.Serialize(writer, SortDescriptor, options); + } + else if (SortDescriptorAction is not null) + { + writer.WritePropertyName("sort"); + JsonSerializer.Serialize(writer, new SortOptionsDescriptor(SortDescriptorAction), options); + } + else if (SortDescriptorActions is not null) + { + writer.WritePropertyName("sort"); + if (SortDescriptorActions.Length > 1) + writer.WriteStartArray(); + foreach (var action in SortDescriptorActions) + { + JsonSerializer.Serialize(writer, new SortOptionsDescriptor(action), options); + } + + if (SortDescriptorActions.Length > 1) + writer.WriteEndArray(); + } + else if (SortValue is not null) + { + writer.WritePropertyName("sort"); + SingleOrManySerializationHelper.Serialize(SortValue, writer, options); + } + + if (FromValue.HasValue) + { + writer.WritePropertyName("from"); + writer.WriteNumberValue(FromValue.Value); + } + + if (GapPolicyValue is not null) + { + writer.WritePropertyName("gap_policy"); + JsonSerializer.Serialize(writer, GapPolicyValue, options); + } + + if (SizeValue.HasValue) + { + writer.WritePropertyName("size"); + writer.WriteNumberValue(SizeValue.Value); + } + + writer.WriteEndObject(); + if (MetaValue is not null) + { + writer.WritePropertyName("meta"); + JsonSerializer.Serialize(writer, MetaValue, options); + } + + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/TopHitsAggregation.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/TopHitsAggregation.g.cs index 092c53975e0..1c7d0cb8cdf 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/TopHitsAggregation.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/TopHitsAggregation.g.cs @@ -316,7 +316,7 @@ public override void Write(Utf8JsonWriter writer, TopHitsAggregation value, Json if (value.Sort is not null) { writer.WritePropertyName("sort"); - JsonSerializer.Serialize(writer, value.Sort, options); + SingleOrManySerializationHelper.Serialize(value.Sort, writer, options); } if (value.StoredFields is not null) diff --git a/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/TopMetricsAggregation.g.cs b/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/TopMetricsAggregation.g.cs index bf85e204124..197122db282 100644 --- a/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/TopMetricsAggregation.g.cs +++ b/src/Elastic.Clients.Elasticsearch/_Generated/Types/Aggregations/TopMetricsAggregation.g.cs @@ -172,7 +172,7 @@ public override void Write(Utf8JsonWriter writer, TopMetricsAggregation value, J if (value.Sort is not null) { writer.WritePropertyName("sort"); - JsonSerializer.Serialize(writer, value.Sort, options); + SingleOrManySerializationHelper.Serialize(value.Sort, writer, options); } writer.WriteEndObject(); diff --git a/tests/Tests/Aggregations/Pipeline/BucketSortAggregationUsageTests.cs b/tests/Tests/Aggregations/Pipeline/BucketSortAggregationUsageTests.cs new file mode 100644 index 00000000000..e4451312ac2 --- /dev/null +++ b/tests/Tests/Aggregations/Pipeline/BucketSortAggregationUsageTests.cs @@ -0,0 +1,116 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Elastic.Clients.Elasticsearch.Aggregations; +using System.Collections.Generic; +using System; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; +using Tests.Framework.EndpointTests.TestState; +using Tests.Core.Extensions; + +namespace Tests.Aggregations.Pipeline; + +public class BucketSortAggregationUsageTests : AggregationUsageTestBase +{ + public BucketSortAggregationUsageTests(ReadOnlyCluster cluster, EndpointUsage usage) : base(cluster, usage) { } + + protected override object AggregationJson => new + { + projects_started_per_month = new + { + date_histogram = new + { + field = "startedOn", + calendar_interval = "month", + }, + aggregations = new + { + commits = new + { + sum = new + { + field = "numberOfCommits" + } + }, + commits_bucket_sort = new + { + bucket_sort = new + { + sort = new { commits = new { order = "desc" } }, + from = 0, + size = 3, + gap_policy = "insert_zeros" + } + } + } + } + }; + +#pragma warning disable 618, 612 + protected override Action> FluentAggs => a => a + .DateHistogram("projects_started_per_month", dh => dh + .Field(p => p.StartedOn) + .CalendarInterval(CalendarInterval.Month) + .Aggregations(aa => aa + .Sum("commits", sm => sm + .Field(p => p.NumberOfCommits) + ) + .BucketSort("commits_bucket_sort", bs => bs + .Sort(s => s + .Field("commits", f => f.Order(SortOrder.Desc)) + ) + .From(0) + .Size(3) + .GapPolicy(GapPolicy.InsertZeros) + ) + ) + ); + + protected override AggregationDictionary InitializerAggs => + new DateHistogramAggregation("projects_started_per_month") + { + Field = "startedOn", + CalendarInterval = CalendarInterval.Month, + Aggregations = + new SumAggregation("commits", "numberOfCommits") && + new BucketSortAggregation("commits_bucket_sort") + { + Sort = new List + { + SortOptions.Field("commits", new FieldSort { Order = SortOrder.Desc }) + }, + From = 0, + Size = 3, + GapPolicy = GapPolicy.InsertZeros + } + }; +#pragma warning restore 618, 612 + + protected override void ExpectResponse(SearchResponse response) + { + response.ShouldBeValid(); + + var projectsPerMonth = response.Aggregations.GetDateHistogram("projects_started_per_month"); + projectsPerMonth.Should().NotBeNull(); + projectsPerMonth.Buckets.Should().NotBeNull(); + projectsPerMonth.Buckets.Count.Should().Be(3); + + double previousCommits = -1; + + // sum of commits should descend over buckets + foreach (var item in projectsPerMonth.Buckets) + { + var value = item.GetSum("commits").Value; + if (value == null) + continue; + + var numberOfCommits = value.Value; + if (Math.Abs(previousCommits - (-1)) > double.Epsilon) + numberOfCommits.Should().BeLessOrEqualTo(previousCommits); + + previousCommits = numberOfCommits; + } + } +}