mirror of https://github.com/pelias/api.git
Julian Simioni
6 years ago
committed by
GitHub
16 changed files with 655 additions and 13 deletions
@ -0,0 +1,121 @@ |
|||||||
|
/** |
||||||
|
This view allows users to specify a custom boost for sources and layers. |
||||||
|
|
||||||
|
The view is implemented using a 'function_score' query, which enumerates multiple 'functions', each |
||||||
|
function will assign a 'score' to each document when matched. |
||||||
|
|
||||||
|
A document can match more than one function, in this case the 'score_mode' is used to decide how these |
||||||
|
scores are combined, the default is 'sum'. |
||||||
|
|
||||||
|
Likewise, a document can also match zero functions, in this case it is assigned a score of 'min_score'. |
||||||
|
|
||||||
|
The computed score is then multiplied by the 'boost' value in order to come up with the final boost value |
||||||
|
which will be assigned to that document. The 'boost' value is essentially a hard-coded multiplier for the score. |
||||||
|
|
||||||
|
The 'max_boost' property is simply a ceiling for this computed boost, if the computed boosted is higher than |
||||||
|
max_boost it will be assigned the value of max_boost instead. |
||||||
|
|
||||||
|
Note: This is a simple use of the 'function_score' query, as such we don't use the 'boost_mode' property |
||||||
|
(because there is no query section) and the 'weight' values we assign are simply returned verbatim |
||||||
|
(because we use filter queries for the function scoring). |
||||||
|
|
||||||
|
ref: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html
|
||||||
|
|
||||||
|
example config section: |
||||||
|
{ |
||||||
|
"source": { |
||||||
|
"openstreetmap": 5 |
||||||
|
}, |
||||||
|
"layer": { |
||||||
|
"street": 3, |
||||||
|
"country": 5 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
example query: |
||||||
|
{ |
||||||
|
"function_score": { |
||||||
|
"query": { |
||||||
|
"match_all": {} |
||||||
|
}, |
||||||
|
"functions": [{ |
||||||
|
"filter": { |
||||||
|
"match": { |
||||||
|
"layer": "intersections" |
||||||
|
} |
||||||
|
}, |
||||||
|
"weight": 1.6 |
||||||
|
},{ |
||||||
|
"filter": { |
||||||
|
"match": { |
||||||
|
"layer": "stops" |
||||||
|
} |
||||||
|
}, |
||||||
|
"weight": 2.4 |
||||||
|
}], |
||||||
|
"boost": 5, |
||||||
|
"max_boost": 40, |
||||||
|
"score_mode": "sum", |
||||||
|
"boost_mode": "multiply", |
||||||
|
"min_score": 1 |
||||||
|
} |
||||||
|
} |
||||||
|
**/ |
||||||
|
|
||||||
|
// supported top-level config items
|
||||||
|
const TARGETS = ['source', 'layer']; |
||||||
|
|
||||||
|
module.exports = function( config ) { |
||||||
|
|
||||||
|
// no valid config to use, fail now, don't render this view.
|
||||||
|
if( !config ) { return function(){ return null; }; } |
||||||
|
|
||||||
|
return function( vs ) { |
||||||
|
|
||||||
|
// validate required params
|
||||||
|
if( !vs.isset('custom:boosting:min_score') || |
||||||
|
!vs.isset('custom:boosting:boost') || |
||||||
|
!vs.isset('custom:boosting:max_boost') || |
||||||
|
!vs.isset('custom:boosting:score_mode') || |
||||||
|
!vs.isset('custom:boosting:boost_mode') ){ |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
// base 'function_score' view
|
||||||
|
var view = { |
||||||
|
'function_score': { |
||||||
|
'query': { 'match_all': {} }, // apply to all documents
|
||||||
|
'functions': [], // a list of functions which contribute to a 'score' for each document
|
||||||
|
'min_score': vs.var('custom:boosting:min_score'), |
||||||
|
'boost': vs.var('custom:boosting:boost'), |
||||||
|
'max_boost': vs.var('custom:boosting:max_boost'), |
||||||
|
'score_mode': vs.var('custom:boosting:score_mode'), |
||||||
|
'boost_mode': vs.var('custom:boosting:boost_mode') |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
// iterate over supported targets and their values
|
||||||
|
TARGETS.forEach( function( target ) { |
||||||
|
if( 'object' === typeof config[target] ) { |
||||||
|
Object.keys(config[target]).forEach(function(value) { |
||||||
|
|
||||||
|
// add a scoring function for this target, assigning a weight
|
||||||
|
let weight = config[target][value]; |
||||||
|
view.function_score.functions.push({ |
||||||
|
'weight': isNaN(weight) ? 1 : weight, |
||||||
|
'filter': { |
||||||
|
'match': { |
||||||
|
[target]: value |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// no functions were generated, fail now, don't render this view.
|
||||||
|
if( view.function_score.functions.length === 0 ) { return null; } |
||||||
|
|
||||||
|
return view; |
||||||
|
}; |
||||||
|
}; |
@ -0,0 +1,106 @@ |
|||||||
|
{ |
||||||
|
"type": "autocomplete", |
||||||
|
"body": { |
||||||
|
"query": { |
||||||
|
"bool": { |
||||||
|
"must": [ |
||||||
|
{ |
||||||
|
"match": { |
||||||
|
"name.default": { |
||||||
|
"analyzer": "peliasQueryFullToken", |
||||||
|
"type": "phrase", |
||||||
|
"boost": 1, |
||||||
|
"slop": 3, |
||||||
|
"query": "foo" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
], |
||||||
|
"should": [ |
||||||
|
{ |
||||||
|
"match": { |
||||||
|
"phrase.default": { |
||||||
|
"analyzer": "peliasPhrase", |
||||||
|
"type": "phrase", |
||||||
|
"boost": 1, |
||||||
|
"slop": 3, |
||||||
|
"query": "foo" |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"function_score": { |
||||||
|
"query": { |
||||||
|
"match_all": {} |
||||||
|
}, |
||||||
|
"max_boost": 20, |
||||||
|
"functions": [ |
||||||
|
{ |
||||||
|
"field_value_factor": { |
||||||
|
"modifier": "log1p", |
||||||
|
"field": "popularity", |
||||||
|
"missing": 1 |
||||||
|
}, |
||||||
|
"weight": 1 |
||||||
|
} |
||||||
|
], |
||||||
|
"score_mode": "first", |
||||||
|
"boost_mode": "replace" |
||||||
|
} |
||||||
|
}, |
||||||
|
{ |
||||||
|
"function_score": { |
||||||
|
"query": { |
||||||
|
"match_all": {} |
||||||
|
}, |
||||||
|
"max_boost": 20, |
||||||
|
"functions": [ |
||||||
|
{ |
||||||
|
"field_value_factor": { |
||||||
|
"modifier": "log1p", |
||||||
|
"field": "population", |
||||||
|
"missing": 1 |
||||||
|
}, |
||||||
|
"weight": 3 |
||||||
|
} |
||||||
|
], |
||||||
|
"score_mode": "first", |
||||||
|
"boost_mode": "replace" |
||||||
|
} |
||||||
|
},{ |
||||||
|
"function_score": { |
||||||
|
"query": { |
||||||
|
"match_all": {} |
||||||
|
}, |
||||||
|
"min_score": 1, |
||||||
|
"boost": 5, |
||||||
|
"max_boost": 50, |
||||||
|
"score_mode": "sum", |
||||||
|
"boost_mode": "multiply", |
||||||
|
"functions": [{ |
||||||
|
"filter": { |
||||||
|
"match": { |
||||||
|
"source": "openstreetmap" |
||||||
|
} |
||||||
|
}, |
||||||
|
"weight": 5 |
||||||
|
},{ |
||||||
|
"filter": { |
||||||
|
"match": { |
||||||
|
"layer": "transit" |
||||||
|
} |
||||||
|
}, |
||||||
|
"weight": 3 |
||||||
|
}] |
||||||
|
} |
||||||
|
} |
||||||
|
] |
||||||
|
} |
||||||
|
}, |
||||||
|
"size": 20, |
||||||
|
"track_scores": true, |
||||||
|
"sort": [ |
||||||
|
"_score" |
||||||
|
] |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,108 @@ |
|||||||
|
{ |
||||||
|
"type": "search_original", |
||||||
|
"body": { |
||||||
|
"query": { |
||||||
|
"bool": { |
||||||
|
"must": [{ |
||||||
|
"match": { |
||||||
|
"name.default": { |
||||||
|
"query": "test", |
||||||
|
"boost": 1, |
||||||
|
"analyzer": "peliasQueryFullToken" |
||||||
|
} |
||||||
|
} |
||||||
|
}], |
||||||
|
"should": [{ |
||||||
|
"match": { |
||||||
|
"phrase.default": { |
||||||
|
"query": "test", |
||||||
|
"analyzer": "peliasPhrase", |
||||||
|
"type": "phrase", |
||||||
|
"boost": 1, |
||||||
|
"slop": 2 |
||||||
|
} |
||||||
|
} |
||||||
|
},{ |
||||||
|
"function_score": { |
||||||
|
"query": { |
||||||
|
"match": { |
||||||
|
"phrase.default": { |
||||||
|
"query": "test", |
||||||
|
"analyzer": "peliasPhrase", |
||||||
|
"type": "phrase", |
||||||
|
"slop": 2, |
||||||
|
"boost": 1 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
"max_boost": 20, |
||||||
|
"score_mode": "first", |
||||||
|
"boost_mode": "replace", |
||||||
|
"functions": [{ |
||||||
|
"field_value_factor": { |
||||||
|
"modifier": "log1p", |
||||||
|
"field": "popularity", |
||||||
|
"missing": 1 |
||||||
|
}, |
||||||
|
"weight": 1 |
||||||
|
}] |
||||||
|
} |
||||||
|
},{ |
||||||
|
"function_score": { |
||||||
|
"query": { |
||||||
|
"match": { |
||||||
|
"phrase.default": { |
||||||
|
"query": "test", |
||||||
|
"analyzer": "peliasPhrase", |
||||||
|
"type": "phrase", |
||||||
|
"slop": 2, |
||||||
|
"boost": 1 |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
"max_boost": 20, |
||||||
|
"score_mode": "first", |
||||||
|
"boost_mode": "replace", |
||||||
|
"functions": [{ |
||||||
|
"field_value_factor": { |
||||||
|
"modifier": "log1p", |
||||||
|
"field": "population", |
||||||
|
"missing": 1 |
||||||
|
}, |
||||||
|
"weight": 2 |
||||||
|
}] |
||||||
|
} |
||||||
|
},{ |
||||||
|
"function_score": { |
||||||
|
"query": { |
||||||
|
"match_all": {} |
||||||
|
}, |
||||||
|
"min_score": 1, |
||||||
|
"boost": 5, |
||||||
|
"max_boost": 50, |
||||||
|
"score_mode": "sum", |
||||||
|
"boost_mode": "multiply", |
||||||
|
"functions": [{ |
||||||
|
"filter": { |
||||||
|
"match": { |
||||||
|
"source": "openstreetmap" |
||||||
|
} |
||||||
|
}, |
||||||
|
"weight": 5 |
||||||
|
},{ |
||||||
|
"filter": { |
||||||
|
"match": { |
||||||
|
"layer": "transit" |
||||||
|
} |
||||||
|
}, |
||||||
|
"weight": 3 |
||||||
|
}] |
||||||
|
} |
||||||
|
}] |
||||||
|
} |
||||||
|
}, |
||||||
|
"sort": [ "_score" ], |
||||||
|
"size": 10, |
||||||
|
"track_scores": true |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,54 @@ |
|||||||
|
const proxyquire = require('proxyquire'); |
||||||
|
|
||||||
|
module.exports.tests = {}; |
||||||
|
|
||||||
|
module.exports.tests.query = function(test, common) { |
||||||
|
test('valid autocomplete with custom boosts', function(t) { |
||||||
|
const clean = { |
||||||
|
tokens: ['foo'], |
||||||
|
tokens_complete: ['foo'], |
||||||
|
tokens_incomplete: [], |
||||||
|
text: 'test', |
||||||
|
querySize: 10 |
||||||
|
}; |
||||||
|
|
||||||
|
const config_with_boosts = { |
||||||
|
generate: function() { |
||||||
|
return { |
||||||
|
api: { |
||||||
|
customBoosts: { |
||||||
|
source: { |
||||||
|
openstreetmap: 5 |
||||||
|
}, |
||||||
|
layer: { |
||||||
|
transit: 3 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
var expected_query = require('../fixture/autocomplete_custom_boosts.json'); |
||||||
|
|
||||||
|
const autocomplete_query_module = proxyquire('../../../query/autocomplete', { |
||||||
|
'pelias-config': config_with_boosts |
||||||
|
}); |
||||||
|
|
||||||
|
const actual_query = JSON.parse( JSON.stringify( autocomplete_query_module(clean) ) ); |
||||||
|
|
||||||
|
t.deepEqual(actual_query, expected_query, 'query as expected'); |
||||||
|
t.pass(); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.all = function (tape, common) { |
||||||
|
function test(name, testFunction) { |
||||||
|
return tape('autocomplete with custom boosts query ' + name, testFunction); |
||||||
|
} |
||||||
|
|
||||||
|
for( var testCase in module.exports.tests ){ |
||||||
|
module.exports.tests[testCase](test, common); |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,53 @@ |
|||||||
|
const proxyquire = require('proxyquire'); |
||||||
|
|
||||||
|
module.exports.tests = {}; |
||||||
|
|
||||||
|
module.exports.tests.query = function(test, common) { |
||||||
|
test('valid search with custom boosts', function(t) { |
||||||
|
const clean = { |
||||||
|
tokens: ['foo'], |
||||||
|
tokens_complete: ['foo'], |
||||||
|
tokens_incomplete: [], |
||||||
|
text: 'test', |
||||||
|
querySize: 10 |
||||||
|
}; |
||||||
|
|
||||||
|
const config_with_boosts = { |
||||||
|
generate: function() { |
||||||
|
return { |
||||||
|
api: { |
||||||
|
customBoosts: { |
||||||
|
source: { |
||||||
|
openstreetmap: 5 |
||||||
|
}, |
||||||
|
layer: { |
||||||
|
transit: 3 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
var expected_query = require('../fixture/search_with_custom_boosts.json'); |
||||||
|
|
||||||
|
const search_query_module = proxyquire('../../../query/search_original', { |
||||||
|
'pelias-config': config_with_boosts |
||||||
|
}); |
||||||
|
|
||||||
|
const actual_query = JSON.parse( JSON.stringify( search_query_module(clean) ) ); |
||||||
|
t.deepEqual(actual_query, expected_query, 'query as expected'); |
||||||
|
t.pass(); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.all = function (tape, common) { |
||||||
|
function test(name, testFunction) { |
||||||
|
return tape('search with custom boosts query ' + name, testFunction); |
||||||
|
} |
||||||
|
|
||||||
|
for( var testCase in module.exports.tests ){ |
||||||
|
module.exports.tests[testCase](test, common); |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,119 @@ |
|||||||
|
const query = require('pelias-query'); |
||||||
|
const vs = new query.Vars(require('../../../../query/search_defaults')); |
||||||
|
const boost_sources_and_layers = require('../../../../query/view/boost_sources_and_layers'); |
||||||
|
|
||||||
|
module.exports.tests = {}; |
||||||
|
|
||||||
|
module.exports.tests.empty_config = function(test, common) { |
||||||
|
test('empty configuration returns empty query', function(t) { |
||||||
|
const view = boost_sources_and_layers({}); |
||||||
|
const rendered = view(vs); |
||||||
|
t.equal(rendered, null, 'query is empty'); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
|
||||||
|
test('undefined configuration returns empty query', function(t) { |
||||||
|
const view = boost_sources_and_layers(undefined); |
||||||
|
const rendered = view(vs); |
||||||
|
t.equal(rendered, null, 'query is empty'); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.tests.single_item_config = function(test, common) { |
||||||
|
test('config with single layer entry produces a single scoring function with weight', function(t) { |
||||||
|
const config = { |
||||||
|
layer: { |
||||||
|
locality: 5 |
||||||
|
} |
||||||
|
}; |
||||||
|
const expected_query = { |
||||||
|
'function_score': { |
||||||
|
'query': { |
||||||
|
'match_all': {} |
||||||
|
}, |
||||||
|
'functions': [{ |
||||||
|
'filter': { |
||||||
|
'match': { |
||||||
|
'layer': 'locality' |
||||||
|
} |
||||||
|
}, |
||||||
|
'weight': 5 |
||||||
|
}], |
||||||
|
'boost': vs.var('custom:boosting:boost'), |
||||||
|
'max_boost': vs.var('custom:boosting:max_boost'), |
||||||
|
'score_mode': vs.var('custom:boosting:score_mode'), |
||||||
|
'boost_mode': vs.var('custom:boosting:boost_mode'), |
||||||
|
'min_score': vs.var('custom:boosting:min_score') |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const view = boost_sources_and_layers(config); |
||||||
|
|
||||||
|
t.deepEquals(view(vs), expected_query, 'query contains a single scoring function'); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.tests.mulitple_item_config = function(test, common) { |
||||||
|
test('config with multiple items produces multiple scoring functions', function(t) { |
||||||
|
const config = { |
||||||
|
source: { |
||||||
|
whosonfirst: 6 |
||||||
|
}, |
||||||
|
layer: { |
||||||
|
country: 2, |
||||||
|
borough: 0.5 |
||||||
|
}, |
||||||
|
}; |
||||||
|
const expected_query = { |
||||||
|
'function_score': { |
||||||
|
'query': { |
||||||
|
'match_all': {} |
||||||
|
}, |
||||||
|
'functions': [{ |
||||||
|
'filter': { |
||||||
|
'match': { |
||||||
|
'source': 'whosonfirst' |
||||||
|
} |
||||||
|
}, |
||||||
|
'weight': 6 |
||||||
|
},{ |
||||||
|
'filter': { |
||||||
|
'match': { |
||||||
|
'layer': 'country' |
||||||
|
} |
||||||
|
}, |
||||||
|
'weight': 2 |
||||||
|
},{ |
||||||
|
'filter': { |
||||||
|
'match': { |
||||||
|
'layer': 'borough' |
||||||
|
} |
||||||
|
}, |
||||||
|
'weight': 0.5 |
||||||
|
}], |
||||||
|
'boost': vs.var('custom:boosting:boost'), |
||||||
|
'max_boost': vs.var('custom:boosting:max_boost'), |
||||||
|
'score_mode': vs.var('custom:boosting:score_mode'), |
||||||
|
'boost_mode': vs.var('custom:boosting:boost_mode'), |
||||||
|
'min_score': vs.var('custom:boosting:min_score') |
||||||
|
} |
||||||
|
}; |
||||||
|
const view = boost_sources_and_layers(config); |
||||||
|
|
||||||
|
t.deepEquals(view(vs), expected_query, 'query contains multiple scoring functions'); |
||||||
|
t.end(); |
||||||
|
|
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.all = function (tape, common) { |
||||||
|
function test(name, testFunction) { |
||||||
|
return tape('boost sources and layers ' + name, testFunction); |
||||||
|
} |
||||||
|
|
||||||
|
for( var testCase in module.exports.tests ){ |
||||||
|
module.exports.tests[testCase](test, common); |
||||||
|
} |
||||||
|
}; |
Loading…
Reference in new issue