Skip to content

Commit 30b6218

Browse files
codebraingithub-actions[bot]
authored andcommitted
Implement Top Metrics aggregation (#4594)
Implement Top Metrics aggregation
1 parent 5b074c3 commit 30b6218

File tree

8 files changed

+255
-0
lines changed

8 files changed

+255
-0
lines changed

src/Nest/Aggregations/AggregateDictionary.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public ScriptedMetricAggregate ScriptedMetric(string key)
8080

8181
public StringStatsAggregate StringStats(string key) => TryGet<StringStatsAggregate>(key);
8282

83+
public TopMetricsAggregate TopMetrics(string key) => TryGet<TopMetricsAggregate>(key);
84+
8385
public StatsAggregate StatsBucket(string key) => TryGet<StatsAggregate>(key);
8486

8587
public ExtendedStatsAggregate ExtendedStats(string key) => TryGet<ExtendedStatsAggregate>(key);

src/Nest/Aggregations/AggregateFormatter.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ internal class AggregateFormatter : IJsonFormatter<IAggregate>
5151
{ Parser.Hits, 8 },
5252
{ Parser.Location, 9 },
5353
{ Parser.Fields, 10 },
54+
{ Parser.Top, 12 },
5455
};
5556

5657
private static readonly byte[] SumOtherDocCount = JsonWriter.GetEncodedPropertyNameWithoutQuotation(Parser.SumOtherDocCount);
@@ -151,6 +152,9 @@ private IAggregate ReadAggregate(ref JsonReader reader, IJsonFormatterResolver f
151152
case 10:
152153
aggregate = GetMatrixStatsAggregate(ref reader, formatterResolver, meta);
153154
break;
155+
case 12:
156+
aggregate = GetTopMetricsAggregate(ref reader, formatterResolver, meta);
157+
break;
154158
}
155159
}
156160
else
@@ -212,6 +216,14 @@ private IAggregate GetMatrixStatsAggregate(ref JsonReader reader, IJsonFormatter
212216
return matrixStats;
213217
}
214218

219+
private IAggregate GetTopMetricsAggregate(ref JsonReader reader, IJsonFormatterResolver formatterResolver, IReadOnlyDictionary<string, object> meta)
220+
{
221+
var topMetrics = new TopMetricsAggregate { Meta = meta };
222+
var formatter = formatterResolver.GetFormatter<List<TopMetric>>();
223+
topMetrics.Top = formatter.Deserialize(ref reader, formatterResolver);
224+
return topMetrics;
225+
}
226+
215227
private IAggregate GetTopHitsAggregate(ref JsonReader reader, IJsonFormatterResolver formatterResolver, IReadOnlyDictionary<string, object> meta)
216228
{
217229
var count = 0;
@@ -972,6 +984,7 @@ private static class Parser
972984
public const string DocCountErrorUpperBound = "doc_count_error_upper_bound";
973985
public const string Fields = "fields";
974986
public const string From = "from";
987+
public const string Top = "top";
975988

976989
public const string FromAsString = "from_as_string";
977990
public const string Hits = "hits";

src/Nest/Aggregations/AggregationContainer.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ public interface IAggregationContainer
262262
[DataMember(Name = "string_stats")]
263263
IStringStatsAggregation StringStats { get; set; }
264264

265+
[DataMember(Name = "top_metrics")]
266+
ITopMetricsAggregation TopMetrics { get; set; }
267+
265268
void Accept(IAggregationVisitor visitor);
266269
}
267270

@@ -382,6 +385,8 @@ public class AggregationContainer : IAggregationContainer
382385

383386
public IStringStatsAggregation StringStats { get; set; }
384387

388+
public ITopMetricsAggregation TopMetrics { get; set; }
389+
385390
public void Accept(IAggregationVisitor visitor)
386391
{
387392
if (visitor.Scope == AggregationVisitorScope.Unknown) visitor.Scope = AggregationVisitorScope.Aggregation;
@@ -533,6 +538,8 @@ public class AggregationContainerDescriptor<T> : DescriptorBase<AggregationConta
533538

534539
IStringStatsAggregation IAggregationContainer.StringStats { get; set; }
535540

541+
ITopMetricsAggregation IAggregationContainer.TopMetrics { get; set; }
542+
536543
public void Accept(IAggregationVisitor visitor)
537544
{
538545
if (visitor.Scope == AggregationVisitorScope.Unknown) visitor.Scope = AggregationVisitorScope.Aggregation;
@@ -831,6 +838,12 @@ Func<StringStatsAggregationDescriptor<T>, IStringStatsAggregation> selector
831838
) =>
832839
_SetInnerAggregation(name, selector, (a, d) => a.StringStats = d);
833840

841+
/// <inheritdoc cref="ITopMetricsAggregation"/>
842+
public AggregationContainerDescriptor<T> TopMetrics(string name,
843+
Func<TopMetricsAggregationDescriptor<T>, ITopMetricsAggregation> selector
844+
) =>
845+
_SetInnerAggregation(name, selector, (a, d) => a.TopMetrics = d);
846+
834847
/// <summary>
835848
/// Fluent methods do not assign to properties on `this` directly but on IAggregationContainers inside
836849
/// `this.Aggregations[string, IContainer]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Collections.Generic;
2+
using System.Runtime.Serialization;
3+
using Elasticsearch.Net;
4+
5+
namespace Nest
6+
{
7+
public class TopMetricsAggregate : MetricAggregateBase
8+
{
9+
public IReadOnlyCollection<TopMetric> Top { get; internal set; } = EmptyReadOnly<TopMetric>.Collection;
10+
}
11+
12+
public class TopMetric
13+
{
14+
/// <summary>
15+
/// The sort values used in sorting the hit relative to other hits
16+
/// </summary>
17+
[DataMember(Name = "sort")]
18+
public IReadOnlyCollection<object> Sort { get; internal set; }
19+
20+
/// <summary>
21+
/// The metrics.
22+
/// </summary>
23+
[DataMember(Name = "metrics")]
24+
public IReadOnlyDictionary<string, object> Metrics { get; internal set; }
25+
}
26+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Runtime.Serialization;
4+
using Elasticsearch.Net.Utf8Json;
5+
6+
namespace Nest
7+
{
8+
[InterfaceDataContract]
9+
[ReadAs(typeof(TopMetricsAggregation))]
10+
public interface ITopMetricsAggregation : IMetricAggregation
11+
{
12+
/// <summary>
13+
/// Metrics selects the fields of the "top" document to return. You can request a single metric or multiple metrics.
14+
/// </summary>
15+
[DataMember(Name ="metrics")]
16+
IList<ITopMetricsValue> Metrics { get; set; }
17+
18+
/// <summary>
19+
/// Return the top few documents worth of metrics using this parameter.
20+
/// </summary>
21+
[DataMember(Name ="size")]
22+
int? Size { get; set; }
23+
24+
/// <summary>
25+
/// The sort field in the metric request functions exactly the same as the sort field in the search request except:
26+
/// * It can’t be used on binary, flattened, ip, keyword, or text fields.
27+
/// * It only supports a single sort value so which document wins ties is not specified.
28+
/// </summary>
29+
[DataMember(Name ="sort")]
30+
IList<ISort> Sort { get; set; }
31+
}
32+
33+
public class TopMetricsAggregation : MetricAggregationBase, ITopMetricsAggregation
34+
{
35+
internal TopMetricsAggregation() { }
36+
37+
public TopMetricsAggregation(string name) : base(name, null) { }
38+
39+
/// <inheritdoc cref="ITopMetricsAggregation.Metrics" />
40+
public IList<ITopMetricsValue> Metrics { get; set; }
41+
42+
/// <inheritdoc cref="ITopMetricsAggregation.Size" />
43+
public int? Size { get; set; }
44+
45+
/// <inheritdoc cref="ITopMetricsAggregation.Sort" />
46+
public IList<ISort> Sort { get; set; }
47+
48+
internal override void WrapInContainer(AggregationContainer c) => c.TopMetrics = this;
49+
}
50+
51+
public class TopMetricsAggregationDescriptor<T>
52+
: MetricAggregationDescriptorBase<TopMetricsAggregationDescriptor<T>, ITopMetricsAggregation, T>, ITopMetricsAggregation where T : class
53+
{
54+
int? ITopMetricsAggregation.Size { get; set; }
55+
56+
IList<ISort> ITopMetricsAggregation.Sort { get; set; }
57+
58+
IList<ITopMetricsValue> ITopMetricsAggregation.Metrics { get; set; }
59+
60+
/// <inheritdoc cref="ITopMetricsAggregation.Size" />
61+
public TopMetricsAggregationDescriptor<T> Size(int? size) => Assign(size, (a, v) =>
62+
a.Size = v);
63+
64+
/// <inheritdoc cref="ITopMetricsAggregation.Sort" />
65+
public TopMetricsAggregationDescriptor<T> Sort(Func<SortDescriptor<T>, IPromise<IList<ISort>>> sortSelector) =>
66+
Assign(sortSelector, (a, v) =>
67+
a.Sort = v?.Invoke(new SortDescriptor<T>())?.Value);
68+
69+
/// <inheritdoc cref="ITopMetricsAggregation.Metrics" />
70+
public TopMetricsAggregationDescriptor<T> Metrics(Func<TopMetricsValuesDescriptor<T>, IPromise<IList<ITopMetricsValue>>> TopMetricsValueSelector) =>
71+
Assign(TopMetricsValueSelector, (a, v) =>
72+
a.Metrics = v?.Invoke(new TopMetricsValuesDescriptor<T>())?.Value);
73+
}
74+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq.Expressions;
4+
using System.Runtime.Serialization;
5+
using Elasticsearch.Net.Utf8Json;
6+
7+
namespace Nest
8+
{
9+
/// <summary>
10+
/// The configuration for a field or script that provides a value or weight
11+
/// for <see cref="TopMetricsAggregation" />
12+
/// </summary>
13+
[InterfaceDataContract]
14+
[ReadAs(typeof(TopMetricsValue))]
15+
public interface ITopMetricsValue
16+
{
17+
/// <summary>
18+
/// The field that values should be extracted from
19+
/// </summary>
20+
[DataMember(Name = "field")]
21+
Field Field { get; set; }
22+
}
23+
24+
/// <inheritdoc />
25+
public class TopMetricsValue : ITopMetricsValue
26+
{
27+
internal TopMetricsValue() { }
28+
29+
public TopMetricsValue(Field field) => Field = field;
30+
31+
/// <inheritdoc />
32+
public Field Field { get; set; }
33+
}
34+
35+
/// <inheritdoc cref="ITopMetricsAggregation" />
36+
public class TopMetricsValuesDescriptor<T> : DescriptorPromiseBase<TopMetricsValuesDescriptor<T>, IList<ITopMetricsValue>>
37+
where T : class
38+
{
39+
public TopMetricsValuesDescriptor() : base(new List<ITopMetricsValue>()) { }
40+
41+
public TopMetricsValuesDescriptor<T> Field(Field field) => AddTopMetrics(new TopMetricsValue { Field = field });
42+
43+
public TopMetricsValuesDescriptor<T> Field<TValue>(Expression<Func<T, TValue>> field) =>
44+
AddTopMetrics(new TopMetricsValue { Field = field});
45+
46+
private TopMetricsValuesDescriptor<T> AddTopMetrics(ITopMetricsValue TopMetrics) => TopMetrics == null ? this : Assign(TopMetrics, (a, v) => a.Add(v));
47+
}
48+
49+
}

src/Nest/Aggregations/Visitor/AggregationVisitor.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ public interface IAggregationVisitor
139139
void Visit(IMovingFunctionAggregation aggregation);
140140

141141
void Visit(IStringStatsAggregation aggregation);
142+
143+
void Visit(ITopMetricsAggregation aggregation);
142144
}
143145

144146
public class AggregationVisitor : IAggregationVisitor
@@ -263,6 +265,8 @@ public virtual void Visit(IMovingFunctionAggregation aggregation) { }
263265

264266
public virtual void Visit(IStringStatsAggregation aggregation) { }
265267

268+
public virtual void Visit(ITopMetricsAggregation aggregation) { }
269+
266270
public virtual void Visit(IAggregation aggregation) { }
267271

268272
public virtual void Visit(IAggregationContainer aggregationContainer) { }
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Elastic.Xunit.XunitPlumbing;
5+
using FluentAssertions;
6+
using Nest;
7+
using Tests.Core.Extensions;
8+
using Tests.Core.ManagedElasticsearch.Clusters;
9+
using Tests.Domain;
10+
using Tests.Framework.EndpointTests.TestState;
11+
using static Nest.Infer;
12+
13+
namespace Tests.Aggregations.Metric.TopMetrics
14+
{
15+
[SkipVersion("<7.7.0", "Available in 7.7.0")]
16+
public class TopMetricsAggregationUsageTests : AggregationUsageTestBase
17+
{
18+
public TopMetricsAggregationUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { }
19+
20+
protected override object AggregationJson => new
21+
{
22+
tm = new
23+
{
24+
top_metrics = new
25+
{
26+
metrics = new []
27+
{
28+
new
29+
{
30+
field = "numberOfContributors"
31+
}
32+
},
33+
size = 10,
34+
sort = new[] { new { numberOfContributors = new { order = "asc" } } }
35+
}
36+
}
37+
};
38+
39+
protected override Func<AggregationContainerDescriptor<Project>, IAggregationContainer> FluentAggs => a => a
40+
.TopMetrics("tm", st => st
41+
.Metrics(m => m.Field(p => p.NumberOfContributors))
42+
.Size(10)
43+
.Sort(sort => sort
44+
.Ascending("numberOfContributors")
45+
)
46+
);
47+
48+
protected override AggregationDictionary InitializerAggs =>
49+
new TopMetricsAggregation("tm")
50+
{
51+
Metrics = new List<ITopMetricsValue>
52+
{
53+
new TopMetricsValue(Field<Project>(p => p.NumberOfContributors))
54+
},
55+
Size = 10,
56+
Sort = new List<ISort> { new FieldSort { Field = "numberOfContributors", Order = SortOrder.Ascending } }
57+
};
58+
59+
protected override void ExpectResponse(ISearchResponse<Project> response)
60+
{
61+
response.ShouldBeValid();
62+
var topMetrics = response.Aggregations.TopMetrics("tm");
63+
topMetrics.Should().NotBeNull();
64+
topMetrics.Top.Should().NotBeNull();
65+
topMetrics.Top.Count.Should().BeGreaterThan(0);
66+
67+
var tipTop = topMetrics.Top.First();
68+
tipTop.Sort.Should().Should().NotBeNull();
69+
tipTop.Sort.Count.Should().BeGreaterThan(0);
70+
tipTop.Metrics.Should().NotBeNull();
71+
tipTop.Metrics.Count.Should().BeGreaterThan(0);
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)