mirror of https://github.com/pelias/api.git
Stephen Hess
7 years ago
2 changed files with 620 additions and 0 deletions
@ -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; |
@ -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…
Reference in new issue