mongoid-mapreduce 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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: