Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Geosearch #1560

Merged
merged 10 commits into from
Jul 2, 2013
68 changes: 68 additions & 0 deletions lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -1666,6 +1666,74 @@ Model.aggregate = function aggregate () {
return aggregate.exec(callback);
}

/**
* Implements $geoSearch functionality for Mongoose
*
* ####Example:
*
* var options = { near: [10, 10], maxDistance: 5 };
* Locations.geoSearch({ type : "house" }, options, function(err, res) {
* console.log(res);
* });
*
* ####Options:
* - `near` {Array} x,y point to search for
* - `maxDistance` {Number} the maximum distance from the point near that a result can be
* - `limit` {Number} The maximum number of results to return
* - `lean` {Boolean} return the raw object instead of the Mongoose Model
*
* @param {Object} condition an object that specifies the match condition (required)
* @param {Object} options for the geoSearch, some (near, maxDistance) are required
* @see http://docs.mongodb.org/manual/reference/command/geoSearch/
* @see http://docs.mongodb.org/manual/core/geohaystack/
* @api public
*/

Model.geoSearch = function (conditions, options, callback) {
if ('function' == typeof options) {
callback = options;
options = {};
}
if (!callback || 'function' != typeof callback) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could drop the !callback since the other check covers it.

throw new Error("Must pass a callback to geoSearch");
}

if (conditions == undefined || !utils.isObject(conditions)) {
return callback(new Error("Must pass conditions to geoSearch"));
}

if (!options.near) {
return callback(new Error("Must specify the near option in geoSearch"));
}

if (!Array.isArray(options.near)) {
return callback(new Error("near option must be an array [x, y]"));
}


// send the conditions in the options object
options.search = conditions;
var self = this;

return this.collection.geoHaystackSearch(options.near[0], options.near[1], options, function (err, res) {
if (err || res.errmsg) return callback(err, res);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if err.errmsg exists, will err be an Error object? if not, we should make sure we pass an error back too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err will not exist if res.errmsg exists. The way that the driver handles these admin commands, it basically just passes back the raw results and doesn't do any error checking. I was trying to decide between just passing the res object as the error if res.errmsg exists, but decided to try it like this first.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah we should follow the node paradigm if at all possible

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so, to confirm: we will pass the res object back as the error if res.errmsg exists?


if (!options.lean) {
var count = res.results.length;
for (var i=0; i < res.results.length; i++) {
var temp = res.results[i];
res.results[i] = new self();
res.results[i].init(temp, {}, function (err) {
if (err) return callback(err);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should guard against potentially calling the callback multiple times.

--count || callback(err, res);
});
}
} else {
callback(err, res);
}
});
};

/**
* Populates document references.
*
Expand Down
155 changes: 155 additions & 0 deletions test/model.geosearch.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@

var start = require('./common')
, assert = require('assert')
, mongoose = start.mongoose
, random = require('../lib/utils').random
, Schema = mongoose.Schema
, DocumentObjectId = mongoose.Types.ObjectId

/**
* Setup
*/

var schema = new Schema({
pos : [Number],
complex : {},
type: String
});

schema.index({ "pos" : "geoHaystack", type : 1},{ bucketSize : 1});

mongoose.model('Geo', schema);

describe('model', function(){
describe.only('geoSearch', function () {
it('works', function (done) {

var db = start();
var Geo = db.model('Geo');
assert.ok(Geo.geoSearch instanceof Function);

var geos = [];
geos[0] = new Geo({ pos : [10,10], type : "place"});
geos[1] = new Geo({ pos : [15,5], type : "place"});
geos[2] = new Geo({ pos : [20,15], type : "house"});
geos[3] = new Geo({ pos : [1,-1], type : "house"});
var count = geos.length;

for (var i=0; i < geos.length; i++) {
geos[i].save(function () {
--count || next();
});
}

function next() {
Geo.geoSearch({ type : "place" }, { near : [9,9], maxDistance : 5 }, function (err, results, stats) {
assert.ifError(err);
assert.equal(1, results.results.length);
assert.equal(1, results.ok);

assert.equal(results.results[0].type, 'place');
assert.equal(results.results[0].pos.length, 2);
assert.equal(results.results[0].pos[0], 10);
assert.equal(results.results[0].pos[1], 10);
assert.equal(results.results[0].id, geos[0].id);
assert.ok(results.results[0] instanceof Geo);
Geo.remove(function () {
db.close();
done();
});
});
}
});
it('works with lean', function (done) {

var db = start();
var Geo = db.model('Geo');
assert.ok(Geo.geoSearch instanceof Function);

var geos = [];
geos[0] = new Geo({ pos : [10,10], type : "place"});
geos[1] = new Geo({ pos : [15,5], type : "place"});
geos[2] = new Geo({ pos : [20,15], type : "house"});
geos[3] = new Geo({ pos : [1,-1], type : "house"});
var count = geos.length;

for (var i=0; i < geos.length; i++) {
geos[i].save(function () {
--count || next();
});
}

function next() {
Geo.geoSearch({ type : "place" }, { near : [9,9], maxDistance : 5, lean : true }, function (err, results, stats) {
assert.ifError(err);
assert.equal(1, results.results.length);
assert.equal(1, results.ok);

assert.equal(results.results[0].type, 'place');
assert.equal(results.results[0].pos.length, 2);
assert.equal(results.results[0].pos[0], 10);
assert.equal(results.results[0].pos[1], 10);
assert.equal(results.results[0]._id, geos[0].id);
assert.strictEqual(results.results[0].id, undefined);
assert.ok(!(results.results[0] instanceof Geo));
Geo.remove(function () {
db.close();
done();
});
});
}
});
it('throws the correct error messages', function (done) {

var db = start();
var Geo = db.model('Geo');
var g = new Geo({ pos : [10,10], type : "place"});
g.save(function() {
var threw = false;
Geo.geoSearch([], {}, function (e) {
assert.ok(e);
assert.equal(e.message, "Must pass conditions to geoSearch");

Geo.geoSearch({ type : "test"}, {}, function (e) {
assert.ok(e);
assert.equal(e.message, "Must specify the near option in geoSearch");

Geo.geoSearch({ type : "test" }, { near : "hello" }, function (e) {
assert.ok(e);
assert.equal(e.message, "near option must be an array [x, y]");

try {
Geo.geoSearch({ type : "test" }, { near : [1,2] }, []);
} catch(e) {
threw = true;
assert.ok(e);
assert.equal(e.message, "Must pass a callback to geoSearch");
}

assert.ok(threw);
threw = false;

try {
Geo.geoSearch({ type : "test" }, { near : [1,2] });
} catch(e) {
threw = true;
assert.ok(e);
assert.equal(e.message, "Must pass a callback to geoSearch");
}

assert.ok(threw);
Geo.geoSearch({ type : "test" }, { near : [1,2] }, function (err, res) {
assert.ifError(err);
assert.ok(res);

assert.equal(res.ok, 0);
assert.equal(res.errmsg, "exception: maxDistance needs a number");
done();
});
});
});
});
});
});
});
});