DrMark-thinking-sphinx 1.1.15 → 1.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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