mirror of https://github.com/pelias/api.git
Browse Source
Dedupe middleware removes __exact__ dupes and truncates the results to the specified size.pull/376/head
Diana Shkolnikov
9 years ago
26 changed files with 479 additions and 24 deletions
@ -0,0 +1,21 @@ |
|||||||
|
/** |
||||||
|
* Utility for calculating query result size |
||||||
|
* incorporating padding for dedupe process |
||||||
|
*/ |
||||||
|
|
||||||
|
var SIZE_PADDING = 2; |
||||||
|
|
||||||
|
/** |
||||||
|
* Add padding or set to 1 |
||||||
|
* |
||||||
|
* @param {number} cleanSize |
||||||
|
* @returns {number} |
||||||
|
*/ |
||||||
|
module.exports = function calculateSize(cleanSize) { |
||||||
|
switch (cleanSize || 1) { |
||||||
|
case 1: |
||||||
|
return 1; |
||||||
|
default: |
||||||
|
return cleanSize * SIZE_PADDING; |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,82 @@ |
|||||||
|
var util = require('util'); |
||||||
|
var logger = require('pelias-logger').get('api:middle:dedupe'); |
||||||
|
var _ = require('lodash'); |
||||||
|
|
||||||
|
function setup() { |
||||||
|
return dedupeResults; |
||||||
|
} |
||||||
|
|
||||||
|
function dedupeResults(req, res, next) { |
||||||
|
// do nothing if no result data set
|
||||||
|
if (_.isUndefined(req.clean) || _.isUndefined(res) || _.isUndefined(res.data)) { |
||||||
|
return next(); |
||||||
|
} |
||||||
|
|
||||||
|
// loop through data items and only copy unique items to uniqueResults
|
||||||
|
var uniqueResults = []; |
||||||
|
|
||||||
|
_.some(res.data, function (hit) { |
||||||
|
if (uniqueResults.length === 0 || _.every(uniqueResults, isDifferent.bind(null, hit)) ) { |
||||||
|
uniqueResults.push(hit); |
||||||
|
} |
||||||
|
else { |
||||||
|
logger.info('[dupe]', { query: req.clean.text, hit: hit.name.default }); |
||||||
|
} |
||||||
|
|
||||||
|
// stop looping when requested size has been reached in uniqueResults
|
||||||
|
return req.clean.size <= uniqueResults.length; |
||||||
|
}); |
||||||
|
|
||||||
|
res.data = uniqueResults; |
||||||
|
|
||||||
|
next(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param {object} item1 |
||||||
|
* @param {object} item2 |
||||||
|
* @returns {boolean} |
||||||
|
*/ |
||||||
|
function isDifferent(item1, item2) { |
||||||
|
try { |
||||||
|
propMatch(item1, item2, 'admin1_abbr'); |
||||||
|
propMatch(item1, item2, 'alpha3'); |
||||||
|
|
||||||
|
if (item1.hasOwnProperty('name') && item2.hasOwnProperty('name')) { |
||||||
|
propMatch(item1.name, item2.name, 'default'); |
||||||
|
} |
||||||
|
else if (item1.name !== item2.name) { |
||||||
|
throw 'different'; |
||||||
|
} |
||||||
|
|
||||||
|
if (item1.hasOwnProperty('address') && item2.hasOwnProperty('address')) { |
||||||
|
propMatch(item1.address, item2.address, 'number'); |
||||||
|
propMatch(item1.address, item2.address, 'street'); |
||||||
|
propMatch(item1.address, item2.address, 'zip'); |
||||||
|
} |
||||||
|
else if (item1.address !== item2.address) { |
||||||
|
throw 'different'; |
||||||
|
} |
||||||
|
} |
||||||
|
catch (err) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Throw exception if properties are different |
||||||
|
* |
||||||
|
* @param item1 |
||||||
|
* @param item2 |
||||||
|
* @param prop |
||||||
|
*/ |
||||||
|
function propMatch(item1, item2, prop) { |
||||||
|
if (item1[prop] !== item2[prop]) { |
||||||
|
throw 'different'; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
module.exports = setup; |
@ -0,0 +1,248 @@ |
|||||||
|
module.exports = [ |
||||||
|
{ |
||||||
|
'center_point': { |
||||||
|
'lon': -76.207456, |
||||||
|
'lat': 40.039265 |
||||||
|
}, |
||||||
|
'address': {}, |
||||||
|
'local_admin': 'East Lampeter', |
||||||
|
'admin1_abbr': 'PA', |
||||||
|
'name': { |
||||||
|
'default': 'East Lampeter High School' |
||||||
|
}, |
||||||
|
'admin1': 'Pennsylvania', |
||||||
|
'locality': 'Smoketown', |
||||||
|
'alpha3': 'USA', |
||||||
|
'admin2': 'Lancaster County', |
||||||
|
'admin0': 'United States', |
||||||
|
'neighborhood': 'Greenland', |
||||||
|
'category': [ |
||||||
|
'education' |
||||||
|
], |
||||||
|
'_id': '357321757', |
||||||
|
'_type': 'osmnode', |
||||||
|
'_score': 1.2367082, |
||||||
|
'confidence': 0.879 |
||||||
|
}, |
||||||
|
{ |
||||||
|
'center_point': { |
||||||
|
'lon': -76.23246, |
||||||
|
'lat': 39.99288 |
||||||
|
}, |
||||||
|
'address': {}, |
||||||
|
'local_admin': 'West Lampeter', |
||||||
|
'admin1_abbr': 'PA', |
||||||
|
'name': { |
||||||
|
'default': 'Lampeter-Strasburg High School' |
||||||
|
}, |
||||||
|
'admin1': 'Pennsylvania', |
||||||
|
'locality': 'Lampeter', |
||||||
|
'alpha3': 'USA', |
||||||
|
'admin2': 'Lancaster County', |
||||||
|
'admin0': 'United States', |
||||||
|
'neighborhood': 'Wheatland Mills', |
||||||
|
'category': [ |
||||||
|
'education' |
||||||
|
], |
||||||
|
'_id': '4559068', |
||||||
|
'_type': 'geoname', |
||||||
|
'_score': 1.2367082, |
||||||
|
'confidence': 0.879 |
||||||
|
}, |
||||||
|
{ |
||||||
|
'center_point': { |
||||||
|
'lon': -76.20746, |
||||||
|
'lat': 40.03927 |
||||||
|
}, |
||||||
|
'address': {}, |
||||||
|
'local_admin': 'East Lampeter', |
||||||
|
'admin1_abbr': 'PA', |
||||||
|
'name': { |
||||||
|
'default': 'East Lampeter High School' |
||||||
|
}, |
||||||
|
'admin1': 'Pennsylvania', |
||||||
|
'locality': 'Smoketown', |
||||||
|
'alpha3': 'USA', |
||||||
|
'admin2': 'Lancaster County', |
||||||
|
'admin0': 'United States', |
||||||
|
'neighborhood': 'Greenland', |
||||||
|
'category': [ |
||||||
|
'education' |
||||||
|
], |
||||||
|
'_id': '5187980', |
||||||
|
'_type': 'geoname', |
||||||
|
'_score': 1.2367082, |
||||||
|
'confidence': 0.879 |
||||||
|
}, |
||||||
|
{ |
||||||
|
'center_point': { |
||||||
|
'lon': -76.232457, |
||||||
|
'lat': 39.992877 |
||||||
|
}, |
||||||
|
'address': {}, |
||||||
|
'local_admin': 'West Lampeter', |
||||||
|
'admin1_abbr': 'PA', |
||||||
|
'name': { |
||||||
|
'default': 'Lampeter-Strasburg High School' |
||||||
|
}, |
||||||
|
'admin1': 'Pennsylvania', |
||||||
|
'locality': 'Lampeter', |
||||||
|
'alpha3': 'USA', |
||||||
|
'admin2': 'Lancaster County', |
||||||
|
'admin0': 'United States', |
||||||
|
'neighborhood': 'Wheatland Mills', |
||||||
|
'category': [ |
||||||
|
'education' |
||||||
|
], |
||||||
|
'_id': '357294404', |
||||||
|
'_type': 'osmnode', |
||||||
|
'_score': 1.2367082, |
||||||
|
'confidence': 0.879 |
||||||
|
}, |
||||||
|
{ |
||||||
|
'center_point': { |
||||||
|
'lon': -76.207456, |
||||||
|
'lat': 40.038987 |
||||||
|
}, |
||||||
|
'address': {}, |
||||||
|
'local_admin': 'East Lampeter', |
||||||
|
'admin1_abbr': 'PA', |
||||||
|
'name': { |
||||||
|
'default': 'East Lampeter School' |
||||||
|
}, |
||||||
|
'admin1': 'Pennsylvania', |
||||||
|
'locality': 'Smoketown', |
||||||
|
'alpha3': 'USA', |
||||||
|
'admin2': 'Lancaster County', |
||||||
|
'admin0': 'United States', |
||||||
|
'neighborhood': 'Greenland', |
||||||
|
'category': [ |
||||||
|
'education' |
||||||
|
], |
||||||
|
'_id': '357283977', |
||||||
|
'_type': 'osmnode', |
||||||
|
'_score': 1.1036991, |
||||||
|
'confidence': 0.664 |
||||||
|
}, |
||||||
|
{ |
||||||
|
'center_point': { |
||||||
|
'lon': -76.20746, |
||||||
|
'lat': 40.03899 |
||||||
|
}, |
||||||
|
'address': {}, |
||||||
|
'local_admin': 'East Lampeter', |
||||||
|
'admin1_abbr': 'PA', |
||||||
|
'name': { |
||||||
|
'default': 'East Lampeter School' |
||||||
|
}, |
||||||
|
'admin1': 'Pennsylvania', |
||||||
|
'locality': 'Smoketown', |
||||||
|
'alpha3': 'USA', |
||||||
|
'admin2': 'Lancaster County', |
||||||
|
'admin0': 'United States', |
||||||
|
'neighborhood': 'Greenland', |
||||||
|
'category': [ |
||||||
|
'education' |
||||||
|
], |
||||||
|
'_id': '5187966', |
||||||
|
'_type': 'geoname', |
||||||
|
'_score': 1.1036991, |
||||||
|
'confidence': 0.664 |
||||||
|
}, |
||||||
|
{ |
||||||
|
'center_point': { |
||||||
|
'lon': -94.167445, |
||||||
|
'lat': 38.762788 |
||||||
|
}, |
||||||
|
'address': {}, |
||||||
|
'local_admin': 'Polk', |
||||||
|
'admin1_abbr': 'MO', |
||||||
|
'name': { |
||||||
|
'default': 'Strasburg School' |
||||||
|
}, |
||||||
|
'admin1': 'Missouri', |
||||||
|
'locality': 'Strasburg', |
||||||
|
'alpha3': 'USA', |
||||||
|
'admin2': 'Cass County', |
||||||
|
'admin0': 'United States', |
||||||
|
'category': [ |
||||||
|
'education' |
||||||
|
], |
||||||
|
'_id': '358058986', |
||||||
|
'_type': 'osmnode', |
||||||
|
'_score': 1.0492544, |
||||||
|
'confidence': 0.658 |
||||||
|
}, |
||||||
|
{ |
||||||
|
'center_point': { |
||||||
|
'lon': -78.36317, |
||||||
|
'lat': 38.98445 |
||||||
|
}, |
||||||
|
'address': {}, |
||||||
|
'admin1_abbr': 'VA', |
||||||
|
'name': { |
||||||
|
'default': 'Strasburg High School' |
||||||
|
}, |
||||||
|
'admin1': 'Virginia', |
||||||
|
'locality': 'Strasburg', |
||||||
|
'alpha3': 'USA', |
||||||
|
'admin2': 'Shenandoah County', |
||||||
|
'admin0': 'United States', |
||||||
|
'neighborhood': 'Strasburg Junction', |
||||||
|
'category': [ |
||||||
|
'education' |
||||||
|
], |
||||||
|
'_id': '4787978', |
||||||
|
'_type': 'geoname', |
||||||
|
'_score': 0.9724125, |
||||||
|
'confidence': 0.649 |
||||||
|
}, |
||||||
|
{ |
||||||
|
'center_point': { |
||||||
|
'lon': -100.16516, |
||||||
|
'lat': 46.13427 |
||||||
|
}, |
||||||
|
'address': {}, |
||||||
|
'local_admin': 'Strasburg', |
||||||
|
'admin1_abbr': 'ND', |
||||||
|
'name': { |
||||||
|
'default': 'Strasburg High School' |
||||||
|
}, |
||||||
|
'admin1': 'North Dakota', |
||||||
|
'locality': 'Strasburg', |
||||||
|
'alpha3': 'USA', |
||||||
|
'admin2': 'Emmons County', |
||||||
|
'admin0': 'United States', |
||||||
|
'category': [ |
||||||
|
'education' |
||||||
|
], |
||||||
|
'_id': '9683163', |
||||||
|
'_type': 'geoname', |
||||||
|
'_score': 0.9724125, |
||||||
|
'confidence': 0.649 |
||||||
|
}, |
||||||
|
{ |
||||||
|
'center_point': { |
||||||
|
'lon': -81.532392, |
||||||
|
'lat': 40.597578 |
||||||
|
}, |
||||||
|
'address': {}, |
||||||
|
'local_admin': 'Franklin', |
||||||
|
'admin1_abbr': 'OH', |
||||||
|
'name': { |
||||||
|
'default': 'Strasburg High School' |
||||||
|
}, |
||||||
|
'admin1': 'Ohio', |
||||||
|
'locality': 'Strasburg', |
||||||
|
'alpha3': 'USA', |
||||||
|
'admin2': 'Tuscarawas County', |
||||||
|
'admin0': 'United States', |
||||||
|
'category': [ |
||||||
|
'education' |
||||||
|
], |
||||||
|
'_id': '356646971', |
||||||
|
'_type': 'osmway', |
||||||
|
'_score': 0.9724125, |
||||||
|
'confidence': 0.649 |
||||||
|
} |
||||||
|
]; |
@ -0,0 +1,39 @@ |
|||||||
|
|
||||||
|
var calcSize = require('../../../helper/sizeCalculator.js'); |
||||||
|
|
||||||
|
module.exports.tests = {}; |
||||||
|
|
||||||
|
module.exports.tests.interface = function(test, common) { |
||||||
|
test('interface', function(t) { |
||||||
|
t.equal(typeof calcSize, 'function', 'valid function'); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.tests.valid = function(test, common) { |
||||||
|
test('size=0', function (t) { |
||||||
|
t.equal(calcSize(0), 1); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
|
||||||
|
test('size=1', function (t) { |
||||||
|
t.equal(calcSize(1), 1); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
|
||||||
|
test('size=10', function (t) { |
||||||
|
t.equal(calcSize(10), 20); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.all = function (tape, common) { |
||||||
|
|
||||||
|
function test(name, testFunction) { |
||||||
|
return tape('sizeCalculator: ' + name, testFunction); |
||||||
|
} |
||||||
|
|
||||||
|
for( var testCase in module.exports.tests ){ |
||||||
|
module.exports.tests[testCase](test, common); |
||||||
|
} |
||||||
|
}; |
@ -0,0 +1,52 @@ |
|||||||
|
var data = require('../fixture/dedupe_elasticsearch_results'); |
||||||
|
var dedupe = require('../../../middleware/dedupe')(); |
||||||
|
|
||||||
|
module.exports.tests = {}; |
||||||
|
|
||||||
|
module.exports.tests.dedupe = function(test, common) { |
||||||
|
test('filter out duplicates', function(t) { |
||||||
|
var req = { |
||||||
|
clean: { |
||||||
|
text: 'lampeter strasburg high school', |
||||||
|
size: 100 |
||||||
|
} |
||||||
|
}; |
||||||
|
var res = { |
||||||
|
data: data |
||||||
|
}; |
||||||
|
|
||||||
|
var expectedCount = 7; |
||||||
|
dedupe(req, res, function () { |
||||||
|
t.equal(res.data.length, expectedCount, 'results have fewer items than before'); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
test('truncate results based on specified size', function(t) { |
||||||
|
var req = { |
||||||
|
clean: { |
||||||
|
text: 'lampeter strasburg high school', |
||||||
|
size: 3 |
||||||
|
} |
||||||
|
}; |
||||||
|
var res = { |
||||||
|
data: data |
||||||
|
}; |
||||||
|
|
||||||
|
dedupe(req, res, function () { |
||||||
|
t.equal(res.data.length, req.clean.size, 'results have fewer items than before'); |
||||||
|
t.end(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
module.exports.all = function (tape, common) { |
||||||
|
|
||||||
|
function test(name, testFunction) { |
||||||
|
return tape('[middleware] dedupe: ' + name, testFunction); |
||||||
|
} |
||||||
|
|
||||||
|
for( var testCase in module.exports.tests ){ |
||||||
|
module.exports.tests[testCase](test, common); |
||||||
|
} |
||||||
|
}; |
Loading…
Reference in new issue