DrMark-thinking-sphinx 1.1.15 → 1.2.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/README.textile +22 -0
  2. data/VERSION.yml +4 -0
  3. data/lib/thinking_sphinx/active_record/scopes.rb +39 -0
  4. data/lib/thinking_sphinx/active_record.rb +27 -7
  5. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +9 -3
  6. data/lib/thinking_sphinx/association.rb +4 -1
  7. data/lib/thinking_sphinx/attribute.rb +91 -30
  8. data/lib/thinking_sphinx/configuration.rb +51 -12
  9. data/lib/thinking_sphinx/deltas/datetime_delta.rb +2 -2
  10. data/lib/thinking_sphinx/deltas/default_delta.rb +1 -1
  11. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +1 -1
  12. data/lib/thinking_sphinx/deltas/delayed_delta.rb +3 -0
  13. data/lib/thinking_sphinx/deploy/capistrano.rb +25 -8
  14. data/lib/thinking_sphinx/excerpter.rb +22 -0
  15. data/lib/thinking_sphinx/facet.rb +1 -1
  16. data/lib/thinking_sphinx/facet_search.rb +134 -0
  17. data/lib/thinking_sphinx/index.rb +2 -1
  18. data/lib/thinking_sphinx/rails_additions.rb +14 -0
  19. data/lib/thinking_sphinx/search.rb +599 -658
  20. data/lib/thinking_sphinx/search_methods.rb +421 -0
  21. data/lib/thinking_sphinx/source/internal_properties.rb +1 -1
  22. data/lib/thinking_sphinx/source/sql.rb +17 -13
  23. data/lib/thinking_sphinx/source.rb +6 -6
  24. data/lib/thinking_sphinx/tasks.rb +42 -8
  25. data/lib/thinking_sphinx.rb +82 -54
  26. data/rails/init.rb +14 -0
  27. data/spec/{unit → lib}/thinking_sphinx/active_record/delta_spec.rb +5 -5
  28. data/spec/{unit → lib}/thinking_sphinx/active_record/has_many_association_spec.rb +0 -0
  29. data/spec/lib/thinking_sphinx/active_record/scopes_spec.rb +96 -0
  30. data/spec/{unit → lib}/thinking_sphinx/active_record_spec.rb +51 -31
  31. data/spec/{unit → lib}/thinking_sphinx/association_spec.rb +4 -5
  32. data/spec/lib/thinking_sphinx/attribute_spec.rb +465 -0
  33. data/spec/{unit → lib}/thinking_sphinx/configuration_spec.rb +161 -29
  34. data/spec/{unit → lib}/thinking_sphinx/core/string_spec.rb +0 -0
  35. data/spec/lib/thinking_sphinx/excerpter_spec.rb +49 -0
  36. data/spec/lib/thinking_sphinx/facet_search_spec.rb +176 -0
  37. data/spec/{unit → lib}/thinking_sphinx/facet_spec.rb +24 -0
  38. data/spec/{unit → lib}/thinking_sphinx/field_spec.rb +8 -8
  39. data/spec/{unit → lib}/thinking_sphinx/index/builder_spec.rb +6 -2
  40. data/spec/{unit → lib}/thinking_sphinx/index/faux_column_spec.rb +0 -0
  41. data/spec/lib/thinking_sphinx/index_spec.rb +45 -0
  42. data/spec/{unit → lib}/thinking_sphinx/rails_additions_spec.rb +25 -5
  43. data/spec/lib/thinking_sphinx/search_methods_spec.rb +152 -0
  44. data/spec/lib/thinking_sphinx/search_spec.rb +960 -0
  45. data/spec/{unit → lib}/thinking_sphinx/source_spec.rb +63 -2
  46. data/spec/{unit → lib}/thinking_sphinx_spec.rb +32 -4
  47. data/tasks/distribution.rb +36 -35
  48. data/vendor/riddle/lib/riddle/client/message.rb +4 -3
  49. data/vendor/riddle/lib/riddle/client.rb +3 -0
  50. data/vendor/riddle/lib/riddle/configuration/section.rb +8 -2
  51. data/vendor/riddle/lib/riddle/controller.rb +17 -7
  52. data/vendor/riddle/lib/riddle.rb +1 -1
  53. metadata +79 -83
  54. data/lib/thinking_sphinx/active_record/search.rb +0 -57
  55. data/lib/thinking_sphinx/collection.rb +0 -148
  56. data/lib/thinking_sphinx/facet_collection.rb +0 -59
  57. data/lib/thinking_sphinx/search/facets.rb +0 -98
  58. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +0 -107
  59. data/spec/unit/thinking_sphinx/attribute_spec.rb +0 -232
  60. data/spec/unit/thinking_sphinx/collection_spec.rb +0 -14
  61. data/spec/unit/thinking_sphinx/facet_collection_spec.rb +0 -64
  62. data/spec/unit/thinking_sphinx/index_spec.rb +0 -139
  63. data/spec/unit/thinking_sphinx/search_spec.rb +0 -130
@@ -1,5 +1,4 @@
1
- require 'thinking_sphinx/search/facets'
2
-
1
+ # encoding: UTF-8
3
2
  module ThinkingSphinx
4
3
  # Once you've got those indexes in and built, this is the stuff that
5
4
  # matters - how to search! This class provides a generic search
@@ -9,702 +8,644 @@ module ThinkingSphinx
9
8
  # called from a model.
10
9
  #
11
10
  class Search
12
- GlobalFacetOptions = {
13
- :all_attributes => false,
14
- :class_facet => true
11
+ CoreMethods = %w( == class class_eval extend frozen? id instance_eval
12
+ instance_of? instance_values instance_variable_defined?
13
+ instance_variable_get instance_variable_set instance_variables is_a?
14
+ kind_of? member? method methods nil? object_id respond_to? send should
15
+ type )
16
+ SafeMethods = %w( partition private_methods protected_methods
17
+ public_methods send )
18
+
19
+ instance_methods.select { |method|
20
+ method.to_s[/^__/].nil? && !CoreMethods.include?(method.to_s)
21
+ }.each { |method|
22
+ undef_method method
15
23
  }
16
24
 
17
- class << self
18
- include ThinkingSphinx::Search::Facets
25
+ HashOptions = [:conditions, :with, :without, :with_all]
26
+ ArrayOptions = [:classes, :without_ids]
27
+
28
+ attr_reader :args, :options, :results
29
+
30
+ # Deprecated. Use ThinkingSphinx.search
31
+ def self.search(*args)
32
+ log 'ThinkingSphinx::Search.search is deprecated. Please use ThinkingSphinx.search instead.'
33
+ ThinkingSphinx.search *args
34
+ end
35
+
36
+ # Deprecated. Use ThinkingSphinx.search_for_ids
37
+ def self.search_for_ids(*args)
38
+ log 'ThinkingSphinx::Search.search_for_ids is deprecated. Please use ThinkingSphinx.search_for_ids instead.'
39
+ ThinkingSphinx.search_for_ids *args
40
+ end
41
+
42
+ # Deprecated. Use ThinkingSphinx.search_for_ids
43
+ def self.search_for_id(*args)
44
+ log 'ThinkingSphinx::Search.search_for_id is deprecated. Please use ThinkingSphinx.search_for_id instead.'
45
+ ThinkingSphinx.search_for_id *args
46
+ end
47
+
48
+ # Deprecated. Use ThinkingSphinx.count
49
+ def self.count(*args)
50
+ log 'ThinkingSphinx::Search.count is deprecated. Please use ThinkingSphinx.count instead.'
51
+ ThinkingSphinx.count *args
52
+ end
53
+
54
+ # Deprecated. Use ThinkingSphinx.facets
55
+ def self.facets(*args)
56
+ log 'ThinkingSphinx::Search.facets is deprecated. Please use ThinkingSphinx.facets instead.'
57
+ ThinkingSphinx.facets *args
58
+ end
59
+
60
+ def initialize(*args)
61
+ @array = []
62
+ @options = args.extract_options!
63
+ @args = args
64
+ end
65
+
66
+ def to_a
67
+ populate
68
+ @array
69
+ end
70
+
71
+ def method_missing(method, *args, &block)
72
+ if is_scope?(method)
73
+ add_scope(method, *args, &block)
74
+ return self
75
+ elsif method.to_s[/^each_with_.*/].nil? && !@array.respond_to?(method)
76
+ super
77
+ elsif !SafeMethods.include?(method.to_s)
78
+ populate
79
+ end
19
80
 
20
- # Searches for results that match the parameters provided. Will only
21
- # return the ids for the matching objects. See #search for syntax
22
- # examples.
23
- #
24
- # Note that this only searches the Sphinx index, with no ActiveRecord
25
- # queries. Thus, if your index is not in sync with the database, this
26
- # method may return ids that no longer exist there.
27
- #
28
- def search_for_ids(*args)
29
- results, client = search_results(*args.clone)
30
-
31
- options = args.extract_options!
32
- page = options[:page] ? options[:page].to_i : 1
33
-
34
- ThinkingSphinx::Collection.ids_from_results(results, page, client.limit, options)
81
+ if method.to_s[/^each_with_.*/] && !@array.respond_to?(method)
82
+ each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
83
+ else
84
+ @array.send(method, *args, &block)
35
85
  end
36
-
37
- # Searches through the Sphinx indexes for relevant matches. There's
38
- # various ways to search, sort, group and filter - which are covered
39
- # below.
40
- #
41
- # Also, if you have WillPaginate installed, the search method can be used
42
- # just like paginate. The same parameters - :page and :per_page - work as
43
- # expected, and the returned result set can be used by the will_paginate
44
- # helper.
45
- #
46
- # == Basic Searching
47
- #
48
- # The simplest way of searching is straight text.
49
- #
50
- # ThinkingSphinx::Search.search "pat"
51
- # ThinkingSphinx::Search.search "google"
52
- # User.search "pat", :page => (params[:page] || 1)
53
- # Article.search "relevant news issue of the day"
54
- #
55
- # If you specify :include, like in an #find call, this will be respected
56
- # when loading the relevant models from the search results.
57
- #
58
- # User.search "pat", :include => :posts
59
- #
60
- # == Match Modes
61
- #
62
- # Sphinx supports 5 different matching modes. By default Thinking Sphinx
63
- # uses :all, which unsurprisingly requires all the supplied search terms
64
- # to match a result.
65
- #
66
- # Alternative modes include:
67
- #
68
- # User.search "pat allan", :match_mode => :any
69
- # User.search "pat allan", :match_mode => :phrase
70
- # User.search "pat | allan", :match_mode => :boolean
71
- # User.search "@name pat | @username pat", :match_mode => :extended
72
- #
73
- # Any will find results with any of the search terms. Phrase treats the search
74
- # terms a single phrase instead of individual words. Boolean and extended allow
75
- # for more complex query syntax, refer to the sphinx documentation for further
76
- # details.
77
- #
78
- # == Weighting
79
- #
80
- # Sphinx has support for weighting, where matches in one field can be considered
81
- # more important than in another. Weights are integers, with 1 as the default.
82
- # They can be set per-search like this:
83
- #
84
- # User.search "pat allan", :field_weights => { :alias => 4, :aka => 2 }
85
- #
86
- # If you're searching multiple models, you can set per-index weights:
87
- #
88
- # ThinkingSphinx::Search.search "pat", :index_weights => { User => 10 }
89
- #
90
- # See http://sphinxsearch.com/doc.html#weighting for further details.
91
- #
92
- # == Searching by Fields
93
- #
94
- # If you want to step it up a level, you can limit your search terms to
95
- # specific fields:
96
- #
97
- # User.search :conditions => {:name => "pat"}
98
- #
99
- # This uses Sphinx's extended match mode, unless you specify a different
100
- # match mode explicitly (but then this way of searching won't work). Also
101
- # note that you don't need to put in a search string.
102
- #
103
- # == Searching by Attributes
104
- #
105
- # Also known as filters, you can limit your searches to documents that
106
- # have specific values for their attributes. There are three ways to do
107
- # this. The first two techniques work in all scenarios - using the :with
108
- # or :with_all options.
109
- #
110
- # ThinkingSphinx::Search.search :with => {:tag_ids => 10}
111
- # ThinkingSphinx::Search.search :with => {:tag_ids => [10,12]}
112
- # ThinkingSphinx::Search.search :with_all => {:tag_ids => [10,12]}
113
- #
114
- # The first :with search will match records with a tag_id attribute of 10.
115
- # The second :with will match records with a tag_id attribute of 10 OR 12.
116
- # If you need to find records that are tagged with ids 10 AND 12, you
117
- # will need to use the :with_all search parameter. This is particuarly
118
- # useful in conjunction with Multi Value Attributes (MVAs).
119
- #
120
- # The third filtering technique is only viable if you're searching with a
121
- # specific model (not multi-model searching). With a single model,
122
- # Thinking Sphinx can figure out what attributes and fields are available,
123
- # so you can put it all in the :conditions hash, and it will sort it out.
124
- #
125
- # Node.search :conditions => {:parent_id => 10}
126
- #
127
- # Filters can be single values, arrays of values, or ranges.
128
- #
129
- # Article.search "East Timor", :conditions => {:rating => 3..5}
130
- #
131
- # == Excluding by Attributes
132
- #
133
- # Sphinx also supports negative filtering - where the filters are of
134
- # attribute values to exclude. This is done with the :without option:
135
- #
136
- # User.search :without => {:role_id => 1}
137
- #
138
- # == Excluding by Primary Key
139
- #
140
- # There is a shortcut to exclude records by their ActiveRecord primary key:
141
- #
142
- # User.search :without_ids => 1
143
- #
144
- # Pass an array or a single value.
145
- #
146
- # The primary key must be an integer as a negative filter is used. Note
147
- # that for multi-model search, an id may occur in more than one model.
148
- #
149
- # == Infix (Star) Searching
150
- #
151
- # By default, Sphinx uses English stemming, e.g. matching "shoes" if you
152
- # search for "shoe". It won't find "Melbourne" if you search for
153
- # "elbourn", though.
154
- #
155
- # Enable infix searching by something like this in config/sphinx.yml:
156
- #
157
- # development:
158
- # enable_star: 1
159
- # min_infix_length: 2
160
- #
161
- # Note that this will make indexing take longer.
162
- #
163
- # With those settings (and after reindexing), wildcard asterisks can be used
164
- # in queries:
165
- #
166
- # Location.search "*elbourn*"
167
- #
168
- # To automatically add asterisks around every token (but not operators),
169
- # pass the :star option:
170
- #
171
- # Location.search "elbourn -ustrali", :star => true, :match_mode => :boolean
172
- #
173
- # This would become "*elbourn* -*ustrali*". The :star option only adds the
174
- # asterisks. You need to make the config/sphinx.yml changes yourself.
175
- #
176
- # By default, the tokens are assumed to match the regular expression /\w+/u.
177
- # If you've modified the charset_table, pass another regular expression, e.g.
178
- #
179
- # User.search("oo@bar.c", :star => /[\w@.]+/u)
180
- #
181
- # to search for "*oo@bar.c*" and not "*oo*@*bar*.*c*".
182
- #
183
- # == Sorting
184
- #
185
- # Sphinx can only sort by attributes, so generally you will need to avoid
186
- # using field names in your :order option. However, if you're searching
187
- # on a single model, and have specified some fields as sortable, you can
188
- # use those field names and Thinking Sphinx will interpret accordingly.
189
- # Remember: this will only happen for single-model searches, and only
190
- # through the :order option.
191
- #
192
- # Location.search "Melbourne", :order => :state
193
- # User.search :conditions => {:role_id => 2}, :order => "name ASC"
194
- #
195
- # Keep in mind that if you use a string, you *must* specify the direction
196
- # (ASC or DESC) else Sphinx won't return any results. If you use a symbol
197
- # then Thinking Sphinx assumes ASC, but if you wish to state otherwise,
198
- # use the :sort_mode option:
199
- #
200
- # Location.search "Melbourne", :order => :state, :sort_mode => :desc
201
- #
202
- # Of course, there are other sort modes - check out the Sphinx
203
- # documentation[http://sphinxsearch.com/doc.html] for that level of
204
- # detail though.
205
- #
206
- # If desired, you can sort by a column in your model instead of a sphinx
207
- # field or attribute. This sort only applies to the current page, so is
208
- # most useful when performing a search with a single page of results.
209
- #
210
- # User.search("pat", :sql_order => "name")
211
- #
212
- # == Grouping
213
- #
214
- # For this you can use the group_by, group_clause and group_function
215
- # options - which are all directly linked to Sphinx's expectations. No
216
- # magic from Thinking Sphinx. It can get a little tricky, so make sure
217
- # you read all the relevant
218
- # documentation[http://sphinxsearch.com/doc.html#clustering] first.
219
- #
220
- # Grouping is done via three parameters within the options hash
221
- # * <tt>:group_function</tt> determines the way grouping is done
222
- # * <tt>:group_by</tt> determines the field which is used for grouping
223
- # * <tt>:group_clause</tt> determines the sorting order
224
- #
225
- # === group_function
226
- #
227
- # Valid values for :group_function are
228
- # * <tt>:day</tt>, <tt>:week</tt>, <tt>:month</tt>, <tt>:year</tt> - Grouping is done by the respective timeframes.
229
- # * <tt>:attr</tt>, <tt>:attrpair</tt> - Grouping is done by the specified attributes(s)
230
- #
231
- # === group_by
232
- #
233
- # This parameter denotes the field by which grouping is done. Note that the
234
- # specified field must be a sphinx attribute or index.
235
- #
236
- # === group_clause
237
- #
238
- # This determines the sorting order of the groups. In a grouping search,
239
- # the matches within a group will sorted by the <tt>:sort_mode</tt> and <tt>:order</tt> parameters.
240
- # The group matches themselves however, will be sorted by <tt>:group_clause</tt>.
241
- #
242
- # The syntax for this is the same as an order parameter in extended sort mode.
243
- # Namely, you can specify an SQL-like sort expression with up to 5 attributes
244
- # (including internal attributes), eg: "@relevance DESC, price ASC, @id DESC"
245
- #
246
- # === Grouping by timestamp
247
- #
248
- # Timestamp grouping groups off items by the day, week, month or year of the
249
- # attribute given. In order to do this you need to define a timestamp attribute,
250
- # which pretty much looks like the standard defintion for any attribute.
251
- #
252
- # define_index do
253
- # #
254
- # # All your other stuff
255
- # #
256
- # has :created_at
257
- # end
258
- #
259
- # When you need to fire off your search, it'll go something to the tune of
260
- #
261
- # Fruit.search "apricot", :group_function => :day, :group_by => 'created_at'
262
- #
263
- # The <tt>@groupby</tt> special attribute will contain the date for that group.
264
- # Depending on the <tt>:group_function</tt> parameter, the date format will be
265
- #
266
- # * <tt>:day</tt> - YYYYMMDD
267
- # * <tt>:week</tt> - YYYYNNN (NNN is the first day of the week in question,
268
- # counting from the start of the year )
269
- # * <tt>:month</tt> - YYYYMM
270
- # * <tt>:year</tt> - YYYY
271
- #
272
- #
273
- # === Grouping by attribute
274
- #
275
- # The syntax is the same as grouping by timestamp, except for the fact that the
276
- # <tt>:group_function</tt> parameter is changed
277
- #
278
- # Fruit.search "apricot", :group_function => :attr, :group_by => 'size'
279
- #
280
- #
281
- # == Geo/Location Searching
282
- #
283
- # Sphinx - and therefore Thinking Sphinx - has the facility to search
284
- # around a geographical point, using a given latitude and longitude. To
285
- # take advantage of this, you will need to have both of those values in
286
- # attributes. To search with that point, you can then use one of the
287
- # following syntax examples:
288
- #
289
- # Address.search "Melbourne", :geo => [1.4, -2.217], :order => "@geodist asc"
290
- # Address.search "Australia", :geo => [-0.55, 3.108], :order => "@geodist asc"
291
- # :latitude_attr => "latit", :longitude_attr => "longit"
292
- #
293
- # The first example applies when your latitude and longitude attributes
294
- # are named any of lat, latitude, lon, long or longitude. If that's not
295
- # the case, you will need to explicitly state them in your search, _or_
296
- # you can do so in your model:
297
- #
298
- # define_index do
299
- # has :latit # Float column, stored in radians
300
- # has :longit # Float column, stored in radians
301
- #
302
- # set_property :latitude_attr => "latit"
303
- # set_property :longitude_attr => "longit"
304
- # end
305
- #
306
- # Now, geo-location searching really only has an affect if you have a
307
- # filter, sort or grouping clause related to it - otherwise it's just a
308
- # normal search, and _will not_ return a distance value otherwise. To
309
- # make use of the positioning difference, use the special attribute
310
- # "@geodist" in any of your filters or sorting or grouping clauses.
311
- #
312
- # And don't forget - both the latitude and longitude you use in your
313
- # search, and the values in your indexes, need to be stored as a float in radians,
314
- # _not_ degrees. Keep in mind that if you do this conversion in SQL
315
- # you will need to explicitly declare a column type of :float.
316
- #
317
- # define_index do
318
- # has 'RADIANS(lat)', :as => :lat, :type => :float
319
- # # ...
320
- # end
321
- #
322
- # Once you've got your results set, you can access the distances as
323
- # follows:
324
- #
325
- # @results.each_with_geodist do |result, distance|
326
- # # ...
327
- # end
328
- #
329
- # The distance value is returned as a float, representing the distance in
330
- # metres.
331
- #
332
- # == Handling a Stale Index
333
- #
334
- # Especially if you don't use delta indexing, you risk having records in the
335
- # Sphinx index that are no longer in the database. By default, those will simply
336
- # come back as nils:
337
- #
338
- # >> pat_user.delete
339
- # >> User.search("pat")
340
- # Sphinx Result: [1,2]
341
- # => [nil, <#User id: 2>]
342
- #
343
- # (If you search across multiple models, you'll get ActiveRecord::RecordNotFound.)
344
- #
345
- # You can simply Array#compact these results or handle the nils in some other way, but
346
- # Sphinx will still report two results, and the missing records may upset your layout.
347
- #
348
- # If you pass :retry_stale => true to a single-model search, missing records will
349
- # cause Thinking Sphinx to retry the query but excluding those records. Since search
350
- # is paginated, the new search could potentially include missing records as well, so by
351
- # default Thinking Sphinx will retry three times. Pass :retry_stale => 5 to retry five
352
- # times, and so on. If there are still missing ids on the last retry, they are
353
- # shown as nils.
354
- #
355
- def search(*args)
356
- query = args.clone # an array
357
- options = query.extract_options!
358
-
359
- retry_search_on_stale_index(query, options) do
360
- results, client = search_results(*(query + [options]))
361
-
362
- ::ActiveRecord::Base.logger.error(
363
- "Sphinx Error: #{results[:error]}"
364
- ) if results[:error]
365
-
366
- klass = options[:class]
367
- page = options[:page] ? options[:page].to_i : 1
86
+ end
368
87
 
369
- ThinkingSphinx::Collection.create_from_results(results, page, client.limit, options)
370
- end
371
- end
372
-
373
- def retry_search_on_stale_index(query, options, &block)
374
- stale_ids = []
375
- stale_retries_left = case options[:retry_stale]
376
- when true
377
- 3 # default to three retries
378
- when nil, false
379
- 0 # no retries
380
- else options[:retry_stale].to_i
381
- end
382
- begin
383
- # Passing this in an option so Collection.create_from_results can see it.
384
- # It should only raise on stale records if there are any retries left.
385
- options[:raise_on_stale] = stale_retries_left > 0
386
- block.call
387
- # If ThinkingSphinx::Collection.create_from_results found records in Sphinx but not
388
- # in the DB and the :raise_on_stale option is set, this exception is raised. We retry
389
- # a limited number of times, excluding the stale ids from the search.
390
- rescue StaleIdsException => e
391
- stale_retries_left -= 1
392
-
393
- stale_ids |= e.ids # For logging
394
- options[:without_ids] = Array(options[:without_ids]) | e.ids # Actual exclusion
395
-
396
- tries = stale_retries_left
397
- ::ActiveRecord::Base.logger.debug("Sphinx Stale Ids (%s %s left): %s" % [
398
- tries, (tries==1 ? 'try' : 'tries'), stale_ids.join(', ')
399
- ])
400
-
401
- retry
402
- end
88
+ # Returns true if the Search object or the underlying Array object respond
89
+ # to the requested method.
90
+ #
91
+ # @param [Symbol] method The method name
92
+ # @return [Boolean] true if either Search or Array responds to the method.
93
+ #
94
+ def respond_to?(method)
95
+ super || @array.respond_to?(method)
96
+ end
97
+
98
+ # The current page number of the result set. Defaults to 1 if no page was
99
+ # explicitly requested.
100
+ #
101
+ # @return [Integer]
102
+ #
103
+ def current_page
104
+ @options[:page].blank? ? 1 : @options[:page].to_i
105
+ end
106
+
107
+ # The next page number of the result set. If there are no more pages
108
+ # available, nil is returned.
109
+ #
110
+ # @return [Integer, nil]
111
+ #
112
+ def next_page
113
+ current_page >= total_pages ? nil : current_page + 1
114
+ end
115
+
116
+ # The previous page number of the result set. If this is the first page,
117
+ # then nil is returned.
118
+ #
119
+ # @return [Integer, nil]
120
+ #
121
+ def previous_page
122
+ current_page == 1 ? nil : current_page - 1
123
+ end
124
+
125
+ # The amount of records per set of paged results. Defaults to 20 unless a
126
+ # specific page size is requested.
127
+ #
128
+ # @return [Integer]
129
+ #
130
+ def per_page
131
+ @options[:limit] || @options[:per_page] || 20
132
+ end
133
+
134
+ # The total number of pages available if the results are paginated.
135
+ #
136
+ # @return [Integer]
137
+ #
138
+ def total_pages
139
+ @total_pages ||= (total_entries / per_page.to_f).ceil
140
+ end
141
+ # Compatibility with older versions of will_paginate
142
+ alias_method :page_count, :total_pages
143
+
144
+ # The total number of search results available.
145
+ #
146
+ # @return [Integer]
147
+ #
148
+ def total_entries
149
+ populate
150
+ @total_entries ||= @results[:total_found]
151
+ end
152
+
153
+ # The current page's offset, based on the number of records per page.
154
+ #
155
+ # @return [Integer]
156
+ #
157
+ def offset
158
+ (current_page - 1) * per_page
159
+ end
160
+
161
+ def each_with_groupby_and_count(&block)
162
+ populate
163
+ results[:matches].each_with_index do |match, index|
164
+ yield self[index],
165
+ match[:attributes]["@groupby"],
166
+ match[:attributes]["@count"]
403
167
  end
404
-
405
- def count(*args)
406
- results, client = search_results(*args.clone)
407
- results[:total_found] || 0
168
+ end
169
+
170
+ def each_with_weighting(&block)
171
+ populate
172
+ results[:matches].each_with_index do |match, index|
173
+ yield self[index], match[:weight]
408
174
  end
409
-
410
- # Checks if a document with the given id exists within a specific index.
411
- # Expected parameters:
412
- #
413
- # - ID of the document
414
- # - Index to check within
415
- # - Options hash (defaults to {})
416
- #
417
- # Example:
418
- #
419
- # ThinkingSphinx::Search.search_for_id(10, "user_core", :class => User)
420
- #
421
- def search_for_id(*args)
422
- options = args.extract_options!
423
- client = client_from_options options
424
-
425
- query, filters = search_conditions(
426
- options[:class], options[:conditions] || {}
427
- )
428
- client.filters += filters
429
- client.match_mode = :extended unless query.empty?
430
- client.id_range = args.first..args.first
431
-
432
- begin
433
- return client.query(query, args[1])[:matches].length > 0
434
- rescue Errno::ECONNREFUSED => err
435
- raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
436
- end
175
+ end
176
+
177
+ def excerpt_for(string, model = nil)
178
+ if model.nil? && options[:classes].length == 1
179
+ model ||= options[:classes].first
437
180
  end
438
181
 
439
- private
182
+ populate
183
+ client.excerpts(
184
+ :docs => [string],
185
+ :words => results[:words].keys.join(' '),
186
+ :index => "#{model.sphinx_name}_core"
187
+ ).first
188
+ end
189
+
190
+ def search(*args)
191
+ merge_search ThinkingSphinx::Search.new(*args)
192
+ self
193
+ end
194
+
195
+ private
196
+
197
+ def config
198
+ ThinkingSphinx::Configuration.instance
199
+ end
200
+
201
+ def populate
202
+ return if @populated
203
+ @populated = true
440
204
 
441
- # This method handles the common search functionality, and returns both
442
- # the result hash and the client. Not super elegant, but it'll do for
443
- # the moment.
444
- #
445
- def search_results(*args)
446
- options = args.extract_options!
447
- query = args.join(' ')
448
- client = client_from_options options
449
-
450
- query = star_query(query, options[:star]) if options[:star]
451
-
452
- extra_query, filters = search_conditions(
453
- options[:class], options[:conditions] || {}
454
- )
455
- client.filters += filters
456
- client.match_mode = :extended unless extra_query.empty?
457
- query = [query, extra_query].join(' ')
458
- query.strip! # Because "" and " " are not equivalent
459
-
460
- set_sort_options! client, options
461
-
462
- client.limit = options[:per_page].to_i if options[:per_page]
463
- page = options[:page] ? options[:page].to_i : 1
464
- page = 1 if page <= 0
465
- client.offset = (page - 1) * client.limit
466
-
205
+ retry_on_stale_index do
467
206
  begin
468
- ::ActiveRecord::Base.logger.debug "Sphinx: #{query}"
469
- results = client.query query
470
- ::ActiveRecord::Base.logger.debug "Sphinx Result: #{results[:matches].collect{|m| m[:attributes]["sphinx_internal_id"]}.inspect}"
207
+ log "Querying Sphinx: #{query}"
208
+ @results = client.query query, index, comment
471
209
  rescue Errno::ECONNREFUSED => err
472
- raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
210
+ raise ThinkingSphinx::ConnectionError,
211
+ 'Connection to Sphinx Daemon (searchd) failed.'
473
212
  end
474
-
475
- return results, client
476
- end
477
213
 
478
- # Set all the appropriate settings for the client, using the provided
479
- # options hash.
480
- #
481
- def client_from_options(options = {})
482
- config = ThinkingSphinx::Configuration.instance
483
- client = Riddle::Client.new config.address, config.port
484
- klass = options[:class]
485
- index_options = klass ? klass.sphinx_index_options : {}
486
-
487
- # The Riddle default is per-query max_matches=1000. If we set the
488
- # per-server max to a smaller value in sphinx.yml, we need to override
489
- # the Riddle default or else we get search errors like
490
- # "per-query max_matches=1000 out of bounds (per-server max_matches=200)"
491
- if per_server_max_matches = config.configuration.searchd.max_matches
492
- options[:max_matches] ||= per_server_max_matches
214
+ if options[:ids_only]
215
+ replace @results[:matches].collect { |match|
216
+ match[:attributes]["sphinx_internal_id"]
217
+ }
218
+ else
219
+ replace instances_from_matches
220
+ add_excerpter
493
221
  end
222
+ end
223
+ end
224
+
225
+ def add_excerpter
226
+ each do |object|
227
+ next if object.respond_to?(:excerpts)
494
228
 
495
- # Turn :index_weights => { "foo" => 2, User => 1 }
496
- # into :index_weights => { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
497
- if iw = options[:index_weights]
498
- options[:index_weights] = iw.inject({}) do |hash, (index,weight)|
499
- if index.is_a?(Class)
500
- name = ThinkingSphinx::Index.name(index)
501
- hash["#{name}_core"] = weight
502
- hash["#{name}_delta"] = weight
503
- else
504
- hash[index] = weight
505
- end
506
- hash
507
- end
508
- end
229
+ excerpter = ThinkingSphinx::Excerpter.new self, object
230
+ block = lambda { excerpter }
509
231
 
510
- [
511
- :max_matches, :match_mode, :sort_mode, :sort_by, :id_range,
512
- :group_by, :group_function, :group_clause, :group_distinct, :cut_off,
513
- :retry_count, :retry_delay, :index_weights, :rank_mode,
514
- :max_query_time, :field_weights, :filters, :anchor, :limit
515
- ].each do |key|
516
- client.send(
517
- key.to_s.concat("=").to_sym,
518
- options[key] || index_options[key] || client.send(key)
519
- )
232
+ object.metaclass.instance_eval do
233
+ define_method(:excerpts, &block)
520
234
  end
521
-
522
- options[:classes] = [klass] if klass
523
-
524
- client.anchor = anchor_conditions(klass, options) || {} if client.anchor.empty?
525
-
526
- client.filters << Riddle::Client::Filter.new(
527
- "sphinx_deleted", [0]
528
- )
529
-
530
- # class filters
531
- client.filters << Riddle::Client::Filter.new(
532
- "class_crc", options[:classes].collect { |k| k.to_crc32s }.flatten
533
- ) if options[:classes]
534
-
535
- # normal attribute filters
536
- client.filters += options[:with].collect { |attr,val|
537
- Riddle::Client::Filter.new attr.to_s, filter_value(val)
538
- } if options[:with]
539
-
540
- # exclusive attribute filters
541
- client.filters += options[:without].collect { |attr,val|
542
- Riddle::Client::Filter.new attr.to_s, filter_value(val), true
543
- } if options[:without]
544
-
545
- # every-match attribute filters
546
- client.filters += options[:with_all].collect { |attr,vals|
547
- Array(vals).collect { |val|
548
- Riddle::Client::Filter.new attr.to_s, filter_value(val)
549
- }
550
- }.flatten if options[:with_all]
551
-
552
- # exclusive attribute filter on primary key
553
- client.filters += Array(options[:without_ids]).collect { |id|
554
- Riddle::Client::Filter.new 'sphinx_internal_id', filter_value(id), true
555
- } if options[:without_ids]
556
-
557
- client
235
+ end
236
+ end
237
+
238
+ def self.log(message, method = :debug)
239
+ return if ::ActiveRecord::Base.logger.nil?
240
+ ::ActiveRecord::Base.logger.send method, message
241
+ end
242
+
243
+ def log(message, method = :debug)
244
+ self.class.log(message, method)
245
+ end
246
+
247
+ def client
248
+ client = config.client
249
+
250
+ index_options = (options[:classes] || []).length != 1 ?
251
+ {} : options[:classes].first.sphinx_indexes.first.local_options
252
+
253
+ [
254
+ :max_matches, :group_by, :group_function, :group_clause,
255
+ :group_distinct, :id_range, :cut_off, :retry_count, :retry_delay,
256
+ :rank_mode, :max_query_time, :field_weights
257
+ ].each do |key|
258
+ # puts "key: #{key}"
259
+ value = options[key] || index_options[key]
260
+ # puts "value: #{value.inspect}"
261
+ client.send("#{key}=", value) if value
558
262
  end
559
263
 
560
- def star_query(query, custom_token = nil)
561
- token = custom_token.is_a?(Regexp) ? custom_token : /\w+/u
264
+ client.limit = per_page
265
+ client.offset = offset
266
+ client.match_mode = match_mode
267
+ client.filters = filters
268
+ client.sort_mode = sort_mode
269
+ client.sort_by = sort_by
270
+ client.group_by = group_by if group_by
271
+ client.group_function = group_function if group_function
272
+ client.index_weights = index_weights
273
+ client.anchor = anchor
274
+
275
+ client
276
+ end
277
+
278
+ def retry_on_stale_index(&block)
279
+ stale_ids = []
280
+ retries = stale_retries
281
+
282
+ begin
283
+ options[:raise_on_stale] = retries > 0
284
+ block.call
285
+
286
+ # If ThinkingSphinx::Search#instances_from_matches found records in
287
+ # Sphinx but not in the DB and the :raise_on_stale option is set, this
288
+ # exception is raised. We retry a limited number of times, excluding the
289
+ # stale ids from the search.
290
+ rescue StaleIdsException => err
291
+ retries -= 1
292
+
293
+ # For logging
294
+ stale_ids |= err.ids
295
+ # ID exclusion
296
+ options[:without_ids] = Array(options[:without_ids]) | err.ids
297
+
298
+ log 'Sphinx Stale Ids (%s %s left): %s' % [
299
+ retries, (retries == 1 ? 'try' : 'tries'), stale_ids.join(', ')
300
+ ]
301
+ retry
302
+ end
303
+ end
304
+
305
+ def query
306
+ @query ||= begin
307
+ q = @args.join(' ') << conditions_as_query
308
+ (options[:star] ? star_query(q) : q).strip
309
+ end
310
+ end
311
+
312
+ def conditions_as_query
313
+ return '' if @options[:conditions].blank?
314
+
315
+ # Soon to be deprecated.
316
+ keys = @options[:conditions].keys.reject { |key|
317
+ attributes.include?(key)
318
+ }
319
+
320
+ ' ' + keys.collect { |key|
321
+ "@#{key} #{options[:conditions][key]}"
322
+ }.join(' ')
323
+ end
324
+
325
+ def star_query(query)
326
+ token = options[:star].is_a?(Regexp) ? options[:star] : /\w+/u
562
327
 
563
- query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
564
- pre, proper, post = $`, $&, $'
565
- is_operator = pre.match(%r{(\W|^)[@~/]\Z}) # E.g. "@foo", "/2", "~3", but not as part of a token
566
- is_quote = proper.starts_with?('"') && proper.ends_with?('"') # E.g. "foo bar", with quotes
567
- has_star = pre.ends_with?("*") || post.starts_with?("*")
568
- if is_operator || is_quote || has_star
569
- proper
570
- else
571
- "*#{proper}*"
572
- end
328
+ query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
329
+ pre, proper, post = $`, $&, $'
330
+ # E.g. "@foo", "/2", "~3", but not as part of a token
331
+ is_operator = pre.match(%r{(\W|^)[@~/]\Z})
332
+ # E.g. "foo bar", with quotes
333
+ is_quote = proper.starts_with?('"') && proper.ends_with?('"')
334
+ has_star = pre.ends_with?("*") || post.starts_with?("*")
335
+ if is_operator || is_quote || has_star
336
+ proper
337
+ else
338
+ "*#{proper}*"
573
339
  end
574
340
  end
575
-
576
- def filter_value(value)
577
- case value
578
- when Range
579
- value.first.is_a?(Time) ? timestamp(value.first)..timestamp(value.last) : value
580
- when Array
581
- value.collect { |val| val.is_a?(Time) ? timestamp(val) : val }
341
+ end
342
+
343
+ def index
344
+ options[:index] || '*'
345
+ end
346
+
347
+ def comment
348
+ options[:comment] || ''
349
+ end
350
+
351
+ def match_mode
352
+ options[:match_mode] || (options[:conditions].blank? ? :all : :extended)
353
+ end
354
+
355
+ def sort_mode
356
+ @sort_mode ||= case options[:sort_mode]
357
+ when :asc
358
+ :attr_asc
359
+ when :desc
360
+ :attr_desc
361
+ when nil
362
+ case options[:order]
363
+ when String
364
+ :extended
365
+ when Symbol
366
+ :attr_asc
582
367
  else
583
- Array(value)
368
+ :relevance
584
369
  end
370
+ else
371
+ options[:sort_mode]
585
372
  end
586
-
587
- # Returns the integer timestamp for a Time object.
588
- #
589
- # If using Rails 2.1+, need to handle timezones to translate them back to
590
- # UTC, as that's what datetimes will be stored as by MySQL.
591
- #
592
- # in_time_zone is a method that was added for the timezone support in
593
- # Rails 2.1, which is why it's used for testing. I'm sure there's better
594
- # ways, but this does the job.
595
- #
596
- def timestamp(value)
597
- value.respond_to?(:in_time_zone) ? value.utc.to_i : value.to_i
373
+ end
374
+
375
+ def sort_by
376
+ case @sort_by = (options[:sort_by] || options[:order])
377
+ when String
378
+ sorted_fields_to_attributes(@sort_by)
379
+ when Symbol
380
+ field_names.include?(@sort_by) ?
381
+ @sort_by.to_s.concat('_sort') : @sort_by.to_s
382
+ else
383
+ ''
598
384
  end
385
+ end
386
+
387
+ def field_names
388
+ return [] if (options[:classes] || []).length != 1
599
389
 
600
- # Translate field and attribute conditions to the relevant search string
601
- # and filters.
602
- #
603
- def search_conditions(klass, conditions={})
604
- attributes = klass ? klass.sphinx_indexes.collect { |index|
605
- index.attributes.collect { |attrib| attrib.unique_name }
606
- }.flatten : []
607
-
608
- search_string = []
609
- filters = []
610
-
611
- conditions.each do |key,val|
612
- if attributes.include?(key.to_sym)
613
- filters << Riddle::Client::Filter.new(
614
- key.to_s, filter_value(val)
615
- )
616
- else
617
- search_string << "@#{key} #{val}"
618
- end
390
+ options[:classes].first.sphinx_indexes.collect { |index|
391
+ index.fields.collect { |field| field.unique_name }
392
+ }.flatten
393
+ end
394
+
395
+ def sorted_fields_to_attributes(order_string)
396
+ field_names.each { |field|
397
+ order_string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
398
+ match.gsub field.to_s, field.to_s.concat("_sort")
399
+ }
400
+ }
401
+
402
+ order_string
403
+ end
404
+
405
+ # Turn :index_weights => { "foo" => 2, User => 1 } into :index_weights =>
406
+ # { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
407
+ #
408
+ def index_weights
409
+ weights = options[:index_weights] || {}
410
+ weights.keys.inject({}) do |hash, key|
411
+ if key.is_a?(Class)
412
+ name = ThinkingSphinx::Index.name(key)
413
+ hash["#{name}_core"] = weights[key]
414
+ hash["#{name}_delta"] = weights[key]
415
+ else
416
+ hash[key] = weights[key]
619
417
  end
620
418
 
621
- return search_string.join(' '), filters
419
+ hash
622
420
  end
421
+ end
422
+
423
+ def group_by
424
+ options[:group] ? options[:group].to_s : nil
425
+ end
426
+
427
+ def group_function
428
+ options[:group] ? :attr : nil
429
+ end
430
+
431
+ def internal_filters
432
+ filters = [Riddle::Client::Filter.new('sphinx_deleted', [0])]
623
433
 
624
- # Return the appropriate latitude and longitude values, depending on
625
- # whether the relevant attributes have been defined, and also whether
626
- # there's actually any values.
627
- #
628
- def anchor_conditions(klass, options)
629
- attributes = klass ? klass.sphinx_indexes.collect { |index|
630
- index.attributes.collect { |attrib| attrib.unique_name }
631
- }.flatten : []
632
-
633
- lat_attr = klass ? klass.sphinx_indexes.collect { |index|
634
- index.options[:latitude_attr]
635
- }.compact.first : nil
636
-
637
- lon_attr = klass ? klass.sphinx_indexes.collect { |index|
638
- index.options[:longitude_attr]
639
- }.compact.first : nil
640
-
641
- lat_attr = options[:latitude_attr] if options[:latitude_attr]
642
- lat_attr ||= :lat if attributes.include?(:lat)
643
- lat_attr ||= :latitude if attributes.include?(:latitude)
644
-
645
- lon_attr = options[:longitude_attr] if options[:longitude_attr]
646
- lon_attr ||= :lng if attributes.include?(:lng)
647
- lon_attr ||= :lon if attributes.include?(:lon)
648
- lon_attr ||= :long if attributes.include?(:long)
649
- lon_attr ||= :longitude if attributes.include?(:longitude)
650
-
651
- lat = options[:lat]
652
- lon = options[:lon]
653
-
654
- if options[:geo]
655
- lat = options[:geo].first
656
- lon = options[:geo].last
434
+ class_crcs = (options[:classes] || []).collect { |klass|
435
+ klass.to_crc32s
436
+ }.flatten
437
+
438
+ unless class_crcs.empty?
439
+ filters << Riddle::Client::Filter.new('class_crc', class_crcs)
440
+ end
441
+
442
+ filters << Riddle::Client::Filter.new(
443
+ 'sphinx_internal_id', filter_value(options[:without_ids]), true
444
+ ) if options[:without_ids]
445
+
446
+ filters
447
+ end
448
+
449
+ def condition_filters
450
+ (options[:conditions] || {}).collect { |attrib, value|
451
+ if attributes.include?(attrib)
452
+ puts <<-MSG
453
+ Deprecation Warning: filters on attributes should be done using the :with
454
+ option, not :conditions. For example:
455
+ :with => {:#{attrib} => #{value.inspect}}
456
+ MSG
457
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value)
458
+ else
459
+ nil
657
460
  end
658
-
659
- lat && lon ? {
660
- :latitude_attribute => lat_attr.to_s,
661
- :latitude => lat,
662
- :longitude_attribute => lon_attr.to_s,
663
- :longitude => lon
664
- } : nil
461
+ }.compact
462
+ end
463
+
464
+ def filters
465
+ internal_filters +
466
+ condition_filters +
467
+ (options[:with] || {}).collect { |attrib, value|
468
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value)
469
+ } +
470
+ (options[:without] || {}).collect { |attrib, value|
471
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value), true
472
+ } +
473
+ (options[:with_all] || {}).collect { |attrib, values|
474
+ Array(values).collect { |value|
475
+ Riddle::Client::Filter.new attrib.to_s, filter_value(value)
476
+ }
477
+ }.flatten
478
+ end
479
+
480
+ # When passed a Time instance, returns the integer timestamp.
481
+ #
482
+ # If using Rails 2.1+, need to handle timezones to translate them back to
483
+ # UTC, as that's what datetimes will be stored as by MySQL.
484
+ #
485
+ # in_time_zone is a method that was added for the timezone support in
486
+ # Rails 2.1, which is why it's used for testing. I'm sure there's better
487
+ # ways, but this does the job.
488
+ #
489
+ def filter_value(value)
490
+ case value
491
+ when Range
492
+ filter_value(value.first).first..filter_value(value.last).first
493
+ when Array
494
+ value.collect { |v| filter_value(v) }.flatten
495
+ when Time
496
+ value.respond_to?(:in_time_zone) ? [value.utc.to_i] : [value.to_i]
497
+ else
498
+ Array(value)
665
499
  end
500
+ end
501
+
502
+ def anchor
503
+ return {} unless options[:geo] || (options[:lat] && options[:lng])
504
+
505
+ {
506
+ :latitude => options[:geo] ? options[:geo].first : options[:lat],
507
+ :longitude => options[:geo] ? options[:geo].last : options[:lng],
508
+ :latitude_attribute => latitude_attr.to_s,
509
+ :longitude_attribute => longitude_attr.to_s
510
+ }
511
+ end
512
+
513
+ def latitude_attr
514
+ options[:latitude_attr] ||
515
+ index_option(:latitude_attr) ||
516
+ attribute(:lat, :latitude)
517
+ end
518
+
519
+ def longitude_attr
520
+ options[:longitude_attr] ||
521
+ index_option(:longitude_attr) ||
522
+ attribute(:lon, :lng, :longitude)
523
+ end
524
+
525
+ def index_option(key)
526
+ return nil if options[:classes].length != 1
666
527
 
667
- # Set the sort options using the :order key as well as the appropriate
668
- # Riddle settings.
669
- #
670
- def set_sort_options!(client, options)
671
- klass = options[:class]
672
- fields = klass ? klass.sphinx_indexes.collect { |index|
673
- index.fields.collect { |field| field.unique_name }
674
- }.flatten : []
675
- index_options = klass ? klass.sphinx_index_options : {}
528
+ options[:classes].first.sphinx_indexes.collect { |index|
529
+ index.local_options[key]
530
+ }.compact.first
531
+ end
532
+
533
+ def attribute(*keys)
534
+ return nil if options[:classes].length != 1
535
+
536
+ keys.detect { |key|
537
+ attributes.include?(key)
538
+ }
539
+ end
540
+
541
+ def attributes
542
+ return [] if (options[:classes] || []).length != 1
543
+
544
+ attributes = options[:classes].first.sphinx_indexes.collect { |index|
545
+ index.attributes.collect { |attrib| attrib.unique_name }
546
+ }.flatten
547
+ end
548
+
549
+ def stale_retries
550
+ case options[:retry_stale]
551
+ when TrueClass
552
+ 3
553
+ when nil, FalseClass
554
+ 0
555
+ else
556
+ options[:retry_stale].to_i
557
+ end
558
+ end
559
+
560
+ def instances_from_class(klass, matches)
561
+ index_options = klass.sphinx_index_options
676
562
 
677
- order = options[:order] || index_options[:order]
678
- case order
679
- when Symbol
680
- client.sort_mode = :attr_asc if client.sort_mode == :relevance || client.sort_mode.nil?
681
- if fields.include?(order)
682
- client.sort_by = order.to_s.concat("_sort")
683
- else
684
- client.sort_by = order.to_s
685
- end
686
- when String
687
- client.sort_mode = :extended
688
- client.sort_by = sorted_fields_to_attributes(order, fields)
689
- else
690
- # do nothing
563
+ ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
564
+ instances = ids.length > 0 ? klass.find(
565
+ :all,
566
+ :joins => options[:joins],
567
+ :conditions => {klass.primary_key_for_sphinx.to_sym => ids},
568
+ :include => (options[:include] || index_options[:include]),
569
+ :select => (options[:select] || index_options[:select]),
570
+ :order => (options[:sql_order] || index_options[:sql_order])
571
+ ) : []
572
+
573
+ # Raise an exception if we find records in Sphinx but not in the DB, so
574
+ # the search method can retry without them. See
575
+ # ThinkingSphinx::Search.retry_search_on_stale_index.
576
+ if options[:raise_on_stale] && instances.length < ids.length
577
+ stale_ids = ids - instances.map { |i| i.id }
578
+ raise StaleIdsException, stale_ids
579
+ end
580
+
581
+ # if the user has specified an SQL order, return the collection
582
+ # without rearranging it into the Sphinx order
583
+ return instances if options[:sql_order]
584
+
585
+ ids.collect { |obj_id|
586
+ instances.detect do |obj|
587
+ obj.primary_key_for_sphinx == obj_id
691
588
  end
692
-
693
- client.sort_mode = :attr_asc if client.sort_mode == :asc
694
- client.sort_mode = :attr_desc if client.sort_mode == :desc
589
+ }
590
+ end
591
+
592
+ # Group results by class and call #find(:all) once for each group to reduce
593
+ # the number of #find's in multi-model searches.
594
+ #
595
+ def instances_from_matches
596
+ groups = results[:matches].group_by { |match|
597
+ match[:attributes]["class_crc"]
598
+ }
599
+ groups.each do |crc, group|
600
+ group.replace(
601
+ instances_from_class(class_from_crc(crc), group)
602
+ )
695
603
  end
696
604
 
697
- # Search through a collection of fields and translate any appearances
698
- # of them in a string to their attribute equivalent for sorting.
699
- #
700
- def sorted_fields_to_attributes(string, fields)
701
- fields.each { |field|
702
- string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
703
- match.gsub field.to_s, field.to_s.concat("_sort")
704
- }
605
+ results[:matches].collect do |match|
606
+ groups.detect { |crc, group|
607
+ crc == match[:attributes]["class_crc"]
608
+ }[1].compact.detect { |obj|
609
+ obj.primary_key_for_sphinx == match[:attributes]["sphinx_internal_id"]
705
610
  }
706
-
707
- string
611
+ end
612
+ end
613
+
614
+ def class_from_crc(crc)
615
+ config.models_by_crc[crc].constantize
616
+ end
617
+
618
+ def each_with_attribute(attribute, &block)
619
+ populate
620
+ results[:matches].each_with_index do |match, index|
621
+ yield self[index],
622
+ (match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
623
+ end
624
+ end
625
+
626
+ def is_scope?(method)
627
+ options[:classes] && options[:classes].length == 1 &&
628
+ options[:classes].first.sphinx_scopes.include?(method)
629
+ end
630
+
631
+ def add_scope(method, *args, &block)
632
+ merge_search options[:classes].first.send(method, *args, &block)
633
+ end
634
+
635
+ def merge_search(search)
636
+ search.args.each { |arg| args << arg }
637
+
638
+ search.options.keys.each do |key|
639
+ if HashOptions.include?(key)
640
+ options[key] ||= {}
641
+ options[key].merge! search.options[key]
642
+ elsif ArrayOptions.include?(key)
643
+ options[key] ||= []
644
+ options[key] += search.options[key]
645
+ options[key].uniq!
646
+ else
647
+ options[key] = search.options[key]
648
+ end
708
649
  end
709
650
  end
710
651
  end