Browse Source

added query frontend for pelias-query VenuesQuery

query-for-venues-on-admin-only
Stephen Hess 7 years ago
parent
commit
eb026f8a05
  1. 165
      query/venues.js
  2. 455
      test/unit/query/venues.js

165
query/venues.js

@ -0,0 +1,165 @@
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 venuesQuery = new peliasQuery.layout.VenuesQuery();
// scoring boost
venuesQuery.score( peliasQuery.view.focus_only_function( peliasQuery.view.phrase ) );
// --------------------------------
// non-scoring hard filters
venuesQuery.filter( peliasQuery.view.boundary_country );
venuesQuery.filter( peliasQuery.view.boundary_circle );
venuesQuery.filter( peliasQuery.view.boundary_rect );
venuesQuery.filter( peliasQuery.view.sources );
// --------------------------------
const adminLayers = ['neighbourhood', 'borough', 'city', 'county', 'state', 'country'];
// This query is a departure from traditional Pelias queries where textual
// names of admin areas were looked up. This query uses the ids returned by
// placeholder for lookups which dramatically reduces the amount of information
// that ES has to store and allows us to have placeholder handle altnames on
// behalf of Pelias.
//
// For the happy path, an input like '30 West 26th Street, Manhattan' would result
// in:
// neighbourhood_id in []
// borough_id in [421205771]
// locality_id in [85945171, 85940551, 85972655]
// localadmin_id in [404502889, 404499147, 404502891, 85972655]
//
// Where the ids are for all the various Manhattans. Each of those could
// conceivably be the Manhattan that the user was referring to so so all must be
// queried for at the same time.
//
// A counter example for this is '1 West Market Street, York, PA' where York, PA
// can be interpreted as a locality OR county. From experience, when there's
// ambiguity between locality and county for an input, the user is, with complete
// metaphysical certitude, referring to the city. If they were referring to the
// county, they would have entered 'York County, PA'. The point is that it's
// insufficient to just query for all ids because, in this case, '1 West Market Street'
// in other cities in York County, PA would be returned and would be both jarring
// to the user and almost certainly leads to incorrect results. For example,
// the following could be returned (all are towns in York County, PA):
// - 1 West Market Street, Dallastown, PA
// - 1 West Market Street, Fawn Grove, PA
// - 1 West Market Street, Shrewsbury, PA
// etc.
//
// To avoid this calamitous response, this query takes the approach of
// "granularity bands". That is, if there are any ids in the first set of any
// of these granularities:
// - neighbourhood
// - borough
// - locality
// - localadmin
// - region
// - macroregion
// - dependency
// - country
//
// then query for all ids in only those layers. Falling back, if there are
// no ids in those layers, query for the county/macrocounty layers.
//
// This methodology ensures that no happened-to-match-on-county results are returned.
//
// The decision was made to include all other layers in one to solve the issue
// where a country and city share a name, such as Mexico, which could be
// interpreted as a country AND city (in Missouri). The data itself will sort
// out which is correct. That is, it's unlikely that "11 Rock Springs Dr" exists
// in Mexico the country due to naming conventions and would be filtered out
// (though it could, but that's good because it's legitimate)
/**
map request variables to query variables for all inputs
provided by this HTTP request. This function operates on res.data which is the
Document-ified placeholder repsonse.
**/
function generateQuery( clean ){
const vs = new peliasQuery.Vars( defaults );
const logParts = ['query:venues', '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');
}
const mostGranularLayer = adminLayers.find(layer => {
return _.has(clean.parsed_text, layer);
});
if (mostGranularLayer) {
vs.var('input:query', clean.parsed_text[mostGranularLayer]);
}
// 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
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 => `[${part}]`).join(' '));
return {
type: 'fallback',
body: venuesQuery.render(vs)
};
}
module.exports = generateQuery;

455
test/unit/query/venues.js

@ -0,0 +1,455 @@
const generateQuery = require('../../../query/venues');
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 = {
focus_only_function: () => 'focus_only_function',
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('neighbourhood from parsed_text should be used for input:query when most granular', t => {
const clean = {
parsed_text: {
neighbourhood: 'neighbourhood value',
borough: 'borough value',
city: 'city value',
county: 'county value',
state: 'state value',
country: 'country value'
}
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
t.equals(generatedQuery.body.vs.var('input:query').toString(), 'neighbourhood value');
t.end();
});
test('borough from parsed_text should be used for input:query when most granular', t => {
const clean = {
parsed_text: {
borough: 'borough value',
city: 'city value',
county: 'county value',
state: 'state value',
country: 'country value'
}
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
t.equals(generatedQuery.body.vs.var('input:query').toString(), 'borough value');
t.end();
});
test('city from parsed_text should be used for input:query when most granular', t => {
const clean = {
parsed_text: {
city: 'city value',
county: 'county value',
state: 'state value',
country: 'country value'
}
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
t.equals(generatedQuery.body.vs.var('input:query').toString(), 'city value');
t.end();
});
test('county from parsed_text should be used for input:query when most granular', t => {
const clean = {
parsed_text: {
county: 'county value',
state: 'state value',
country: 'country value'
}
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
t.equals(generatedQuery.body.vs.var('input:query').toString(), 'county value');
t.end();
});
test('state from parsed_text should be used for input:query when most granular', t => {
const clean = {
parsed_text: {
state: 'state value',
country: 'country value'
}
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
t.equals(generatedQuery.body.vs.var('input:query').toString(), 'state value');
t.end();
});
test('country from parsed_text should be used for input:query when most granular', t => {
const clean = {
parsed_text: {
country: 'country value'
}
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
t.equals(generatedQuery.body.vs.var('input:query').toString(), 'country value');
t.end();
});
test('no input:query should be set when no admin layers in parsed_text', t => {
const clean = {
parsed_text: {
}
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
t.notOk(generatedQuery.body.vs.isset('input:query'));
t.end();
});
test('scores and filters should be added', t => {
const logger = mock_logger();
const clean = {
parsed_text: {
city: 'city value'
}
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-logger': logger,
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
t.equals(generatedQuery.type, 'fallback');
t.equals(generatedQuery.body.vs.var('input:query').toString(), 'city value');
t.notOk(generatedQuery.body.vs.isset('sources'));
t.equals(generatedQuery.body.vs.var('size').toString(), 20);
t.deepEquals(generatedQuery.body.score_functions, [
'focus_only_function'
]);
t.deepEquals(generatedQuery.body.filter_functions, [
'boundary_country view',
'boundary_circle view',
'boundary_rect view',
'sources view'
]);
t.deepEquals(logger.getInfoMessages(), ['[query:venues] [parser:libpostal]']);
t.end();
});
};
module.exports.tests.other_parameters = (test, common) => {
test('explicit size set', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
city: 'city value'
},
querySize: 'querySize value'
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-logger': logger,
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
t.equals(generatedQuery.body.vs.var('size').toString(), 'querySize value');
t.deepEquals(logger.getInfoMessages(), ['[query:venues] [parser:libpostal] [param:querySize]']);
t.end();
});
test('explicit sources set', (t) => {
const logger = mock_logger();
const clean = {
parsed_text: {
city: 'city value'
},
sources: ['source 1', 'source 2']
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-logger': logger,
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
t.deepEquals(generatedQuery.body.vs.var('sources').toString(), ['source 1', 'source 2']);
t.deepEquals(logger.getInfoMessages(), ['[query:venues] [parser:libpostal] [param:sources]']);
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: {
city: 'city value'
},
'boundary.country': 'boundary.country value'
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-logger': logger,
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
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: {
city: 'city value'
},
'focus.point.lat': 12.121212,
'focus.point.lon': 21.212121
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-logger': logger,
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
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: {
city: 'city 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 generatedQuery = proxyquire('../../../query/venues', {
'pelias-logger': logger,
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
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: {
city: 'city value'
},
'boundary.circle.lat': 12.121212,
'boundary.circle.lon': 21.212121
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-logger': logger,
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
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: {
city: 'city value'
},
'boundary.circle.lat': 12.121212,
'boundary.circle.lon': 21.212121,
'boundary.circle.radius': 17.6
};
const generatedQuery = proxyquire('../../../query/venues', {
'pelias-logger': logger,
'pelias-query': {
layout: {
VenuesQuery: MockQuery
},
view: views,
Vars: require('pelias-query').Vars
}
})(clean);
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);
}
};
Loading…
Cancel
Save