From 30b621865b7acc364702d2da7a818fe06b09753b Mon Sep 17 00:00:00 2001 From: Stuart Cam Date: Fri, 17 Apr 2020 14:13:49 +1000 Subject: [PATCH] Implement Top Metrics aggregation (#4594) Implement Top Metrics aggregation --- src/Nest/Aggregations/AggregateDictionary.cs | 2 + src/Nest/Aggregations/AggregateFormatter.cs | 13 ++++ src/Nest/Aggregations/AggregationContainer.cs | 13 ++++ .../Metric/TopMetrics/TopMetricsAggregate.cs | 26 +++++++ .../TopMetrics/TopMetricsAggregation.cs | 74 +++++++++++++++++++ .../Metric/TopMetrics/TopMetricsValue.cs | 49 ++++++++++++ .../Visitor/AggregationVisitor.cs | 4 + .../TopMetricsAggregationUsageTests.cs | 74 +++++++++++++++++++ 8 files changed, 255 insertions(+) create mode 100644 src/Nest/Aggregations/Metric/TopMetrics/TopMetricsAggregate.cs create mode 100644 src/Nest/Aggregations/Metric/TopMetrics/TopMetricsAggregation.cs create mode 100644 src/Nest/Aggregations/Metric/TopMetrics/TopMetricsValue.cs create mode 100644 tests/Tests/Aggregations/Metric/TopMetrics/TopMetricsAggregationUsageTests.cs diff --git a/src/Nest/Aggregations/AggregateDictionary.cs b/src/Nest/Aggregations/AggregateDictionary.cs index defa6e149c8..6d01e76c91a 100644 --- a/src/Nest/Aggregations/AggregateDictionary.cs +++ b/src/Nest/Aggregations/AggregateDictionary.cs @@ -80,6 +80,8 @@ public ScriptedMetricAggregate ScriptedMetric(string key) public StringStatsAggregate StringStats(string key) => TryGet(key); + public TopMetricsAggregate TopMetrics(string key) => TryGet(key); + public StatsAggregate StatsBucket(string key) => TryGet(key); public ExtendedStatsAggregate ExtendedStats(string key) => TryGet(key); diff --git a/src/Nest/Aggregations/AggregateFormatter.cs b/src/Nest/Aggregations/AggregateFormatter.cs index 406802df356..bcdfea8b06a 100644 --- a/src/Nest/Aggregations/AggregateFormatter.cs +++ b/src/Nest/Aggregations/AggregateFormatter.cs @@ -51,6 +51,7 @@ internal class AggregateFormatter : IJsonFormatter { Parser.Hits, 8 }, { Parser.Location, 9 }, { Parser.Fields, 10 }, + { Parser.Top, 12 }, }; private static readonly byte[] SumOtherDocCount = JsonWriter.GetEncodedPropertyNameWithoutQuotation(Parser.SumOtherDocCount); @@ -151,6 +152,9 @@ private IAggregate ReadAggregate(ref JsonReader reader, IJsonFormatterResolver f case 10: aggregate = GetMatrixStatsAggregate(ref reader, formatterResolver, meta); break; + case 12: + aggregate = GetTopMetricsAggregate(ref reader, formatterResolver, meta); + break; } } else @@ -212,6 +216,14 @@ private IBucket ReadBucket(ref JsonReader reader, IJsonFormatterResolver formatt return matrixStats; } + private IAggregate GetTopMetricsAggregate(ref JsonReader reader, IJsonFormatterResolver formatterResolver, IReadOnlyDictionary meta) + { + var topMetrics = new TopMetricsAggregate { Meta = meta }; + var formatter = formatterResolver.GetFormatter>(); + topMetrics.Top = formatter.Deserialize(ref reader, formatterResolver); + return topMetrics; + } + private IAggregate GetTopHitsAggregate(ref JsonReader reader, IJsonFormatterResolver formatterResolver, IReadOnlyDictionary meta) { var count = 0; @@ -972,6 +984,7 @@ private static class Parser public const string DocCountErrorUpperBound = "doc_count_error_upper_bound"; public const string Fields = "fields"; public const string From = "from"; + public const string Top = "top"; public const string FromAsString = "from_as_string"; public const string Hits = "hits"; diff --git a/src/Nest/Aggregations/AggregationContainer.cs b/src/Nest/Aggregations/AggregationContainer.cs index e342a76c42a..416b31c38ed 100644 --- a/src/Nest/Aggregations/AggregationContainer.cs +++ b/src/Nest/Aggregations/AggregationContainer.cs @@ -262,6 +262,9 @@ public interface IAggregationContainer [DataMember(Name = "string_stats")] IStringStatsAggregation StringStats { get; set; } + [DataMember(Name = "top_metrics")] + ITopMetricsAggregation TopMetrics { get; set; } + void Accept(IAggregationVisitor visitor); } @@ -382,6 +385,8 @@ public class AggregationContainer : IAggregationContainer public IStringStatsAggregation StringStats { get; set; } + public ITopMetricsAggregation TopMetrics { get; set; } + public void Accept(IAggregationVisitor visitor) { if (visitor.Scope == AggregationVisitorScope.Unknown) visitor.Scope = AggregationVisitorScope.Aggregation; @@ -533,6 +538,8 @@ public class AggregationContainerDescriptor : DescriptorBase _SetInnerAggregation(name, selector, (a, d) => a.StringStats = d); + /// + public AggregationContainerDescriptor TopMetrics(string name, + Func, ITopMetricsAggregation> selector + ) => + _SetInnerAggregation(name, selector, (a, d) => a.TopMetrics = d); + /// /// Fluent methods do not assign to properties on `this` directly but on IAggregationContainers inside /// `this.Aggregations[string, IContainer] diff --git a/src/Nest/Aggregations/Metric/TopMetrics/TopMetricsAggregate.cs b/src/Nest/Aggregations/Metric/TopMetrics/TopMetricsAggregate.cs new file mode 100644 index 00000000000..c621a61c85c --- /dev/null +++ b/src/Nest/Aggregations/Metric/TopMetrics/TopMetricsAggregate.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; +using Elasticsearch.Net; + +namespace Nest +{ + public class TopMetricsAggregate : MetricAggregateBase + { + public IReadOnlyCollection Top { get; internal set; } = EmptyReadOnly.Collection; + } + + public class TopMetric + { + /// + /// The sort values used in sorting the hit relative to other hits + /// + [DataMember(Name = "sort")] + public IReadOnlyCollection Sort { get; internal set; } + + /// + /// The metrics. + /// + [DataMember(Name = "metrics")] + public IReadOnlyDictionary Metrics { get; internal set; } + } +} diff --git a/src/Nest/Aggregations/Metric/TopMetrics/TopMetricsAggregation.cs b/src/Nest/Aggregations/Metric/TopMetrics/TopMetricsAggregation.cs new file mode 100644 index 00000000000..91ba54ea57b --- /dev/null +++ b/src/Nest/Aggregations/Metric/TopMetrics/TopMetricsAggregation.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Elasticsearch.Net.Utf8Json; + +namespace Nest +{ + [InterfaceDataContract] + [ReadAs(typeof(TopMetricsAggregation))] + public interface ITopMetricsAggregation : IMetricAggregation + { + /// + /// Metrics selects the fields of the "top" document to return. You can request a single metric or multiple metrics. + /// + [DataMember(Name ="metrics")] + IList Metrics { get; set; } + + /// + /// Return the top few documents worth of metrics using this parameter. + /// + [DataMember(Name ="size")] + int? Size { get; set; } + + /// + /// The sort field in the metric request functions exactly the same as the sort field in the search request except: + /// * It can’t be used on binary, flattened, ip, keyword, or text fields. + /// * It only supports a single sort value so which document wins ties is not specified. + /// + [DataMember(Name ="sort")] + IList Sort { get; set; } + } + + public class TopMetricsAggregation : MetricAggregationBase, ITopMetricsAggregation + { + internal TopMetricsAggregation() { } + + public TopMetricsAggregation(string name) : base(name, null) { } + + /// + public IList Metrics { get; set; } + + /// + public int? Size { get; set; } + + /// + public IList Sort { get; set; } + + internal override void WrapInContainer(AggregationContainer c) => c.TopMetrics = this; + } + + public class TopMetricsAggregationDescriptor + : MetricAggregationDescriptorBase, ITopMetricsAggregation, T>, ITopMetricsAggregation where T : class + { + int? ITopMetricsAggregation.Size { get; set; } + + IList ITopMetricsAggregation.Sort { get; set; } + + IList ITopMetricsAggregation.Metrics { get; set; } + + /// + public TopMetricsAggregationDescriptor Size(int? size) => Assign(size, (a, v) => + a.Size = v); + + /// + public TopMetricsAggregationDescriptor Sort(Func, IPromise>> sortSelector) => + Assign(sortSelector, (a, v) => + a.Sort = v?.Invoke(new SortDescriptor())?.Value); + + /// + public TopMetricsAggregationDescriptor Metrics(Func, IPromise>> TopMetricsValueSelector) => + Assign(TopMetricsValueSelector, (a, v) => + a.Metrics = v?.Invoke(new TopMetricsValuesDescriptor())?.Value); + } +} diff --git a/src/Nest/Aggregations/Metric/TopMetrics/TopMetricsValue.cs b/src/Nest/Aggregations/Metric/TopMetrics/TopMetricsValue.cs new file mode 100644 index 00000000000..d2df88e9bf0 --- /dev/null +++ b/src/Nest/Aggregations/Metric/TopMetrics/TopMetricsValue.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Runtime.Serialization; +using Elasticsearch.Net.Utf8Json; + +namespace Nest +{ + /// + /// The configuration for a field or script that provides a value or weight + /// for + /// + [InterfaceDataContract] + [ReadAs(typeof(TopMetricsValue))] + public interface ITopMetricsValue + { + /// + /// The field that values should be extracted from + /// + [DataMember(Name = "field")] + Field Field { get; set; } + } + + /// + public class TopMetricsValue : ITopMetricsValue + { + internal TopMetricsValue() { } + + public TopMetricsValue(Field field) => Field = field; + + /// + public Field Field { get; set; } + } + + /// + public class TopMetricsValuesDescriptor : DescriptorPromiseBase, IList> + where T : class + { + public TopMetricsValuesDescriptor() : base(new List()) { } + + public TopMetricsValuesDescriptor Field(Field field) => AddTopMetrics(new TopMetricsValue { Field = field }); + + public TopMetricsValuesDescriptor Field(Expression> field) => + AddTopMetrics(new TopMetricsValue { Field = field}); + + private TopMetricsValuesDescriptor AddTopMetrics(ITopMetricsValue TopMetrics) => TopMetrics == null ? this : Assign(TopMetrics, (a, v) => a.Add(v)); + } + +} diff --git a/src/Nest/Aggregations/Visitor/AggregationVisitor.cs b/src/Nest/Aggregations/Visitor/AggregationVisitor.cs index 3620ac20a94..f86ace4273b 100644 --- a/src/Nest/Aggregations/Visitor/AggregationVisitor.cs +++ b/src/Nest/Aggregations/Visitor/AggregationVisitor.cs @@ -139,6 +139,8 @@ public interface IAggregationVisitor void Visit(IMovingFunctionAggregation aggregation); void Visit(IStringStatsAggregation aggregation); + + void Visit(ITopMetricsAggregation aggregation); } public class AggregationVisitor : IAggregationVisitor @@ -263,6 +265,8 @@ public class AggregationVisitor : IAggregationVisitor public virtual void Visit(IStringStatsAggregation aggregation) { } + public virtual void Visit(ITopMetricsAggregation aggregation) { } + public virtual void Visit(IAggregation aggregation) { } public virtual void Visit(IAggregationContainer aggregationContainer) { } diff --git a/tests/Tests/Aggregations/Metric/TopMetrics/TopMetricsAggregationUsageTests.cs b/tests/Tests/Aggregations/Metric/TopMetrics/TopMetricsAggregationUsageTests.cs new file mode 100644 index 00000000000..dea3d857040 --- /dev/null +++ b/tests/Tests/Aggregations/Metric/TopMetrics/TopMetricsAggregationUsageTests.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Elastic.Xunit.XunitPlumbing; +using FluentAssertions; +using Nest; +using Tests.Core.Extensions; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; +using Tests.Framework.EndpointTests.TestState; +using static Nest.Infer; + +namespace Tests.Aggregations.Metric.TopMetrics +{ + [SkipVersion("<7.7.0", "Available in 7.7.0")] + public class TopMetricsAggregationUsageTests : AggregationUsageTestBase + { + public TopMetricsAggregationUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override object AggregationJson => new + { + tm = new + { + top_metrics = new + { + metrics = new [] + { + new + { + field = "numberOfContributors" + } + }, + size = 10, + sort = new[] { new { numberOfContributors = new { order = "asc" } } } + } + } + }; + + protected override Func, IAggregationContainer> FluentAggs => a => a + .TopMetrics("tm", st => st + .Metrics(m => m.Field(p => p.NumberOfContributors)) + .Size(10) + .Sort(sort => sort + .Ascending("numberOfContributors") + ) + ); + + protected override AggregationDictionary InitializerAggs => + new TopMetricsAggregation("tm") + { + Metrics = new List + { + new TopMetricsValue(Field(p => p.NumberOfContributors)) + }, + Size = 10, + Sort = new List { new FieldSort { Field = "numberOfContributors", Order = SortOrder.Ascending } } + }; + + protected override void ExpectResponse(ISearchResponse response) + { + response.ShouldBeValid(); + var topMetrics = response.Aggregations.TopMetrics("tm"); + topMetrics.Should().NotBeNull(); + topMetrics.Top.Should().NotBeNull(); + topMetrics.Top.Count.Should().BeGreaterThan(0); + + var tipTop = topMetrics.Top.First(); + tipTop.Sort.Should().Should().NotBeNull(); + tipTop.Sort.Count.Should().BeGreaterThan(0); + tipTop.Metrics.Should().NotBeNull(); + tipTop.Metrics.Count.Should().BeGreaterThan(0); + } + } +}