Skip to content

Commit bd2359a

Browse files
Mpdreamzrusscam
authored andcommitted
Add support for Weighted Avg Aggregation (#3417)
This commit adds support for weighted average aggregation (cherry picked from commit fdb05e8)
1 parent 156d4bb commit bd2359a

File tree

7 files changed

+294
-13
lines changed

7 files changed

+294
-13
lines changed

src/Nest/Aggregations/AggregateDictionary.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ internal static string[] TypedKeyTokens(string key)
5656

5757
public ValueAggregate SerialDifferencing(string key) => this.TryGet<ValueAggregate>(key);
5858

59+
public ValueAggregate WeightedAverage(string key) => this.TryGet<ValueAggregate>(key);
60+
5961
public KeyedValueAggregate MaxBucket(string key) => this.TryGet<KeyedValueAggregate>(key);
6062

6163
public KeyedValueAggregate MinBucket(string key) => this.TryGet<KeyedValueAggregate>(key);

src/Nest/Aggregations/Aggregation.cs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Newtonsoft.Json;
22
using System.Collections.Generic;
3+
using System.Linq;
34

45
namespace Nest
56
{
@@ -9,14 +10,25 @@ namespace Nest
910
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
1011
public interface IAggregation
1112
{
13+
/// <summary>
14+
/// name of the aggregation
15+
/// </summary>
1216
string Name { get; set; }
17+
18+
/// <summary>
19+
/// metadata to associate with the individual aggregation at request time that
20+
/// will be returned in place at response time
21+
/// </summary>
1322
IDictionary<string, object> Meta { get; set; }
1423
}
1524

25+
/// <inheritdoc />
1626
public abstract class AggregationBase : IAggregation
1727
{
28+
/// <inheritdoc />
1829
string IAggregation.Name { get; set; }
1930

31+
/// <inheritdoc />
2032
public IDictionary<string, object> Meta { get; set; }
2133

2234
internal AggregationBase() { }
@@ -34,12 +46,13 @@ protected AggregationBase(string name)
3446
//always evaluate to false so that each side of && equation is evaluated
3547
public static bool operator true(AggregationBase a) => false;
3648

37-
public static AggregationBase operator &(AggregationBase left, AggregationBase right)
38-
{
39-
return new AggregationCombinator(null, left, right);
40-
}
49+
public static AggregationBase operator &(AggregationBase left, AggregationBase right) =>
50+
new AggregationCombinator(null, left, right);
4151
}
4252

53+
/// <summary>
54+
/// Combines aggregations into a single list of aggregations
55+
/// </summary>
4356
internal class AggregationCombinator : AggregationBase, IAggregation
4457
{
4558
internal List<AggregationBase> Aggregations { get; } = new List<AggregationBase>();
@@ -54,13 +67,17 @@ public AggregationCombinator(string name, AggregationBase left, AggregationBase
5467

5568
private void AddAggregation(AggregationBase agg)
5669
{
57-
if (agg == null) return;
58-
var combinator = agg as AggregationCombinator;
59-
if ((combinator?.Aggregations.HasAny()).GetValueOrDefault(false))
70+
switch (agg)
6071
{
61-
this.Aggregations.AddRange(combinator.Aggregations);
72+
case null:
73+
return;
74+
case AggregationCombinator combinator when combinator.Aggregations.Any():
75+
this.Aggregations.AddRange(combinator.Aggregations);
76+
break;
77+
default:
78+
this.Aggregations.Add(agg);
79+
break;
6280
}
63-
else this.Aggregations.Add(agg);
6481
}
6582
}
6683
}

src/Nest/Aggregations/AggregationContainer.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,9 @@ public interface IAggregationContainer
218218
[JsonProperty("composite")]
219219
ICompositeAggregation Composite { get; set; }
220220

221+
[JsonProperty("weighted_avg")]
222+
IWeightedAverageAggregation WeightedAverage { get; set; }
223+
221224
[JsonProperty("aggs")]
222225
AggregationDictionary Aggregations { get; set; }
223226

@@ -320,6 +323,8 @@ public class AggregationContainer : IAggregationContainer
320323

321324
public ICompositeAggregation Composite { get; set; }
322325

326+
public IWeightedAverageAggregation WeightedAverage { get; set; }
327+
323328
public AggregationDictionary Aggregations { get; set; }
324329

325330
public static implicit operator AggregationContainer(AggregationBase aggregator)
@@ -457,6 +462,8 @@ public class AggregationContainerDescriptor<T> : DescriptorBase<AggregationConta
457462

458463
ICompositeAggregation IAggregationContainer.Composite { get; set; }
459464

465+
IWeightedAverageAggregation IAggregationContainer.WeightedAverage { get; set; }
466+
460467
public AggregationContainerDescriptor<T> Average(string name,
461468
Func<AverageAggregationDescriptor<T>, IAverageAggregation> selector) =>
462469
_SetInnerAggregation(name, selector, (a, d) => a.Average = d);
@@ -657,6 +664,10 @@ public AggregationContainerDescriptor<T> Composite(string name,
657664
Func<CompositeAggregationDescriptor<T>, ICompositeAggregation> selector) =>
658665
_SetInnerAggregation(name, selector, (a, d) => a.Composite = d);
659666

667+
public AggregationContainerDescriptor<T> WeightedAverage(string name,
668+
Func<WeightedAverageAggregationDescriptor<T>, IWeightedAverageAggregation> selector) =>
669+
_SetInnerAggregation(name, selector, (a, d) => a.WeightedAverage = d);
670+
660671
/// <summary>
661672
/// Fluent methods do not assign to properties on `this` directly but on IAggregationContainers inside `this.Aggregations[string, IContainer]
662673
/// </summary>

src/Nest/Aggregations/Metric/MetricAggregation.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ public abstract class MetricAggregationBase : AggregationBase, IMetricAggregatio
2121
{
2222
internal MetricAggregationBase() { }
2323

24-
protected MetricAggregationBase(string name, Field field) : base(name)
25-
{
26-
this.Field = field;
27-
}
24+
protected MetricAggregationBase(string name, Field field) : base(name) => this.Field = field;
2825

2926
public Field Field { get; set; }
3027
public virtual IScript Script { get; set; }
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq.Expressions;
4+
using Newtonsoft.Json;
5+
6+
namespace Nest
7+
{
8+
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
9+
[ContractJsonConverter(typeof(AggregationJsonConverter<WeightedAverageAggregation>))]
10+
public interface IWeightedAverageAggregation : IAggregation
11+
{
12+
/// <summary> The configuration for the field or script that provides the values</summary>
13+
[JsonProperty("value")]
14+
IWeightedAverageValue Value { get; set; }
15+
/// <summary> The configuration for the field or script that provides the weights</summary>
16+
[JsonProperty("weight")]
17+
IWeightedAverageValue Weight { get; set; }
18+
/// <summary> The optional numeric response formatter</summary>
19+
[JsonProperty("format")]
20+
string Format { get; set; }
21+
/// <summary> A hint about the values for pure scripts or unmapped fields </summary>
22+
[JsonProperty("value_type")]
23+
ValueType? ValueType { get; set; }
24+
}
25+
26+
public class WeightedAverageAggregation : AggregationBase, IWeightedAverageAggregation
27+
{
28+
internal WeightedAverageAggregation() { }
29+
public WeightedAverageAggregation(string name) : base(name) { }
30+
31+
internal override void WrapInContainer(AggregationContainer c) => c.WeightedAverage = this;
32+
33+
/// <inheritdoc cref="IWeightedAverageAggregation.Value"/>
34+
public IWeightedAverageValue Value { get; set; }
35+
/// <inheritdoc cref="IWeightedAverageAggregation.Weight"/>
36+
public IWeightedAverageValue Weight { get; set; }
37+
/// <inheritdoc cref="IWeightedAverageAggregation.Format"/>
38+
public string Format { get; set; }
39+
/// <inheritdoc cref="IWeightedAverageAggregation.ValueType"/>
40+
public ValueType? ValueType { get; set; }
41+
}
42+
43+
public class WeightedAverageAggregationDescriptor<T>
44+
: DescriptorBase<WeightedAverageAggregationDescriptor<T>, IWeightedAverageAggregation>
45+
, IWeightedAverageAggregation
46+
where T : class
47+
{
48+
IWeightedAverageValue IWeightedAverageAggregation.Value { get; set; }
49+
IWeightedAverageValue IWeightedAverageAggregation.Weight { get; set; }
50+
string IWeightedAverageAggregation.Format { get; set; }
51+
ValueType? IWeightedAverageAggregation.ValueType { get; set; }
52+
string IAggregation.Name { get; set; }
53+
IDictionary<string, object> IAggregation.Meta { get; set; }
54+
55+
/// <inheritdoc cref="IAggregation.Meta"/>
56+
public WeightedAverageAggregationDescriptor<T> Meta(Func<FluentDictionary<string, object>, FluentDictionary<string, object>> selector) =>
57+
Assign(a => a.Meta = selector?.Invoke(new FluentDictionary<string, object>()));
58+
59+
/// <inheritdoc cref="IWeightedAverageAggregation.Value"/>
60+
public WeightedAverageAggregationDescriptor<T> Value(Func<WeightedAverageValueDescriptor<T>, IWeightedAverageValue> selector) =>
61+
Assign(a => a.Value = selector?.Invoke(new WeightedAverageValueDescriptor<T>()));
62+
63+
/// <inheritdoc cref="IWeightedAverageAggregation.Weight"/>
64+
public WeightedAverageAggregationDescriptor<T> Weight(Func<WeightedAverageValueDescriptor<T>, IWeightedAverageValue> selector) =>
65+
Assign(a => a.Weight = selector?.Invoke(new WeightedAverageValueDescriptor<T>()));
66+
67+
/// <inheritdoc cref="IWeightedAverageAggregation.Format"/>
68+
public WeightedAverageAggregationDescriptor<T> Format(string format) => Assign(a => a.Format = format);
69+
70+
/// <inheritdoc cref="IWeightedAverageAggregation.ValueType"/>
71+
public WeightedAverageAggregationDescriptor<T> ValueType(ValueType? valueType) => Assign(a => a.ValueType = valueType);
72+
}
73+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System;
2+
using System.Linq.Expressions;
3+
using System.Runtime.Serialization;
4+
using Newtonsoft.Json;
5+
using Newtonsoft.Json.Converters;
6+
7+
namespace Nest
8+
{
9+
/// <summary>
10+
/// The configuration for a field or scrip that provides a value or weight
11+
/// for <see cref="WeightedAverageAggregation"/>
12+
/// </summary>
13+
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
14+
[ContractJsonConverter(typeof(ReadAsTypeJsonConverter<WeightedAverageValue>))]
15+
public interface IWeightedAverageValue
16+
{
17+
/// <summary>
18+
/// The field that values should be extracted from
19+
/// </summary>
20+
[JsonProperty("field")]
21+
Field Field { get; set; }
22+
23+
/// <summary>
24+
/// A script to derive the value and the weight from
25+
/// </summary>
26+
[JsonProperty("script")]
27+
IScript Script { get; set; }
28+
29+
/// <summary>
30+
/// defines how documents that are missing a value should be treated.
31+
/// The default behavior is different for value and weight:
32+
/// By default, if the value field is missing the document is ignored and the aggregation
33+
/// moves on to the next document.
34+
/// If the weight field is missing, it is assumed to have a weight of 1 (like a normal average).
35+
/// </summary>
36+
[JsonProperty("missing")]
37+
double? Missing { get; set; }
38+
}
39+
40+
/// <inheritdoc />
41+
public class WeightedAverageValue : IWeightedAverageValue
42+
{
43+
internal WeightedAverageValue() { }
44+
public WeightedAverageValue(Field field) => this.Field = field;
45+
public WeightedAverageValue(IScript script) => this.Script = script;
46+
47+
/// <inheritdoc />
48+
public Field Field { get; set; }
49+
/// <inheritdoc />
50+
public IScript Script { get; set; }
51+
/// <inheritdoc />
52+
public double? Missing { get; set; }
53+
}
54+
55+
/// <inheritdoc cref="IWeightedAverageAggregation" />
56+
public class WeightedAverageValueDescriptor<T> : DescriptorBase<WeightedAverageValueDescriptor<T>, IWeightedAverageValue>
57+
, IWeightedAverageValue
58+
where T : class
59+
{
60+
Field IWeightedAverageValue.Field { get; set; }
61+
IScript IWeightedAverageValue.Script { get; set; }
62+
double? IWeightedAverageValue.Missing { get; set; }
63+
64+
/// <inheritdoc cref="IWeightedAverageValue.Field" />
65+
public WeightedAverageValueDescriptor<T> Field(Field field) => Assign(a => a.Field = field);
66+
67+
/// <inheritdoc cref="IWeightedAverageValue.Field" />
68+
public WeightedAverageValueDescriptor<T> Field(Expression<Func<T, object>> field) => Assign(a => a.Field = field);
69+
70+
/// <inheritdoc cref="IWeightedAverageValue.Script" />
71+
public virtual WeightedAverageValueDescriptor<T> Script(string script) => Assign(a => a.Script = new InlineScript(script));
72+
73+
/// <inheritdoc cref="IWeightedAverageValue.Script" />
74+
public virtual WeightedAverageValueDescriptor<T> Script(Func<ScriptDescriptor, IScript> scriptSelector) =>
75+
Assign(a => a.Script = scriptSelector?.Invoke(new ScriptDescriptor()));
76+
77+
/// <inheritdoc cref="IWeightedAverageValue.Missing" />
78+
public WeightedAverageValueDescriptor<T> Missing(double? missing) => Assign(a => a.Missing = missing);
79+
}
80+
81+
/// <summary>
82+
/// The type of value
83+
/// </summary>
84+
[JsonConverter(typeof(StringEnumConverter))]
85+
public enum ValueType
86+
{
87+
/// <summary>A string value</summary>
88+
[EnumMember(Value = "string")] String,
89+
/// <summary>A long value that can be used to represent byte, short, integer and long</summary>
90+
[EnumMember(Value = "long")] Long,
91+
/// <summary>A double value that can be used to represent float and double</summary>
92+
[EnumMember(Value = "double")] Double,
93+
/// <summary>A number value</summary>
94+
[EnumMember(Value = "number")] Number,
95+
/// <summary>A date value</summary>
96+
[EnumMember(Value = "date")] Date,
97+
/// <summary>An IP value</summary>
98+
[EnumMember(Value = "ip")] Ip,
99+
/// <summary>A numeric value</summary>
100+
[EnumMember(Value = "numeric")] Numeric,
101+
/// <summary>A geo_point value</summary>
102+
[EnumMember(Value = "geo_point")] GeoPoint,
103+
/// <summary>A boolean value</summary>
104+
[EnumMember(Value = "boolean")] Boolean,
105+
}
106+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System;
2+
using FluentAssertions;
3+
using Nest;
4+
using Tests.Framework.Integration;
5+
using static Nest.Infer;
6+
using Tests.Core.Extensions;
7+
using Tests.Core.ManagedElasticsearch.Clusters;
8+
using Tests.Domain;
9+
using ValueType = Nest.ValueType;
10+
11+
namespace Tests.Aggregations.Metric.WeightedAverage
12+
{
13+
/**
14+
* A single-value metrics aggregation that computes the weighted average of numeric values that are extracted
15+
* from the aggregated documents. These values can be extracted either from specific numeric fields in the documents.
16+
* When calculating a regular average, each datapoint has an equal "weight" i.e. it contributes equally to the final
17+
* value. Weighted averages, on the other hand, weight each datapoint differently. The amount that each
18+
* datapoint contributes to the final value is extracted from the document, or provided by a script.
19+
*
20+
* Be sure to read the Elasticsearch documentation on {ref_current}/search-aggregations-metrics-weight-avg-aggregation.html[Weighted Avg Aggregation]
21+
*/
22+
public class WeightedAverageAggregationUsageTests : AggregationUsageTestBase
23+
{
24+
public WeightedAverageAggregationUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { }
25+
26+
protected override object AggregationJson => new
27+
{
28+
weighted_avg_commits = new
29+
{
30+
weighted_avg = new
31+
{
32+
value = new
33+
{
34+
field = "numberOfCommits",
35+
missing = 0.0
36+
},
37+
weight = new
38+
{
39+
script = new
40+
{
41+
source = "doc.numberOfContributors.value + 1"
42+
}
43+
},
44+
value_type = "long"
45+
}
46+
}
47+
};
48+
49+
protected override Func<AggregationContainerDescriptor<Project>, IAggregationContainer> FluentAggs => a => a
50+
.WeightedAverage("weighted_avg_commits", avg => avg
51+
.Value(v => v.Field(p => p.NumberOfCommits).Missing(0))
52+
.Weight(w => w.Script("doc.numberOfContributors.value + 1"))
53+
.ValueType(ValueType.Long)
54+
);
55+
56+
protected override AggregationDictionary InitializerAggs =>
57+
new WeightedAverageAggregation("weighted_avg_commits")
58+
{
59+
Value = new WeightedAverageValue(Field<Project>(p => p.NumberOfCommits))
60+
{
61+
Missing = 0
62+
},
63+
Weight = new WeightedAverageValue(new InlineScript("doc.numberOfContributors.value + 1")),
64+
ValueType = ValueType.Long
65+
};
66+
67+
protected override void ExpectResponse(ISearchResponse<Project> response)
68+
{
69+
response.ShouldBeValid();
70+
var commitsAvg = response.Aggregations.WeightedAverage("weighted_avg_commits");
71+
commitsAvg.Should().NotBeNull();
72+
commitsAvg.Value.Should().BeGreaterThan(0);
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)