tag mongodb, mongomapper, geolocation, ip address

Easy IP Geotargeting with Geokit and MongoMapper

There are several cases in which it might make sense to tailor your app's content based on a user's physical location. But asking them directly is a bit of a pain. Luckily, it's extremely simple to find a user's location knowing only something you will always know about a visitor: their IP address. Today I'll walk you through how to use IPs to geolocate your visitors in a Rails application using Geokit and MongoDB's geospatial indexing with MongoMapper.

First up, you'll need to add the gems to your application's Gemfile. I'm assuming you're using MongoDB as your application's primary datastore...if not, you may want to look into a third-party geostore such as SimpleGeo or find another way to locate records based on location. In fact, there's a Rails plugin for Geokit that offers some of these very features for ActiveRecord. You can find it at the same address linked above.

Getting Setup

First you'll need to add the necessary gems to your Gemfile:

gem 'geokit'
gem 'mongo_mapper', :git => 'git://github.com/jnunemaker/mongomapper.git', :branch => 'rails3'

Next, you'll want to generate the MongoMapper configuration file for Rails:

rails g mongo_mapper:config

That's really about all you need to do.

Finding the right "Market"

For the purposes of this tutorial we're going to assume that you already have a database of the geographical "markets" that you need to target in your application. If you don't, it's surprisingly simple to generate one using the same tools we're using elsewhere, this gist might point you on the way.

So here we assume basically that you have a Market model that contains a location which, when stored in MongoDB, is a two-member array of a longitude and latitude. The model (simplified) might look like this (and would be located at app/models/market.rb):

class Market
  include MongoMapper::Document

  key :name, String
  key :location, Array
end

The first thing you'll want to do is make a geospatial index in MongoDB. This can be done by adding the following line to your model:

ensure_index [[:location, '2d']]

Now MongoDB will automatically perform the necessary indexing to allow you to query geographically based on the data. Note that MongoDB's defaults are set up for latitude and longitude so no further configuration is required.

So in the most simple case, what if we happen to know the user's longitude and latitude already? We should make a method that will find the nearest market to that user in our Market model:

def self.nearest(location)
  where(:location => {'$near' => location}).limit(1).first
end

Well, that's pretty simple. Using the MongoDB geospatial querying format, we can easily find documents that are close to the specified latitude and longitude and return the first one.

Adding IP Geolocation to the Mix

But I don't know my user's longitude and latitude, you say. Not to fear! We can simply modify our nearest method to handle additional cases, in this case that an IP address is passed in:

def self.nearest(query)
  case query
    when Array
      location = query
    when String
      geo = Geokit::Geocoders::MultiGeocoder.geocode(location)
      if geo.lat.nil?
        # Return a default location, the geocoder couldn't find it.
        # How about New York City?
        location = [-73.98,40.77]
      else
        location = [geo.long, geo.lat]
  end
  where(:location => {'$near' => location}).limit(1).first
end

This looks much more complicated than it is (and for everything it's doing, it doesn't really look that complicated). Essentially our nearest method now checks the passed value to see if it's already a long/lat tuple. If it is, it passes that along. If, however, it's a string (such as '123.455.231.23') then it will use Geokit's brilliant multi-geocoder to automagically pull a location from the cloud.

Of course, sometimes an IP won't map properly to a location (such as 127.0.0.1), and so we need a backup plan. In this case, we're just returning the coordinates of New York, NY (it's the biggest market in America, so that's probably your best guess!).

So now we can call Market.nearest([-55,23]) or Market.nearest('123.455.231.23') or actually even Market.nearest('Smallville, KS') (though this is beyond the intended scope of the tutorial) and we will automatically be given the nearest target market.

Making it Railsy

So now that we've done the hard part, how do we integrate this into our Rails app? Why, we just add a quick helper to the application_controller.rb:

class ApplicationController < ActionController::Base
  # ...
  protected

  def nearest_market
    @nearest_market ||= Market.nearest(request.remote_ip)
  end
  helper_method :nearest_market
end

And that's all there is to it! Now in any view in my app, I could easily write something like:

<h1>Welcome, citizen of <b><%= nearest_market.name %></b></h1>

Of course, I can also do many more interesting things, such as find nearby users, or events, or companies, or whatever else my app might need to find.

This is a simple way to make the very first impression that someone has of your application a little bit more personalized. To make your app have a good experience, you should allow people to choose their target market (even without signing up, perhaps by storing it in a cookie), but this is a strong start. So get out there and geolocate!