handle deep geo-near queries (#1216)
a dedicated mongKey is added in geo.nearFilter for mongoDB fixes geo min distance tests as filter now expects an array
This commit is contained in:
parent
fc2f66c514
commit
6f88cf1930
106
lib/geo.js
106
lib/geo.js
|
@ -12,32 +12,65 @@ var assert = require('assert');
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.nearFilter = function nearFilter(where) {
|
exports.nearFilter = function nearFilter(where) {
|
||||||
var result = false;
|
function nearSearch(clause, parentKeys) {
|
||||||
|
if (typeof clause !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
parentKeys = parentKeys || [];
|
||||||
|
|
||||||
if (where && typeof where === 'object') {
|
Object.keys(clause).forEach(function(clauseKey) {
|
||||||
Object.keys(where).forEach(function(key) {
|
if (Array.isArray(clause[clauseKey])) {
|
||||||
var ex = where[key];
|
clause[clauseKey].forEach(function(el, index) {
|
||||||
|
var ret = nearSearch(el, parentKeys.concat(clauseKey).concat(index));
|
||||||
if (ex && ex.near) {
|
if (ret) return ret;
|
||||||
result = {
|
});
|
||||||
near: ex.near,
|
} else {
|
||||||
maxDistance: ex.maxDistance,
|
if (clause[clauseKey].hasOwnProperty('near')) {
|
||||||
minDistance: ex.minDistance,
|
var result = clause[clauseKey];
|
||||||
unit: ex.unit,
|
nearResults.push({
|
||||||
key: key,
|
near: result.near,
|
||||||
};
|
maxDistance: result.maxDistance,
|
||||||
|
minDistance: result.minDistance,
|
||||||
|
unit: result.unit,
|
||||||
|
// If key is at root, define a single string, otherwise append it to the full path array
|
||||||
|
mongoKey: parentKeys.length ? parentKeys.concat(clauseKey) : clauseKey,
|
||||||
|
key: clauseKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
var nearResults = [];
|
||||||
|
nearSearch(where);
|
||||||
|
|
||||||
return result;
|
return (!nearResults.length ? false : nearResults);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* Filter a set of objects using the given `nearFilter`.
|
* Filter a set of results using the given filters returned by `nearFilter()`.
|
||||||
|
* Can support multiple locations, but will include results from all of them.
|
||||||
|
*
|
||||||
|
* WARNING: "or" operator with GeoPoint does not work as expected, eg:
|
||||||
|
* {where: {or: [{location: {near: (29,-90)}},{name:'Sean'}]}}
|
||||||
|
* Will actually work as if you had used "and". This is because geo filtering
|
||||||
|
* takes place outside of the SQL query, so the result set of "name = Sean" is
|
||||||
|
* returned by the database, and then the location filtering happens in the app
|
||||||
|
* logic. So the "near" operator is always an "and" of the SQL filters, and "or"
|
||||||
|
* of other GeoPoint filters.
|
||||||
|
*
|
||||||
|
* Additionally, since this step occurs after the SQL result set is returned,
|
||||||
|
* if using GeoPoints with pagination the result set may be smaller than the
|
||||||
|
* page size. The page size is enforced at the DB level, and then we may
|
||||||
|
* remove results at the Geo-app level. If we "limit: 25", but 4 of those results
|
||||||
|
* do not have a matching geopoint field, the request will only return 21 results.
|
||||||
|
* This may make it erroneously look like a given page is the end of the result set.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
exports.filter = function(arr, filter) {
|
exports.filter = function(rawResults, filters) {
|
||||||
|
var distances = {};
|
||||||
|
var results = [];
|
||||||
|
|
||||||
|
filters.forEach(function(filter) {
|
||||||
var origin = filter.near;
|
var origin = filter.near;
|
||||||
var max = filter.maxDistance > 0 ? filter.maxDistance : false;
|
var max = filter.maxDistance > 0 ? filter.maxDistance : false;
|
||||||
var min = filter.minDistance > 0 ? filter.minDistance : false;
|
var min = filter.minDistance > 0 ? filter.minDistance : false;
|
||||||
|
@ -45,42 +78,33 @@ exports.filter = function(arr, filter) {
|
||||||
var key = filter.key;
|
var key = filter.key;
|
||||||
|
|
||||||
// create distance index
|
// create distance index
|
||||||
var distances = {};
|
rawResults.forEach(function(result) {
|
||||||
var result = [];
|
var loc = result[key];
|
||||||
|
|
||||||
arr.forEach(function(obj) {
|
// filter out results without locations
|
||||||
var loc = obj[key];
|
|
||||||
|
|
||||||
// filter out objects without locations
|
|
||||||
if (!loc) return;
|
if (!loc) return;
|
||||||
|
|
||||||
if (!(loc instanceof GeoPoint)) {
|
if (!(loc instanceof GeoPoint)) loc = GeoPoint(loc);
|
||||||
loc = GeoPoint(loc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof loc.lat !== 'number') return;
|
if (typeof loc.lat !== 'number') return;
|
||||||
if (typeof loc.lng !== 'number') return;
|
if (typeof loc.lng !== 'number') return;
|
||||||
|
|
||||||
var d = GeoPoint.distanceBetween(origin, loc, {type: unit});
|
var d = GeoPoint.distanceBetween(origin, loc, {type: unit});
|
||||||
|
|
||||||
if (min && d < min) {
|
// filter result if distance is either < minDistance or > maxDistance
|
||||||
return;
|
if ((min && d < min) || (max && d > max)) return;
|
||||||
}
|
|
||||||
if (max && d > max) {
|
distances[result.id] = d;
|
||||||
// dont add
|
results.push(result);
|
||||||
} else {
|
|
||||||
distances[obj.id] = d;
|
|
||||||
result.push(obj);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.sort(function(objA, objB) {
|
results.sort(function(resA, resB) {
|
||||||
var a = objA[key];
|
var a = resA[key];
|
||||||
var b = objB[key];
|
var b = resB[key];
|
||||||
|
|
||||||
if (a && b) {
|
if (a && b) {
|
||||||
var da = distances[objA.id];
|
var da = distances[resA.id];
|
||||||
var db = distances[objB.id];
|
var db = distances[resB.id];
|
||||||
|
|
||||||
if (db === da) return 0;
|
if (db === da) return 0;
|
||||||
return da > db ? 1 : -1;
|
return da > db ? 1 : -1;
|
||||||
|
@ -88,6 +112,9 @@ exports.filter = function(arr, filter) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.GeoPoint = GeoPoint;
|
exports.GeoPoint = GeoPoint;
|
||||||
|
@ -285,4 +312,3 @@ function geoDistance(x1, y1, x2, y2, options) {
|
||||||
|
|
||||||
return 2 * Math.asin(f) * EARTH_RADIUS[type];
|
return 2 * Math.asin(f) * EARTH_RADIUS[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ describe('basic-querying', function() {
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
addressLoc: {type: 'GeoPoint'},
|
||||||
});
|
});
|
||||||
|
|
||||||
db.automigrate(done);
|
db.automigrate(done);
|
||||||
|
@ -553,6 +554,147 @@ describe('basic-querying', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('geo queries', function() {
|
||||||
|
describe('near filter', function() {
|
||||||
|
it('supports a basic "near" query', function(done) {
|
||||||
|
User.find({
|
||||||
|
where: {
|
||||||
|
addressLoc: {
|
||||||
|
near: '29.9,-90.07',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, function(err, users) {
|
||||||
|
if (err) done(err);
|
||||||
|
users.should.have.property('length', 3);
|
||||||
|
users[0].name.should.equal('John Lennon');
|
||||||
|
users[0].addressLoc.should.not.be.null();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports "near" inside a coumpound query with "and"', function(done) {
|
||||||
|
User.find({
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
addressLoc: {
|
||||||
|
near: '29.9,-90.07',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vip: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}, function(err, users) {
|
||||||
|
if (err) done(err);
|
||||||
|
users.should.have.property('length', 2);
|
||||||
|
users[0].name.should.equal('John Lennon');
|
||||||
|
users[0].addressLoc.should.not.be.null();
|
||||||
|
users[0].vip.should.be.true();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports "near" inside a complex coumpound query with multiple "and"', function(done) {
|
||||||
|
User.find({
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
addressLoc: {
|
||||||
|
near: '29.9,-90.07',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vip: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}, function(err, users) {
|
||||||
|
if (err) done(err);
|
||||||
|
users.should.have.property('length', 1);
|
||||||
|
users[0].name.should.equal('John Lennon');
|
||||||
|
users[0].addressLoc.should.not.be.null();
|
||||||
|
users[0].vip.should.be.true();
|
||||||
|
users[0].order.should.equal(2);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports multiple "near" queries with "or"', function(done) {
|
||||||
|
User.find({
|
||||||
|
where: {
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
addressLoc: {
|
||||||
|
near: '29.9,-90.04',
|
||||||
|
maxDistance: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
addressLoc: {
|
||||||
|
near: '22.97,-88.03',
|
||||||
|
maxDistance: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}, function(err, users) {
|
||||||
|
if (err) done(err);
|
||||||
|
users.should.have.property('length', 2);
|
||||||
|
users[0].addressLoc.should.not.be.null();
|
||||||
|
users[0].name.should.equal('Paul McCartney');
|
||||||
|
users[1].addressLoc.should.not.equal(null);
|
||||||
|
users[1].name.should.equal('John Lennon');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports multiple "near" queries with "or" ' +
|
||||||
|
'inside a coumpound query with "and"', function(done) {
|
||||||
|
User.find({
|
||||||
|
where: {
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
addressLoc: {
|
||||||
|
near: '29.9,-90.04',
|
||||||
|
maxDistance: 300,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
addressLoc: {
|
||||||
|
near: '22.7,-89.03',
|
||||||
|
maxDistance: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vip: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}, function(err, users) {
|
||||||
|
if (err) done(err);
|
||||||
|
users.should.have.property('length', 1);
|
||||||
|
users[0].addressLoc.should.not.be.null();
|
||||||
|
users[0].name.should.equal('John Lennon');
|
||||||
|
users[0].vip.should.be.true();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should only include fields as specified', function(done) {
|
it('should only include fields as specified', function(done) {
|
||||||
var remaining = 0;
|
var remaining = 0;
|
||||||
|
|
||||||
|
@ -590,8 +732,9 @@ describe('basic-querying', function() {
|
||||||
}
|
}
|
||||||
|
|
||||||
sample({name: true}).expect(['name']);
|
sample({name: true}).expect(['name']);
|
||||||
sample({name: false}).expect(['id', 'seq', 'email', 'role', 'order', 'birthday', 'vip',
|
sample({name: false}).expect([
|
||||||
'address', 'friends']);
|
'id', 'seq', 'email', 'role', 'order', 'birthday', 'vip', 'address', 'friends', 'addressLoc',
|
||||||
|
]);
|
||||||
sample({name: false, id: true}).expect(['id']);
|
sample({name: false, id: true}).expect(['id']);
|
||||||
sample({id: true}).expect(['id']);
|
sample({id: true}).expect(['id']);
|
||||||
sample('id').expect(['id']);
|
sample('id').expect(['id']);
|
||||||
|
@ -1005,6 +1148,7 @@ function seed(done) {
|
||||||
{name: 'George Harrison'},
|
{name: 'George Harrison'},
|
||||||
{name: 'Ringo Starr'},
|
{name: 'Ringo Starr'},
|
||||||
],
|
],
|
||||||
|
addressLoc: {lat: 29.97, lng: -90.03},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
seq: 1,
|
seq: 1,
|
||||||
|
@ -1025,8 +1169,15 @@ function seed(done) {
|
||||||
{name: 'George Harrison'},
|
{name: 'George Harrison'},
|
||||||
{name: 'Ringo Starr'},
|
{name: 'Ringo Starr'},
|
||||||
],
|
],
|
||||||
|
addressLoc: {lat: 22.97, lng: -88.03},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seq: 2,
|
||||||
|
name: 'George Harrison',
|
||||||
|
order: 5,
|
||||||
|
vip: false,
|
||||||
|
addressLoc: {lat: 22.7, lng: -89.03},
|
||||||
},
|
},
|
||||||
{seq: 2, name: 'George Harrison', order: 5, vip: false},
|
|
||||||
{seq: 3, name: 'Ringo Starr', order: 6, vip: false},
|
{seq: 3, name: 'Ringo Starr', order: 6, vip: false},
|
||||||
{seq: 4, name: 'Pete Best', order: 4},
|
{seq: 4, name: 'Pete Best', order: 4},
|
||||||
{seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true},
|
{seq: 5, name: 'Stuart Sutcliffe', order: 3, vip: true},
|
||||||
|
|
|
@ -147,8 +147,8 @@ describe('GeoPoint', function() {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
var filter = nearFilter(where);
|
var filter = nearFilter(where);
|
||||||
filter.key.should.equal('location');
|
filter[0].key.should.equal('location');
|
||||||
filter.should.have.properties({
|
filter[0].should.have.properties({
|
||||||
key: 'location',
|
key: 'location',
|
||||||
near: {
|
near: {
|
||||||
lat: 40.77492964101182,
|
lat: 40.77492964101182,
|
||||||
|
@ -187,7 +187,7 @@ describe('GeoPoint', function() {
|
||||||
lng: 121.483687,
|
lng: 121.483687,
|
||||||
},
|
},
|
||||||
}];
|
}];
|
||||||
var filter = {
|
var filter = [{
|
||||||
key: 'location',
|
key: 'location',
|
||||||
near: {
|
near: {
|
||||||
lat: 30.278562,
|
lat: 30.278562,
|
||||||
|
@ -195,7 +195,7 @@ describe('GeoPoint', function() {
|
||||||
},
|
},
|
||||||
unit: 'meters',
|
unit: 'meters',
|
||||||
minDistance: 10000,
|
minDistance: 10000,
|
||||||
};
|
}];
|
||||||
var results = geoFilter(points, filter);
|
var results = geoFilter(points, filter);
|
||||||
results.length.should.be.equal(3);
|
results.length.should.be.equal(3);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue