diff --git a/src/aggregations/metrics-aggregations/index.js b/src/aggregations/metrics-aggregations/index.js index 79a3b391..c30b1b30 100644 --- a/src/aggregations/metrics-aggregations/index.js +++ b/src/aggregations/metrics-aggregations/index.js @@ -16,3 +16,4 @@ exports.StatsAggregation = require('./stats-aggregation'); exports.SumAggregation = require('./sum-aggregation'); exports.TopHitsAggregation = require('./top-hits-aggregation'); exports.ValueCountAggregation = require('./value-count-aggregation'); +exports.WeightedAverageAggregation = require('./weighted-average-aggregation'); diff --git a/src/aggregations/metrics-aggregations/metrics-aggregation-base.js b/src/aggregations/metrics-aggregations/metrics-aggregation-base.js index 9d4924cf..ca84dd47 100644 --- a/src/aggregations/metrics-aggregations/metrics-aggregation-base.js +++ b/src/aggregations/metrics-aggregations/metrics-aggregation-base.js @@ -75,7 +75,7 @@ class MetricsAggregationBase extends Aggregation { } /** - * Sets the missing parameter ehich defines how documents + * Sets the missing parameter which defines how documents * that are missing a value should be treated. * * @example diff --git a/src/aggregations/metrics-aggregations/weighted-average-aggregation.js b/src/aggregations/metrics-aggregations/weighted-average-aggregation.js new file mode 100644 index 00000000..51a1ffc7 --- /dev/null +++ b/src/aggregations/metrics-aggregations/weighted-average-aggregation.js @@ -0,0 +1,171 @@ +'use strict'; + +const { Script } = require('../../core'); +const MetricsAggregationBase = require('./metrics-aggregation-base'); +const isNil = require('lodash.isnil'); + +const ES_REF_URL = + 'https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-weight-avg-aggregation.html'; + +/** + * 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. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-weight-avg-aggregation.html) + * + * Added in Elasticsearch v6.4.0 + * [Release notes](https://www.elastic.co/guide/en/elasticsearch/reference/6.4/release-notes-6.4.0.html) + * + * As a formula, a weighted average is ∑(value * weight) / ∑(weight) + * + * @example + * // Compute the average grade over all documents, weighing by teacher score. + * const agg = esb.weightedAverageAggregation('avg_grade', 'grade', 'teacher_score'); + * + * @example + * // Compute the average grade where the weight is calculated by a script. + * // Filling in missing values as '10'. + * const agg = esb.weightedAverageAggregation('avg_grade', 'grade') + * .weight(esb.script('inline', "doc['teacher_score'].value").lang('painless'), 10) + * ); + * + * @example + * // Compute the average grade, weighted by teacher score, filling in missing values. + * const agg = esb.weightedAverageAggregation('avg_grade').value('grade', 5).weight('teacher_score', 10)); + * + * @example + * // Compute the average grade over all documents, weighing by teacher score. + * const agg = esb.weightedAverageAggregation('avg_grade').value('grade').weight('teacher_score'); + * + * + * @param {string} name The name which will be used to refer to this aggregation. + * @param {string=} value The field or script to use as the value + * @param {string=} weight The field or script to use as the weight + * + * @extends MetricsAggregationBase + */ +class WeightedAverageAggregation extends MetricsAggregationBase { + /** + * Creates an instance of `WeightedAverageAggregation` + * + * @param {string} name The name which will be used to refer to this aggregation. + * @param {string=} value The field or script to be used as the value. + * @param {string=} weight The field or script to be used as the weighting. + */ + constructor(name, value, weight) { + super(name, 'weighted_avg'); + + this._aggsDef.value = {}; + this._aggsDef.weight = {}; + + if (!isNil(value)) { + this.value(value); + } + + if (!isNil(weight)) { + this.weight(weight); + } + } + + /** + * Sets the value + * + * @param {string | Script} value Field name or script to use as the value. + * + * @param {number=} missing Sets the missing parameter which defines how documents + * that are missing a value should be treated. + * @returns {WeightedAverageAggregation} returns `this` so that calls can be chained + */ + value(value, missing) { + if (typeof value !== 'string' && !(value instanceof Script)) { + throw new TypeError( + 'Value must be either a string or instanceof Script' + ); + } + + if (value instanceof Script) { + if (this._aggsDef.value.field) { + delete this._aggsDef.value.field; + } + this._aggsDef.value.script = value; + } else { + if (this._aggsDef.value.script) { + delete this._aggsDef.value.script; + } + this._aggsDef.value.field = value; + } + + if (!isNil(missing)) { + this._aggsDef.value.missing = missing; + } + + return this; + } + + /** + * Sets the weight + * + * @param {string | Script} weight Field name or script to use as the weight. + * @param {number=} missing Sets the missing parameter which defines how documents + * that are missing a value should be treated. + * @returns {WeightedAverageAggregation} returns `this` so that calls can be chained + */ + weight(weight, missing) { + if (typeof weight !== 'string' && !(weight instanceof Script)) { + throw new TypeError( + 'Weight must be either a string or instanceof Script' + ); + } + + if (weight instanceof Script) { + if (this._aggsDef.weight.field) { + delete this._aggsDef.weight.field; + } + this._aggsDef.weight.script = weight; + } else { + if (this._aggsDef.weight.script) { + delete this._aggsDef.weight.script; + } + this._aggsDef.weight.field = weight; + } + + if (!isNil(missing)) { + this._aggsDef.weight.missing = missing; + } + + return this; + } + + /** + * @override + * @throws {Error} This method cannot be called on WeightedAverageAggregation + */ + script() { + console.log(`Please refer ${ES_REF_URL}`); + throw new Error( + 'script is not supported in WeightedAverageAggregation' + ); + } + + /** + * @override + * @throws {Error} This method cannot be called on WeightedAverageAggregation + */ + missing() { + console.log(`Please refer ${ES_REF_URL}`); + throw new Error( + 'missing is not supported in WeightedAverageAggregation' + ); + } + + /** + * @override + * @throws {Error} This method cannot be called on WeightedAverageAggregation + */ + field() { + console.log(`Please refer ${ES_REF_URL}`); + throw new Error('field is not supported in WeightedAverageAggregation'); + } +} + +module.exports = WeightedAverageAggregation; diff --git a/src/index.d.ts b/src/index.d.ts index db3b6740..606d1d49 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -3843,6 +3843,81 @@ declare namespace esb { field?: string ): AvgAggregation; + /** + * 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. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-weight-avg-aggregation.html) + * + * Added in Elasticsearch v6.4.0 + * [Release notes](https://www.elastic.co/guide/en/elasticsearch/reference/6.4/release-notes-6.4.0.html) + * + * @param {string} name The name which will be used to refer to this aggregation. + * @param {string=} value The field or script to be used as the value. + * @param {string | Script =} weight The field or script to be used as the weighting. + * @extends MetricsAggregationBase + */ + export class WeightedAverageAggregation extends MetricsAggregationBase { + constructor(name: string, value?: string | Script, weight?: string | Script); + + /** + * Sets the value + * + * @param {string | Script} value Field name or script to be used as the value + * @param {number=} missing Sets the missing parameter which defines how documents + * that are missing a value should be treated. + * @return {WeightedAverageAggregation} returns `this` so that calls can be chained + */ + value(value: string | Script, missing?: number): WeightedAverageAggregation + + /** + * Sets the weight + * + * @param {string | Script} weight Field name or script to be used as the weight + * @param {number=} missing Sets the missing parameter which defines how documents + * that are missing a value should be treated. + * @return {WeightedAverageAggregation} returns `this` so that calls can be chained + */ + weight(weight: string | Script, missing?: number): WeightedAverageAggregation + + /** + * @override + * @throws {Error} This method cannot be called on WeightedAverageAggregation + */ + script(): never; + + /** + * @override + * @throws {Error} This method cannot be called on WeightedAverageAggregation + */ + missing(): never; + + /** + * @override + * @throws {Error} This method cannot be called on WeightedAverageAggregation + */ + field(): never; + } + + /** + * 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. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-weight-avg-aggregation.html) + * + * Added in Elasticsearch v6.4.0 + * [Release notes](https://www.elastic.co/guide/en/elasticsearch/reference/6.4/release-notes-6.4.0.html) + * + * @param {string} name The name which will be used to refer to this aggregation. + * @param {string | Script =} value The field or script to be used as the value. + * @param {string | Script =} weight The field or script to be used as the weighting. + */ + export function weightedAverageAggregation( + name: string, + value?: string | Script, + weight?: string | Script + ): WeightedAverageAggregation; + /** * A single-value metrics aggregation that calculates an approximate count of * distinct values. Values can be extracted either from specific fields in the diff --git a/src/index.js b/src/index.js index 92e27bcf..8c2e2c7e 100644 --- a/src/index.js +++ b/src/index.js @@ -104,7 +104,8 @@ const { StatsAggregation, SumAggregation, TopHitsAggregation, - ValueCountAggregation + ValueCountAggregation, + WeightedAverageAggregation }, bucketAggregations: { AdjacencyMatrixAggregation, @@ -343,6 +344,11 @@ exports.spanFieldMaskingQuery = constructorWrapper(SpanFieldMaskingQuery); exports.AvgAggregation = AvgAggregation; exports.avgAggregation = constructorWrapper(AvgAggregation); +exports.WeightedAverageAggregation = WeightedAverageAggregation; +exports.weightedAverageAggregation = constructorWrapper( + WeightedAverageAggregation +); + exports.CardinalityAggregation = CardinalityAggregation; exports.cardinalityAggregation = constructorWrapper(CardinalityAggregation); diff --git a/test/aggregations-test/weighted-average-aggregation.test.js b/test/aggregations-test/weighted-average-aggregation.test.js new file mode 100644 index 00000000..555fe55c --- /dev/null +++ b/test/aggregations-test/weighted-average-aggregation.test.js @@ -0,0 +1,206 @@ +import test from 'ava'; +import { WeightedAverageAggregation, Script } from '../../src'; +import { + illegalCall, + makeSetsOptionMacro, + nameTypeExpectStrategy, + setsAggType +} from '../_macros'; + +test(setsAggType, WeightedAverageAggregation, 'weighted_avg', { + value: {}, + weight: {} +}); + +const getInstance = (...args) => + new WeightedAverageAggregation('my_agg', ...args); + +const setsOption = makeSetsOptionMacro( + getInstance, + nameTypeExpectStrategy('my_agg', 'weighted_avg', { value: {}, weight: {} }) +); +test(illegalCall, WeightedAverageAggregation, 'field', 'my_agg'); +test(illegalCall, WeightedAverageAggregation, 'script', 'my_agg'); +test(illegalCall, WeightedAverageAggregation, 'missing', 'my_agg'); + +test(setsOption, 'weight', { + param: 'my_weight_field', + propValue: { field: 'my_weight_field' } +}); +test(setsOption, 'value', { + param: 'my_value_field', + propValue: { field: 'my_value_field' } +}); + +test(setsOption, 'weight', { + param: ['my_weight_field', 20], + spread: true, + propValue: { field: 'my_weight_field', missing: 20 } +}); +test(setsOption, 'value', { + param: ['my_value_field', 10], + spread: true, + propValue: { field: 'my_value_field', missing: 10 } +}); + +test(setsOption, 'value', { + param: new Script('inline', "doc['field'].value"), + propValue: { script: { inline: "doc['field'].value" } } +}); +test(setsOption, 'weight', { + param: new Script('inline', "doc['field'].value"), + propValue: { script: { inline: "doc['field'].value" } } +}); + +test('throws if value is not a script or string', t => { + const error = t.throws( + () => new WeightedAverageAggregation('my_agg').value(10, 10), + TypeError + ); + t.is(error.message, 'Value must be either a string or instanceof Script'); +}); + +test('throws if weight is not a script or string', t => { + const error = t.throws( + () => new WeightedAverageAggregation('my_agg').weight(10, 10), + TypeError + ); + t.is(error.message, 'Weight must be either a string or instanceof Script'); +}); + +test('removes previously set value field/script when updated', t => { + const valueOne = new WeightedAverageAggregation('my_agg').value( + new Script('inline', 'script') + ); + const expectedOne = { + my_agg: { + weighted_avg: { + value: { + script: { + inline: 'script' + } + }, + weight: {} + } + } + }; + t.deepEqual(valueOne.toJSON(), expectedOne); + const valueTwo = valueOne.value('my_field'); + const expectedTwo = { + my_agg: { + weighted_avg: { + value: { + field: 'my_field' + }, + weight: {} + } + } + }; + t.deepEqual(valueTwo.toJSON(), expectedTwo); + const valueThree = valueOne.value(new Script('inline', 'script2')); + const expectedThree = { + my_agg: { + weighted_avg: { + value: { + script: { + inline: 'script2' + } + }, + weight: {} + } + } + }; + t.deepEqual(valueThree.toJSON(), expectedThree); +}); + +test('removes previously set weight field/script when updated', t => { + const valueOne = new WeightedAverageAggregation('my_agg').weight( + new Script('inline', 'script') + ); + const expectedOne = { + my_agg: { + weighted_avg: { + weight: { + script: { + inline: 'script' + } + }, + value: {} + } + } + }; + t.deepEqual(valueOne.toJSON(), expectedOne); + const valueTwo = valueOne.weight('my_field'); + const expectedTwo = { + my_agg: { + weighted_avg: { + weight: { + field: 'my_field' + }, + value: {} + } + } + }; + t.deepEqual(valueTwo.toJSON(), expectedTwo); + const valueThree = valueOne.weight(new Script('inline', 'script2')); + const expectedThree = { + my_agg: { + weighted_avg: { + weight: { + script: { + inline: 'script2' + } + }, + value: {} + } + } + }; + t.deepEqual(valueThree.toJSON(), expectedThree); +}); + +test('constructor sets value and weight', t => { + const value = new WeightedAverageAggregation( + 'my_agg', + 'my_value', + 'my_weight' + ).toJSON(); + const expected = { + my_agg: { + weighted_avg: { + value: { + field: 'my_value' + }, + weight: { + field: 'my_weight' + } + } + } + }; + t.deepEqual(value, expected); +}); + +test('constructor sets value and weight as scripts', t => { + const getScript = arg => new Script('inline', `doc['${arg}'].value`); + const value = new WeightedAverageAggregation( + 'my_agg', + getScript('value'), + getScript('weight') + ).toJSON(); + const expected = { + my_agg: { + weighted_avg: { + value: { + script: { + inline: "doc['value'].value" + } + }, + weight: { + script: { + inline: "doc['weight'].value" + } + } + } + } + }; + t.deepEqual(value, expected); +});