diff --git a/query/reverse.js b/query/reverse.js index 35b86ade..185d312d 100644 --- a/query/reverse.js +++ b/query/reverse.js @@ -1,7 +1,6 @@ -var logger = require('../src/logger'), - queries = require('geopipes-elasticsearch-backend').queries, - sort = require('../query/sort'); +var queries = require('geopipes-elasticsearch-backend').queries, + sort = require('./sort'); function generate( params ){ @@ -13,7 +12,23 @@ function generate( params ){ var query = queries.distance( centroid, { size: params.size || 1 } ); query.sort = query.sort.concat(sort); + if ( params.categories && params.categories.length > 0 ) { + addCategoriesFilter( query, params.categories ); + } + return query; } +function addCategoriesFilter( query, categories ) { + query.query.filtered.query = { + match: { + category: { + query: categories.join(' '), + analyzer: 'standard', + operator: 'or' + } + } + }; +} + module.exports = generate; \ No newline at end of file diff --git a/sanitiser/_categories.js b/sanitiser/_categories.js new file mode 100644 index 00000000..f29c9fd0 --- /dev/null +++ b/sanitiser/_categories.js @@ -0,0 +1,38 @@ + +var isObject = require('is-object'); + +// validate inputs, convert types and apply defaults +function sanitize( req ){ + + var clean = req.clean || {}; + var params= req.query; + + // ensure the input params are a valid object + if( !isObject( params ) ){ + params = {}; + } + + // default case (no categories specified in GET params) + if('string' !== typeof params.categories || !params.categories.length){ + clean.categories = []; + } + else { + // parse GET params + clean.categories = params.categories.split(',') + .map(function (cat) { + return cat.toLowerCase().trim(); // lowercase inputs + }) + .filter( function( cat ) { + return ( cat.length > 0 ); + }); + } + + // pass validated params to next middleware + req.clean = clean; + + return { 'error': false }; + +} + +// export function +module.exports = sanitize; diff --git a/sanitiser/reverse.js b/sanitiser/reverse.js index 117b67d2..9804b4e9 100644 --- a/sanitiser/reverse.js +++ b/sanitiser/reverse.js @@ -1,6 +1,5 @@ -var logger = require('../src/logger'), - _sanitize = require('../sanitiser/_sanitize'), +var _sanitize = require('../sanitiser/_sanitize'), sanitiser = { latlonzoom: function( req ) { var geo = require('../sanitiser/_geo'); @@ -10,6 +9,10 @@ var logger = require('../src/logger'), size: function( req ) { var size = require('../sanitiser/_size'); return size(req, 1); + }, + categories: function ( req ) { + var categories = require('../sanitiser/_categories'); + return categories(req); } }; diff --git a/test/unit/query/reverse.js b/test/unit/query/reverse.js index 0380fb85..a2878e94 100644 --- a/test/unit/query/reverse.js +++ b/test/unit/query/reverse.js @@ -111,6 +111,72 @@ module.exports.tests.query = function(test, common) { }); t.end(); }); + + test('valid query with categories', function(t) { + var params = { lat: 29.49136, lon: -82.50622, categories: ['food', 'education', 'entertainment'] }; + var query = generate(params); + + var expected = { + 'query': { + 'filtered': { + 'query': { + 'match': { + 'category': { + 'query': 'food education entertainment', + 'analyzer': 'standard', + 'operator': 'or' + } + } + }, + 'filter': { + 'bool': { + 'must': [ + { + 'geo_distance': { + 'distance': '50km', + 'distance_type': 'plane', + 'optimize_bbox': 'indexed', + '_cache': true, + 'center_point': { + 'lat': '29.49', + 'lon': '-82.51' + } + } + } + ] + } + } + } + }, + 'sort': [ + '_score', + { + '_geo_distance': { + 'center_point': { + 'lat': 29.49136, + 'lon': -82.50622 + }, + 'order': 'asc', + 'unit': 'km' + } + } + ].concat(sort.slice(1)), + 'size': 1, + 'track_scores': true + }; + + t.deepEqual(query, expected, 'valid reverse query with categories'); + + // test different sizes + var sizes = [1,2,10,undefined,null]; + sizes.forEach( function(size) { + params.size = size; + query = generate(params); + expected.size = size ? size : 1; + t.deepEqual(query, expected, 'valid reverse query for size: '+ size); + }); + t.end(); + }); }; module.exports.all = function (tape, common) { diff --git a/test/unit/sanitiser/reverse.js b/test/unit/sanitiser/reverse.js index 3a06eaef..ab47b334 100644 --- a/test/unit/sanitiser/reverse.js +++ b/test/unit/sanitiser/reverse.js @@ -8,7 +8,8 @@ var suggest = require('../../../sanitiser/reverse'), layers: [ 'geoname', 'osmnode', 'osmway', 'admin0', 'admin1', 'admin2', 'neighborhood', 'locality', 'local_admin', 'osmaddress', 'openaddresses' ], lon: 0, - size: 1 + size: 1, + categories: [] }, sanitize = function(query, cb) { _sanitize({'query':query}, cb); }; @@ -200,6 +201,45 @@ module.exports.tests.sanitize_layers = function(test, common) { }); }; +module.exports.tests.sanitize_categories = function(test, common) { + var queryParams = { input: 'test', lat: 0, lon: 0 }; + test('unspecified', function(t) { + queryParams.categories = undefined; + sanitize(queryParams, function( err, clean ){ + t.deepEqual(clean.categories, defaultClean.categories, 'default to empty categories array'); + t.end(); + }); + }); + test('single category', function(t) { + queryParams.categories = 'food'; + sanitize(queryParams, function( err, clean ){ + t.deepEqual(clean.categories, ['food'], 'category set'); + t.end(); + }); + }); + test('multiple categories', function(t) { + queryParams.categories = 'food,education,nightlife'; + sanitize(queryParams, function( err, clean ){ + t.deepEqual(clean.categories, ['food', 'education', 'nightlife'], 'categories set'); + t.end(); + }); + }); + test('whitespace and empty strings', function(t) { + queryParams.categories = 'food, , nightlife ,'; + sanitize(queryParams, function( err, clean ){ + t.deepEqual(clean.categories, ['food', 'nightlife'], 'categories set'); + t.end(); + }); + }); + test('all empty strings', function(t) { + queryParams.categories = ', , ,'; + sanitize(queryParams, function( err, clean ){ + t.deepEqual(clean.categories, defaultClean.categories, 'empty strings filtered out'); + t.end(); + }); + }); +}; + module.exports.tests.middleware_failure = function(test, common) { test('middleware failure', function(t) { var res = { status: function( code ){