pixeltrix-thinking-sphinx 1.1.5 → 1.2.1

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 (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