Skip to content
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 @@ -56,6 +56,8 @@ internal static string[] TypedKeyTokens(string key)

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

public ValueAggregate WeightedAverage(string key) => this.TryGet<ValueAggregate>(key);

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

public KeyedValueAggregate MinBucket(string key) => this.TryGet<KeyedValueAggregate>(key);
Expand Down
35 changes: 26 additions & 9 deletions src/Nest/Aggregations/Aggregation.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;

namespace Nest
{
Expand All @@ -9,14 +10,25 @@ namespace Nest
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
public interface IAggregation
{
/// <summary>
/// name of the aggregation
/// </summary>
string Name { get; set; }

/// <summary>
/// metadata to associate with the individual aggregation at request time that
/// will be returned in place at response time
/// </summary>
IDictionary<string, object> Meta { get; set; }
}

/// <inheritdoc />
public abstract class AggregationBase : IAggregation
{
/// <inheritdoc />
string IAggregation.Name { get; set; }

/// <inheritdoc />
public IDictionary<string, object> Meta { get; set; }

internal AggregationBase() { }
Expand All @@ -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);
}

/// <summary>
/// Combines aggregations into a single list of aggregations
/// </summary>
internal class AggregationCombinator : AggregationBase, IAggregation
{
internal List<AggregationBase> Aggregations { get; } = new List<AggregationBase>();
Expand All @@ -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);
}
}
}
11 changes: 11 additions & 0 deletions src/Nest/Aggregations/AggregationContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -450,6 +455,8 @@ public class AggregationContainerDescriptor<T> : DescriptorBase<AggregationConta

ICompositeAggregation IAggregationContainer.Composite { get; set; }

IWeightedAverageAggregation IAggregationContainer.WeightedAverage { get; set; }

public AggregationContainerDescriptor<T> Average(string name,
Func<AverageAggregationDescriptor<T>, IAverageAggregation> selector) =>
_SetInnerAggregation(name, selector, (a, d) => a.Average = d);
Expand Down Expand Up @@ -646,6 +653,10 @@ public AggregationContainerDescriptor<T> Composite(string name,
Func<CompositeAggregationDescriptor<T>, ICompositeAggregation> selector) =>
_SetInnerAggregation(name, selector, (a, d) => a.Composite = d);

public AggregationContainerDescriptor<T> WeightedAverage(string name,
Func<WeightedAverageAggregationDescriptor<T>, IWeightedAverageAggregation> selector) =>
_SetInnerAggregation(name, selector, (a, d) => a.WeightedAverage = d);

/// <summary>
/// Fluent methods do not assign to properties on `this` directly but on IAggregationContainers inside `this.Aggregations[string, IContainer]
/// </summary>
Expand Down
5 changes: 1 addition & 4 deletions src/Nest/Aggregations/Metric/MetricAggregation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WeightedAverageAggregation>))]
public interface IWeightedAverageAggregation : IAggregation
{
/// <summary> The configuration for the field or script that provides the values</summary>
[JsonProperty("value")]
IWeightedAverageValue Value { get; set; }
/// <summary> The configuration for the field or script that provides the weights</summary>
[JsonProperty("weight")]
IWeightedAverageValue Weight { get; set; }
/// <summary> The optional numeric response formatter</summary>
[JsonProperty("format")]
string Format { get; set; }
/// <summary> A hint about the values for pure scripts or unmapped fields </summary>
[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;

/// <inheritdoc cref="IWeightedAverageAggregation.Value"/>
public IWeightedAverageValue Value { get; set; }
/// <inheritdoc cref="IWeightedAverageAggregation.Weight"/>
public IWeightedAverageValue Weight { get; set; }
/// <inheritdoc cref="IWeightedAverageAggregation.Format"/>
public string Format { get; set; }
/// <inheritdoc cref="IWeightedAverageAggregation.ValueType"/>
public ValueType? ValueType { get; set; }
}

public class WeightedAverageAggregationDescriptor<T>
: DescriptorBase<WeightedAverageAggregationDescriptor<T>, 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<string, object> IAggregation.Meta { get; set; }

/// <inheritdoc cref="IAggregation.Meta"/>
public WeightedAverageAggregationDescriptor<T> Meta(Func<FluentDictionary<string, object>, FluentDictionary<string, object>> selector) =>
Assign(a => a.Meta = selector?.Invoke(new FluentDictionary<string, object>()));

/// <inheritdoc cref="IWeightedAverageAggregation.Value"/>
public WeightedAverageAggregationDescriptor<T> Value(Func<WeightedAverageValueDescriptor<T>, IWeightedAverageValue> selector) =>
Assign(a => a.Value = selector?.Invoke(new WeightedAverageValueDescriptor<T>()));

/// <inheritdoc cref="IWeightedAverageAggregation.Weight"/>
public WeightedAverageAggregationDescriptor<T> Weight(Func<WeightedAverageValueDescriptor<T>, IWeightedAverageValue> selector) =>
Assign(a => a.Weight = selector?.Invoke(new WeightedAverageValueDescriptor<T>()));

/// <inheritdoc cref="IWeightedAverageAggregation.Format"/>
public WeightedAverageAggregationDescriptor<T> Format(string format) => Assign(a => a.Format = format);

/// <inheritdoc cref="IWeightedAverageAggregation.ValueType"/>
public WeightedAverageAggregationDescriptor<T> ValueType(ValueType? valueType) => Assign(a => a.ValueType = valueType);
}
}
106 changes: 106 additions & 0 deletions src/Nest/Aggregations/Metric/WeightedAverage/WeightedAverageValue.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System;
using System.Linq.Expressions;
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace Nest
{
/// <summary>
/// The configuration for a field or scrip that provides a value or weight
/// for <see cref="WeightedAverageAggregation"/>
/// </summary>
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
[ContractJsonConverter(typeof(ReadAsTypeJsonConverter<WeightedAverageValue>))]
public interface IWeightedAverageValue
{
/// <summary>
/// The field that values should be extracted from
/// </summary>
[JsonProperty("field")]
Field Field { get; set; }

/// <summary>
/// A script to derive the value and the weight from
/// </summary>
[JsonProperty("script")]
IScript Script { get; set; }

/// <summary>
/// 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).
/// </summary>
[JsonProperty("missing")]
double? Missing { get; set; }
}

/// <inheritdoc />
public class WeightedAverageValue : IWeightedAverageValue
{
internal WeightedAverageValue() { }
public WeightedAverageValue(Field field) => this.Field = field;
public WeightedAverageValue(IScript script) => this.Script = script;

/// <inheritdoc />
public Field Field { get; set; }
/// <inheritdoc />
public IScript Script { get; set; }
/// <inheritdoc />
public double? Missing { get; set; }
}

/// <inheritdoc cref="IWeightedAverageAggregation" />
public class WeightedAverageValueDescriptor<T> : DescriptorBase<WeightedAverageValueDescriptor<T>, IWeightedAverageValue>
, IWeightedAverageValue
where T : class
{
Field IWeightedAverageValue.Field { get; set; }
IScript IWeightedAverageValue.Script { get; set; }
double? IWeightedAverageValue.Missing { get; set; }

/// <inheritdoc cref="IWeightedAverageValue.Field" />
public WeightedAverageValueDescriptor<T> Field(Field field) => Assign(a => a.Field = field);

/// <inheritdoc cref="IWeightedAverageValue.Field" />
public WeightedAverageValueDescriptor<T> Field(Expression<Func<T, object>> field) => Assign(a => a.Field = field);

/// <inheritdoc cref="IWeightedAverageValue.Script" />
public virtual WeightedAverageValueDescriptor<T> Script(string script) => Assign(a => a.Script = new InlineScript(script));

/// <inheritdoc cref="IWeightedAverageValue.Script" />
public virtual WeightedAverageValueDescriptor<T> Script(Func<ScriptDescriptor, IScript> scriptSelector) =>
Assign(a => a.Script = scriptSelector?.Invoke(new ScriptDescriptor()));

/// <inheritdoc cref="IWeightedAverageValue.Missing" />
public WeightedAverageValueDescriptor<T> Missing(double? missing) => Assign(a => a.Missing = missing);
}

/// <summary>
/// The type of value
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum ValueType
{
/// <summary>A string value</summary>
[EnumMember(Value = "string")] String,
/// <summary>A long value that can be used to represent byte, short, integer and long</summary>
[EnumMember(Value = "long")] Long,
/// <summary>A double value that can be used to represent float and double</summary>
[EnumMember(Value = "double")] Double,
/// <summary>A number value</summary>
[EnumMember(Value = "number")] Number,
/// <summary>A date value</summary>
[EnumMember(Value = "date")] Date,
/// <summary>An IP value</summary>
[EnumMember(Value = "ip")] Ip,
/// <summary>A numeric value</summary>
[EnumMember(Value = "numeric")] Numeric,
/// <summary>A geo_point value</summary>
[EnumMember(Value = "geo_point")] GeoPoint,
/// <summary>A boolean value</summary>
[EnumMember(Value = "boolean")] Boolean,
}
}
Original file line number Diff line number Diff line change
@@ -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<AggregationContainerDescriptor<Project>, 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<Project>(p => p.NumberOfCommits))
{
Missing = 0
},
Weight = new WeightedAverageValue(new InlineScript("doc.numberOfContributors.value + 1")),
ValueType = ValueType.Long
};

protected override void ExpectResponse(ISearchResponse<Project> response)
{
response.ShouldBeValid();
var commitsAvg = response.Aggregations.WeightedAverage("weighted_avg_commits");
commitsAvg.Should().NotBeNull();
commitsAvg.Value.Should().BeGreaterThan(0);
}
}
}