Geolocation, Rails and MongoDB- a recipe for success

‘Geolocation’ seems to be the best dish being served today. Every web-portal, every mobile app wants to be sensitive to a persons location. Everyone wants to see information that is ‘relative’ or location sensitive. Whether its a deal portal, travel portal, social network – giving users information that is relevant to their location bring not only a personalized touch but also keeps tuned in to the portal.

So, how do we do this? In a change of writing style, I am going to ‘cook’ up this recipe!

The Ingredients

Geokit was good with Rails 2.x but it doesn’t work well with Rails3. Geocoder rocks! There is this new kid on the block too Geo Mere Lal – neat stuff but with limited browser support. My personal preference is a slice of Geocoder – it smartly adds some Rack methods on the request object. To get geo-location (via IP or router) is as simple as:

request.location

HTML5 does provide geolocation services and is also a neat way to get the coordinates (directly from the machine or connected GPS devise) but this requires user intervention to post the co-ordinates and hence not always acceptable.

MongoDB is the next ingredient in our recipe. An excellent scalable document database for storing huge amounts of data. Mongoid is the gem flavour I use MongoDB with.

The Blend

Rails is the blending mixture which holds all this together! (obviously). A simple example of a model with geospatial indexing is here:


class Business
 include Mongoid::Document

 field :name
 field :domain
 field :address
 field :location, type => Array

 index [[ :location, Mongo::GEO2D ]]

 before_save :set_location

 def set_location
  self.location = Geocoder.search(self.address).first.coordinates if self.address_changed? || self.new_record?
 end
end

To taste the mixture, you need to simply do this:

lat, lng = Geocoder.search('some location').first.coordinates
result = Business.near(:location => [lat, lng])

Some tips and tricks to cook this well:

  • Don’t forget the index by running ‘rake db:mongoid:create_indexes’
  • Remember that mongoid returns Mongoid::Criteria by default. Mongoid delays till absolutely necessary to fetch the information. (just like scopes). What could be confusing is that on the rails console, Business.all returns an array if you are using ActiveRecord (i.e. the scope is resolved) but Mongoid will still return the Criteria untill you fetch the first object.

The Garnishing

Google Maps Javascript (V3) has finally got rid of Google API keys setup! Use at will. Here is the basic JS snippet I used to get my map markers up and running:

// Somewhere earlier: http://maps.google.com/maps/api/js?sensor=false

var startZoom = 9;
var map;
var map_markers = {};

function init_gmap(centerLatitude, centerLongitude) {
 var options = {zoom: startZoom,
 center: new google.maps.LatLng(centerLatitude,centerLongitude),
 zoomControl: true,
 panControl: true,
 mapTypeId: google.maps.MapTypeId.ROADMAP };

 map = new google.maps.Map(document.getElementById("gmap"), options);
 var infoWindow = new google.maps.InfoWindow({ maxWidth: 100 });
 // Ensure 'markers' array is populated with JSon Array of your objects.
 for(id in markers) {
   var loc = new google.maps.LatLng(markers[id].latitude, markers[id].longitude);
   var marker = new google.maps.Marker({ position: loc,
                                map: map,
                                name: markers[id].name,
                                subdomain: markers[id].subdomain
                });

 google.maps.event.addListener(marker, 'mouseover', function() {
   infoWindow.setContent(this.name);
   infoWindow.open(map, this);
 });

 google.maps.event.addListener(marker, 'click', function() {
   window.location = 'http://' + this.subdomain;
 });
 }
}

Voila! Keep frozen for 2 minutes (Read: refresh screen) and you have a winner!

Adding the Twist

I found after some research that geo-spatial indexing is a standard 2-dimensional indexing. In the case of geolocation, we need a spherical mode for searching and after v1.3.3 of MongoDB, this too is supported nicely. mongoid-geo is a nice gem for making this really simple:

class Business
 include Mongoid::Document
 extend Mongoid::Geo::Near

 field :address
 field :location, :type => Array, :geo => true
 geo_index :location

 before_save :update_location

 def update_location
   self.location = Geocoder.search(self.address).first.coordinates if self.location_changed? || self.new_record?
 end
end

Please give me feedback on my recipe — it would be more than welcome! I am a lousy cook (literally) but this could be a start of something new!

Update1

Source code for the above app is available at https://github.com/joshsoftware/mongo_geo_demo

11 thoughts on “Geolocation, Rails and MongoDB- a recipe for success

  1. Took me sometimes to read some the comments, any way I easily enjoyed the post. It proved to be pretty useful to me and I am certain to all the commenters right here! It’s all the time good when you can not only be informed, but also entertained!

  2. Very useful article. Do you know if there’s a way to route users to different subdomains depending on their IP address? i.e. if my IP is from the US, then I’m routed to us.mysite.com, but if my IP is UK based, then I’m routed to uk.mysite.com and so on… Any help is much appreciated.

    Riki

    1. Well, I have not tried it (shall do so soon) but I think this is what you need to do:

      location = request.location # => This is a rack request

      Using Geocoder::Result ‘country’ method, you then use rack rewrite (https://github.com/jtrupiano/rack-rewrite) and rewrite / redirect the URL to your sub-domain.

      Of course, ensure that your cookie settings support wild-card domain otherwise you may have a session sharing problem.

  3. I liked your article. The ‘cooking’ metaphor made it easier to read.
    Ingrediants is spelled “Ingredients” (you might want to change that).
    Also, you should show other alternatives besides just MongoDB. (Does the developer HAVE to use MongoDB?)

    1. @webmasta – Glad you liked the article and thanks for the correction. I have updated the post.
      We can use other databases besides MongoDB — but what takes the cake (no pun intended here) with MongoDB is that it inherently supports geo-spatial indexing. Otherwise, there is a *huge* complex cos / radians SQL query that does this in a Relational Database. See (geocoder 1.0.2)/lib/geocoder/stores/active_record.rb: full_near_scope_options method.

      Considering mongodb does this internally, it cooks quite well!

  4. Nice writeup there, but you might want to add a bit left and right…

    You could also mention that when you are heading onto a map representation with some maps API, geocoder is kinda included in gmaps4rails(which now is called GlobalMaps4Rails; was portes to other map backends too…). You could have a Map on your site with all the goodness of this gem in like 20 minutes =)

    Just FYI, keep it up,
    Kjellski

  5. Need Some help in project.
    In my case I have the coordinates. And I have to fetch the Address from it.
    please suggest me solution

Leave a reply to canercakner Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.