diff --git a/index.js b/index.js index 692d171..061167f 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,8 @@ module.exports.view = { leaf: { match_all: require('./view/leaf/match_all'), match_phrase: require('./view/leaf/match_phrase'), - match: require('./view/leaf/match') + match: require('./view/leaf/match'), + multi_match: require('./view/leaf/multi_match') }, focus: require('./view/focus'), focus_only_function: require('./view/focus_only_function'), diff --git a/lib/leaf/multi_match.js b/lib/leaf/multi_match.js new file mode 100644 index 0000000..026fa7b --- /dev/null +++ b/lib/leaf/multi_match.js @@ -0,0 +1,36 @@ +const MATCH_PARAMS = [ + 'tie_breaker', 'analyzer', 'boost', 'operator', 'minimum_should_match', 'fuzziness', + 'lenient', 'prefix_length', 'max_expansions', 'rewrite', 'zero_terms_query', 'cutoff_frequency' +]; +const PHRASE_PARAMS = ['analyzer', 'boost', 'lenient', 'slop', 'zero_terms_query']; +const OPTIONAL_PARAMS = { + 'best_fields': MATCH_PARAMS, + 'most_fields': MATCH_PARAMS, + 'cross_fields': ['analyzer', 'boost', 'operator', 'minimum_should_match', 'lenient', 'zero_terms_query', 'cutoff_frequency'], + 'phrase': PHRASE_PARAMS, + 'phrase_prefix': PHRASE_PARAMS.concat('max_expansions') +}; + +module.exports = function( type, fields, value, params ) { + if( !type || !value || !OPTIONAL_PARAMS[type] ) { + return null; + } + + const query = { + multi_match: { + type: type, + query: value, + fields: fields + } + }; + + OPTIONAL_PARAMS[type].forEach(function(param) { + if (params && params[param] && params[param].toString() !== '') { + query.multi_match[param] = params[param]; + } + }); + + return query; +}; + +module.exports.OPTIONAL_PARAMS = OPTIONAL_PARAMS; \ No newline at end of file diff --git a/test/lib/leaf/multi_match.js b/test/lib/leaf/multi_match.js new file mode 100644 index 0000000..e11868a --- /dev/null +++ b/test/lib/leaf/multi_match.js @@ -0,0 +1,153 @@ +const multi_match = require('../../../lib/leaf/multi_match'); +const Variable = require('../../../lib/Variable'); + +module.exports.tests = {}; + +module.exports.tests.multi_match = function(test, common) { + test('null returned if property missing', function(t) { + const query = multi_match(); + + t.equal(null, query, 'null query returned'); + t.end(); + }); + + test('null returned if fields are missing', function(t) { + const query = multi_match('best_fields'); + + t.equal(null, query, 'null query returned'); + t.end(); + }); + + test('null returned if input is missing', function(t) { + const query = multi_match('best_fields', ['fields']); + + t.equal(null, query, 'null query returned'); + t.end(); + }); + + test('null returned if type is not supported', function(t) { + const query = multi_match('type', ['fields'], 'input'); + + t.equal(null, query, 'null query returned'); + t.end(); + }); + + test('multi_match query returned with type, fields and input', function(t) { + const query = multi_match('best_fields', ['fields'], 'input'); + + const expected = { + multi_match: { + type: 'best_fields', + fields: ['fields'], + query: 'input' + } + }; + + t.deepEqual(query, expected, 'valid multi_match query'); + t.end(); + }); + + test('multi_match query can handle optional boost parameter', function(t) { + const query = multi_match('phrase', ['fields'], 'input', { boost: 5}); + + + const expected = { + multi_match: { + type: 'phrase', + fields: ['fields'], + query: 'input', + boost: 5 + } + }; + + t.deepEqual(query, expected, 'valid multi_match query with boost'); + t.end(); + }); + + test('multi_match phrase query can handle optional slop parameter', function(t) { + const query = multi_match('phrase', ['fields'], 'input', { slop: 1}); + + const expected = { + multi_match: { + type: 'phrase', + fields: ['fields'], + query: 'input', + slop: 1 + } + }; + + t.deepEqual(query, expected, 'valid multi_match query with slop'); + t.end(); + }); + + test('multi_match query can handle optional analyzer parameter', function(t) { + const query = multi_match('phrase', ['fields'], 'input', { analyzer: 'customAnalyzer'}); + + const expected = { + multi_match: { + type: 'phrase', + fields: ['fields'], + query: 'input', + analyzer: 'customAnalyzer' + } + }; + + t.deepEqual(query, expected, 'valid multi_match query with analyzer'); + t.end(); + }); + + test('multi_match query does not allow empty string as optional param input', function(t) { + const query = multi_match('phrase', ['fields'], 'input', { slop: '' }); + + const expected = { + multi_match: { + type: 'phrase', + fields: ['fields'], + query: 'input' + } + }; + + t.deepEqual(query, expected, 'valid multi_match query without empty string input'); + t.end(); + }); + + test('multi_match query does not allow empty Variable as optional param input', function(t) { + const query = multi_match('phrase', ['fields'], 'input', { analyzer: new Variable() }); + + const expected = { + multi_match: { + type: 'phrase', + fields: ['fields'], + query: 'input' + } + }; + + t.deepEqual(query, expected, 'valid multi_match query with out empty string input'); + t.end(); + }); + + test('multi_match query with type phrase_prefix should accept max_expansions', function(t) { + const query = multi_match('phrase_prefix', ['fields'], 'input', { max_expansions: 25 }); + + const expected = { + multi_match: { + type: 'phrase_prefix', + fields: ['fields'], + query: 'input', + max_expansions: 25 + } + }; + + t.deepEqual(query, expected, 'valid multi_match query with out empty string input'); + t.end(); + }); +}; + +module.exports.all = function (tape, common) { + function test(name, testFunction) { + return tape('lib/leaf/multi_match ' + name, testFunction); + } + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; diff --git a/test/run.js b/test/run.js index bfb1f03..160161d 100644 --- a/test/run.js +++ b/test/run.js @@ -10,6 +10,7 @@ var tests = [ require('./layout/VenuesQuery.js'), require('./lib/leaf/match.js'), require('./lib/leaf/match_phrase.js'), + require('./lib/leaf/multi_match.js'), require('./lib/leaf/terms.js'), require('./lib/Variable.js'), require('./lib/VariableStore.js'), @@ -34,7 +35,8 @@ var tests = [ require('./view/sources.js'), require('./view/boundary_gid.js'), require('./view/leaf/match.js'), - require('./view/leaf/match_phrase.js') + require('./view/leaf/match_phrase.js'), + require('./view/leaf/multi_match.js') ]; tests.map(function(t) { diff --git a/test/view/leaf/multi_match.js b/test/view/leaf/multi_match.js new file mode 100644 index 0000000..906e098 --- /dev/null +++ b/test/view/leaf/multi_match.js @@ -0,0 +1,110 @@ +const VariableStore = require('../../../lib/VariableStore'); +const multi_match = require('../../../view/leaf/multi_match'); + +module.exports.tests = {}; + +module.exports.tests.base_usage = function(test, common) { + test('input and fields specified', function(t) { + + const vs = new VariableStore(); + vs.var('multi_match:example:input', 'input value'); + vs.var('multi_match:example:fields', ['fields', 'values']); + + const view = multi_match('example')(vs); + + const actual = JSON.parse(JSON.stringify(view)); + + const expected = { + multi_match: { + type: 'best_fields', + fields: ['fields', 'values'], + query: 'input value' + } + }; + + t.deepEqual(actual, expected, 'multi_match view rendered as expected'); + t.end(); + }); + + test('optional fields specified', function(t) { + const vs = new VariableStore(); + vs.var('multi_match:example2:input', 'input value'); + vs.var('multi_match:example2:fields', ['fields', 'values']); + vs.var('multi_match:example2:cutoff_frequency', 0.001); + vs.var('multi_match:example2:analyzer', 'customAnalyzer'); + + const view = multi_match('example2')(vs); + + const actual = JSON.parse(JSON.stringify(view)); + + const expected = { + multi_match: { + type: 'best_fields', + fields: ['fields', 'values'], + query: 'input value', + analyzer: 'customAnalyzer', + cutoff_frequency: 0.001 + } + }; + + t.deepEqual(actual, expected, 'multi_match view rendered as expected'); + t.end(); + }); + + test('specific multi_match type', function(t) { + const vs = new VariableStore(); + vs.var('multi_match:example2:input', 'input value'); + vs.var('multi_match:example2:type', 'phrase'); + vs.var('multi_match:example2:fields', ['fields', 'values']); + vs.var('multi_match:example2:cutoff_frequency', 0.001); // this will be ignored because it's a phrase type + vs.var('multi_match:example2:analyzer', 'customAnalyzer'); + vs.var('multi_match:example2:slop', 3); + + const view = multi_match('example2')(vs); + + const actual = JSON.parse(JSON.stringify(view)); + + const expected = { + multi_match: { + type: 'phrase', + fields: ['fields', 'values'], + query: 'input value', + analyzer: 'customAnalyzer', + slop: 3 + } + }; + + t.deepEqual(actual, expected, 'multi_match view rendered as expected'); + t.end(); + }); +}; + +module.exports.tests.incorrect_usage = function(test, common) { + test('no variables specified', function(t) { + const vs = new VariableStore(); + + const view = multi_match('broken_example')(vs); + + t.equal(view, null, 'should return null'); + t.end(); + }); + + test('no input specified', function(t) { + const vs = new VariableStore(); + vs.var('multi_match:broken_example:fields', 'fields value'); + + const view = multi_match('broken_example')(vs); + + t.equal(view, null, 'should return null'); + t.end(); + }); +}; + +module.exports.all = function (tape, common) { + function test(name, testFunction) { + return tape('leaf/multi_match ' + name, testFunction); + } + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; diff --git a/view/leaf/multi_match.js b/view/leaf/multi_match.js new file mode 100644 index 0000000..61b27e0 --- /dev/null +++ b/view/leaf/multi_match.js @@ -0,0 +1,28 @@ +const multi_match = require('../../lib/leaf/multi_match'); +const OPTIONAL_PARAMS = multi_match.OPTIONAL_PARAMS; + +module.exports = function( prefix ){ + const type_variable = `multi_match:${prefix}:type`; + const fields_variable = `multi_match:${prefix}:fields`; + const input_variable = `multi_match:${prefix}:input`; + + return function( vs ){ + if( !vs.isset(fields_variable)|| + !vs.isset(input_variable) ) { + return null; + } + + // best_fields is the default value in ES + const type = vs.isset(type_variable) ? vs.var(type_variable) : 'best_fields'; + const options = { }; + + OPTIONAL_PARAMS[type].forEach(function(param) { + const variable_name = `multi_match:${prefix}:${param}`; + if (vs.isset(variable_name)) { + options[param] = vs.var(variable_name); + } + }); + + return multi_match(type, vs.var(fields_variable), vs.var(input_variable), options); + }; +};