MaxMind GeoLite on Rails with PostGIS

Posted October 15th, 2006 in Mapping, Programming, Rails and or Ruby, Tutorials

Ughhhh, many hours of frustration have been spent trying to push the MaxMind GeoLite City CSV files into a database configured with PostGIS. Anyway, I figured I would document the process that I went through incase someone else wants some free IP based geocoding in there Rails app.

MaxMind gives you two CSV files for the city database. One has an assload (2,783,434) of IP blocks that map to the id of a row in the other table containing location data. The location data is pretty rich. It contains a postal code, area code, dma code, country, region (state in US), and a latitude and longitude. Because I wanted to be cool and more efficient, I decided to store the lat / lon as a point in PostGIS format. Using PostGIS along with Guilhem Vellut’s Spatial Adaptor plugin for Rails, makes doing geo-spatial operations sexier and easier than ever. You can use the Spatial Adaptor with MySQL, but you’ll never be as cool as the guy using it with PostgreSQL.

First off I had to create my tables.

create_table "geo_ip_locations" do |t|
  t.column "country", :string, :limit => 2
  t.column "region", :string, :limit => 2
  t.column "city", :string
  t.column "postal_code", :string, :limit => 7
  t.column "dma_code", :integer, :limit => 3
  t.column "area_code", :integer, :limit => 3
  t.column "geom", :point, :null => false, :srid => 4269, :with_z => false
end

add_index :geo_ip_locations, :geom, :spatial => true

create_table "geo_ips" do |t|
  t.column "start_ip", :integer, :limit => 10
  t.column "end_ip", :integer, :limit => 10
  t.column "geo_ip_location_id", :integer
end

add_index :gep_ips, :start_ip
add_index :geo_ips, :end_ip
add_index :geo_ips, :geo_ip_location_id

A few things to note:

  • The geom column has the srid set to 4269, this threw me for a loop for way too long. Make sure you have the spatial_ref_sys table populated. I’m also assuming that the MaxMind data is mapped to the WGS 84 standard.
  • The limit’s on the start and end ip’s are set to 10. This creates bigint columns in my DB. I’m not sure what version of Rails started doing this (I’m on Edge), but the columns have to be bigints for the IP block information to be imported.
  • Remember to create a couple models for the tables
  • You may want to add an index on postal_code or anything else you’ll be using in a SQL WHERE clause

Now because the CSV file with the blocks of IP addresses in it is so large I went with an importing tool to get it into the table. I was going to use PostgreSQL’s COPY command, but it doesn’t seem to support CSV’s with double quotes. I went with Navicat and took advantage of my 30 day trial. The CSV with the locations in it is another story. We need to convert the lat / lon pairs into points for PostGIS. Not only that, but if we want to be really cool we need to convert the city names into UTF-8. The file that MaxMind gives us has ISO-8859-1 encoding. Iconv comes to the rescue. You can either convert the entire file from the command line, or just use the ruby library to do it in the import script below.

require "#{File.dirname(__FILE__)}/../../config/environment"
require 'fastercsv'
require 'iconv'
ICONV = Iconv.new( 'UTF-8', 'ISO-8859-1' )

FasterCSV::HeaderConverters[:underscore] = lambda { |h| h.underscore }

FasterCSV.foreach('geo_ip_locations.csv', {:headers => :first_row, :col_sep => ",", :header_converters => :underscore}) do |row|
  begin
    row['id'] = row.delete('loc_id')[1]
    row['geom'] = Point.from_x_y(row.delete('longitude')[1].to_f, row.delete('latitude')[1].to_f, 4326)
    row['city'] = ICONV.iconv(row['city'])
    cool = GeoIpLocation.new(row.to_hash)
    cool.id = row['id']
    cool.save!
    $stdout.print '.'
  rescue
    puts $!.message
    $stdout.print 'f'
  end
  $stdout.flush
end

Things to note:

  • You need FasterCSV for this to work gem install fastercsv
  • You need to delete the first line (with the copyright info in it) in the CSV for this script to work.
  • If you converted the file from the command line, comment out the three lines referencing iconv to improve performance.
  • I created a directory called transform in my db directory, renamed and moved the location CSV there, and created a file with the above code in it.
  • Always remember to put the srid in the Point.from_x_y call, otherwise you’ll be very very frustrated.
  • You’ll need to specify encoding: unicode Postgres, encoding: utf8 MySQL in database.yml to use UTF-8 with the DB. See the Rails wiki
  • The id is set this way to preserve it.

That’s it! There you have it, the possibilities are now endless. As I said before, I set this up on PostgreSQL, but this should work the same on MySQL. The one main difference will be the srid’s. MySQL doesn’t support them, so you don’t need to specify them. Goodluck!

Kinda like: | | | | | | | |

5 Responses to “MaxMind GeoLite on Rails with PostGIS”

  1. Maurits Maurits Says:

    your demo is dead

  2. Frenchy Frenchy Says:

    Just wondering. Were there restrictions in place that meant you had to use SQL instead of the MaxMind Binary? I have had much better, and faster results using the binary especially when doing many lookups at a time. Maxmind GeoIP (GeoIPCity) Binary Database with Ruby on Rails for Geocoding

  3. Extefearady Extefearady Says:

    It’s a funny theme! Extrefox

  4. Evandro Evandro Says:

    Nice post!

  5. td visa td visa Says:

    Nice Site! http://google.com


Leave a Reply