Sönke Rohde

Location Based Services With MongoDB

MongoDB is an open-source NoSQL database. It offers great features to build Location Based Services with build in support for Geospatial Indexes and Queries.

I’ve been using MongoDB with Node.js on Heroku so here a quick walk through on how to set it up and use it:

Setup

Local

Install and Run:

$ brew install mongo
$ mongod

On lesson I’ve learned is to always try to use MongoDB from the shell first before you start with actual code. After having the command right in the shell translate it into code and test your actual implementation. Here some shell commands to get you started:

$ mongo
> use mydbname

For this example we store locations in a collection called locations Make sure you have set the geo spacial index:

> db.locations.ensureIndex({loc: '2d'})
> db.locations.getIndexKeys()

Should output [ { "_id" : 1 }, { "loc" : "2d" } ]

Heroku

On the Heroku side it’s pretty easy to set up as well:

$ heroku addons:add mongohq:sandbox

To make sure the location index is also working on Heroku, login, select your app and select the MongoHQ add-on. This brings you to the MongoHQ admin interface. Then select your locations collection and make sure the Indexes are configured correctly. In my case it shows as { key: { loc: "2d" }, v: 1, ns: "appXXX.locations", name: "loc_2d" }

MongoDB Shell

Insert Location

When you insert a new location it’s essential to store the location in an array with [lon, lat] and not [lat, lon]. This was a mistake I made first and it took me a while to figure that out because all my queries were returning the wrong stuff.

Let’s insert some example locations (I’ve been using LATLONG.net):

> db.locations.insert({name: "Ferry Building", city: "San Francisco", loc:[-122.3937, 37.7955]})
> db.locations.insert({name: "Union Square", city: "San Francisco", loc:[-122.407437, 37.787994]})
> db.locations.insert({name: "Duboce Park", city: "San Francisco", loc:[-122.433453, 37.769422]}) 
> db.locations.insert({name: "Parque Ibirapuera", city: "São Paulo",loc:[-46.657490, -23.586996]})
> db.locations.insert({name: "Big Ben", city: "London",loc:[-0.124575, 51.500705]})
> db.locations.insert({name: "Fischmarkt", city: "Hamburg",loc:[9.937740, 53.544527]})
> db.locations.insert({name: "Opera House", city: "Sydney",loc:[151.214993, -33.857767]})

Now you can query all locations with:

> db.locations.find()

Just as a sanity check: Everything west of Greenwich has a longitude < 0 and east > 0. Everything in the northern hemisphere has a latitude > 0 and in the southern hemisphere < 0 or as Wikipedia puts it:

In geography, latitude (φ) is a geographic coordinate that specifies the north-south position of a point on the Earth’s surface. Latitude is an angle (defined below) which ranges from 0° at the Equator to 90° (North or South) at the poles.

http://en.wikipedia.org/wiki/Latitude

Longitude (/ˈlɒndʒɨtjuːd/ or /ˈlɒŋɡɨtjuːd/),[1] is a geographic coordinate that specifies the east-west position of a point on the Earth’s surface. […] The longitude of other places is measured as an angle east or west from the Prime Meridian, ranging from 0° at the Prime Meridian to +180° eastward and −180° westward.

http://en.wikipedia.org/wiki/Longitude

Query Locations

After we have inserted some test locations let’s try to make a geospatial query. In my case I wanted to find locations close to the current location so let’s pretend we are at the Ferry Building in San Francisco.

> db.runCommand({geoNear: "locations", near: [-122.3937, 37.7955], num: 10, spherical:true})

The result is ordered by distance and the Ferry Building should be on top with a dis of 0. To have the distance in a human readable unit you can use the distanceMultiplier attribute and select for instance the earth radius in miles which is 3959. This will show all distances in miles:

> db.runCommand({geoNear: "locations", near: [-122.3937, 37.7955], num: 10, spherical:true,
distanceMultiplier: 3959})

If you want to restrict the search to a radius like 10 miles you’ll have to use the maxDistance attribute:

> db.runCommand({geoNear: "locations", near: [-122.3937, 37.7955], num: 10, spherical:true,
distanceMultiplier: 3959, maxDistance:10/3959})

If you want kilometers use 6371m as the radius of the earth.

Now that everything is working in the shell we can start with actual code.

Node.js Code

This example is written with CoffeeScript and has been extracted from an actual project and usually you would do more validation and parsing but you should get the idea on how the Express route is working:

1
2
3
4
5
6
7
8
# app.coffee
express = require 'express'
api = require './routes/api'
app = module.exports = express()

apiVersion = 'v1'
app.post "/api/#{apiVersion}/location", api.location
app.get "/api/#{apiVersion}/location", api.location
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# routes/api.coffee
mongo = require 'mongodb'
assert = require 'assert'

ObjectID = mongo.BSONPure.ObjectID

dbConnection = process.env.MONGOHQ_URL or 'mongodb://localhost/mydbname'

locations = null
db = null

mongo.Db.connect dbConnection, (err, database) ->
  db = database
  assert.equal null, err

  database.collection 'locations', (err, collection) ->
    assert.equal null, err
    locations = collection

exports.location = (req, res) ->
  switch req.method

    # Create location
    when 'POST'
      # In this example I am using upsert to update the record with the existing _id when present or create a new ObjectID if not:

      location = req.body
      location.loc = [parseFloat(location.loc[0]), parseFloat(location.loc[1])]

      isUpdate = location._id?
      id = if isUpdate then location._id else new ObjectID()

      locations.update {_id:id}, location, {upsert:true, safe:true}, (err, result) ->
        assert.equal null, err
        res.send JSON.stringify(location)

    # Query locations
    when 'GET'
      lat = req.query["lat"]
      lon = req.query["lon"]

      if lat and lon
        lat = parseFloat lat
        lon = parseFloat lon

        # validate that lat/lon are numeric
        return res.send 400 if isNaN(lat) or isNaN(lon)

        # validate that lat/lon are in the right range
        return res.send 400 if lon < -180 or lat > 180

        geoNear = 
          geoNear: locations
          near: [lon, lat]
          spherical: true
          maxDistance: 10 / 3959
          distanceMultiplier: 3959

        db.command geoNear, (err, locations) ->
          assert.equal null, err
          res.send locations

Comments