How to use Geospatial Indexing in MongoDB with Express and Mongoose

For a couple of months I worked on a project that needed check-ins as a feature and location service. So I have decided to use as a database Mongo because of it’s very useful geospatial indexing. Foursquare is using MongoDB to power their location-based data queries, you can read more about his here.

The app is available on Github - please take a look for the full codebase.

As for this tutorial I will keep the back-end logic simple and write an API in node.js using Express 3.0 to query the Mongo location collection. There are a few things to cover so before starting might get handy to read through the official documentation

Let’s take a quick look on what we will cover:

  • Setting up the API initial structure
  • Defining the Schema for our collections
  • Adding an index to our database
  • Getting location(s) using our API

Setup

You should add the following dependencies to your package.json:

"dependencies": {
    "express": "3.5.0",
    "mongoose": "3.8.8",
    "async": "0.2.10"
}
  • Mongoose is an ORM wrapper for talking to MongoDB.
  • Express web application framework for node.
  • async is an utility module.

Prepare the Mongoose

I am going to store location in Mongo, for this tutorial our location schema is going to be simple. This code assumes you have Mongo running on your local machine, with default settings.

Connecting to Mongo with Mongoose is simple:

mongoose.connect('mongodb://localhost/geospatial_db', function(err) {  
        if (err) throw err;

        // do something...
});

First we need to create a schema. The docs give us some examples on how to store geospatial data. We are going to use the legacy format for our example. It’s recommended to store the longitude and latitude in an array. The docs warn use about the order of the values, longitude comes first.

Note that you should always store longitude first!
var LocationSchema = new Schema({  
    name: String,
    loc: {
    type: [Number],  // [<longitude>, <latitude>]
    index: '2d'      // create the geospatial index
    }
});

Locations are going to have a name and a loc property. The loc is going to be an array holding the values for the coordinates [ , ]. Next we are going to tell Mongo to create a geospatial index for the loc. For this example we are going to use a 2d index, you can read more about index here.

Create a location object which we can use to query for specific locations.

// register the mongoose model
mongoose.model('Location', LocationSchema);  

Grabbing locations

First you can create a method in your controller that can look something like this:

findLocation: function(req, res, next) {  
    var limit = req.query.limit || 10;

    // get the max distance or set it to 8 kilometers
    var maxDistance = req.query.distance || 8;

    // we need to convert the distance to radians
    // the raduis of Earth is approximately 6371 kilometers
    maxDistance /= 6371;

    // get coordinates [ <longitude> , <latitude> ]
    var coords = [];
    coords[0] = req.query.longitude;
    coords[1] = req.query.latitude;

    // find a location
    Location.find({
      loc: {
        $near: coords,
        $maxDistance: maxDistance
      }
    }).limit(limit).exec(function(err, locations) {
      if (err) {
        return res.json(500, err);
      }

      res.json(200, locations);
    });
}

What this all means:

We are going to set a default 8 kilometers radius to search for locations within a set of coordinates:

// get the max distance or set it to 8 kilometers
var maxDistance = req.query.distance || 8;  

One important thing to note here is that our query will use radians for distance, so we need to transform our distance to radians.

distance to radians: divide the distance by the radius of the sphere (e.g. the Earth) in the same units as the distance measurement.

You should better follow up on this in the docs.

So to transform our distance to radians, we need to divide with 6371, the Earth’s radius approximately in kilometers.

// we need to convert the distance to radians
// the raduis of Earth is approximately 6371 kilometers
maxDistance /= 6371;  

Getting the coordinates in the right place:

// get coordinates [<longitude>,<latitude>]
var coords = [];  
coords[0] = req.query.longitude || 0;  
coords[1] = req.query.latitude || 0;  
Again: Note that longitude comes first!

Now for finding a location, we are going to use $near operator, which is going to return the closest location first. This operator sorts the returned objects from the nearest to the farthest. In combination with the $maxDistance operator, the results can be limited within a maximum distance to the specified point. Our query will look something like this:

// find a location
Location.find({  
    loc: {
        $near: coords,
        $maxDistance: maxDistance
    }
}).limit(limit).exec(function(err, locations) {
    if (err) {
        return res.json(500, err);
    }

    res.json(200, locations);
});

Having the full codebase we can test our small api by running this in the browser:

http://localhost:3000/api/locations?longitude=23.600800037384033&latitude=46.76758746952729

* the sample application contains a mock data source which loads some locations to you database bye default *

[
  {
    "name": "Piaţa Cipariu",
    "loc": [
      23.600800037384033,
      46.76758746952729
    ],
    "_id": "5335e5a7c39d60402849e38f",
    "__v": 0
  },
  {
    "name": "Stația Piața Cipariu",
    "loc": [
      23.601171912820668,
      46.76771454984428
    ],
    "_id": "5335e5a7c39d60402849e3a5",
    "__v": 0
  },
  ...
]

As you can see the server will return a list of existing locations for the specified point within the default max distance.

Mongo also supports the GeoJSON location data which is also a nice thing to check-out, there are a lots of interesting things related to the geospatial support, might be a good idea to read more about it in the docs.

Thank you for reading. Until next time.

You can leave comments on hackernews.

comments powered by Disqus