diff --git a/src/Nest/Aggregations/AggregateDictionary.cs b/src/Nest/Aggregations/AggregateDictionary.cs index c59b27d8abf..94fcf5ab8c6 100644 --- a/src/Nest/Aggregations/AggregateDictionary.cs +++ b/src/Nest/Aggregations/AggregateDictionary.cs @@ -56,6 +56,8 @@ internal static string[] TypedKeyTokens(string key) public ValueAggregate SerialDifferencing(string key) => this.TryGet(key); + public ValueAggregate WeightedAverage(string key) => this.TryGet(key); + public KeyedValueAggregate MaxBucket(string key) => this.TryGet(key); public KeyedValueAggregate MinBucket(string key) => this.TryGet(key); diff --git a/src/Nest/Aggregations/Aggregation.cs b/src/Nest/Aggregations/Aggregation.cs index 9faa0506a03..a718e82731d 100644 --- a/src/Nest/Aggregations/Aggregation.cs +++ b/src/Nest/Aggregations/Aggregation.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; namespace Nest { @@ -9,14 +10,25 @@ namespace Nest [JsonObject(MemberSerialization = MemberSerialization.OptIn)] public interface IAggregation { + /// + /// name of the aggregation + /// string Name { get; set; } + + /// + /// metadata to associate with the individual aggregation at request time that + /// will be returned in place at response time + /// IDictionary Meta { get; set; } } + /// public abstract class AggregationBase : IAggregation { + /// string IAggregation.Name { get; set; } + /// public IDictionary Meta { get; set; } internal AggregationBase() { } @@ -34,12 +46,13 @@ protected AggregationBase(string name) //always evaluate to false so that each side of && equation is evaluated public static bool operator true(AggregationBase a) => false; - public static AggregationBase operator &(AggregationBase left, AggregationBase right) - { - return new AggregationCombinator(null, left, right); - } + public static AggregationBase operator &(AggregationBase left, AggregationBase right) => + new AggregationCombinator(null, left, right); } + /// + /// Combines aggregations into a single list of aggregations + /// internal class AggregationCombinator : AggregationBase, IAggregation { internal List Aggregations { get; } = new List(); @@ -54,13 +67,17 @@ public AggregationCombinator(string name, AggregationBase left, AggregationBase private void AddAggregation(AggregationBase agg) { - if (agg == null) return; - var combinator = agg as AggregationCombinator; - if ((combinator?.Aggregations.HasAny()).GetValueOrDefault(false)) + switch (agg) { - this.Aggregations.AddRange(combinator.Aggregations); + case null: + return; + case AggregationCombinator combinator when combinator.Aggregations.Any(): + this.Aggregations.AddRange(combinator.Aggregations); + break; + default: + this.Aggregations.Add(agg); + break; } - else this.Aggregations.Add(agg); } } } diff --git a/src/Nest/Aggregations/AggregationContainer.cs b/src/Nest/Aggregations/AggregationContainer.cs index 83e9f931c2f..402ee64018c 100644 --- a/src/Nest/Aggregations/AggregationContainer.cs +++ b/src/Nest/Aggregations/AggregationContainer.cs @@ -215,6 +215,9 @@ public interface IAggregationContainer [JsonProperty("composite")] ICompositeAggregation Composite { get; set; } + [JsonProperty("weighted_avg")] + IWeightedAverageAggregation WeightedAverage { get; set; } + [JsonProperty("aggs")] AggregationDictionary Aggregations { get; set; } @@ -315,6 +318,8 @@ public class AggregationContainer : IAggregationContainer public ICompositeAggregation Composite { get; set; } + public IWeightedAverageAggregation WeightedAverage { get; set; } + public AggregationDictionary Aggregations { get; set; } public static implicit operator AggregationContainer(AggregationBase aggregator) @@ -450,6 +455,8 @@ public class AggregationContainerDescriptor : DescriptorBase Average(string name, Func, IAverageAggregation> selector) => _SetInnerAggregation(name, selector, (a, d) => a.Average = d); @@ -646,6 +653,10 @@ public AggregationContainerDescriptor Composite(string name, Func, ICompositeAggregation> selector) => _SetInnerAggregation(name, selector, (a, d) => a.Composite = d); + public AggregationContainerDescriptor WeightedAverage(string name, + Func, IWeightedAverageAggregation> selector) => + _SetInnerAggregation(name, selector, (a, d) => a.WeightedAverage = 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/MetricAggregation.cs b/src/Nest/Aggregations/Metric/MetricAggregation.cs index accd30227c3..eb429e81b02 100644 --- a/src/Nest/Aggregations/Metric/MetricAggregation.cs +++ b/src/Nest/Aggregations/Metric/MetricAggregation.cs @@ -21,10 +21,7 @@ public abstract class MetricAggregationBase : AggregationBase, IMetricAggregatio { internal MetricAggregationBase() { } - protected MetricAggregationBase(string name, Field field) : base(name) - { - this.Field = field; - } + protected MetricAggregationBase(string name, Field field) : base(name) => this.Field = field; public Field Field { get; set; } public virtual IScript Script { get; set; } diff --git a/src/Nest/Aggregations/Metric/WeightedAverage/WeightedAverageAggregation.cs b/src/Nest/Aggregations/Metric/WeightedAverage/WeightedAverageAggregation.cs new file mode 100644 index 00000000000..678fd785b31 --- /dev/null +++ b/src/Nest/Aggregations/Metric/WeightedAverage/WeightedAverageAggregation.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using Newtonsoft.Json; + +namespace Nest +{ + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + [ContractJsonConverter(typeof(AggregationJsonConverter))] + public interface IWeightedAverageAggregation : IAggregation + { + /// The configuration for the field or script that provides the values + [JsonProperty("value")] + IWeightedAverageValue Value { get; set; } + /// The configuration for the field or script that provides the weights + [JsonProperty("weight")] + IWeightedAverageValue Weight { get; set; } + /// The optional numeric response formatter + [JsonProperty("format")] + string Format { get; set; } + /// A hint about the values for pure scripts or unmapped fields + [JsonProperty("value_type")] + ValueType? ValueType { get; set; } + } + + public class WeightedAverageAggregation : AggregationBase, IWeightedAverageAggregation + { + internal WeightedAverageAggregation() { } + public WeightedAverageAggregation(string name) : base(name) { } + + internal override void WrapInContainer(AggregationContainer c) => c.WeightedAverage = this; + + /// + public IWeightedAverageValue Value { get; set; } + /// + public IWeightedAverageValue Weight { get; set; } + /// + public string Format { get; set; } + /// + public ValueType? ValueType { get; set; } + } + + public class WeightedAverageAggregationDescriptor + : DescriptorBase, IWeightedAverageAggregation> + , IWeightedAverageAggregation + where T : class + { + IWeightedAverageValue IWeightedAverageAggregation.Value { get; set; } + IWeightedAverageValue IWeightedAverageAggregation.Weight { get; set; } + string IWeightedAverageAggregation.Format { get; set; } + ValueType? IWeightedAverageAggregation.ValueType { get; set; } + string IAggregation.Name { get; set; } + IDictionary IAggregation.Meta { get; set; } + + /// + public WeightedAverageAggregationDescriptor Meta(Func, FluentDictionary> selector) => + Assign(a => a.Meta = selector?.Invoke(new FluentDictionary())); + + /// + public WeightedAverageAggregationDescriptor Value(Func, IWeightedAverageValue> selector) => + Assign(a => a.Value = selector?.Invoke(new WeightedAverageValueDescriptor())); + + /// + public WeightedAverageAggregationDescriptor Weight(Func, IWeightedAverageValue> selector) => + Assign(a => a.Weight = selector?.Invoke(new WeightedAverageValueDescriptor())); + + /// + public WeightedAverageAggregationDescriptor Format(string format) => Assign(a => a.Format = format); + + /// + public WeightedAverageAggregationDescriptor ValueType(ValueType? valueType) => Assign(a => a.ValueType = valueType); + } +} diff --git a/src/Nest/Aggregations/Metric/WeightedAverage/WeightedAverageValue.cs b/src/Nest/Aggregations/Metric/WeightedAverage/WeightedAverageValue.cs new file mode 100644 index 00000000000..8c83af09915 --- /dev/null +++ b/src/Nest/Aggregations/Metric/WeightedAverage/WeightedAverageValue.cs @@ -0,0 +1,106 @@ +using System; +using System.Linq.Expressions; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace Nest +{ + /// + /// The configuration for a field or scrip that provides a value or weight + /// for + /// + [JsonObject(MemberSerialization = MemberSerialization.OptIn)] + [ContractJsonConverter(typeof(ReadAsTypeJsonConverter))] + public interface IWeightedAverageValue + { + /// + /// The field that values should be extracted from + /// + [JsonProperty("field")] + Field Field { get; set; } + + /// + /// A script to derive the value and the weight from + /// + [JsonProperty("script")] + IScript Script { get; set; } + + /// + /// defines how documents that are missing a value should be treated. + /// The default behavior is different for value and weight: + /// By default, if the value field is missing the document is ignored and the aggregation + /// moves on to the next document. + /// If the weight field is missing, it is assumed to have a weight of 1 (like a normal average). + /// + [JsonProperty("missing")] + double? Missing { get; set; } + } + + /// + public class WeightedAverageValue : IWeightedAverageValue + { + internal WeightedAverageValue() { } + public WeightedAverageValue(Field field) => this.Field = field; + public WeightedAverageValue(IScript script) => this.Script = script; + + /// + public Field Field { get; set; } + /// + public IScript Script { get; set; } + /// + public double? Missing { get; set; } + } + + /// + public class WeightedAverageValueDescriptor : DescriptorBase, IWeightedAverageValue> + , IWeightedAverageValue + where T : class + { + Field IWeightedAverageValue.Field { get; set; } + IScript IWeightedAverageValue.Script { get; set; } + double? IWeightedAverageValue.Missing { get; set; } + + /// + public WeightedAverageValueDescriptor Field(Field field) => Assign(a => a.Field = field); + + /// + public WeightedAverageValueDescriptor Field(Expression> field) => Assign(a => a.Field = field); + + /// + public virtual WeightedAverageValueDescriptor Script(string script) => Assign(a => a.Script = new InlineScript(script)); + + /// + public virtual WeightedAverageValueDescriptor Script(Func scriptSelector) => + Assign(a => a.Script = scriptSelector?.Invoke(new ScriptDescriptor())); + + /// + public WeightedAverageValueDescriptor Missing(double? missing) => Assign(a => a.Missing = missing); + } + + /// + /// The type of value + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum ValueType + { + /// A string value + [EnumMember(Value = "string")] String, + /// A long value that can be used to represent byte, short, integer and long + [EnumMember(Value = "long")] Long, + /// A double value that can be used to represent float and double + [EnumMember(Value = "double")] Double, + /// A number value + [EnumMember(Value = "number")] Number, + /// A date value + [EnumMember(Value = "date")] Date, + /// An IP value + [EnumMember(Value = "ip")] Ip, + /// A numeric value + [EnumMember(Value = "numeric")] Numeric, + /// A geo_point value + [EnumMember(Value = "geo_point")] GeoPoint, + /// A boolean value + [EnumMember(Value = "boolean")] Boolean, + } +} diff --git a/src/Tests/Tests/Aggregations/Metric/WeightedAverage/WeightedAverageAggregationUsageTests.cs b/src/Tests/Tests/Aggregations/Metric/WeightedAverage/WeightedAverageAggregationUsageTests.cs new file mode 100644 index 00000000000..9cee7caa097 --- /dev/null +++ b/src/Tests/Tests/Aggregations/Metric/WeightedAverage/WeightedAverageAggregationUsageTests.cs @@ -0,0 +1,75 @@ +using System; +using FluentAssertions; +using Nest; +using Tests.Framework.Integration; +using static Nest.Infer; +using Tests.Core.Extensions; +using Tests.Core.ManagedElasticsearch.Clusters; +using Tests.Domain; +using ValueType = Nest.ValueType; + +namespace Tests.Aggregations.Metric.WeightedAverage +{ + /** + * A single-value metrics aggregation that computes the weighted average of numeric values that are extracted + * from the aggregated documents. These values can be extracted either from specific numeric fields in the documents. + * When calculating a regular average, each datapoint has an equal "weight" i.e. it contributes equally to the final + * value. Weighted averages, on the other hand, weight each datapoint differently. The amount that each + * datapoint contributes to the final value is extracted from the document, or provided by a script. + * + * Be sure to read the Elasticsearch documentation on {ref_current}/search-aggregations-metrics-weight-avg-aggregation.html[Weighted Avg Aggregation] + */ + public class WeightedAverageAggregationUsageTests : AggregationUsageTestBase + { + public WeightedAverageAggregationUsageTests(ReadOnlyCluster i, EndpointUsage usage) : base(i, usage) { } + + protected override object AggregationJson => new + { + weighted_avg_commits = new + { + weighted_avg = new + { + value = new + { + field = "numberOfCommits", + missing = 0.0 + }, + weight = new + { + script = new + { + source = "doc.numberOfContributors.value + 1" + } + }, + value_type = "long" + } + } + }; + + protected override Func, IAggregationContainer> FluentAggs => a => a + .WeightedAverage("weighted_avg_commits", avg => avg + .Value(v => v.Field(p => p.NumberOfCommits).Missing(0)) + .Weight(w => w.Script("doc.numberOfContributors.value + 1")) + .ValueType(ValueType.Long) + ); + + protected override AggregationDictionary InitializerAggs => + new WeightedAverageAggregation("weighted_avg_commits") + { + Value = new WeightedAverageValue(Field(p => p.NumberOfCommits)) + { + Missing = 0 + }, + Weight = new WeightedAverageValue(new InlineScript("doc.numberOfContributors.value + 1")), + ValueType = ValueType.Long + }; + + protected override void ExpectResponse(ISearchResponse response) + { + response.ShouldBeValid(); + var commitsAvg = response.Aggregations.WeightedAverage("weighted_avg_commits"); + commitsAvg.Should().NotBeNull(); + commitsAvg.Value.Should().BeGreaterThan(0); + } + } +}