pixeltrix-thinking-sphinx 1.1.5 → 1.2.1

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