outoftime-sunspot 0.7.3 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sunspot/query.rb CHANGED
@@ -1,92 +1,124 @@
1
+ %w(dynamic_query field_facet pagination restriction sort).each do |file|
2
+ require File.join(File.dirname(__FILE__), 'query', file)
3
+ end
4
+
1
5
  module Sunspot
2
6
  #
3
7
  # This class encapsulates a query that is to be sent to Solr. The query is
4
8
  # constructed in the block passed to the Sunspot.search method, using the
5
- # Sunspot::DSL::Query interface. Instances of Query, as well as all of the
6
- # components it contains, respond to the #to_params method, which returns
7
- # a hash of parameters in the format recognized by the solr-ruby API.
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.
8
17
  #
9
- class Query #:nodoc:
18
+ class Query
10
19
  attr_writer :keywords # <String> full-text keyword boolean phrase
11
20
 
12
- def initialize(types, params, configuration)
21
+ def initialize(types, configuration) #:nodoc:
13
22
  @types, @configuration = types, configuration
14
- @rows = @configuration.pagination.default_per_page
15
- apply_params(params)
23
+ @components = []
24
+ @components << @pagination = Pagination.new(@configuration)
16
25
  end
17
26
 
18
27
  #
19
- # Representation of this query as solr-ruby parameters. Constructs the hash
20
- # by deep-merging scope and facet parameters, adding in various other
21
- # parameters from instance data.
22
- #
23
- # Note that solr-ruby takes the :q parameter as a separate argument; for
24
- # the sake of consistency, the Query object ignores this fact (the Search
25
- # object extracts it back out).
26
- #
27
- # ==== Returns
28
- #
29
- # Hash:: Representation of query in solr-ruby form
30
- #
31
- def to_params
32
- params = {}
33
- query_components = []
34
- query_components << @keywords if @keywords
35
- query_components << types_phrase if types_phrase
36
- params[:q] = query_components.map { |component| "(#{component})"} * ' AND '
37
- params[:sort] = @sort if @sort
38
- params[:start] = @start if @start
39
- params[:rows] = @rows if @rows
40
- for component in components
41
- Util.deep_merge!(params, component.to_params)
42
- end
43
- params
44
- end
45
-
28
+ # Add a restriction to the query.
46
29
  #
47
- # Add a query component
48
- #
49
30
  # ==== Parameters
50
31
  #
51
- # component<~to_params>:: A restriction query component
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)
52
41
  #
53
- def add_component(component)
54
- components << component
42
+ def add_restriction(field_name, restriction_type, value, negated = false)
43
+ if restriction_type.is_a?(Symbol)
44
+ restriction_type = Restriction[restriction_type]
45
+ end
46
+ @components << restriction = restriction_type.new(field(field_name), value, negated)
47
+ restriction
55
48
  end
56
49
 
57
50
  #
58
- # Add instance of Sunspot::Restriction::Base to query components. This
59
- # method is exposed to the DSL because the Query instance holds field
60
- # definitions and is able to translate field names into full field
61
- # definitions, and memoize # the result.
62
- #
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
+ #
63
55
  # ==== Parameters
64
56
  #
65
57
  # field_name<Symbol>:: Name of the field to which the restriction applies
66
- # restriction_clazz<Class>::
67
- # Subclass of Sunspot::Restriction::Base to instantiate
58
+ # restriction_type<Class>::
59
+ # Subclass of Sunspot::Query::Restriction::Base to instantiate
68
60
  # value<Object>::
69
61
  # Value against which the restriction applies (e.g. less_than(2) has a
70
62
  # value of 2)
71
- # negative:: Whether this restriction should be negated
72
63
  #
73
- # ==== Returns
64
+ def add_negated_restriction(field_name, restriction_type, value)
65
+ add_restriction(field_name, restriction_type, value, true)
66
+ end
67
+
74
68
  #
75
- # Sunspot::Restriction::Base:: Restriction instance
69
+ # Exclude a particular instance from the search results
76
70
  #
77
- def add_restriction(field_name, restriction_clazz, value, negative = false)
78
- add_component(restriction_clazz.new(field(field_name), value, negative))
71
+ # ==== Parameters
72
+ #
73
+ # instance<Object>:: instance to exclude from results
74
+ #
75
+ def exclude_instance(instance)
76
+ @components << Restriction::SameAs.new(instance, true)
79
77
  end
80
78
 
81
79
  #
82
- # Add a field facet
80
+ # Add a field facet. See Sunspot::Facet for more information.
83
81
  #
84
82
  # ==== Parameters
85
83
  #
86
84
  # field_name<Symbol>:: Name of the field on which to get a facet
87
85
  #
88
86
  def add_field_facet(field_name)
89
- add_component(Facets::FieldFacet.new(field(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
90
122
  end
91
123
 
92
124
  #
@@ -100,9 +132,7 @@ module Sunspot
100
132
  # Sunspot.config.pagination.default_per_page
101
133
  #
102
134
  def paginate(page, per_page = nil)
103
- per_page ||= @configuration.pagination.default_per_page
104
- @start = (page - 1) * per_page
105
- @rows = per_page
135
+ @pagination.page, @pagination.per_page = page, per_page
106
136
  end
107
137
 
108
138
  #
@@ -114,8 +144,44 @@ module Sunspot
114
144
  # direction<Symbol>:: :asc or :desc (default :asc)
115
145
  #
116
146
  def order_by(field_name, direction = nil)
117
- direction ||= :asc
118
- (@sort ||= []) << { field(field_name).indexed_name.to_sym => (direction.to_s == 'asc' ? :ascending : :descending) }
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)
183
+ end
184
+ params
119
185
  end
120
186
 
121
187
  #
@@ -126,12 +192,8 @@ module Sunspot
126
192
  #
127
193
  # Integer:: Page number
128
194
  #
129
- def page
130
- if @start && @rows
131
- @start / @rows + 1
132
- else
133
- 1
134
- end
195
+ def page #:nodoc:
196
+ @pagination.page
135
197
  end
136
198
 
137
199
  #
@@ -142,8 +204,8 @@ module Sunspot
142
204
  #
143
205
  # Integer:: Rows per page
144
206
  #
145
- def per_page
146
- @rows
207
+ def per_page #:nodoc:
208
+ @pagination.per_page
147
209
  end
148
210
 
149
211
  #
@@ -153,7 +215,7 @@ module Sunspot
153
215
  #
154
216
  # Sunspot::DSL::Query:: DSL instance
155
217
  #
156
- def dsl
218
+ def dsl #:nodoc:
157
219
  @dsl ||= DSL::Query.new(self)
158
220
  end
159
221
 
@@ -173,20 +235,70 @@ module Sunspot
173
235
  # ArgumentError::
174
236
  # If the given field name is not configured for the types being queried
175
237
  #
176
- def field(field_name)
238
+ def field(field_name) #:nodoc:
177
239
  fields_hash[field_name.to_sym] || raise(UnrecognizedFieldError, "No field configured for #{@types * ', '} with name '#{field_name}'")
178
240
  end
179
241
 
180
- private
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
181
245
 
182
- # ==== Returns
183
- #
184
- # Array:: Collection of query components
185
- #
186
- def components
187
- @components ||= []
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]
271
+ 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
288
+ end
289
+ end
290
+ if options.has_key?(:order)
291
+ for order in Array(options[:order])
292
+ order_by(*order.split(' '))
293
+ end
294
+ end
295
+ if options.has_key?(:page)
296
+ paginate(options[:page], options[:per_page])
297
+ end
188
298
  end
189
299
 
300
+ private
301
+
190
302
  #
191
303
  # Boolean phrase that restricts results to objects of the type(s) under
192
304
  # query. If this is an open query (no types specified) then it sends a
@@ -218,55 +330,43 @@ module Sunspot
218
330
  # Hash:: field names keyed to field objects
219
331
  #
220
332
  def fields_hash
221
- @fields_hash ||= begin
222
- fields_hash = @types.inject({}) do |hash, type|
223
- Setup.for(type).fields.each do |field|
224
- (hash[field.name.to_sym] ||= {})[type.name] = field
333
+ @fields_hash ||=
334
+ begin
335
+ fields_hash = @types.inject({}) do |hash, type|
336
+ Setup.for(type).fields.each do |field|
337
+ (hash[field.name.to_sym] ||= {})[type.name] = field
338
+ end
339
+ hash
225
340
  end
226
- hash
227
- end
228
- fields_hash.each_pair do |field_name, field_configurations_hash|
229
- if @types.any? { |type| field_configurations_hash[type.name].nil? } # at least one type doesn't have this field configured
230
- fields_hash.delete(field_name)
231
- elsif field_configurations_hash.values.map { |configuration| configuration.indexed_name }.uniq.length != 1 # fields with this name have different configs
232
- fields_hash.delete(field_name)
233
- else
234
- fields_hash[field_name] = field_configurations_hash.values.first
341
+ fields_hash.each_pair do |field_name, field_configurations_hash|
342
+ if @types.any? { |type| field_configurations_hash[type.name].nil? } # at least one type doesn't have this field configured
343
+ fields_hash.delete(field_name)
344
+ elsif field_configurations_hash.values.map { |configuration| configuration.indexed_name }.uniq.length != 1 # fields with this name have different configs
345
+ fields_hash.delete(field_name)
346
+ else
347
+ fields_hash[field_name] = field_configurations_hash.values.first
348
+ end
235
349
  end
236
350
  end
237
- end
238
351
  end
239
352
 
240
- def apply_params(params)
241
- if params.has_key?(:keywords)
242
- self.keywords = params[:keywords]
243
- end
244
- if params.has_key?(:conditions)
245
- params[:conditions].each_pair do |field_name, value|
246
- begin
247
- restriction_type =
248
- case value
249
- when Array
250
- Restriction::AnyOf
251
- when Range
252
- Restriction::Between
253
- else
254
- Restriction::EqualTo
255
- end
256
- add_restriction(field_name, restriction_type, value)
257
- rescue UnrecognizedFieldError
258
- # ignore fields we don't recognize
353
+ def dynamic_fields_hash
354
+ @dynamic_fields_hash ||=
355
+ begin
356
+ dynamic_fields_hash = @types.inject({}) do |hash, type|
357
+ Setup.for(type).dynamic_fields.each do |field|
358
+ (hash[field.name.to_sym] ||= {})[type.name] = field
359
+ end
360
+ hash
361
+ end
362
+ dynamic_fields_hash.each_pair do |field_name, field_configurations_hash|
363
+ if @types.any? { |type| field_configurations_hash[type.name].nil? }
364
+ dynamic_fields_hash.delete(field_name)
365
+ else
366
+ dynamic_fields_hash[field_name] = field_configurations_hash.values.first
367
+ end
259
368
  end
260
369
  end
261
- end
262
- if params.has_key?(:order)
263
- for order in Array(params[:order])
264
- order_by(*order.split(' '))
265
- end
266
- end
267
- if params.has_key?(:page)
268
- paginate(params[:page], params[:per_page])
269
- end
270
370
  end
271
371
  end
272
372
  end
@@ -5,14 +5,18 @@ module Sunspot
5
5
  # Instances of Search are returned by the Sunspot.search method.
6
6
  #
7
7
  class Search
8
+ # Objects of this class are returned by the #raw_results method.
8
9
  RawResult = Struct.new(:class_name, :primary_key)
9
10
 
10
- def initialize(connection, configuration, *types, &block) #:nodoc:
11
+ # Query information for this search. If you wish to build the query without
12
+ # using the search DSL, this method allows you to access the query API
13
+ # directly. See Sunspot#new_search for how to construct the search object
14
+ # in this case.
15
+ attr_reader :query
16
+
17
+ def initialize(connection, query) #:nodoc:
11
18
  @connection = connection
12
- params = types.last.is_a?(Hash) ? types.pop : {}
13
- @query = Query.new(types, params, configuration)
14
- @query.dsl.instance_eval(&block) if block
15
- @types = types
19
+ @query = query
16
20
  end
17
21
 
18
22
  #
@@ -88,6 +92,40 @@ module Sunspot
88
92
  end
89
93
  end
90
94
 
95
+ #
96
+ # Get the facet object for a given dynamic field. This dynamic field will
97
+ # need to have been requested as a field facet inside the search block.
98
+ #
99
+ # ==== Parameters
100
+ #
101
+ # base_name<Symbol>::
102
+ # Base name of the dynamic field definiton (as specified in the setup
103
+ # block)
104
+ # dynamic_name<Symbol>::
105
+ # Dynamic field name to facet on
106
+ #
107
+ # ==== Returns
108
+ #
109
+ # Sunspot::Facet:: Facet object for given dynamic field
110
+ #
111
+ # ==== Example
112
+ #
113
+ # search = Sunspot.search(Post) do
114
+ # dynamic :custom do
115
+ # facet :cuisine
116
+ # end
117
+ # end
118
+ # search.dynamic_facet(:custom, :cuisine)
119
+ # #=> Facet for the dynamic field :cuisine in the :custom field definition
120
+ #
121
+ def dynamic_facet(base_name, dynamic_name)
122
+ (@dynamic_facets_cache ||= {})[[base_name.to_sym, dynamic_name.to_sym]] ||=
123
+ begin
124
+ field = @query.dynamic_field(base_name).build(dynamic_name)
125
+ Facet.new(@solr_result.field_facets(field.indexed_name), field)
126
+ end
127
+ end
128
+
91
129
  private
92
130
 
93
131
  #
@@ -22,12 +22,23 @@ module Sunspot
22
22
  @updates = 0
23
23
  end
24
24
 
25
+ #
26
+ # See Sunspot.new_search
27
+ #
28
+ def new_search(*types)
29
+ types.flatten!
30
+ Search.new(connection, Query.new(types, @config))
31
+ end
32
+
25
33
  #
26
34
  # See Sunspot.search
27
35
  #
28
36
  def search(*types, &block)
29
- types.flatten!
30
- Search.new(connection, @config, *types, &block).execute!
37
+ options = types.last.is_a?(Hash) ? types.pop : {}
38
+ search = new_search(*types)
39
+ search.query.build(&block) if block
40
+ search.query.options = options
41
+ search.execute!
31
42
  end
32
43
 
33
44
  #
@@ -37,7 +48,7 @@ module Sunspot
37
48
  objects.flatten!
38
49
  @updates += objects.length
39
50
  for object in objects
40
- setup_for(object).indexer(connection).add(object)
51
+ indexer_for(object).add(object)
41
52
  end
42
53
  end
43
54
 
@@ -50,7 +61,7 @@ module Sunspot
50
61
  end
51
62
 
52
63
  #
53
- # See Sunspot.commit!
64
+ # See Sunspot.commit
54
65
  #
55
66
  def commit
56
67
  @updates = 0
@@ -64,7 +75,7 @@ module Sunspot
64
75
  objects.flatten!
65
76
  @updates += objects.length
66
77
  for object in objects
67
- setup_for(object).indexer(connection).remove(object)
78
+ indexer_for(object).remove(object)
68
79
  end
69
80
  end
70
81
 
@@ -131,6 +142,10 @@ module Sunspot
131
142
  Setup.for(object.class) || raise(NoSetupError, "Sunspot is not configured for #{object.class.inspect}")
132
143
  end
133
144
 
145
+ def indexer_for(object)
146
+ setup_for(object).indexer(connection)
147
+ end
148
+
134
149
  #
135
150
  # Retrieve the Solr connection for this session, creating one if it does not
136
151
  # already exist.
data/lib/sunspot/setup.rb CHANGED
@@ -6,7 +6,7 @@ module Sunspot
6
6
  class Setup #:nodoc:
7
7
  def initialize(clazz)
8
8
  @class_name = clazz.name
9
- @fields, @text_fields = [], []
9
+ @fields, @text_fields, @dynamic_fields = [], [], []
10
10
  @dsl = DSL::Fields.new(self)
11
11
  end
12
12
 
@@ -32,6 +32,17 @@ module Sunspot
32
32
  @text_fields.concat(Array(fields))
33
33
  end
34
34
 
35
+ #
36
+ # Add dynamic fields
37
+ #
38
+ # ==== Parameters
39
+ #
40
+ # fields<Array>:: Array of dynamic field objects
41
+ #
42
+ def add_dynamic_fields(fields)
43
+ @dynamic_fields.concat(Array(fields))
44
+ end
45
+
35
46
  #
36
47
  # Builder method for evaluating the setup DSL
37
48
  #
@@ -47,9 +58,7 @@ module Sunspot
47
58
  # Array:: Collection of all fields associated with this setup
48
59
  #
49
60
  def fields
50
- fields = @fields.dup
51
- fields.concat(parent.fields) if parent
52
- fields
61
+ get_inheritable_collection(:fields)
53
62
  end
54
63
 
55
64
  #
@@ -61,21 +70,32 @@ module Sunspot
61
70
  # Array:: Collection of all text fields associated with this setup
62
71
  #
63
72
  def text_fields
64
- text_fields = @text_fields.dup
65
- text_fields.concat(parent.text_fields) if parent
66
- text_fields
73
+ get_inheritable_collection(:text_fields)
67
74
  end
68
75
 
69
76
  #
70
- # Get all scope and text fields associated with this setup as well as all
71
- # inherited fields
77
+ # Get all static, dynamic, and text fields associated with this setup as
78
+ # well as all inherited fields
72
79
  #
73
80
  # ==== Returns
74
81
  #
75
82
  # Array:: Collection of all text and scope fields associated with this setup
76
83
  #
77
84
  def all_fields
78
- fields + text_fields
85
+ all_fields = []
86
+ all_fields.concat(fields).concat(text_fields).concat(dynamic_fields)
87
+ all_fields
88
+ end
89
+
90
+ #
91
+ # Get all dynamic fields for this and parent setups
92
+ #
93
+ # ==== Returns
94
+ #
95
+ # Array:: Dynamic fields
96
+ #
97
+ def dynamic_fields
98
+ get_inheritable_collection(:dynamic_fields)
79
99
  end
80
100
 
81
101
  #
@@ -84,6 +104,7 @@ module Sunspot
84
104
  # ==== Returns
85
105
  #
86
106
  # Sunspot::Indexer:: Indexer configured with this setup
107
+ #
87
108
  def indexer(connection)
88
109
  Indexer.new(connection, self)
89
110
  end
@@ -112,6 +133,14 @@ module Sunspot
112
133
  Setup.for(clazz.superclass)
113
134
  end
114
135
 
136
+ private
137
+
138
+ def get_inheritable_collection(name)
139
+ collection = instance_variable_get(:"@#{name}").dup
140
+ collection.concat(parent.send(name)) if parent
141
+ collection
142
+ end
143
+
115
144
  class <<self
116
145
  #
117
146
  # Retrieve or create the Setup instance for the given class, evaluating