From 229765f33a0f3ea21ba62d294e3ad5c978d87c0f Mon Sep 17 00:00:00 2001 From: Suhas Karanth Date: Sun, 3 Jun 2018 09:59:53 +0530 Subject: [PATCH] feat: Implement 'Terms Set Query' --- src/index.d.ts | 79 +++++++++++ src/index.js | 4 + src/queries/term-level-queries/index.js | 1 + .../term-level-queries/terms-set-query.js | 123 ++++++++++++++++++ test/index.test.js | 3 + test/queries-test/terms-set-query.test.js | 66 ++++++++++ 6 files changed, 276 insertions(+) create mode 100644 src/queries/term-level-queries/terms-set-query.js create mode 100644 test/queries-test/terms-set-query.test.js diff --git a/src/index.d.ts b/src/index.d.ts index c40d0b01..556f1243 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1450,6 +1450,85 @@ declare namespace esb { values?: string[] | string | number | boolean ): TermsQuery; + /** + * Returns any documents that match with at least one or more of the provided + * terms. The terms are not analyzed and thus must match exactly. The number of + * terms that must match varies per document and is either controlled by a + * minimum should match field or computed per document in a minimum should match + * script. + * + * NOTE: This query was added in elasticsearch v6.1. + * + * @param {string=} field + * @param {Array|string|number=} terms + * + * @extends Query + */ + export class TermsSetQuery extends Query { + constructor( + field?: string, + terms?: string[] | number[] | boolean[] | string | number + ); + + /** + * Sets the field to search on. + * + * @param {string} field + */ + field(field: string): this; + + /** + * Append given term to set of terms to run Terms Set Query with. + * + * @param {string|number|boolean} term + */ + term(term: string | number | boolean): this; + + /** + * Specifies the terms to run query for. + * + * @param {Array} terms Terms set to run query for. + * @throws {TypeError} If `terms` is not an instance of Array + */ + terms(terms: string[] | number[] | boolean[]): this; + + /** + * Controls the number of terms that must match per document. + * + * @param {string} fieldName + */ + minimumShouldMatchField(fieldName: string): this; + + /** + * Sets the `script` for query. It controls how many terms are required to + * match in a more dynamic way. + * + * The `params.num_terms` parameter is available in the script to indicate + * the number of terms that have been specified. + * + * @param {Script|string|Object} script + * @returns {ScriptQuery} returns `this` so that calls can be chained. + */ + minimumShouldMatchScript(script: Script | string | object): this; + } + + /** + * Returns any documents that match with at least one or more of the provided + * terms. The terms are not analyzed and thus must match exactly. The number of + * terms that must match varies per document and is either controlled by a + * minimum should match field or computed per document in a minimum should match + * script. + * + * NOTE: This query was added in elasticsearch v6.1. + * + * @param {string=} field + * @param {Array|string|number=} terms + */ + export function termsSetQuery( + field?: string, + terms?: string[] | number[] | boolean[] | string | number + ): TermsSetQuery; + /** * Interface-like class used to group and identify various implementations of * multi term queries: diff --git a/src/index.js b/src/index.js index ed2db40c..3f67c60f 100644 --- a/src/index.js +++ b/src/index.js @@ -31,6 +31,7 @@ const { termLevelQueries: { TermQuery, TermsQuery, + TermsSetQuery, RangeQuery, ExistsQuery, PrefixQuery, @@ -190,6 +191,9 @@ exports.termQuery = constructorWrapper(TermQuery); exports.TermsQuery = TermsQuery; exports.termsQuery = constructorWrapper(TermsQuery); +exports.TermsSetQuery = TermsSetQuery; +exports.termsSetQuery = constructorWrapper(TermsSetQuery); + exports.RangeQuery = RangeQuery; exports.rangeQuery = constructorWrapper(RangeQuery); diff --git a/src/queries/term-level-queries/index.js b/src/queries/term-level-queries/index.js index 48e3ec64..7678969f 100644 --- a/src/queries/term-level-queries/index.js +++ b/src/queries/term-level-queries/index.js @@ -4,6 +4,7 @@ exports.MultiTermQueryBase = require('./multi-term-query-base'); exports.TermQuery = require('./term-query'); exports.TermsQuery = require('./terms-query'); +exports.TermsSetQuery = require('./terms-set-query'); exports.RangeQuery = require('./range-query'); exports.ExistsQuery = require('./exists-query'); exports.PrefixQuery = require('./prefix-query'); diff --git a/src/queries/term-level-queries/terms-set-query.js b/src/queries/term-level-queries/terms-set-query.js new file mode 100644 index 00000000..5c17a3e0 --- /dev/null +++ b/src/queries/term-level-queries/terms-set-query.js @@ -0,0 +1,123 @@ +'use strict'; + +const isNil = require('lodash.isnil'); + +const { Query, util: { checkType } } = require('../../core'); + +/** + * Returns any documents that match with at least one or more of the provided + * terms. The terms are not analyzed and thus must match exactly. The number of + * terms that must match varies per document and is either controlled by a + * minimum should match field or computed per document in a minimum should match + * script. + * + * [Elasticsearch reference](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-set-query.html) + * + * NOTE: This query was added in elasticsearch v6.1. + * + * @example + * const qry = esb.termsSetQuery('codes', ['abc', 'def', 'ghi']) + * .minimumShouldMatchField('required_matches') + * + * @param {string=} field + * @param {Array|string|number=} terms + * + * @extends Query + */ +class TermsSetQuery extends Query { + // eslint-disable-next-line require-jsdoc + constructor(field, terms) { + super('terms_set'); + + this._queryOpts.terms = []; + + if (!isNil(field)) this._field = field; + if (!isNil(terms)) { + if (Array.isArray(terms)) this.terms(terms); + else this.term(terms); + } + } + + /** + * Sets the field to search on. + * + * @param {string} field + * @returns {TermsSetQuery} returns `this` so that calls can be chained. + */ + field(field) { + this._field = field; + return this; + } + + /** + * Append given term to set of terms to run Terms Set Query with. + * + * @param {string|number|boolean} term + * @returns {TermsSetQuery} returns `this` so that calls can be chained + */ + term(term) { + this._queryOpts.terms.push(term); + return this; + } + + /** + * Specifies the terms to run query for. + * + * @param {Array} terms Terms set to run query for. + * @returns {TermsSetQuery} returns `this` so that calls can be chained + * @throws {TypeError} If `terms` is not an instance of Array + */ + terms(terms) { + checkType(terms, Array); + + this._queryOpts.terms = this._queryOpts.terms.concat(terms); + return this; + } + + /** + * Controls the number of terms that must match per document. + * + * @param {string} fieldName + * @returns {TermsSetQuery} returns `this` so that calls can be chained + */ + minimumShouldMatchField(fieldName) { + this._queryOpts.minimum_should_match_field = fieldName; + return this; + } + + /** + * Sets the `script` for query. It controls how many terms are required to + * match in a more dynamic way. + * + * The `params.num_terms` parameter is available in the script to indicate + * the number of terms that have been specified. + * + * @example + * const qry = esb.termsSetQuery('codes', ['abc', 'def', 'ghi']) + * .minimumShouldMatchScript({ + * source: "Math.min(params.num_terms, doc['required_matches'].value)" + * }) + * + * @param {Script|string|Object} script + * @returns {ScriptQuery} returns `this` so that calls can be chained. + */ + minimumShouldMatchScript(script) { + this._queryOpts.minimum_should_match_script = script; + return this; + } + + /** + * Override default `toJSON` to return DSL representation of the term level query + * class instance. + * + * @override + * @returns {Object} returns an Object which maps to the elasticsearch query DSL + */ + toJSON() { + return { + [this.queryType]: { [this._field]: this._queryOpts } + }; + } +} + +module.exports = TermsSetQuery; diff --git a/test/index.test.js b/test/index.test.js index 8e665176..f832ac11 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -50,6 +50,9 @@ test('queries are exported', t => { t.truthy(esb.TermsQuery); t.truthy(esb.termsQuery); + t.truthy(esb.TermsSetQuery); + t.truthy(esb.termsSetQuery); + t.truthy(esb.RangeQuery); t.truthy(esb.rangeQuery); diff --git a/test/queries-test/terms-set-query.test.js b/test/queries-test/terms-set-query.test.js new file mode 100644 index 00000000..e9d036d4 --- /dev/null +++ b/test/queries-test/terms-set-query.test.js @@ -0,0 +1,66 @@ +import test from 'ava'; +import { TermsSetQuery } from '../../src'; +import { + illegalParamType, + nameFieldExpectStrategy, + makeSetsOptionMacro +} from '../_macros'; + +const getInstance = () => new TermsSetQuery('my_field'); + +const setsOption = makeSetsOptionMacro( + getInstance, + nameFieldExpectStrategy('terms_set', { terms: [] }) +); + +test(illegalParamType, getInstance(), 'terms', 'Array'); +test(setsOption, 'term', { + param: 'my-value', + propValue: ['my-value'], + keyName: 'terms' +}); +test(setsOption, 'terms', { + param: ['my-value-1', 'my-value-2'], + spread: false +}); +test(setsOption, 'minimumShouldMatchField', { param: 'required_matches' }); +test(setsOption, 'minimumShouldMatchScript', { + param: { + source: "Math.min(params.num_terms, doc['required_matches'].value)" + } +}); + +test('constructor sets arguments', t => { + let valueA = new TermsSetQuery('my_field', 'my-value').toJSON(); + let valueB = new TermsSetQuery() + .field('my_field') + .term('my-value') + .toJSON(); + t.deepEqual(valueA, valueB); + + let expected = { + terms_set: { + my_field: { terms: ['my-value'] } + } + }; + t.deepEqual(valueA, expected); + + valueA = new TermsSetQuery('my_field', [ + 'my-value-1', + 'my-value-2' + ]).toJSON(); + valueB = new TermsSetQuery() + .field('my_field') + .terms(['my-value-1', 'my-value-2']) + .toJSON(); + t.deepEqual(valueA, valueB); + + expected = { + terms_set: { + my_field: { + terms: ['my-value-1', 'my-value-2'] + } + } + }; + t.deepEqual(valueA, expected); +});