Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Top Metrics aggregation #4594

Merged
merged 2 commits into from
Apr 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}