diff --git a/query/autocomplete.js b/query/autocomplete.js index b0a9039c..620dbb81 100644 --- a/query/autocomplete.js +++ b/query/autocomplete.js @@ -6,11 +6,12 @@ const logger = require('pelias-logger').get('api'); // additional views (these may be merged in to pelias/query at a later date) var views = { - ngrams_strict: require('./view/ngrams_strict'), - ngrams_last_token_only: require('./view/ngrams_last_token_only'), - phrase_first_tokens_only: require('./view/phrase_first_tokens_only'), - pop_subquery: require('./view/pop_subquery'), - boost_exact_matches: require('./view/boost_exact_matches') + ngrams_strict: require('./view/ngrams_strict'), + ngrams_last_token_only: require('./view/ngrams_last_token_only'), + phrase_first_tokens_only: require('./view/phrase_first_tokens_only'), + pop_subquery: require('./view/pop_subquery'), + boost_exact_matches: require('./view/boost_exact_matches'), + max_character_count_layer_filter: require('./view/max_character_count_layer_filter') }; //------------------------------ @@ -45,6 +46,7 @@ query.score( peliasQuery.view.popularity( views.pop_subquery ) ); query.score( peliasQuery.view.population( views.pop_subquery ) ); // non-scoring hard filters +query.score( views.max_character_count_layer_filter('address', 2), 'must_not' ); query.filter( peliasQuery.view.sources ); query.filter( peliasQuery.view.layers ); query.filter( peliasQuery.view.boundary_rect ); diff --git a/query/view/max_character_count_layer_filter.js b/query/view/max_character_count_layer_filter.js new file mode 100644 index 00000000..0cfd7b59 --- /dev/null +++ b/query/view/max_character_count_layer_filter.js @@ -0,0 +1,47 @@ +const _ = require('lodash'); +const peliasQuery = require('pelias-query'); + +/** + Layer terms filter view which counts the length of 'input:name' and only + applies the filter condition if the text is shorter than or equal to $maxCharCount. + + eg. to filter by 'layer=address' for all one & two digit inputs: + view = filter('address',2) +**/ + +// lowest and highest valid character count (enforced) +const MIN_CHAR_COUNT = 1; +const MAX_CHAR_COUNT = 99; + +module.exports = function( layerName, maxCharCount ) { + + // validate args, return no-op view if invalid + if( !_.isString(layerName) || _.isEmpty(layerName) || + !_.isNumber(maxCharCount) ){ + return () => null; + } + + // ensure char count is within a reasonable range + maxCharCount = _.clamp(maxCharCount, MIN_CHAR_COUNT, MAX_CHAR_COUNT); + + return function( vs ){ + + // validate required params + if( !vs.isset('input:name') ){ + return null; + } + + + // enforce maximum character length + let charCount = vs.var('input:name').toString().length; + if( !_.inRange(charCount, 1, maxCharCount+1) ){ + return null; + } + + return { + terms: { + layer: layerName + } + }; + }; +}; \ No newline at end of file diff --git a/test/unit/query/view/max_character_count_layer_filter.js b/test/unit/query/view/max_character_count_layer_filter.js new file mode 100644 index 00000000..d2934407 --- /dev/null +++ b/test/unit/query/view/max_character_count_layer_filter.js @@ -0,0 +1,117 @@ +const VariableStore = require('pelias-query').Vars; +const maxCharFilter = require('../../../../query/view/max_character_count_layer_filter'); + +module.exports.tests = {}; + +module.exports.tests.interface = function(test, common) { + test('interface: factory', function(t) { + t.equal(typeof maxCharFilter, 'function', 'valid factory function'); + t.equal(maxCharFilter.length, 2, 'factory takes 2 args'); + t.end(); + }); + test('interface: view', function(t) { + let view = maxCharFilter('layer_name', 1); + t.equal(typeof view, 'function', 'returns view'); + t.equal(view.length, 1, 'view takes 1 arg'); + t.end(); + }); +}; + +module.exports.tests.factory_missing_required_args = function(test, common) { + test('layerName undefined', function(t) { + let view = maxCharFilter(undefined, 1); + t.equal(view(), null, 'should have returned null'); + t.end(); + }); + test('layerName not string', function(t) { + let view = maxCharFilter([], 1); + t.equal(view(), null, 'should have returned null'); + t.end(); + }); + test('layerName too short', function(t) { + let view = maxCharFilter('', 1); + t.equal(view(), null, 'should have returned null'); + t.end(); + }); + test('maxCharCount undefined', function(t) { + let view = maxCharFilter('layer_name', undefined); + t.equal(view(), null, 'should have returned null'); + t.end(); + }); + test('maxCharCount not number', function(t) { + let view = maxCharFilter('layer_name', '1'); + t.equal(view(), null, 'should have returned null'); + t.end(); + }); +}; + +module.exports.tests.view_missing_required_params = function(test, common) { + test('input:name not set in VariableStore should return null', function(t) { + let view = maxCharFilter('layer_name', 1); + let vs = new VariableStore(); + t.equal(view(vs), null, 'should have returned null'); + t.end(); + }); +}; + +module.exports.tests.view_within_range = function(test, common) { + test('text length within range', function(t) { + let view = maxCharFilter('layer_name', 99); + let vs = new VariableStore(); + vs.var('input:name', 'example text'); + + let actual = view(vs); + let expected = { + terms: { + layer: 'layer_name' + } + }; + + t.deepEquals(actual, expected, 'should have returned object'); + t.end(); + }); +}; + +module.exports.tests.view_exceeds_range = function(test, common) { + test('text length exceeds range', function(t) { + let view = maxCharFilter('layer_name', 11); + let vs = new VariableStore(); + vs.var('input:name', 'example text'); + t.equal(view(vs), null, 'should have returned null'); + t.end(); + }); +}; + +module.exports.tests.view_clamp_range_low = function(test, common) { + test('maxCharCount less than one is equal to one', function(t) { + let view = maxCharFilter('layer_name', -999); + let vs = new VariableStore(); + vs.var('input:name', 'ex'); + t.equal(view(vs), null, 'should have returned null'); + t.end(); + }); + test('maxCharCount less than one is equal to one', function(t) { + let view = maxCharFilter('layer_name', -999); + let vs = new VariableStore(); + vs.var('input:name', 'e'); + + let actual = view(vs); + let expected = { + terms: { + layer: 'layer_name' + } + }; + + t.deepEquals(actual, expected, 'should have returned object'); + t.end(); + }); +}; + +module.exports.all = function (tape, common) { + function test(name, testFunction) { + return tape('filter ' + name, testFunction); + } + for( let testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; \ No newline at end of file diff --git a/test/unit/run.js b/test/unit/run.js index c8eee73a..1727400d 100644 --- a/test/unit/run.js +++ b/test/unit/run.js @@ -71,6 +71,7 @@ var tests = [ require('./query/search_original'), require('./query/structured_geocoding'), require('./query/text_parser'), + require('./query/view/max_character_count_layer_filter'), require('./sanitizer/_boundary_country'), require('./sanitizer/_debug'), require('./sanitizer/_flag_bool'),