outoftime-sunspot 0.8.9 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/README.rdoc +13 -21
  2. data/Rakefile +0 -2
  3. data/TODO +2 -15
  4. data/VERSION.yml +2 -2
  5. data/bin/sunspot-configure-solr +46 -0
  6. data/bin/sunspot-solr +15 -7
  7. data/lib/sunspot/adapters.rb +5 -1
  8. data/lib/sunspot/composite_setup.rb +186 -0
  9. data/lib/sunspot/configuration.rb +7 -1
  10. data/lib/sunspot/data_extractor.rb +10 -0
  11. data/lib/sunspot/date_facet.rb +36 -0
  12. data/lib/sunspot/date_facet_row.rb +17 -0
  13. data/lib/sunspot/dsl/field_query.rb +72 -0
  14. data/lib/sunspot/dsl/fields.rb +30 -3
  15. data/lib/sunspot/dsl/query.rb +16 -35
  16. data/lib/sunspot/dsl/query_facet.rb +31 -0
  17. data/lib/sunspot/dsl/scope.rb +76 -20
  18. data/lib/sunspot/dsl/search.rb +30 -0
  19. data/lib/sunspot/dsl.rb +1 -1
  20. data/lib/sunspot/facet.rb +17 -3
  21. data/lib/sunspot/facet_row.rb +4 -4
  22. data/lib/sunspot/field.rb +130 -207
  23. data/lib/sunspot/field_factory.rb +126 -0
  24. data/lib/sunspot/indexer.rb +61 -14
  25. data/lib/sunspot/instantiated_facet.rb +38 -0
  26. data/lib/sunspot/instantiated_facet_row.rb +12 -0
  27. data/lib/sunspot/query/base_query.rb +90 -0
  28. data/lib/sunspot/query/connective.rb +77 -0
  29. data/lib/sunspot/query/dynamic_query.rb +39 -56
  30. data/lib/sunspot/query/field_facet.rb +132 -4
  31. data/lib/sunspot/query/field_query.rb +57 -0
  32. data/lib/sunspot/query/pagination.rb +1 -1
  33. data/lib/sunspot/query/query_facet.rb +72 -0
  34. data/lib/sunspot/query/query_facet_row.rb +19 -0
  35. data/lib/sunspot/query/restriction.rb +9 -7
  36. data/lib/sunspot/query/scope.rb +165 -0
  37. data/lib/sunspot/query/sort.rb +17 -14
  38. data/lib/sunspot/query/sort_composite.rb +33 -0
  39. data/lib/sunspot/query.rb +162 -351
  40. data/lib/sunspot/query_facet.rb +33 -0
  41. data/lib/sunspot/query_facet_row.rb +21 -0
  42. data/lib/sunspot/schema.rb +165 -0
  43. data/lib/sunspot/search/hit.rb +62 -0
  44. data/lib/sunspot/search.rb +104 -41
  45. data/lib/sunspot/session.rb +64 -32
  46. data/lib/sunspot/setup.rb +119 -48
  47. data/lib/sunspot/type.rb +48 -2
  48. data/lib/sunspot.rb +74 -8
  49. data/solr/solr/conf/schema.xml +44 -225
  50. data/spec/api/build_search_spec.rb +557 -63
  51. data/spec/api/indexer_spec.rb +156 -74
  52. data/spec/api/query_spec.rb +55 -31
  53. data/spec/api/search_retrieval_spec.rb +210 -33
  54. data/spec/api/session_spec.rb +81 -26
  55. data/spec/api/sunspot_spec.rb +5 -7
  56. data/spec/integration/faceting_spec.rb +130 -0
  57. data/spec/integration/keyword_search_spec.rb +72 -31
  58. data/spec/integration/scoped_search_spec.rb +13 -0
  59. data/spec/integration/stored_fields_spec.rb +10 -0
  60. data/spec/mocks/blog.rb +3 -0
  61. data/spec/mocks/comment.rb +12 -23
  62. data/spec/mocks/connection.rb +84 -0
  63. data/spec/mocks/mock_adapter.rb +11 -3
  64. data/spec/mocks/mock_record.rb +41 -0
  65. data/spec/mocks/photo.rb +8 -0
  66. data/spec/mocks/post.rb +18 -23
  67. data/spec/spec_helper.rb +29 -14
  68. data/tasks/gemspec.rake +4 -3
  69. data/tasks/rdoc.rake +2 -2
  70. data/tasks/schema.rake +19 -0
  71. data/templates/schema.xml.haml +24 -0
  72. metadata +48 -7
  73. data/spec/mocks/base_class.rb +0 -2
data/lib/sunspot/query.rb CHANGED
@@ -1,379 +1,190 @@
1
- %w(dynamic_query field_facet pagination restriction sort).each do |file|
1
+ %w(base_query scope field_query connective dynamic_query field_facet query_facet
2
+ query_facet_row pagination restriction sort sort_composite).each do |file|
2
3
  require File.join(File.dirname(__FILE__), 'query', file)
3
4
  end
4
5
 
5
6
  module Sunspot
6
- #
7
- # This class encapsulates a query that is to be sent to Solr. The query is
8
- # constructed in the block passed to the Sunspot.search method, using the
9
- # Sunspot::DSL::Query interface. It can also be accessed directly by calling
10
- # #query on a Search object (presumably a not-yet-run one created using
11
- # Sunspot#new_search), which might be more suitable than the DSL when an
12
- # intermediate object has responsibility for building the query dynamically.
13
- #--
14
- # Instances of Query, as well as all of the components it contains, respond to
15
- # the #to_params method, which returns a hash of parameters in the format
16
- # recognized by the solr-ruby API.
17
- #
18
- class Query
19
- attr_writer :keywords # <String> full-text keyword boolean phrase
20
-
21
- def initialize(types, configuration) #:nodoc:
22
- @types, @configuration = types, configuration
23
- @components = []
24
- @components << @pagination = Pagination.new(@configuration)
25
- end
26
-
27
- #
28
- # Add a restriction to the query.
29
- #
30
- # ==== Parameters
31
- #
32
- # field_name<Symbol>:: Name of the field to which the restriction applies
33
- # restriction_type<Class,Symbol>::
34
- # Subclass of Sunspot::Query::Restriction::Base, or snake_cased name as symbol
35
- # (e.g., +:equal_to+)
36
- # value<Object>::
37
- # Value against which the restriction applies (e.g. less_than(2) has a
38
- # value of 2)
39
- # negated::
40
- # Whether this restriction should be negated (use add_negated_restriction)
41
- #
42
- def add_restriction(field_name, restriction_type, value, negated = false)
43
- if restriction_type.is_a?(Symbol)
44
- restriction_type = Restriction[restriction_type]
7
+ module Query #:nodoc:
8
+ #
9
+ # This class encapsulates a query that is to be sent to Solr. The query is
10
+ # constructed in the block passed to the Sunspot.search method, using the
11
+ # Sunspot::DSL::Query interface. It can also be accessed directly by calling
12
+ # #query on a Search object (presumably a not-yet-run one created using
13
+ # Sunspot#new_search), which might be more suitable than the DSL when an
14
+ # intermediate object has responsibility for building the query dynamically.
15
+ #--
16
+ # Instances of Query, as well as all of the components it contains, respond to
17
+ # the #to_params method, which returns a hash of parameters in the format
18
+ # recognized by the solr-ruby API.
19
+ #
20
+ class Query < FieldQuery
21
+ attr_reader :query_facets #:nodoc:
22
+
23
+ def initialize(setup, configuration) #:nodoc:
24
+ @setup, @configuration = setup, configuration
25
+ @components = []
26
+ @query_facets = {}
27
+ @components << @base_query = BaseQuery.new(setup)
28
+ @components << @pagination = Pagination.new(@configuration)
29
+ @components << @sort = SortComposite.new
45
30
  end
46
- @components << restriction = restriction_type.new(field(field_name), value, negated)
47
- restriction
48
- end
49
31
 
50
- #
51
- # Add a negated restriction to the query. The restriction will be taken as
52
- # the opposite of its usual meaning (e.g., an :equal_to restriction will
53
- # be "not equal to".
54
- #
55
- # ==== Parameters
56
- #
57
- # field_name<Symbol>:: Name of the field to which the restriction applies
58
- # restriction_type<Class>::
59
- # Subclass of Sunspot::Query::Restriction::Base to instantiate
60
- # value<Object>::
61
- # Value against which the restriction applies (e.g. less_than(2) has a
62
- # value of 2)
63
- #
64
- def add_negated_restriction(field_name, restriction_type, value)
65
- add_restriction(field_name, restriction_type, value, true)
66
- end
67
-
68
- #
69
- # Exclude a particular instance from the search results
70
- #
71
- # ==== Parameters
72
- #
73
- # instance<Object>:: instance to exclude from results
74
- #
75
- def exclude_instance(instance)
76
- @components << Restriction::SameAs.new(instance, true)
77
- end
78
-
79
- #
80
- # Add a field facet. See Sunspot::Facet for more information.
81
- #
82
- # ==== Parameters
83
- #
84
- # field_name<Symbol>:: Name of the field on which to get a facet
85
- #
86
- def add_field_facet(field_name)
87
- @components << FieldFacet.new(field(field_name))
88
- end
89
-
90
- #
91
- # Generate a DynamicQuery instance for the given base name.
92
- # This gives you access to a subset of the Query API but the operations
93
- # apply to dynamic fields inside the dynamic field definition specified
94
- # by +base_name+.
95
- #
96
- # ==== Parameters
97
- #
98
- # base_name<Symbol>::
99
- # Base name of the dynamic field definition to use in the dynamic query
100
- # operations
101
- #
102
- # ==== Returns
103
- #
104
- # DynamicQuery::
105
- # Instance providing dynamic query functionality for the given field
106
- # definitions.
107
- #
108
- def dynamic_query(base_name)
109
- DynamicQuery.new(dynamic_field(base_name), self)
110
- end
111
-
112
- #
113
- # Add a component to the query. Used by objects that proxy to the query
114
- # object.
115
- #
116
- # ==== Parameters
117
- #
118
- # component<~to_params>:: Query component to add.
119
- #
120
- def add_component(component) #:nodoc:
121
- @components << component
122
- end
123
-
124
- #
125
- # Sets @start and @rows instance variables using pagination semantics
126
- #
127
- # ==== Parameters
128
- #
129
- # page<Integer>:: Page on which to start
130
- # per_page<Integer>::
131
- # How many rows to display per page. Default taken from
132
- # Sunspot.config.pagination.default_per_page
133
- #
134
- def paginate(page, per_page = nil)
135
- @pagination.page, @pagination.per_page = page, per_page
136
- end
137
-
138
- #
139
- # Set result ordering.
140
- #
141
- # ==== Parameters
142
- #
143
- # field_name<Symbol>:: Name of the field on which to order
144
- # direction<Symbol>:: :asc or :desc (default :asc)
145
- #
146
- def order_by(field_name, direction = nil)
147
- @components << Sort.new(field(field_name), direction)
148
- end
149
-
150
- #
151
- # Build the query using the DSL block passed into Sunspot.search
152
- #
153
- # ==== Returns
154
- #
155
- # Sunspot::Query:: self
156
- #
157
- def build(&block)
158
- Util.instance_eval_or_call(dsl, &block)
159
- self
160
- end
161
-
162
- #
163
- # Representation of this query as solr-ruby parameters. Constructs the hash
164
- # by deep-merging scope and facet parameters, adding in various other
165
- # parameters from instance data.
166
- #
167
- # Note that solr-ruby takes the :q parameter as a separate argument; for
168
- # the sake of consistency, the Query object ignores this fact (the Search
169
- # object extracts it back out).
170
- #
171
- # ==== Returns
172
- #
173
- # Hash:: Representation of query in solr-ruby form
174
- #
175
- def to_params #:nodoc:
176
- params = {}
177
- query_components = []
178
- query_components << @keywords if @keywords
179
- query_components << types_phrase if types_phrase
180
- params[:q] = query_components.map { |component| "(#{component})"} * ' AND '
181
- for component in @components
182
- Util.deep_merge!(params, component.to_params)
32
+ #
33
+ # Set the keywords for this query. Keywords are parsed with Solr's dismax
34
+ # handler.
35
+ #
36
+ def keywords=(keywords)
37
+ set_keywords(keywords)
183
38
  end
184
- params
185
- end
186
39
 
187
- #
188
- # Page that this query will return (used by Sunspot::Search to expose
189
- # pagination)
190
- #
191
- # ==== Returns
192
- #
193
- # Integer:: Page number
194
- #
195
- def page #:nodoc:
196
- @pagination.page
197
- end
198
-
199
- #
200
- # Number of rows per page that this query will return (used by
201
- # Sunspot::Search to expose pagination)
202
- #
203
- # ==== Returns
204
- #
205
- # Integer:: Rows per page
206
- #
207
- def per_page #:nodoc:
208
- @pagination.per_page
209
- end
210
-
211
- #
212
- # Get a DSL instance for building this query.
213
- #
214
- # ==== Returns
215
- #
216
- # Sunspot::DSL::Query:: DSL instance
217
- #
218
- def dsl #:nodoc:
219
- @dsl ||= DSL::Query.new(self)
220
- end
221
-
222
- #
223
- # Get a Sunspot::Field::Base instance corresponding to the given field name
224
- #
225
- # ==== Parameters
226
- #
227
- # field_name<Symbol>:: The field name for which to find a field
228
- #
229
- # ==== Returns
230
- #
231
- # Sunspot::Field::Base:: The field object corresponding to the given name
232
- #
233
- # ==== Raises
234
- #
235
- # ArgumentError::
236
- # If the given field name is not configured for the types being queried
237
- #
238
- def field(field_name) #:nodoc:
239
- fields_hash[field_name.to_sym] || raise(UnrecognizedFieldError, "No field configured for #{@types * ', '} with name '#{field_name}'")
240
- end
40
+ #
41
+ # Add a component to the query. Used by objects that proxy to the query
42
+ # object.
43
+ #
44
+ # ==== Parameters
45
+ #
46
+ # component<~to_params>:: Query component to add.
47
+ #
48
+ def add_component(component) #:nodoc:
49
+ @components << component
50
+ end
241
51
 
242
- def dynamic_field(field_name)
243
- field = dynamic_fields_hash[field_name.to_sym] || raise(UnrecognizedFieldError, "No dynamic field configured for #{@types * ', '} with name #{field_name.inspect}")
244
- end
52
+ #
53
+ # Sets @start and @rows instance variables using pagination semantics
54
+ #
55
+ # ==== Parameters
56
+ #
57
+ # page<Integer>:: Page on which to start
58
+ # per_page<Integer>::
59
+ # How many rows to display per page. Default taken from
60
+ # Sunspot.config.pagination.default_per_page
61
+ #
62
+ def paginate(page, per_page = nil)
63
+ @pagination.page, @pagination.per_page = page, per_page
64
+ end
245
65
 
246
- #
247
- # Pass in search options as a hash. This is not the preferred way of
248
- # building a Sunspot search, but it is made available as experience shows
249
- # Ruby developers like to pass in hashes. Probably nice for quick one-offs
250
- # on the console, anyway.
251
- #
252
- # ==== Options (+options+)
253
- #
254
- # :keywords:: Keyword string for fulltext search
255
- # :conditions::
256
- # Hash of key-value pairs, where keys are field names, and values are one
257
- # of scalar, Array, or Range. Scalars are evaluated as EqualTo
258
- # restrictions; Arrays are AnyOf restrictions, and Ranges are Between
259
- # restrictions.
260
- # :order::
261
- # Order the search results. Either a string or array of strings of the
262
- # form "field_name direction"
263
- # :page::
264
- # Page to use for pagination
265
- # :per_page::
266
- # Number of results to show per page
267
- #
268
- def options=(options) #:nodoc:
269
- if options.has_key?(:keywords)
270
- self.keywords = options[:keywords]
66
+ #
67
+ # Add random ordering to the search. This can be added after other
68
+ # field-based sorts if desired.
69
+ #
70
+ def order_by_random
71
+ add_sort(Sort.new(RandomField.new))
271
72
  end
272
- if options.has_key?(:conditions)
273
- options[:conditions].each_pair do |field_name, value|
274
- begin
275
- restriction_type =
276
- case value
277
- when Array
278
- Restriction::AnyOf
279
- when Range
280
- Restriction::Between
281
- else
282
- Restriction::EqualTo
283
- end
284
- add_restriction(field_name, restriction_type, value)
285
- rescue UnrecognizedFieldError
286
- # ignore fields we don't recognize
287
- end
73
+
74
+ #
75
+ # Representation of this query as solr-ruby parameters. Constructs the hash
76
+ # by deep-merging scope and facet parameters, adding in various other
77
+ # parameters from instance data.
78
+ #
79
+ # Note that solr-ruby takes the :q parameter as a separate argument; for
80
+ # the sake of consistency, the Query object ignores this fact (the Search
81
+ # object extracts it back out).
82
+ #
83
+ # ==== Returns
84
+ #
85
+ # Hash:: Representation of query in solr-ruby form
86
+ #
87
+ def to_params #:nodoc:
88
+ params = {}
89
+ query_components = []
90
+ for component in @components
91
+ Util.deep_merge!(params, component.to_params)
288
92
  end
93
+ params
289
94
  end
290
- if options.has_key?(:order)
291
- for order in Array(options[:order])
292
- order_by(*order.split(' '))
293
- end
95
+
96
+ #
97
+ # Page that this query will return (used by Sunspot::Search to expose
98
+ # pagination)
99
+ #
100
+ # ==== Returns
101
+ #
102
+ # Integer:: Page number
103
+ #
104
+ def page #:nodoc:
105
+ @pagination.page
294
106
  end
295
- if options.has_key?(:page)
296
- paginate(options[:page], options[:per_page])
107
+
108
+ #
109
+ # Number of rows per page that this query will return (used by
110
+ # Sunspot::Search to expose pagination)
111
+ #
112
+ # ==== Returns
113
+ #
114
+ # Integer:: Rows per page
115
+ #
116
+ def per_page #:nodoc:
117
+ @pagination.per_page
297
118
  end
298
- end
299
119
 
300
- private
120
+ #
121
+ # Get the query facet with the given name. Used by the Search object to
122
+ # match query facet results with the requested query facets.
123
+ #
124
+ def query_facet(name) #:nodoc:
125
+ @query_facets[name.to_sym]
126
+ end
301
127
 
302
- #
303
- # Boolean phrase that restricts results to objects of the type(s) under
304
- # query. If this is an open query (no types specified) then it sends a
305
- # no-op phrase because Solr requires that the :q parameter not be empty.
306
- #
307
- # TODO don't send a noop if we have a keyword phrase
308
- # TODO this should be sent as a filter query when possible, especially
309
- # if there is a single type, so that Solr can cache it
310
- #
311
- # ==== Returns
312
- #
313
- # String:: Boolean phrase for type restriction
314
- #
315
- def types_phrase
316
- if @types.nil? || @types.empty? then "type:[* TO *]"
317
- elsif @types.length == 1 then "type:#{escaped_types.first}"
318
- else "type:(#{escaped_types * ' OR '})"
128
+ #
129
+ # Add a Sort object into this query's sort composite.
130
+ #
131
+ def add_sort(sort) #:nodoc:
132
+ @sort << sort
319
133
  end
320
- end
321
134
 
322
- #
323
- # Wraps each type in quotes to escape names of the form Namespace::Class
324
- #
325
- def escaped_types
326
- @types.map { |t| Solr::Util.query_parser_escape(t.name)}
327
- end
135
+ #
136
+ # Set the keywords for this query, along with keyword options. See
137
+ # Query::BaseQuery for information on what the options do.
138
+ #
139
+ def set_keywords(keywords, options = {}) #:nodoc:
140
+ @base_query.keywords = keywords
141
+ @base_query.keyword_options = options
142
+ end
328
143
 
329
- #
330
- # Return a hash of field names to field objects, containing all fields
331
- # that are common to all of the classes under search. In order for fields
332
- # to be common, they must be of the same type and have the same
333
- # value for allow_multiple?. This method is memoized.
334
- #
335
- # ==== Returns
336
- #
337
- # Hash:: field names keyed to field objects
338
- #
339
- def fields_hash
340
- @fields_hash ||=
341
- begin
342
- fields_hash = @types.inject({}) do |hash, type|
343
- Setup.for(type).fields.each do |field|
344
- (hash[field.name.to_sym] ||= {})[type.name] = field
345
- end
346
- hash
347
- end
348
- fields_hash.each_pair do |field_name, field_configurations_hash|
349
- if @types.any? { |type| field_configurations_hash[type.name].nil? } # at least one type doesn't have this field configured
350
- fields_hash.delete(field_name)
351
- elsif field_configurations_hash.values.map { |configuration| configuration.indexed_name }.uniq.length != 1 # fields with this name have different configs
352
- fields_hash.delete(field_name)
353
- else
354
- fields_hash[field_name] = field_configurations_hash.values.first
355
- end
356
- end
144
+ #
145
+ # Pass in search options as a hash. This is not the preferred way of
146
+ # building a Sunspot search, but it is made available as experience shows
147
+ # Ruby developers like to pass in hashes. Probably nice for quick one-offs
148
+ # on the console, anyway.
149
+ #
150
+ # ==== Options (+options+)
151
+ #
152
+ # :keywords:: Keyword string for fulltext search
153
+ # :conditions::
154
+ # Hash of key-value pairs, where keys are field names, and values are one
155
+ # of scalar, Array, or Range. Scalars are evaluated as EqualTo
156
+ # restrictions; Arrays are AnyOf restrictions, and Ranges are Between
157
+ # restrictions.
158
+ # :order::
159
+ # Order the search results. Either a string or array of strings of the
160
+ # form "field_name direction"
161
+ # :page::
162
+ # Page to use for pagination
163
+ # :per_page::
164
+ # Number of results to show per page
165
+ #
166
+ def options=(options) #:nodoc:
167
+ if options.has_key?(:keywords)
168
+ self.keywords = options[:keywords]
357
169
  end
358
- end
359
-
360
- def dynamic_fields_hash
361
- @dynamic_fields_hash ||=
362
- begin
363
- dynamic_fields_hash = @types.inject({}) do |hash, type|
364
- Setup.for(type).dynamic_fields.each do |field|
365
- (hash[field.name.to_sym] ||= {})[type.name] = field
170
+ if options.has_key?(:conditions)
171
+ options[:conditions].each_pair do |field_name, value|
172
+ begin
173
+ add_shorthand_restriction(field_name, value)
174
+ rescue UnrecognizedFieldError
175
+ # ignore fields we don't recognize
366
176
  end
367
- hash
368
177
  end
369
- dynamic_fields_hash.each_pair do |field_name, field_configurations_hash|
370
- if @types.any? { |type| field_configurations_hash[type.name].nil? }
371
- dynamic_fields_hash.delete(field_name)
372
- else
373
- dynamic_fields_hash[field_name] = field_configurations_hash.values.first
374
- end
178
+ end
179
+ if options.has_key?(:order)
180
+ for order in Array(options[:order])
181
+ order_by(*order.split(' '))
375
182
  end
376
183
  end
184
+ if options.has_key?(:page)
185
+ paginate(options[:page], options[:per_page])
186
+ end
187
+ end
377
188
  end
378
189
  end
379
190
  end
@@ -0,0 +1,33 @@
1
+ module Sunspot
2
+ #
3
+ # QueryFacet instances encapsulate a set of query facet results. Each facet
4
+ # corresponds to a group of rows defined inside a DSL::FieldQuery#facet block.
5
+ #
6
+ class QueryFacet
7
+ def initialize(outgoing_query_facet, row_data) #:nodoc:
8
+ @outgoing_query_facet, @row_data = outgoing_query_facet, row_data
9
+ end
10
+
11
+ #
12
+ # Get the rows associated with this query facet. Returned rows are always
13
+ # ordered by count.
14
+ #
15
+ # ==== Returns
16
+ #
17
+ # Array:: Collection of QueryFacetRow objects, ordered by count
18
+ #
19
+ def rows
20
+ @rows ||=
21
+ begin
22
+ rows = []
23
+ for row in @outgoing_query_facet.rows
24
+ row_query = row.to_boolean_phrase
25
+ if @row_data.has_key?(row_query)
26
+ rows << QueryFacetRow.new(row.label, @row_data[row_query])
27
+ end
28
+ end
29
+ rows.sort! { |x, y| y.count <=> x.count }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,21 @@
1
+ module Sunspot
2
+ #
3
+ # Objects of this class encapsulate a single query facet row returned for a
4
+ # query facet.
5
+ #
6
+ class QueryFacetRow
7
+ #
8
+ # This is the "label" passed into the query facet row when it is defined in
9
+ # the search.
10
+ #
11
+ attr_reader :value
12
+ #
13
+ # Number of documents in the result set that match this facet's scope.
14
+ #
15
+ attr_reader :count
16
+
17
+ def initialize(value, count) #:nodoc:
18
+ @value, @count = value, count
19
+ end
20
+ end
21
+ end