Browse Source

feat(max_character_count_layer_filter): refactor to use positive filter instead of negative filter

max_character_count_layer_filter
Peter Johnson 6 years ago
parent
commit
d945b974c5
  1. 2
      query/autocomplete.js
  2. 29
      query/view/max_character_count_layer_filter.js
  3. 86
      test/unit/fixture/autocomplete_linguistic_one_char_token.js
  4. 60
      test/unit/fixture/autocomplete_linguistic_three_char_token.js
  5. 86
      test/unit/fixture/autocomplete_linguistic_two_char_token.js
  6. 48
      test/unit/query/autocomplete.js
  7. 41
      test/unit/query/view/max_character_count_layer_filter.js

2
query/autocomplete.js

@ -46,7 +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( views.max_character_count_layer_filter(['address'], 2) );
query.filter( peliasQuery.view.sources );
query.filter( peliasQuery.view.layers );
query.filter( peliasQuery.view.boundary_rect );

29
query/view/max_character_count_layer_filter.js

@ -1,26 +1,41 @@
const _ = require('lodash');
const peliasQuery = require('pelias-query');
const allLayers = require('../../helper/type_mapping').layers;
/**
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.
You must provide a list of $excludedLayers, all layers listed in the type mapping
will be targeted, minus any listed in $excludedLayers.
eg. to filter by 'layer=address' for all one & two digit inputs:
view = filter('address',2)
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 ) {
module.exports = function( excludedLayers, maxCharCount ) {
// validate args, return no-op view if invalid
if( !_.isString(layerName) || _.isEmpty(layerName) ||
if( !_.isArray(excludedLayers) || _.isEmpty(excludedLayers) ||
!_.isNumber(maxCharCount) ){
return () => null;
}
// create an array containing all layers minus excluded layers
let includedLayers = _.difference(allLayers, excludedLayers);
// included layers is equal to all layers, return no-op view
if( includedLayers.length === allLayers.length ){
return () => null;
}
// create a new VariableStore with only the layers property
var vsWithOnlyIncludedLayers = new peliasQuery.Vars({ 'layers': includedLayers });
// ensure char count is within a reasonable range
maxCharCount = _.clamp(maxCharCount, MIN_CHAR_COUNT, MAX_CHAR_COUNT);
@ -31,17 +46,13 @@ module.exports = function( layerName, maxCharCount ) {
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
}
};
// use existing 'layers' query
return peliasQuery.view.layers(vsWithOnlyIncludedLayers);
};
};

86
test/unit/fixture/autocomplete_linguistic_one_char_token.js

@ -0,0 +1,86 @@
module.exports = {
'query': {
'bool': {
'must': [{
'constant_score': {
'query': {
'match': {
'name.default': {
'analyzer': 'peliasQueryPartialToken',
'boost': 100,
'query': 't',
'type': 'phrase',
'operator': 'and',
'slop': 3
}
}
}
}
}],
'should':[{
'function_score': {
'query': {
'match_all': {}
},
'max_boost': 20,
'score_mode': 'first',
'boost_mode': 'replace',
'functions': [{
'field_value_factor': {
'modifier': 'log1p',
'field': 'popularity',
'missing': 1
},
'weight': 1
}]
}
},{
'function_score': {
'query': {
'match_all': {}
},
'max_boost': 20,
'score_mode': 'first',
'boost_mode': 'replace',
'functions': [{
'field_value_factor': {
'modifier': 'log1p',
'field': 'population',
'missing': 1
},
'weight': 3
}]
}
}],
'filter': [{
'terms': {
'layer': [
'venue',
'street',
'country',
'macroregion',
'region',
'county',
'localadmin',
'locality',
'borough',
'neighbourhood',
'continent',
'empire',
'dependency',
'macrocounty',
'macrohood',
'microhood',
'disputed',
'postalcode',
'ocean',
'marinearea'
]
}
}]
}
},
'sort': [ '_score' ],
'size': 20,
'track_scores': true
};

60
test/unit/fixture/autocomplete_linguistic_three_char_token.js

@ -0,0 +1,60 @@
module.exports = {
'query': {
'bool': {
'must': [{
'constant_score': {
'query': {
'match': {
'name.default': {
'analyzer': 'peliasQueryPartialToken',
'boost': 100,
'query': 'tes',
'type': 'phrase',
'operator': 'and',
'slop': 3
}
}
}
}
}],
'should':[{
'function_score': {
'query': {
'match_all': {}
},
'max_boost': 20,
'score_mode': 'first',
'boost_mode': 'replace',
'functions': [{
'field_value_factor': {
'modifier': 'log1p',
'field': 'popularity',
'missing': 1
},
'weight': 1
}]
}
},{
'function_score': {
'query': {
'match_all': {}
},
'max_boost': 20,
'score_mode': 'first',
'boost_mode': 'replace',
'functions': [{
'field_value_factor': {
'modifier': 'log1p',
'field': 'population',
'missing': 1
},
'weight': 3
}]
}
}]
}
},
'sort': [ '_score' ],
'size': 20,
'track_scores': true
};

86
test/unit/fixture/autocomplete_linguistic_two_char_token.js

@ -0,0 +1,86 @@
module.exports = {
'query': {
'bool': {
'must': [{
'constant_score': {
'query': {
'match': {
'name.default': {
'analyzer': 'peliasQueryPartialToken',
'boost': 100,
'query': 'te',
'type': 'phrase',
'operator': 'and',
'slop': 3
}
}
}
}
}],
'should':[{
'function_score': {
'query': {
'match_all': {}
},
'max_boost': 20,
'score_mode': 'first',
'boost_mode': 'replace',
'functions': [{
'field_value_factor': {
'modifier': 'log1p',
'field': 'popularity',
'missing': 1
},
'weight': 1
}]
}
},{
'function_score': {
'query': {
'match_all': {}
},
'max_boost': 20,
'score_mode': 'first',
'boost_mode': 'replace',
'functions': [{
'field_value_factor': {
'modifier': 'log1p',
'field': 'population',
'missing': 1
},
'weight': 3
}]
}
}],
'filter': [{
'terms': {
'layer': [
'venue',
'street',
'country',
'macroregion',
'region',
'county',
'localadmin',
'locality',
'borough',
'neighbourhood',
'continent',
'empire',
'dependency',
'macrocounty',
'macrohood',
'microhood',
'disputed',
'postalcode',
'ocean',
'marinearea'
]
}
}]
}
},
'sort': [ '_score' ],
'size': 20,
'track_scores': true
};

48
test/unit/query/autocomplete.js

@ -82,6 +82,54 @@ module.exports.tests.query = function(test, common) {
t.end();
});
test('valid lingustic autocomplete one character token', function(t) {
var query = generate({
text: 't',
tokens: ['t'],
tokens_complete: [],
tokens_incomplete: ['t']
});
var compiled = JSON.parse( JSON.stringify( query ) );
var expected = require('../fixture/autocomplete_linguistic_one_char_token');
t.deepEqual(compiled.type, 'autocomplete', 'query type set');
t.deepEqual(compiled.body, expected, 'autocomplete_linguistic_one_char_token');
t.end();
});
test('valid lingustic autocomplete two character token', function(t) {
var query = generate({
text: 'te',
tokens: ['te'],
tokens_complete: [],
tokens_incomplete: ['te']
});
var compiled = JSON.parse( JSON.stringify( query ) );
var expected = require('../fixture/autocomplete_linguistic_two_char_token');
t.deepEqual(compiled.type, 'autocomplete', 'query type set');
t.deepEqual(compiled.body, expected, 'autocomplete_linguistic_two_char_token');
t.end();
});
test('valid lingustic autocomplete three character token', function(t) {
var query = generate({
text: 'tes',
tokens: ['tes'],
tokens_complete: [],
tokens_incomplete: ['tes']
});
var compiled = JSON.parse( JSON.stringify( query ) );
var expected = require('../fixture/autocomplete_linguistic_three_char_token');
t.deepEqual(compiled.type, 'autocomplete', 'query type set');
t.deepEqual(compiled.body, expected, 'autocomplete_linguistic_three_char_token');
t.end();
});
test('autocomplete + focus', function(t) {
var query = generate({
text: 'test',

41
test/unit/query/view/max_character_count_layer_filter.js

@ -1,5 +1,7 @@
const _ = require('lodash');
const VariableStore = require('pelias-query').Vars;
const maxCharFilter = require('../../../../query/view/max_character_count_layer_filter');
const allLayers = require('../../../../helper/type_mapping').layers;
module.exports.tests = {};
@ -10,7 +12,7 @@ module.exports.tests.interface = function(test, common) {
t.end();
});
test('interface: view', function(t) {
let view = maxCharFilter('layer_name', 1);
let view = maxCharFilter(['address'], 1);
t.equal(typeof view, 'function', 'returns view');
t.equal(view.length, 1, 'view takes 1 arg');
t.end();
@ -18,28 +20,28 @@ module.exports.tests.interface = function(test, common) {
};
module.exports.tests.factory_missing_required_args = function(test, common) {
test('layerName undefined', function(t) {
test('excludedLayers 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);
test('excludedLayers not array', function(t) {
let view = maxCharFilter('test', 1);
t.equal(view(), null, 'should have returned null');
t.end();
});
test('layerName too short', function(t) {
let view = maxCharFilter('', 1);
test('excludedLayers empty', 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);
let view = maxCharFilter(['address'], undefined);
t.equal(view(), null, 'should have returned null');
t.end();
});
test('maxCharCount not number', function(t) {
let view = maxCharFilter('layer_name', '1');
let view = maxCharFilter(['address'], '1');
t.equal(view(), null, 'should have returned null');
t.end();
});
@ -47,62 +49,61 @@ module.exports.tests.factory_missing_required_args = function(test, common) {
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 view = maxCharFilter(['address'], 1);
let vs = new VariableStore();
t.equal(view(vs), null, 'should have returned null');
t.equal(view(vs), null, 'view_missing_required_params');
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 view = maxCharFilter(['address'], 99);
let vs = new VariableStore();
vs.var('input:name', 'example text');
let actual = view(vs);
let expected = {
terms: {
layer: 'layer_name'
layer: { $: _.difference(allLayers, ['address']) }
}
};
t.deepEquals(actual, expected, 'should have returned object');
t.deepEquals(actual, expected, 'view_within_range');
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 view = maxCharFilter(['address'], 11);
let vs = new VariableStore();
vs.var('input:name', 'example text');
t.equal(view(vs), null, 'should have returned null');
t.equal(view(vs), null, 'view_exceeds_range');
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 view = maxCharFilter(['address'], -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 view = maxCharFilter(['address'], -999);
let vs = new VariableStore();
vs.var('input:name', 'e');
let actual = view(vs);
let expected = {
terms: {
layer: 'layer_name'
layer: { $: _.difference(allLayers, ['address']) }
}
};
t.deepEquals(actual, expected, 'should have returned object');
t.deepEquals(actual, expected, 'view_clamp_range_low');
t.end();
});
};

Loading…
Cancel
Save