Skip to main content

Mobomo webinars-now on demand! | learn more.

I really like using MongoDB and Mongoid, but a while back I ran into some shortcomings with querying timestamps. The problem was that I wanted to query only part of a timestamp, such as the day, week or year. So for example, let's say we need to find all users that signed up on a Wednesday.

In SQL there are date functions that let you to parse dates inside your query (although they seem to vary between engines). So in Postgres, you could do something like this:

select * from users where extract(dow from created_at) = 3; 

Note: Wednesday is the 3rd day of the week.

But MongoDB doesn’t have any native support for parsing a date/time inside the query. The best you can do is compare ranges, like this example using Mongoid:

User.where(:created_at.gte => "2012-05-30", :created_at.lt => "2012-05-31") 

Great, that finds us all users created last Wednesday. But what about all users created on any Wednesday, say in 2012? That would typically require building a query with different ranges for every Wednesday in 2012. Talk about tedious and repetitive. I think it’s safe to say that when faced with such a task most developers will end up just looping over each user, comparing the dates in Ruby.

User.scoped.select { |u| u.created_at.wday == 3 && u.created_at.year == 2012 } 

Eeek! This might work with small collections, but once you have a bunch of users it’s sub-optimal.

So I know I just said there were no native date functions in Mongo. But recently I was excited to find a solution that kind of works. It turns out that date/time types in Mongo get stored as UTC datetimes, which are basically just javascript dates stored in BSON. So it’s possible to drop down into javascript in your query using $where. With Mongoid it might look something like this:

User.where("return this.created_at.getDay() == 2 && this.created_at.getFullYear() == 2012") 

Note: in Javascript the day of week starts with 0 instead of 1. So Wednesdays are 2.

Now things seem to be looking up for us. But alas, the MongoDB documentation for $where warns of major performance issues. This makes sense because what’s really happening here is each user record is still getting accessed and each date is still getting parsed with javascript. Furthermore, we can’t index our search. So this solution is probably only marginally better than looping over each record in Ruby.

What I really wanted was a way to query by just day of week, or month, or hour, minute, second, etc. And I decided the best way to accomplish that would be to parse each timestamp before it gets saved, and then store all the additional timestamp metadata along with the record. That way I could query timestamp parts just like any other field, with no parsing. And as an added bonus, it should be even faster than using the native date functions with SQL!

So I started thinking of all the fields I would want to store, and I came up with the following list:

  • year
  • month
  • day
  • wday
  • hour
  • min
  • sec
  • zone
  • offset

But that’s a lot of fields cluttering up our model, especially if we’re storing two different timestamps like a created_at and updated_at. Well fortunately this is one area where MongoDB really shines. We can simply nest all this metadata under each timestamp field as BSON. And since we’re using Mongoid, we can also override the serialize and deserialize methods to make the interface behave just like a regular time field. So this is where the idea for the mongoid-metastamp gem came from. Here’s a simple usage example:

class MyEvent   include Mongoid::Document   field :timestamp, type: Mongoid::Metastamp::Time end  event = MyEvent.new event.timestamp = "2012-05-30 10:00" 

Now, calling a timestamp field returns a regular time:

event.timestamp => Wed, 30 May 2012 10:00:00 UTC +00:00 

But you can also access all the other timestamp metadata like this:

event['timestamp'] => {"time"=>2012-05-30 10:00:00 UTC, "year"=>2012, "month"=>5, "day"=>30, "wday"=>3, "hour"=>10, "min"=>0, "sec"=>0, "zone"=>"UTC", "offset"=>0} 

Now at last, we can performantly search for all Wednesday events in 2012:

hump_days = MyEvent.where("timestamp.wday" => 3, "timestamp.year" => 2012) 

If you were paying close attention you may have also noticed that zone is included in the metadata. That's because Mongoid Metastamp has some powerful features that allow you to store and query timestamps relative to the local time they were created in. But I’ll have to write more about that in a follow up post.

Categories
Author

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!

Categories
Author

10gen, the purveyors of everyone's favorite document-oriented database MongoDB, recently held a gathering of developers, sysadmins, devops, and other parties interested in MongoDB in D.C. appropriately named MongoDC. I was fortunate enough to be asked to speak on how we use MongoDB here at Intridea, so I delivered a little treatise on one of our client projects. Although we've moved most of the project operations off of MongoDB now for a few reasons, working with such a deep and wide dataset was an interesting experience that a lot of people don't encounter when using document-oriented databases. (You can find my slides below)


MongoDC was one of the only non-Ruby conferences I've attended in the past few years. Actually, the only other one in recent memory has been the MySQL Conference and Expo, which created an interesting and stark contrast to how these companies and communities interact. At the MySQL conference, the conversations and gathering were largely dominated by vendors and enterprise software product pitches. The event felt rather out of touch and sterile to me, but that may have been attributed partially to it being a larger, O'Reilly-powered event and partially to the fact that I'm not really a "MySQL guy." On the other hand, the 10gen event felt warm from the second I walked through the door. A 10gen rep greeted me, attendees were having interesting conversations, and the whole day felt like people were excited and interested in the content.

I think 10gen has cultivated a great community of people interested in their products, but I do wonder how to get more people interested in contributing. Currently, the core server is built about 90% to 95% by 10gen's employees, which is not really how an open source company is supposed to work, right? But it seems like the contribution bug hasn't really caught on among those interested just yet. I attended a talk about contributing to MongoDB in various capacities and was joined by about 4 other people (including a 10gen employee). Given MongoDC was a 2 track event, that means people cared more about "neat tricks" with MongoDB than how to improve the core product. Sort of disappointing. This isn't to say that 10gen is doing a bad job at getting people excited (they're doing awesome), but I guess my question is this: how can those of us who are starting to contribute to MongoDB in meaningful ways get others as excited about it as people are about Rails or jQuery? Not sure there's a good answer at this point as 10gen's community stuff is still taking shape, but I'm very hopeful and interested to see how it shapes up.

I'll be speaking at another MongoDB event in Atlanta in February, and I hope to get a feel for why people aren't contributing more. Even if I don't, I'm still really looking forward to it! The event in D.C. was smashing, and from what I can tell, the other events 10gen has been putting on around the nation are just as good. If you're able to make it to any of these events, I heartily suggest you do so. It'll be worth your time!

Categories
Author

Earlier this month Pradeep and I had the opportunity to fly to Montreal to speak at Confoo, a multi-disciplinary web technology conference that grew out of a PHP conference. One of the days had a Ruby track and I had the opportunity to present a talk entitled Persistence Smoothie: Blending SQL and NoSQL. While the sessions weren’t recorded, I’ve taken up the practice of screencasting my talks to be able to provide some semblance of a recording afterwards:

I had a great time at the conference and got to speak to some others who were using NoSQL systems in the real world. Montreal was a lot of fun (getting to eat foie gras at Pied de Cochon certainly didn’t hurt) and I’m looking forward to giving a refined version of the same talk at RubyNation 2010

Update: I forgot to include a link to the source code for the application demoed during the talk. Here it is on GitHub.

Categories
Author
1
Subscribe to Mongodb