Skip to content

Commit

Permalink
Implement Top Metrics aggregation (#4594) (#4620)
Browse files Browse the repository at this point in the history
Implement Top Metrics aggregation

Co-authored-by: Stuart Cam <stuart.cam@elastic.co>
  • Loading branch information
github-actions[bot] and codebrain committed Apr 17, 2020
1 parent 5b074c3 commit 30364be
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/Nest/Aggregations/AggregateDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ public ScriptedMetricAggregate ScriptedMetric(string key)

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

public TopMetricsAggregate TopMetrics(string key) => TryGet<TopMetricsAggregate>(key);

public StatsAggregate StatsBucket(string key) => TryGet<StatsAggregate>(key);

public ExtendedStatsAggregate ExtendedStats(string key) => TryGet<ExtendedStatsAggregate>(key);
Expand Down
13 changes: 13 additions & 0 deletions src/Nest/Aggregations/AggregateFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ internal class AggregateFormatter : IJsonFormatter<IAggregate>
{ Parser.Hits, 8 },
{ Parser.Location, 9 },
{ Parser.Fields, 10 },
{ Parser.Top, 12 },
};

private static readonly byte[] SumOtherDocCount = JsonWriter.GetEncodedPropertyNameWithoutQuotation(Parser.SumOtherDocCount);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -212,6 +216,14 @@ private IBucket ReadBucket(ref JsonReader reader, IJsonFormatterResolver formatt
return matrixStats;
}

private IAggregate GetTopMetricsAggregate(ref JsonReader reader, IJsonFormatterResolver formatterResolver, IReadOnlyDictionary<string, object> meta)
{
var topMetrics = new TopMetricsAggregate { Meta = meta };
var formatter = formatterResolver.GetFormatter<List<TopMetric>>();
topMetrics.Top = formatter.Deserialize(ref reader, formatterResolver);
return topMetrics;
}

private IAggregate GetTopHitsAggregate(ref JsonReader reader, IJsonFormatterResolver formatterResolver, IReadOnlyDictionary<string, object> meta)
{
var count = 0;
Expand Down Expand Up @@ -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";
Expand Down
13 changes: 13 additions & 0 deletions src/Nest/Aggregations/AggregationContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -533,6 +538,8 @@ public class AggregationContainerDescriptor<T> : DescriptorBase<AggregationConta

IStringStatsAggregation IAggregationContainer.StringStats { get; set; }

ITopMetricsAggregation IAggregationContainer.TopMetrics { get; set; }

public void Accept(IAggregationVisitor visitor)
{
if (visitor.Scope == AggregationVisitorScope.Unknown) visitor.Scope = AggregationVisitorScope.Aggregation;
Expand Down Expand Up @@ -831,6 +838,12 @@ public void Accept(IAggregationVisitor visitor)
) =>
_SetInnerAggregation(name, selector, (a, d) => a.StringStats = d);

/// <inheritdoc cref="ITopMetricsAggregation"/>
public AggregationContainerDescriptor<T> TopMetrics(string name,
Func<TopMetricsAggregationDescriptor<T>, ITopMetricsAggregation> selector
) =>
_SetInnerAggregation(name, selector, (a, d) => a.TopMetrics = d);

/// <summary>
/// Fluent methods do not assign to properties on `this` directly but on IAggregationContainers inside
/// `this.Aggregations[string, IContainer]
Expand Down
26 changes: 26 additions & 0 deletions src/Nest/Aggregations/Metric/TopMetrics/TopMetricsAggregate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Elasticsearch.Net;

namespace Nest
{
public class TopMetricsAggregate : MetricAggregateBase
{
public IReadOnlyCollection<TopMetric> Top { get; internal set; } = EmptyReadOnly<TopMetric>.Collection;
}

public class TopMetric
{
/// <summary>
/// The sort values used in sorting the hit relative to other hits
/// </summary>
[DataMember(Name = "sort")]
public IReadOnlyCollection<object> Sort { get; internal set; }

/// <summary>
/// The metrics.
/// </summary>
[DataMember(Name = "metrics")]
public IReadOnlyDictionary<string, object> Metrics { get; internal set; }
}
}
74 changes: 74 additions & 0 deletions src/Nest/Aggregations/Metric/TopMetrics/TopMetricsAggregation.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Metrics selects the fields of the "top" document to return. You can request a single metric or multiple metrics.
/// </summary>
[DataMember(Name ="metrics")]
IList<ITopMetricsValue> Metrics { get; set; }

/// <summary>
/// Return the top few documents worth of metrics using this parameter.
/// </summary>
[DataMember(Name ="size")]
int? Size { get; set; }

/// <summary>
/// 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.
/// </summary>
[DataMember(Name ="sort")]
IList<ISort> Sort { get; set; }
}

public class TopMetricsAggregation : MetricAggregationBase, ITopMetricsAggregation
{
internal TopMetricsAggregation() { }

public TopMetricsAggregation(string name) : base(name, null) { }

/// <inheritdoc cref="ITopMetricsAggregation.Metrics" />
public IList<ITopMetricsValue> Metrics { get; set; }

/// <inheritdoc cref="ITopMetricsAggregation.Size" />
public int? Size { get; set; }

/// <inheritdoc cref="ITopMetricsAggregation.Sort" />
public IList<ISort> Sort { get; set; }

internal override void WrapInContainer(AggregationContainer c) => c.TopMetrics = this;
}

public class TopMetricsAggregationDescriptor<T>
: MetricAggregationDescriptorBase<TopMetricsAggregationDescriptor<T>, ITopMetricsAggregation, T>, ITopMetricsAggregation where T : class
{
int? ITopMetricsAggregation.Size { get; set; }

IList<ISort> ITopMetricsAggregation.Sort { get; set; }

IList<ITopMetricsValue> ITopMetricsAggregation.Metrics { get; set; }

/// <inheritdoc cref="ITopMetricsAggregation.Size" />
public TopMetricsAggregationDescriptor<T> Size(int? size) => Assign(size, (a, v) =>
a.Size = v);

/// <inheritdoc cref="ITopMetricsAggregation.Sort" />
public TopMetricsAggregationDescriptor<T> Sort(Func<SortDescriptor<T>, IPromise<IList<ISort>>> sortSelector) =>
Assign(sortSelector, (a, v) =>
a.Sort = v?.Invoke(new SortDescriptor<T>())?.Value);

/// <inheritdoc cref="ITopMetricsAggregation.Metrics" />
public TopMetricsAggregationDescriptor<T> Metrics(Func<TopMetricsValuesDescriptor<T>, IPromise<IList<ITopMetricsValue>>> TopMetricsValueSelector) =>
Assign(TopMetricsValueSelector, (a, v) =>
a.Metrics = v?.Invoke(new TopMetricsValuesDescriptor<T>())?.Value);
}
}
49 changes: 49 additions & 0 deletions src/Nest/Aggregations/Metric/TopMetrics/TopMetricsValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Runtime.Serialization;
using Elasticsearch.Net.Utf8Json;

namespace Nest
{
/// <summary>
/// The configuration for a field or script that provides a value or weight
/// for <see cref="TopMetricsAggregation" />
/// </summary>
[InterfaceDataContract]
[ReadAs(typeof(TopMetricsValue))]
public interface ITopMetricsValue
{
/// <summary>
/// The field that values should be extracted from
/// </summary>
[DataMember(Name = "field")]
Field Field { get; set; }
}

/// <inheritdoc />
public class TopMetricsValue : ITopMetricsValue
{
internal TopMetricsValue() { }

public TopMetricsValue(Field field) => Field = field;

/// <inheritdoc />
public Field Field { get; set; }
}

/// <inheritdoc cref="ITopMetricsAggregation" />
public class TopMetricsValuesDescriptor<T> : DescriptorPromiseBase<TopMetricsValuesDescriptor<T>, IList<ITopMetricsValue>>
where T : class
{
public TopMetricsValuesDescriptor() : base(new List<ITopMetricsValue>()) { }

public TopMetricsValuesDescriptor<T> Field(Field field) => AddTopMetrics(new TopMetricsValue { Field = field });

public TopMetricsValuesDescriptor<T> Field<TValue>(Expression<Func<T, TValue>> field) =>
AddTopMetrics(new TopMetricsValue { Field = field});

private TopMetricsValuesDescriptor<T> AddTopMetrics(ITopMetricsValue TopMetrics) => TopMetrics == null ? this : Assign(TopMetrics, (a, v) => a.Add(v));
}

}
4 changes: 4 additions & 0 deletions src/Nest/Aggregations/Visitor/AggregationVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ public interface IAggregationVisitor
void Visit(IMovingFunctionAggregation aggregation);

void Visit(IStringStatsAggregation aggregation);

void Visit(ITopMetricsAggregation aggregation);
}

public class AggregationVisitor : IAggregationVisitor
Expand Down Expand Up @@ -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) { }
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AggregationContainerDescriptor<Project>, 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<ITopMetricsValue>
{
new TopMetricsValue(Field<Project>(p => p.NumberOfContributors))
},
Size = 10,
Sort = new List<ISort> { new FieldSort { Field = "numberOfContributors", Order = SortOrder.Ascending } }
};

protected override void ExpectResponse(ISearchResponse<Project> 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);
}
}
}

0 comments on commit 30364be

Please sign in to comment.