From 386897c2c41f6676d3c17345b150565b1859b58d Mon Sep 17 00:00:00 2001 From: Stephen Hess Date: Tue, 27 Jun 2017 16:13:06 -0400 Subject: [PATCH] added query for address search with ids --- query/address_search_using_ids.js | 179 ++++++ test/unit/query/MockQuery.js | 27 + test/unit/query/address_search_using_ids.js | 617 ++++++++++++++++++++ 3 files changed, 823 insertions(+) create mode 100644 query/address_search_using_ids.js create mode 100644 test/unit/query/MockQuery.js create mode 100644 test/unit/query/address_search_using_ids.js diff --git a/query/address_search_using_ids.js b/query/address_search_using_ids.js new file mode 100644 index 00000000..432f38bc --- /dev/null +++ b/query/address_search_using_ids.js @@ -0,0 +1,179 @@ +const peliasQuery = require('pelias-query'); +const defaults = require('./search_defaults'); +const logger = require('pelias-logger').get('api'); +const _ = require('lodash'); +const check = require('check-types'); + +//------------------------------ +// general-purpose search query +//------------------------------ +const addressUsingIdsQuery = new peliasQuery.layout.AddressesUsingIdsQuery(); + +// scoring boost +// addressUsingIdsQuery.score( peliasQuery.view.focus_only_function( peliasQuery.view.phrase ) ); +addressUsingIdsQuery.score( peliasQuery.view.popularity_only_function ); +addressUsingIdsQuery.score( peliasQuery.view.population_only_function ); +// -------------------------------- + +// non-scoring hard filters +addressUsingIdsQuery.filter( peliasQuery.view.boundary_country ); +addressUsingIdsQuery.filter( peliasQuery.view.boundary_circle ); +addressUsingIdsQuery.filter( peliasQuery.view.boundary_rect ); +addressUsingIdsQuery.filter( peliasQuery.view.sources ); +// -------------------------------- + + +// Red Lion, PA -- parsed as locality/state, localadmin/state, and neighbourhood/state +// Chelsea -- parsed as neighbourhood, localadmin, and locality +// Manhattan -- parsed as borough, locality, and localadmin +// Luxembourg -- parsed as country, locality, and region + +// if any placeholder results are at neighbourhood, borough, locality, or localadmin layers, filter by those ids at those layers +// fallback to county +// if any placeholder results are at county or macrocounty layers, filter by those ids at those layers +// fallback to region +// if any placeholder results are at region or macroregion layers, filter by those ids at those layers +// fallback to dependency/country +// if any placeholder results are at dependency or country layers, filter by those ids at those layers + + +// address in Red Lion, PA -- find results at layer=address +// neighbourhood_id in [85844063, 85844067] +// locality_id in [101717221] +// localadmin_id in [404487867] +// search all of the above + +// address in Chelsea +// neighbourhood_id in [85786511, 85810589, 85769021, 85890029, 85810579, 85810591, 85810575, 85772883, 420514219] +// locality_id in [85950359, 85914491, 101932747, 85951865, 101715289, 85943049, 101733697, 101722101, 101738587] +// localadmin_id in [404476575, 404508239, 404474971, 404527169, 404494675, 404503811, 404519887, 404488679, 404538119] + +// address in Manhattan +// neighbourhood_id in [] +// borough_id in [421205771] +// locality_id in [85945171, 85940551, 85972655] +// localadmin_id in [404502889, 404499147, 404502891, 85972655] +// search all of the above + +// address in Luxembourg +// country_id in [85633275] +// region_id in [85681727, 85673875] +// locality_id in [101751765] +// search locality first, then region perhaps + + +// if there are locality/localadmin layers, return ['locality', 'localadmin'] +// if there are region/macroregion layers, return ['region', 'macroregion'] + +const granularity_bands = [ + ['neighbourhood', 'borough', 'locality', 'localadmin'], + ['county', 'macrocounty'], + ['region', 'macroregion'], + ['dependency', 'country'] +]; + +function anyResultsAtGranularityBand(results, band) { + return results.some((result) => { return _.includes(band, result.layer); }); +} + +function getIdsAtLayer(results, layer) { + return results.filter((result) => { return result.layer === layer; }).map(_.property('source_id')); +} + +/** + map request variables to query variables for all inputs + provided by this HTTP request. +**/ +function generateQuery( clean, res ){ + const vs = new peliasQuery.Vars( defaults ); + const results = _.defaultTo(res.data, []); + + const logParts = ['query:address_search_using_ids', 'parser:libpostal']; + + // sources + if( !_.isEmpty(clean.sources) ) { + vs.var( 'sources', clean.sources); + logParts.push('param:sources'); + } + + // size + if( clean.querySize ) { + vs.var( 'size', clean.querySize ); + logParts.push('param:querySize'); + } + + if( ! _.isEmpty(clean.parsed_text.number) ){ + vs.var( 'input:housenumber', clean.parsed_text.number ); + } + vs.var( 'input:street', clean.parsed_text.street ); + + const granularity_band = granularity_bands.find((band) => { + return anyResultsAtGranularityBand(results, band); + }); + + if (granularity_band) { + const layers_to_ids = granularity_band.reduce((acc, layer) => { + acc[layer] = getIdsAtLayer(res.data, layer); + return acc; + }, {}); + + vs.var('input:layers', JSON.stringify(layers_to_ids)); + + } + + // focus point + if( check.number(clean['focus.point.lat']) && + check.number(clean['focus.point.lon']) ){ + vs.set({ + 'focus:point:lat': clean['focus.point.lat'], + 'focus:point:lon': clean['focus.point.lon'] + }); + } + + // boundary rect + if( check.number(clean['boundary.rect.min_lat']) && + check.number(clean['boundary.rect.max_lat']) && + check.number(clean['boundary.rect.min_lon']) && + check.number(clean['boundary.rect.max_lon']) ){ + vs.set({ + 'boundary:rect:top': clean['boundary.rect.max_lat'], + 'boundary:rect:right': clean['boundary.rect.max_lon'], + 'boundary:rect:bottom': clean['boundary.rect.min_lat'], + 'boundary:rect:left': clean['boundary.rect.min_lon'] + }); + } + + // boundary circle + // @todo: change these to the correct request variable names + if( check.number(clean['boundary.circle.lat']) && + check.number(clean['boundary.circle.lon']) ){ + vs.set({ + 'boundary:circle:lat': clean['boundary.circle.lat'], + 'boundary:circle:lon': clean['boundary.circle.lon'] + }); + + if( check.number(clean['boundary.circle.radius']) ){ + vs.set({ + 'boundary:circle:radius': Math.round( clean['boundary.circle.radius'] ) + 'km' + }); + } + } + + // boundary country + if( check.string(clean['boundary.country']) ){ + vs.set({ + 'boundary:country': clean['boundary.country'] + }); + } + + // format the log parts into a single coherent string + logger.info(logParts.map((part) => { return `[${part}]`;} ).join(' ') ); + + return { + type: 'fallback_using_ids', + body: addressUsingIdsQuery.render(vs) + }; + +} + +module.exports = generateQuery; diff --git a/test/unit/query/MockQuery.js b/test/unit/query/MockQuery.js new file mode 100644 index 00000000..f97bfb27 --- /dev/null +++ b/test/unit/query/MockQuery.js @@ -0,0 +1,27 @@ +'use strict'; + +module.exports = class MockQuery { + constructor() { + this._score_functions = []; + this._filter_functions = []; + } + + render(vs) { + return { + vs: vs, + score_functions: this._score_functions, + filter_functions: this._filter_functions + }; + } + + score(view) { + this._score_functions.push(view); + return this; + } + + filter(view) { + this._filter_functions.push(view); + return this; + } + +}; diff --git a/test/unit/query/address_search_using_ids.js b/test/unit/query/address_search_using_ids.js new file mode 100644 index 00000000..f6d623e2 --- /dev/null +++ b/test/unit/query/address_search_using_ids.js @@ -0,0 +1,617 @@ +const generateQuery = require('../../../query/address_search_using_ids'); +const _ = require('lodash'); +const proxyquire = require('proxyquire').noCallThru(); +const mock_logger = require('pelias-mock-logger'); +const MockQuery = require('./MockQuery'); + +module.exports.tests = {}; + +module.exports.tests.interface = (test, common) => { + test('valid interface', (t) => { + t.ok(_.isFunction(generateQuery)); + t.end(); + }); +}; + +// helper for canned views +const views = { + popularity_only_function: 'popularity_only_function view', + population_only_function: 'population_only_function view', + boundary_country: 'boundary_country view', + boundary_circle: 'boundary_circle view', + boundary_rect: 'boundary_rect view', + sources: 'sources view' +}; + +module.exports.tests.base_query = (test, common) => { + test('basic', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + } + }; + const res = { + data: [] + }; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + }); + + const generatedQuery = generateQuery(clean, res); + + t.equals(generatedQuery.type, 'fallback_using_ids'); + + t.equals(generatedQuery.body.vs.var('input:housenumber').toString(), 'housenumber value'); + t.equals(generatedQuery.body.vs.var('input:street').toString(), 'street value'); + t.notOk(generatedQuery.body.vs.isset('sources')); + t.equals(generatedQuery.body.vs.var('size').toString(), 20); + + t.deepEquals(generatedQuery.body.score_functions, [ + 'popularity_only_function view', + 'population_only_function view' + ]); + + t.deepEquals(generatedQuery.body.filter_functions, [ + 'boundary_country view', + 'boundary_circle view', + 'boundary_rect view', + 'sources view' + ]); + + t.deepEquals(logger.getInfoMessages(), ['[query:address_search_using_ids] [parser:libpostal]']); + t.end(); + + }); +}; + +module.exports.tests.other_parameters = (test, common) => { + test('explicit size set', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + }, + querySize: 'querySize value' + }; + const res = { + data: [] + }; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + }); + + const generatedQuery = generateQuery(clean, res); + + t.equals(generatedQuery.body.vs.var('size').toString(), 'querySize value'); + t.deepEquals(logger.getInfoMessages(), ['[query:address_search_using_ids] [parser:libpostal] [param:querySize]']); + t.end(); + + }); + + test('explicit sources set', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + }, + sources: ['source 1', 'source 2'] + }; + const res = { + data: [] + }; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + }); + + const generatedQuery = generateQuery(clean, res); + + t.deepEquals(generatedQuery.body.vs.var('sources').toString(), ['source 1', 'source 2']); + t.deepEquals(logger.getInfoMessages(), ['[query:address_search_using_ids] [parser:libpostal] [param:sources]']); + t.end(); + + }); + +}; + +module.exports.tests.granularity_bands = (test, common) => { + test('neighbourhood/borough/locality/localadmin granularity band', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + } + }; + const res = { + data: [ + { + layer: 'neighbourhood', + source_id: 1 + }, + { + layer: 'borough', + source_id: 2 + }, + { + layer: 'locality', + source_id: 3 + }, + { + layer: 'localadmin', + source_id: 4 + }, + { + layer: 'county', + source_id: 5 + }, + { + layer: 'neighbourhood', + source_id: 6 + }, + { + layer: 'borough', + source_id: 7 + }, + { + layer: 'locality', + source_id: 8 + }, + { + layer: 'localadmin', + source_id: 9 + } + ] + }; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + + }); + + const generatedQuery = generateQuery(clean, res); + + t.deepEquals(JSON.parse(generatedQuery.body.vs.var('input:layers')), { + neighbourhood: [1, 6], + borough: [2, 7], + locality: [3, 8], + localadmin: [4, 9] + }); + + t.end(); + }); + + test('only band members with ids should be passed', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + } + }; + const res = { + data: [ + { + layer: 'neighbourhood', + source_id: 1 + } + ] + }; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + }); + + const generatedQuery = generateQuery(clean, res); + + t.deepEquals(JSON.parse(generatedQuery.body.vs.var('input:layers')), { + neighbourhood: [1], + borough: [], + locality: [], + localadmin: [] + }); + + t.end(); + }); + + test('county/macrocounty granularity band', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + } + }; + const res = { + data: [ + { + layer: 'county', + source_id: 1 + }, + { + layer: 'macrocounty', + source_id: 2 + }, + { + layer: 'region', + source_id: 3 + }, + { + layer: 'county', + source_id: 4 + }, + { + layer: 'macrocounty', + source_id: 5 + } + ] + }; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + + }); + + const generatedQuery = generateQuery(clean, res); + + t.deepEquals(JSON.parse(generatedQuery.body.vs.var('input:layers')), { + county: [1, 4], + macrocounty: [2, 5] + }); + + t.end(); + }); + + test('region/macroregion granularity band', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + } + }; + const res = { + data: [ + { + layer: 'region', + source_id: 1 + }, + { + layer: 'macroregion', + source_id: 2 + }, + { + layer: 'country', + source_id: 3 + }, + { + layer: 'region', + source_id: 4 + }, + { + layer: 'macroregion', + source_id: 5 + } + ] + }; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + + }); + + const generatedQuery = generateQuery(clean, res); + + t.deepEquals(JSON.parse(generatedQuery.body.vs.var('input:layers')), { + region: [1, 4], + macroregion: [2, 5] + }); + + t.end(); + + }); + + test('dependency/country granularity band', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + } + }; + const res = { + data: [ + { + layer: 'dependency', + source_id: 1 + }, + { + layer: 'country', + source_id: 2 + }, + { + layer: 'dependency', + source_id: 3 + }, + { + layer: 'country', + source_id: 4 + } + ] + }; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + + }); + + const generatedQuery = generateQuery(clean, res); + + t.deepEquals(JSON.parse(generatedQuery.body.vs.var('input:layers')), { + dependency: [1, 3], + country: [2, 4] + }); + + t.end(); + + }); + +}; + +module.exports.tests.boundary_filters = (test, common) => { + test('boundary.country available should add to query', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + }, + 'boundary.country': 'boundary.country value' + }; + const res = {}; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + + }); + + const generatedQuery = generateQuery(clean, res); + + t.equals(generatedQuery.body.vs.var('boundary:country').toString(), 'boundary.country value'); + + t.end(); + + }); + + test('focus.point.lat/lon w/both numbers should add to query', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + }, + 'focus.point.lat': 12.121212, + 'focus.point.lon': 21.212121 + }; + const res = {}; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + + }); + + const generatedQuery = generateQuery(clean, res); + + t.equals(generatedQuery.body.vs.var('focus:point:lat').toString(), 12.121212); + t.equals(generatedQuery.body.vs.var('focus:point:lon').toString(), 21.212121); + + t.end(); + + }); + + test('boundary.rect with all numbers should add to query', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + }, + 'boundary.rect.min_lat': 12.121212, + 'boundary.rect.max_lat': 13.131313, + 'boundary.rect.min_lon': 21.212121, + 'boundary.rect.max_lon': 31.313131 + }; + const res = {}; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + + }); + + const generatedQuery = generateQuery(clean, res); + + t.equals(generatedQuery.body.vs.var('boundary:rect:top').toString(), 13.131313); + t.equals(generatedQuery.body.vs.var('boundary:rect:right').toString(), 31.313131); + t.equals(generatedQuery.body.vs.var('boundary:rect:bottom').toString(), 12.121212); + t.equals(generatedQuery.body.vs.var('boundary:rect:left').toString(), 21.212121); + + t.end(); + + }); + + test('boundary circle without radius should set radius to default', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + }, + 'boundary.circle.lat': 12.121212, + 'boundary.circle.lon': 21.212121 + }; + const res = {}; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + + }); + + const generatedQuery = generateQuery(clean, res); + + t.equals(generatedQuery.body.vs.var('boundary:circle:lat').toString(), 12.121212); + t.equals(generatedQuery.body.vs.var('boundary:circle:lon').toString(), 21.212121); + t.equals(generatedQuery.body.vs.var('boundary:circle:radius').toString(), '50km'); + + t.end(); + + }); + + test('boundary circle with radius set radius to that value rounded', (t) => { + const logger = mock_logger(); + + const clean = { + parsed_text: { + number: 'housenumber value', + street: 'street value' + }, + 'boundary.circle.lat': 12.121212, + 'boundary.circle.lon': 21.212121, + 'boundary.circle.radius': 17.6 + }; + const res = {}; + + const generateQuery = proxyquire('../../../query/address_search_using_ids', { + 'pelias-logger': logger, + 'pelias-query': { + layout: { + AddressesUsingIdsQuery: MockQuery + }, + view: views, + Vars: require('pelias-query').Vars + } + + }); + + const generatedQuery = generateQuery(clean, res); + + t.equals(generatedQuery.body.vs.var('boundary:circle:lat').toString(), 12.121212); + t.equals(generatedQuery.body.vs.var('boundary:circle:lon').toString(), 21.212121); + t.equals(generatedQuery.body.vs.var('boundary:circle:radius').toString(), '18km'); + + t.end(); + + }); + +}; + +module.exports.all = (tape, common) => { + function test(name, testFunction) { + return tape(`address_search_using_ids query ${name}`, testFunction); + } + + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +};