mongoid-mapreduce 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -6,7 +6,13 @@ Mongoid MapReduce provides simple aggregation functions to your models using Mon
6
6
 
7
7
  ## How simple is simple?
8
8
 
9
- Very. You provide a Mongoid model, criteria, map key and a list of fields to be aggregated. It returns a list of results (one per unique map key value).
9
+ Short answer: very!
10
+
11
+ There are two map/reduce formulae:
12
+
13
+ **Aggregates:** Provide a map key and a list of fields to be aggregated via addition.
14
+
15
+ **Array List:** Provide an array field, the values will be individually aggregated via addition.
10
16
 
11
17
  ## Getting Started
12
18
 
@@ -28,6 +34,7 @@ class Employee
28
34
  field :awards, :type => Integer
29
35
  field :age, :type => Integer
30
36
  field :male, :type => Integer
37
+ field :rooms, :type => Array
31
38
  end
32
39
  ```
33
40
 
@@ -35,12 +42,12 @@ You can now use the *map_reduce* method on your model to aggregate data:
35
42
 
36
43
  ```ruby
37
44
  # Create a few example employees
38
- Employee.create :name => 'Alan', :division => 'Software', :age => 20, :awards => 5, :male => 1
39
- Employee.create :name => 'Bob', :division => 'Software', :age => 25, :awards => 4, :male => 1
40
- Employee.create :name => 'Chris', :division => 'Hardware', :age => 30, :awards => 3, :male => 1
41
- Employee.create :name => 'Darcy', :division => 'Sales', :age => 35, :awards => 3, :male => 0
45
+ Employee.create :name => 'Alan', :division => 'Software', :age => 20, :awards => 5, :male => 1, :rooms => [1,2,3]
46
+ Employee.create :name => 'Bob', :division => 'Software', :age => 25, :awards => 4, :male => 1, :rooms => [1,2,3]
47
+ Employee.create :name => 'Chris', :division => 'Hardware', :age => 30, :awards => 3, :male => 1, :rooms => [4,5,6]
48
+ Employee.create :name => 'Darcy', :division => 'Sales', :age => 35, :awards => 3, :male => 0, :rooms => [1,2,3,4,5,6]
42
49
 
43
- # Produces 3 records, one for each division.
50
+ # Aggregate formula: produces 3 records, one for each division.
44
51
  divs = Employee.map_reduce(:division, :fields => [:age, :awards])
45
52
  divs.length # => 3
46
53
  divs.find('Software').age # => 45
@@ -50,6 +57,15 @@ divs.last.age # => 35
50
57
  divs.keys # => ['Hardware', 'Software', 'Sales']
51
58
  divs.has_key?('Sales') # => true
52
59
  divs.to_hash # => { "Software" => ..., "Hardware" => ..., "Sales" => ... }
60
+
61
+ # Array Value formula: produces 6 records, one for each room.
62
+ rooms = Employee.map_reduce do
63
+ field :rooms, :formula => :array_values
64
+ end
65
+ rooms.length # => 6
66
+ rooms.find(1)._count # => 3
67
+ rooms.counts["5"] # => 2
68
+ rooms.counts # => { "1" => 3, "2" => 3, "3" => 3, "4" => 2, "5" => 2, "6" => 2 }
53
69
  ```
54
70
 
55
71
  You can also add Mongoid criteria before the operation:
@@ -61,6 +77,29 @@ divs.length # => 2
61
77
  divs.has_key?('Sales') # => false
62
78
  ```
63
79
 
80
+ You choose to supply fields as arguments or in a block:
81
+
82
+ ```ruby
83
+ # These are the same:
84
+ Employee.where(:age.gt => 20).map_reduce(:division, :fields => [:age, :awards])
85
+ Employee.where(:age.gt => 20).map_reduce(:division) do
86
+ field :age
87
+ field :awards
88
+ end
89
+ ```
90
+
91
+ Fields can be of any type supported by Mongoid serialization, and field type is specified in block configuration:
92
+
93
+ ```ruby
94
+ divs = Employee.map_reduce(:division) do
95
+ field :age, :type => Integer
96
+ field :awards, :type => Float
97
+ end
98
+
99
+ divs.find('Software').age # => 60
100
+ divs.find('Software').awards # => 9.0
101
+ ```
102
+
64
103
  Additional meta fields are included in the results:
65
104
 
66
105
  NOTE: _key_name and _key_value are discarded when converting to Hash.
@@ -77,15 +116,6 @@ divs.find('Software')._count # => 2
77
116
  Employee.map_reduce(:division, :count_field => :num).find('Software').num #=> 2
78
117
  ```
79
118
 
80
- You can also choose to supply fields in a block:
81
-
82
- ```ruby
83
- Employee.where(:age.gt => 20).map_reduce(:division) do
84
- field :age
85
- field :awards
86
- end
87
- ```
88
-
89
119
  ## Enhancements and Pull Requests
90
120
 
91
121
  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.
@@ -18,7 +18,9 @@ module Mongoid
18
18
  end
19
19
 
20
20
  if options.key?(:fields)
21
- reducer.fields = options[:fields].collect {|f| f.to_sym }
21
+ options[:fields].each do |f|
22
+ reducer.field f.to_sym
23
+ end
22
24
  end
23
25
 
24
26
  reducer.instance_eval(&block) if block.present?
@@ -10,7 +10,7 @@ module Mongoid
10
10
  # Returns value of super
11
11
  def initialize(attrs)
12
12
  attrs.each do |k, v|
13
- self[k.to_sym] = v
13
+ self[k] = v
14
14
  end
15
15
  super
16
16
  end
@@ -23,7 +23,9 @@ module Mongoid
23
23
  #
24
24
  # Returns value of supplied symbol/string if exists
25
25
  def method_missing(sym, *args, &block)
26
- if self.has_key?(sym.to_sym)
26
+ if self.has_key?(sym)
27
+ return self[sym]
28
+ elsif self.has_key?(sym.to_sym)
27
29
  return self[sym.to_sym]
28
30
  elsif self.has_key?(sym.to_s)
29
31
  return self[sym.to_s]
@@ -3,7 +3,7 @@ module Mongoid
3
3
 
4
4
  class Reducer
5
5
 
6
- attr_accessor :fields, :count_field
6
+ attr_accessor :count_field
7
7
 
8
8
  # Initialize the reducer with given values
9
9
  #
@@ -17,21 +17,92 @@ module Mongoid
17
17
  @selector = selector
18
18
  @map_key = map_key
19
19
  @count_field = :_count
20
- @fields = []
20
+ @fields = {}
21
+ end
22
+
23
+ # Obtain the field we are using for the map, if using an array values map.
24
+ #
25
+ # Returns true or false
26
+ def map_array_field
27
+ @fields.select { |k, v| v[:formula] == :array_values }.first
28
+ end
29
+
30
+ # Determines whether or not we are mapping from an array value
31
+ #
32
+ # Returns true or false
33
+ def map_from_array?
34
+ @fields.select { |k, v| v[:formula] == :array_values }.any?
21
35
  end
22
36
 
23
37
  # Generates the JavaScript map function
24
38
  #
39
+ # If we have any fields defined with a map function of :array_values, use that to map
40
+ # otherwise, use our aggregate map function.
41
+ #
25
42
  # Returns String
26
43
  def map
27
- "function() { emit(this.#{@map_key}, [1, #{@fields.collect{|k| "this.#{k}"}.join(", ")}]); }"
44
+ fn = "function() { "
45
+ if map_from_array?
46
+ fn << map_array_values(map_array_field)
47
+ else
48
+ fn << map_aggregates
49
+ end
50
+ fn << "}"
51
+ fn
52
+ end
53
+
54
+ # Generate a map function from one unique map key and a number of aggregate sources
55
+ #
56
+ #
57
+ def map_aggregates
58
+ fields = @fields.select { |k, v| v[:formula] == :aggregate }
59
+ "emit (this.#{@map_key}, [#{[1, fields.collect{|k,v| "this.#{k}"}].flatten.join(", ")}]); "
60
+ end
61
+
62
+ # Generate a map function from one unique map key and a number of aggregate sources
63
+ #
64
+ #
65
+ def map_array_values(field)
66
+ "this.#{field[0].to_s}.forEach(function(value) { emit(value, 1); }); "
28
67
  end
29
68
 
30
69
  # Generates the JavaScript reduce function
31
70
  #
32
71
  # Returns String
33
72
  def reduce
34
- "function(k, v) { var results = [0#{",0" * @fields.length}]; v.forEach(function(v){ [0,#{@fields.collect.with_index{|k,i| i+1}.join(",")}].forEach(function(k){ results[k] += v[k] }) }); return results.toString(); }"
73
+ fn = "function(k, v) { "
74
+ if map_from_array?
75
+ fn << reduce_array_values
76
+ else
77
+ fn << reduce_aggregates
78
+ end
79
+ fn << "}"
80
+ fn
81
+ end
82
+
83
+ # Generates a reduce function for aggregate map
84
+ #
85
+ # Returns String
86
+ def reduce_aggregates
87
+ fields = @fields.select { |k, v| v[:formula] == :aggregate }
88
+ fn = ""
89
+ fn << "var results = [#{(["0"] * (fields.length + 1)).flatten.join(", ")}]; "
90
+ fn << "v.forEach(function(val) { "
91
+ fn << "for(var i=0; i<= #{fields.length}; i++) { "
92
+ fn << "results[i] += val[i] "
93
+ fn << "} "
94
+ fn << "}); "
95
+ fn << "return results.toString(); "
96
+ end
97
+
98
+ # Generates a reduce function for array values
99
+ #
100
+ # Returns String
101
+ def reduce_array_values
102
+ fn = ""
103
+ fn << "var result = 0; "
104
+ fn << "v.forEach(function(val) { result += val; }); "
105
+ fn << "return result; "
35
106
  end
36
107
 
37
108
  # Adds a field to the map/reduce operation
@@ -39,8 +110,22 @@ module Mongoid
39
110
  # sym - String or Symbol, name of field to add
40
111
  #
41
112
  # Returns nothing.
42
- def field(sym)
43
- @fields << sym.to_sym
113
+ def field(sym, options={})
114
+ options[:type] ||= Integer
115
+ options[:formula] ||= :aggregate
116
+ @fields[sym.to_sym] = options
117
+ end
118
+
119
+ # Serialize an object to the specified class
120
+ #
121
+ # obj - Object to serialize
122
+ # klass - Class to prefer
123
+ #
124
+ # Returns serialized object or nil
125
+ def serialize(obj, klass)
126
+ return nil if obj.blank?
127
+ obj = obj.to_s =~ /(^[-+]?[0-9]+$)|(\.0+)$/ ? Integer(obj) : Float(obj)
128
+ Mongoid::Fields::Mappings.for(klass).allocate.serialize(obj)
44
129
  end
45
130
 
46
131
  # Runs the map/reduce operation and returns the result
@@ -52,10 +137,18 @@ module Mongoid
52
137
  res = @klass.collection.map_reduce(map, reduce, { query: @selector, out: "#map_reduce" } ).find.to_a
53
138
  return res.inject(Results.new) do |h, k|
54
139
  idx = k.values[0]
55
- d = (k.values[1].is_a?(String) ? k.values[1].split(',') : k.values[1]).collect {|i| i.is_a?(Boolean) ? (i ? 1 : 0) : i.to_i }
56
- doc = Document.new :_key_name => @map_key.to_sym, :_key_value => idx, @map_key.to_sym => idx, @count_field.to_sym => d[0]
57
- @fields.flatten.each_with_index do |k, i|
58
- doc[k.to_sym] = d[i + 1]
140
+ d = (k.values[1].is_a?(String) ? k.values[1].split(',') : k.values[1])
141
+
142
+ if d.is_a?(Array)
143
+ doc = Document.new :_key_name => @map_key.to_s, :_key_value => idx, @map_key => idx, @count_field => d[0].to_i
144
+ @fields.each_with_index do |h, i|
145
+ doc[h[0].to_sym] = serialize(d[i + 1], h[1][:type])
146
+ end
147
+ else
148
+ f = map_array_field[0]
149
+ k = serialize(idx, map_array_field[1][:type])
150
+ v = d.to_i
151
+ doc = Document.new :_key_name => f, :_key_value => k, k.to_s => v, @count_field => v
59
152
  end
60
153
  h << doc
61
154
  end
@@ -45,7 +45,14 @@ module Mongoid
45
45
  #
46
46
  # Returns Hash
47
47
  def to_hash
48
- self.each.inject({}){|h, doc| h[doc._key_value] = doc.to_hash; h }
48
+ self.each.inject({}){|h, doc| h[doc._key_value.to_s] = doc.to_hash; h }
49
+ end
50
+
51
+ # Simplifies the Results to a Hash containing only a key and a single value (the count)
52
+ #
53
+ # Returns Hash
54
+ def counts
55
+ self.each.inject({}) {|h, doc| h[doc._key_value.to_s] = doc._count; h }
49
56
  end
50
57
 
51
58
  end
@@ -1,7 +1,7 @@
1
1
  module Mongoid
2
2
  module MapReduce
3
3
 
4
- VERSION = '0.1.0'
4
+ VERSION = '0.1.1'
5
5
 
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid-mapreduce
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-09-22 00:00:00.000000000Z
12
+ date: 2011-09-23 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mongoid
16
- requirement: &70365613354940 !ruby/object:Gem::Requirement
16
+ requirement: &70109620996440 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '2.0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70365613354940
24
+ version_requirements: *70109620996440
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: bson_ext
27
- requirement: &70365613354440 !ruby/object:Gem::Requirement
27
+ requirement: &70109620995920 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '1.3'
33
33
  type: :runtime
34
34
  prerelease: false
35
- version_requirements: *70365613354440
35
+ version_requirements: *70109620995920
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: growl
38
- requirement: &70365613354060 !ruby/object:Gem::Requirement
38
+ requirement: &70109620995540 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70365613354060
46
+ version_requirements: *70109620995540
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: rake
49
- requirement: &70365613353520 !ruby/object:Gem::Requirement
49
+ requirement: &70109620994980 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ~>
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: 0.9.2
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *70365613353520
57
+ version_requirements: *70109620994980
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: rspec
60
- requirement: &70365613353020 !ruby/object:Gem::Requirement
60
+ requirement: &70109620994340 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ~>
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '2.6'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *70365613353020
68
+ version_requirements: *70109620994340
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: guard-rspec
71
- requirement: &70365613352560 !ruby/object:Gem::Requirement
71
+ requirement: &70109620993880 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ~>
@@ -76,7 +76,7 @@ dependencies:
76
76
  version: 0.4.3
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *70365613352560
79
+ version_requirements: *70109620993880
80
80
  description: Mongoid MapReduce provides simple aggregation features for your Mongoid
81
81
  models
82
82
  email: