mirror of https://github.com/pelias/api.git
Diana Shkolnikov
9 years ago
26 changed files with 645 additions and 111 deletions
@ -0,0 +1,99 @@
|
||||
var logger = require('pelias-logger').get('api'); |
||||
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} |
||||
* @throws {Error} |
||||
*/ |
||||
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 { |
||||
propMatch(item1, item2, 'name'); |
||||
} |
||||
|
||||
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 new Error('different'); |
||||
} |
||||
} |
||||
catch (err) { |
||||
if (err.message === 'different') { |
||||
return true; |
||||
} |
||||
throw err; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Throw exception if properties are different |
||||
* |
||||
* @param {object} item1 |
||||
* @param {object} item2 |
||||
* @param {string} prop |
||||
* @throws {Error} |
||||
*/ |
||||
function propMatch(item1, item2, prop) { |
||||
if (normalizeString(item1[prop]) !== normalizeString(item2[prop])) { |
||||
throw new Error('different'); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Remove punctuation and lowercase |
||||
* |
||||
* @param {string} str |
||||
* @returns {string} |
||||
*/ |
||||
function normalizeString(str) { |
||||
if (!str) { |
||||
return ''; |
||||
} |
||||
return _.words(str.toLowerCase()).join(' '); |
||||
} |
||||
|
||||
|
||||
module.exports = setup; |
@ -0,0 +1,35 @@
|
||||
var _ = require('lodash'); |
||||
|
||||
var SIZE_PADDING = 2; |
||||
|
||||
/** |
||||
* Utility for calculating query result size |
||||
* incorporating padding for dedupe process |
||||
*/ |
||||
function setup() { |
||||
return function setQuerySize(req, res, next) { |
||||
if (_.isUndefined(req.clean) || _.isUndefined(req.clean.size)) { |
||||
return next(); |
||||
} |
||||
|
||||
req.clean.querySize = calculateSize(req.clean.size); |
||||
next(); |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Add padding or set to 1 |
||||
* |
||||
* @param {number} cleanSize |
||||
* @returns {number} |
||||
*/ |
||||
function calculateSize(cleanSize) { |
||||
switch (cleanSize || 1) { |
||||
case 1: |
||||
return 1; |
||||
default: |
||||
return cleanSize * SIZE_PADDING; |
||||
} |
||||
} |
||||
|
||||
module.exports = setup; |
@ -0,0 +1,273 @@
|
||||
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.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,67 @@
|
||||
|
||||
var calcSize = require('../../../middleware/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) { |
||||
var req = { clean: {} }; |
||||
function setup(val) { |
||||
if (isNaN(val)) { |
||||
delete req.clean.size; |
||||
} |
||||
else { |
||||
req.clean.size = val; |
||||
} |
||||
delete req.clean.querySize; |
||||
} |
||||
|
||||
test('size=0', function (t) { |
||||
setup(0); |
||||
calcSize(req, {}, function () { |
||||
t.equal(req.clean.querySize, 1); |
||||
t.end(); |
||||
}); |
||||
}); |
||||
|
||||
test('size=1', function (t) { |
||||
setup(1); |
||||
calcSize(req, {}, function () { |
||||
t.equal(req.clean.querySize, 1); |
||||
t.end(); |
||||
}); |
||||
}); |
||||
|
||||
test('size=10', function (t) { |
||||
setup(10); |
||||
calcSize(req, {}, function () { |
||||
t.equal(req.clean.querySize, 20); |
||||
t.end(); |
||||
}); |
||||
}); |
||||
|
||||
test('no size', function (t) { |
||||
setup(); |
||||
calcSize(req, {}, function () { |
||||
t.equal(req.clean.hasOwnProperty('querySize'), false); |
||||
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