Mongoid MapReduce

Mongoid MapReduce provides simple aggregation functions to your models using MongoDB map/reduce.

travis

How simple is simple?

Short answer: very!

There are two map/reduce formulae:

Aggregates: Provide a map key and a list of fields to be aggregated via addition.

Array List: Provide an array field, the values will be individually aggregated via addition.

Getting Started

First, add mongoid-mapreduce to your Gemfile:

gem 'mongoid-mapreduce'

Next, include the module in any models for collections you'll be wanting to map/reduce on:

class Employee
  include Mongoid::Document
  include Mongoid::MapReduce

  field :name
  field :division
  field :awards, :type => Integer
  field :age, :type => Integer
  field :male, :type => Integer
  field :rooms, :type => Array
end

You can now use the map_reduce method on your model to aggregate data using your choice of aggregation formula.

# Create a few example employees
Employee.create :name => 'Alan', :division => 'Software', :age => 20, :awards => 5, :male => 1, :rooms => [1,2,3]
Employee.create :name => 'Bob', :division => 'Software', :age => 25, :awards => 4, :male => 1, :rooms => [1,2,3]
Employee.create :name => 'Chris', :division => 'Hardware', :age => 30, :awards => 3, :male => 1, :rooms => [4,5,6]
Employee.create :name => 'Darcy', :division => 'Sales', :age => 35, :awards => 3, :male => 0, :rooms => [1,2,3,4,5,6]

# Aggregate formula (the default): produces 3 records, one for each division.
divs = Employee.map_reduce(:division, :fields => [:age, :awards])
divs.length               # => 3
divs.find('Software').age # => 45
divs['Hardware'].awards   # => 3
divs.first.awards         # => 9
divs.last.age             # => 35
divs.keys                 # => ['Hardware', 'Software', 'Sales']
divs.has_key?('Sales')    # => true
divs.to_hash              # => { "Software" => ..., "Hardware" => ..., "Sales" => ... }

# Array Value formula: produces 6 records, one for each room. Does not take any fields.
rooms = Employee.map_reduce(:rooms, :formula => :array_values)
rooms.length          # => 6
rooms.find(1)._count  # => 3
rooms.counts["5"]     # => 2
rooms.counts          # => { "1" => 3, "2" => 3, "3" => 3, "4" => 2, "5" => 2, "6" => 2 }

You can also add Mongoid criteria before the operation:

# Produces 2 records, one for each matching division (men only!)
divs = Employee.where(:male => 1).map_reduce(:division, :fields => [:age, :awards])
divs.length               # => 2
divs.has_key?('Sales')    # => false

You choose to supply fields as arguments or in a block:

# These are the same:
Employee.where(:age.gt => 20).map_reduce(:division, :fields => [:age, :awards])
Employee.where(:age.gt => 20).map_reduce(:division) do
  field :age
  field :awards
end

Fields can be of any type supported by Mongoid serialization, and field type is specified in block configuration:

divs = Employee.map_reduce(:division) do
  field :age, :type => Integer
  field :awards, :type => Float
end

divs.find('Software').age     # => 60
divs.find('Software').awards  # => 9.0

Additional meta fields are included in the results:

NOTE: _key_name and _key_value are discarded when converting to Hash.

# Produces 2 records, one for each matching division (men only!)
divs = Employee.map_reduce(:division, :fields => [:age, :awards])
divs.find('Software')._key_name   # => :division
divs.find('Software')._key_value  # => "Software"
divs.find('Software').division    # => "Software" (_key_name => _key_value)
divs.find('Software')._count      # => 2

# You can choose another name for the count field
Employee.map_reduce(:division, :count_field => :num).find('Software').num  #=> 2

Enhancements and Pull Requests

If you find the project useful but it doesn't meet all of your needs, feel free to fork it and send a pull request.

License

MIT license, go wild.