mirror of https://github.com/pelias/api.git
Peter Johnson
9 years ago
1 changed files with 130 additions and 157 deletions
@ -1,188 +1,161 @@ |
|||||||
|
|
||||||
var queries = require('geopipes-elasticsearch-backend').queries, |
var peliasQuery = require('pelias-query'), |
||||||
sort = require('../query/sort'), |
sort = require('../query/sort'), |
||||||
adminFields = require('../helper/adminFields')(), |
adminFields = require('../helper/adminFields')(); |
||||||
addressWeights = require('../helper/address_weights'); |
|
||||||
|
|
||||||
|
//------------------------------
|
||||||
|
// general-purpose search query
|
||||||
|
//------------------------------
|
||||||
|
|
||||||
function generate( params ){ |
var query = new peliasQuery.layout.FilteredBooleanQuery(); |
||||||
var centroid = null; |
|
||||||
|
|
||||||
if ( params.lat && params.lon ){ |
// mandatory matches
|
||||||
centroid = { |
query.score( peliasQuery.view.boundary_country, 'must' ); |
||||||
lat: params.lat, |
query.score( peliasQuery.view.ngrams, 'must' ); |
||||||
lon: params.lon |
|
||||||
}; |
|
||||||
}
|
|
||||||
|
|
||||||
var query = queries.distance( centroid, { size: params.size } ); |
// scoring boost
|
||||||
var input = params.input; |
query.score( peliasQuery.view.phrase ); |
||||||
|
query.score( peliasQuery.view.focus ); |
||||||
|
|
||||||
if (params.bbox) { |
// address components
|
||||||
query = queries.bbox ( centroid, { size: params.size, bbox: params.bbox } ); |
query.score( peliasQuery.view.address('housenumber') ); |
||||||
} |
query.score( peliasQuery.view.address('street') ); |
||||||
|
query.score( peliasQuery.view.address('postcode') ); |
||||||
|
|
||||||
query.query.filtered.query = { |
// admin components
|
||||||
'bool': { |
query.score( peliasQuery.view.admin('alpha3') ); |
||||||
'must': [], |
query.score( peliasQuery.view.admin('admin0') ); |
||||||
'should': [] |
query.score( peliasQuery.view.admin('admin1') ); |
||||||
} |
query.score( peliasQuery.view.admin('admin1_abbr') ); |
||||||
}; |
query.score( peliasQuery.view.admin('admin2') ); |
||||||
|
query.score( peliasQuery.view.admin('local_admin') ); |
||||||
|
query.score( peliasQuery.view.admin('locality') ); |
||||||
|
query.score( peliasQuery.view.admin('neighborhood') ); |
||||||
|
|
||||||
|
// non-scoring hard filters
|
||||||
|
query.filter( peliasQuery.view.boundary_circle, 'must' ); |
||||||
|
query.filter( peliasQuery.view.boundary_rect, 'must' ); |
||||||
|
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
if (params.parsed_input) { |
function generate( clean ){ |
||||||
// update input
|
|
||||||
if (params.parsed_input.number && params.parsed_input.street) { |
var vs = new peliasQuery.Vars( peliasQuery.defaults ); |
||||||
input = params.parsed_input.number + ' ' + params.parsed_input.street; |
|
||||||
} else if (params.parsed_input.admin_parts) { |
// set input text
|
||||||
input = params.parsed_input.name; |
vs.var( 'input:name', clean.input ); |
||||||
|
|
||||||
|
// set size
|
||||||
|
if( clean.size ){ |
||||||
|
vs.var( 'size', clean.size ); |
||||||
} |
} |
||||||
|
|
||||||
addParsedMatch(query, input, params.parsed_input); |
// focus point
|
||||||
|
if( clean.lat && clean.lon ){ |
||||||
|
vs.set({ |
||||||
|
'focus:point:lat': clean.lat, |
||||||
|
'focus:point:lon': clean.lon |
||||||
|
}); |
||||||
} |
} |
||||||
|
|
||||||
// add search condition to distance query
|
// bbox
|
||||||
query.query.filtered.query.bool.must.push({
|
if( clean.bbox ){ |
||||||
'match': { |
vs.set({ |
||||||
'name.default': { |
'boundary:rect:top': clean.bbox.top, |
||||||
'query': input, |
'boundary:rect:right': clean.bbox.right, |
||||||
'analyzer': 'peliasOneEdgeGram' |
'boundary:rect:bottom': clean.bbox.bottom, |
||||||
|
'boundary:rect:left': clean.bbox.left |
||||||
|
}); |
||||||
} |
} |
||||||
|
|
||||||
|
// address parsing
|
||||||
|
if( clean.parsed_input ){ |
||||||
|
|
||||||
|
// is it a street address?
|
||||||
|
var isStreetAddress = clean.parsed_input.hasOwnProperty('number') && clean.parsed_input.hasOwnProperty('street'); |
||||||
|
if( isStreetAddress ){ |
||||||
|
vs.var( 'input:name', clean.parsed_input.number + ' ' + clean.parsed_input.street ); |
||||||
} |
} |
||||||
}); |
|
||||||
|
|
||||||
// add phrase matching query
|
// I don't understand this
|
||||||
// note: this is required for shingle/phrase matching
|
else if( clean.parsed_input.admin_parts ) { |
||||||
query.query.filtered.query.bool.should.push({ |
vs.var( 'input:name', clean.parsed_input.name ); |
||||||
'match': { |
|
||||||
'phrase.default': { |
|
||||||
'query': input, |
|
||||||
'analyzer': 'peliasPhrase', |
|
||||||
'type': 'phrase', |
|
||||||
'slop': 2 |
|
||||||
} |
} |
||||||
|
|
||||||
|
// or this..
|
||||||
|
else { |
||||||
|
console.warn( 'chaos monkey asks: what happens now?' ); |
||||||
|
console.log( clean ); |
||||||
|
try{ throw new Error(); } catch(e){ console.error( e.stack ); } // print a stack trace
|
||||||
} |
} |
||||||
}); |
|
||||||
|
|
||||||
query.sort = query.sort.concat( sort( params ) ); |
// ==== add parsed matches [address components] ====
|
||||||
|
|
||||||
return query; |
// house number
|
||||||
|
if( clean.parsed_input.hasOwnProperty('number') ){ |
||||||
|
vs.var( 'input:housenumber', clean.parsed_input.number ); |
||||||
} |
} |
||||||
|
|
||||||
/** |
// street name
|
||||||
* Traverse the parsed input object, containing all the address parts detected in query string. |
if( clean.parsed_input.hasOwnProperty('street') ){ |
||||||
* Add matches to query for each identifiable component. |
vs.var( 'input:street', clean.parsed_input.street ); |
||||||
* |
} |
||||||
* @param {Object} query |
|
||||||
* @param {string} defaultInput |
|
||||||
* @param {Object} parsedInput |
|
||||||
*/ |
|
||||||
function addParsedMatch(query, defaultInput, parsedInput) { |
|
||||||
query.query.filtered.query.bool.should = query.query.filtered.query.bool.should || []; |
|
||||||
|
|
||||||
// copy expected admin fields so we can remove them as we parse the address
|
// postal code
|
||||||
var unmatchedAdminFields = adminFields.slice(); |
if( clean.parsed_input.hasOwnProperty('postalcode') ){ |
||||||
|
vs.var( 'input:postcode', clean.parsed_input.postalcode ); |
||||||
|
} |
||||||
|
|
||||||
// address
|
// ==== add parsed matches [admin components] ====
|
||||||
// number, street, postalcode
|
|
||||||
addMatch(query, unmatchedAdminFields, 'address.number', parsedInput.number, addressWeights.number); |
|
||||||
addMatch(query, unmatchedAdminFields, 'address.street', parsedInput.street, addressWeights.street); |
|
||||||
addMatch(query, unmatchedAdminFields, 'address.zip', parsedInput.postalcode, addressWeights.zip); |
|
||||||
|
|
||||||
// city
|
// city
|
||||||
// admin2, locality, local_admin, neighborhood
|
if( clean.parsed_input.hasOwnProperty('city') ){ |
||||||
addMatch(query, unmatchedAdminFields, 'admin2', parsedInput.city, addressWeights.admin2); |
vs.var( 'input:admin2', clean.parsed_input.city ); |
||||||
|
} |
||||||
|
|
||||||
// state
|
// state
|
||||||
// admin1, admin1_abbr
|
if( clean.parsed_input.hasOwnProperty('state') ){ |
||||||
addMatch(query, unmatchedAdminFields, 'admin1_abbr', parsedInput.state, addressWeights.admin1_abbr); |
vs.var( 'input:admin1_abbr', clean.parsed_input.state ); |
||||||
|
} |
||||||
|
|
||||||
// country
|
// country
|
||||||
// admin0, alpha3
|
if( clean.parsed_input.hasOwnProperty('country') ){ |
||||||
addMatch(query, unmatchedAdminFields, 'alpha3', parsedInput.country, addressWeights.alpha3); |
vs.var( 'input:alpha3', clean.parsed_input.country ); |
||||||
|
|
||||||
addUnmatchedAdminFieldsToQuery(query, unmatchedAdminFields, parsedInput, defaultInput); |
|
||||||
} |
} |
||||||
|
|
||||||
/** |
// ==== deal with the 'leftover' components ====
|
||||||
* Check for additional admin fields in the parsed input, and if any was found |
// @todo: clean up this code
|
||||||
* combine into single string and match against all unmatched admin fields. |
|
||||||
* |
|
||||||
* @param {Object} query |
|
||||||
* @param {Array} unmatchedAdminFields |
|
||||||
* @param {Object} parsedInput |
|
||||||
* @param {string} defaultInput |
|
||||||
*/ |
|
||||||
function addUnmatchedAdminFieldsToQuery(query, unmatchedAdminFields, parsedInput, defaultInput) { |
|
||||||
if (unmatchedAdminFields.length === 0 ) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
|
// a concept called 'leftovers' which is just 'admin_parts' plus 'regions'.
|
||||||
var leftovers = []; |
var leftovers = []; |
||||||
|
if( clean.parsed_input.hasOwnProperty('admin_parts') ){ |
||||||
if (parsedInput.admin_parts) { |
leftovers.push( clean.parsed_input.admin_parts ); |
||||||
leftovers.push(parsedInput.admin_parts); |
|
||||||
} |
} |
||||||
else if (parsedInput.regions) { |
else if( clean.parsed_input.hasOwnProperty('regions') ){ |
||||||
leftovers.push(parsedInput.regions); |
leftovers.push( clean.parsed_input.regions ); |
||||||
} |
} |
||||||
|
|
||||||
if (leftovers.length === 0) { |
// if we have 'leftovers' then assign them to any fields which
|
||||||
return; |
// currently don't have a value assigned.
|
||||||
} |
if( leftovers.length ){ |
||||||
|
var leftoversString = leftovers.join(' '); |
||||||
leftovers = leftovers.join(' '); |
var unmatchedAdminFields = adminFields.slice(); |
||||||
|
|
||||||
// if there are additional regions/admin_parts found
|
// cycle through fields and set fields which
|
||||||
if (leftovers !== defaultInput) { |
// are still currently unset
|
||||||
unmatchedAdminFields.forEach( function( key ){ |
unmatchedAdminFields.forEach( function( key ){ |
||||||
// combine all the leftover parts into one string
|
if( !vs.isset( 'input:' + key ) ){ |
||||||
addMatch(query, [], key, leftovers); |
vs.var( 'input:' + key, leftoversString ); |
||||||
}); |
|
||||||
} |
|
||||||
} |
} |
||||||
|
}); |
||||||
/** |
|
||||||
* Add key:value match to query. Apply boost if specified. |
|
||||||
* |
|
||||||
* @param {Object} query |
|
||||||
* @param {Array} unmatched |
|
||||||
* @param {string} key |
|
||||||
* @param {string|number|undefined} value |
|
||||||
* @param {number|undefined} [boost] optional |
|
||||||
*/ |
|
||||||
function addMatch(query, unmatched, key, value, boost) { // jshint ignore:line
|
|
||||||
if (typeof value === 'undefined') { |
|
||||||
return; |
|
||||||
} |
} |
||||||
|
|
||||||
var match = {}; |
|
||||||
|
|
||||||
if (boost) { |
|
||||||
match[key] = { |
|
||||||
query: value, |
|
||||||
boost: boost |
|
||||||
}; |
|
||||||
} |
} |
||||||
else { |
|
||||||
match[key] = value; |
|
||||||
} |
|
||||||
|
|
||||||
query.query.filtered.query.bool.should.push({ 'match': match }); |
|
||||||
|
|
||||||
removeFromUnmatched(unmatched, key); |
var result = query.render( vs ); |
||||||
} |
result.sort = result.sort.concat( sort( clean ) ); |
||||||
|
|
||||||
/** |
// @todo: remove this hack
|
||||||
* If key is found in unmatched list, remove it from the array |
return JSON.parse( JSON.stringify( result ) ); |
||||||
* |
|
||||||
* @param {Array} unmatched |
|
||||||
* @param {string} key |
|
||||||
*/ |
|
||||||
function removeFromUnmatched(unmatched, key) { |
|
||||||
var index = unmatched.indexOf(key); |
|
||||||
if (index !== -1) { |
|
||||||
unmatched.splice(index, 1); |
|
||||||
} |
|
||||||
} |
} |
||||||
|
|
||||||
module.exports = generate; |
module.exports = generate; |
||||||
|
Loading…
Reference in new issue