jaikoo-thinking-sphinx 0.9.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/LICENCE +20 -0
  2. data/README +76 -0
  3. data/lib/thinking_sphinx.rb +112 -0
  4. data/lib/thinking_sphinx/active_record.rb +153 -0
  5. data/lib/thinking_sphinx/active_record/delta.rb +80 -0
  6. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  7. data/lib/thinking_sphinx/active_record/search.rb +50 -0
  8. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +27 -0
  9. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +9 -0
  10. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +84 -0
  11. data/lib/thinking_sphinx/association.rb +144 -0
  12. data/lib/thinking_sphinx/attribute.rb +284 -0
  13. data/lib/thinking_sphinx/collection.rb +105 -0
  14. data/lib/thinking_sphinx/configuration.rb +314 -0
  15. data/lib/thinking_sphinx/field.rb +206 -0
  16. data/lib/thinking_sphinx/index.rb +432 -0
  17. data/lib/thinking_sphinx/index/builder.rb +220 -0
  18. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  19. data/lib/thinking_sphinx/rails_additions.rb +68 -0
  20. data/lib/thinking_sphinx/search.rb +436 -0
  21. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +132 -0
  22. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  23. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
  24. data/spec/unit/thinking_sphinx/active_record_spec.rb +295 -0
  25. data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
  26. data/spec/unit/thinking_sphinx/attribute_spec.rb +360 -0
  27. data/spec/unit/thinking_sphinx/collection_spec.rb +71 -0
  28. data/spec/unit/thinking_sphinx/configuration_spec.rb +512 -0
  29. data/spec/unit/thinking_sphinx/field_spec.rb +224 -0
  30. data/spec/unit/thinking_sphinx/index/builder_spec.rb +34 -0
  31. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +68 -0
  32. data/spec/unit/thinking_sphinx/index_spec.rb +317 -0
  33. data/spec/unit/thinking_sphinx/search_spec.rb +203 -0
  34. data/spec/unit/thinking_sphinx_spec.rb +129 -0
  35. data/tasks/thinking_sphinx_tasks.rake +1 -0
  36. data/tasks/thinking_sphinx_tasks.rb +100 -0
  37. metadata +103 -0
@@ -0,0 +1,110 @@
1
+ module ThinkingSphinx
2
+ class Index
3
+ # Instances of this class represent database columns and the stack of
4
+ # associations that lead from the base model to them.
5
+ #
6
+ # The name and stack are accessible through methods starting with __ to
7
+ # avoid conflicting with the method_missing calls that build the stack.
8
+ #
9
+ class FauxColumn
10
+ # Create a new column with a pre-defined stack. The top element in the
11
+ # stack will get shifted to be the name value.
12
+ #
13
+ def initialize(*stack)
14
+ @name = stack.pop
15
+ @stack = stack
16
+ end
17
+
18
+ def self.coerce(columns)
19
+ case columns
20
+ when Symbol, String
21
+ FauxColumn.new(columns)
22
+ when Array
23
+ columns.collect { |col| FauxColumn.coerce(col) }
24
+ when FauxColumn
25
+ columns
26
+ else
27
+ nil
28
+ end
29
+ end
30
+
31
+ # Can't use normal method name, as that could be an association or
32
+ # column name.
33
+ #
34
+ def __name
35
+ @name
36
+ end
37
+
38
+ # Can't use normal method name, as that could be an association or
39
+ # column name.
40
+ #
41
+ def __stack
42
+ @stack
43
+ end
44
+
45
+ # Returns true if the stack is empty *and* if the name is a string -
46
+ # which is an indication that of raw SQL, as opposed to a value from a
47
+ # table's column.
48
+ #
49
+ def is_string?
50
+ @name.is_a?(String) && @stack.empty?
51
+ end
52
+
53
+ # This handles any 'invalid' method calls and sets them as the name,
54
+ # and pushing the previous name into the stack. The object returns
55
+ # itself.
56
+ #
57
+ # If there's a single argument, it becomes the name, and the method
58
+ # symbol goes into the stack as well. Multiple arguments means new
59
+ # columns with the original stack and new names (from each argument) gets
60
+ # returned.
61
+ #
62
+ # Easier to explain with examples:
63
+ #
64
+ # col = FauxColumn.new :a, :b, :c
65
+ # col.__name #=> :c
66
+ # col.__stack #=> [:a, :b]
67
+ #
68
+ # col.whatever #=> col
69
+ # col.__name #=> :whatever
70
+ # col.__stack #=> [:a, :b, :c]
71
+ #
72
+ # col.something(:id) #=> col
73
+ # col.__name #=> :id
74
+ # col.__stack #=> [:a, :b, :c, :whatever, :something]
75
+ #
76
+ # cols = col.short(:x, :y, :z)
77
+ # cols[0].__name #=> :x
78
+ # cols[0].__stack #=> [:a, :b, :c, :whatever, :something, :short]
79
+ # cols[1].__name #=> :y
80
+ # cols[1].__stack #=> [:a, :b, :c, :whatever, :something, :short]
81
+ # cols[2].__name #=> :z
82
+ # cols[2].__stack #=> [:a, :b, :c, :whatever, :something, :short]
83
+ #
84
+ # Also, this allows method chaining to build up a relevant stack:
85
+ #
86
+ # col = FauxColumn.new :a, :b
87
+ # col.__name #=> :b
88
+ # col.__stack #=> [:a]
89
+ #
90
+ # col.one.two.three #=> col
91
+ # col.__name #=> :three
92
+ # col.__stack #=> [:a, :b, :one, :two]
93
+ #
94
+ def method_missing(method, *args)
95
+ @stack << @name
96
+ @name = method
97
+
98
+ if (args.empty?)
99
+ self
100
+ elsif (args.length == 1)
101
+ method_missing(args.first)
102
+ else
103
+ args.collect { |arg|
104
+ FauxColumn.new(@stack + [@name, arg])
105
+ }
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,68 @@
1
+ module ThinkingSphinx
2
+ module HashExcept
3
+ # Returns a new hash without the given keys.
4
+ def except(*keys)
5
+ rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
6
+ reject { |key,| rejected.include?(key) }
7
+ end
8
+
9
+ # Replaces the hash without only the given keys.
10
+ def except!(*keys)
11
+ replace(except(*keys))
12
+ end
13
+ end
14
+ end
15
+
16
+ Hash.send(
17
+ :include, ThinkingSphinx::HashExcept
18
+ ) unless Hash.instance_methods.include?("except")
19
+
20
+ module ThinkingSphinx
21
+ module ArrayExtractOptions
22
+ def extract_options!
23
+ last.is_a?(::Hash) ? pop : {}
24
+ end
25
+ end
26
+ end
27
+
28
+ Array.send(
29
+ :include, ThinkingSphinx::ArrayExtractOptions
30
+ ) unless Array.instance_methods.include?("extract_options!")
31
+
32
+ module ThinkingSphinx
33
+ module MysqlQuotedTableName
34
+ def quote_table_name(name) #:nodoc:
35
+ quote_column_name(name).gsub('.', '`.`')
36
+ end
37
+ end
38
+ end
39
+
40
+ if ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter")
41
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.send(
42
+ :include, ThinkingSphinx::MysqlQuotedTableName
43
+ ) unless ActiveRecord::ConnectionAdapters::MysqlAdapter.instance_methods.include?("quote_table_name")
44
+ end
45
+
46
+ module ThinkingSphinx
47
+ module ActiveRecordQuotedName
48
+ def quoted_table_name
49
+ self.connection.quote_table_name(self.table_name)
50
+ end
51
+ end
52
+ end
53
+
54
+ ActiveRecord::Base.extend(
55
+ ThinkingSphinx::ActiveRecordQuotedName
56
+ ) unless ActiveRecord::Base.respond_to?("quoted_table_name")
57
+
58
+ module ThinkingSphinx
59
+ module ActiveRecordStoreFullSTIClass
60
+ def store_full_sti_class
61
+ false
62
+ end
63
+ end
64
+ end
65
+
66
+ ActiveRecord::Base.extend(
67
+ ThinkingSphinx::ActiveRecordStoreFullSTIClass
68
+ ) unless ActiveRecord::Base.respond_to?(:store_full_sti_class)
@@ -0,0 +1,436 @@
1
+ module ThinkingSphinx
2
+ # Once you've got those indexes in and built, this is the stuff that
3
+ # matters - how to search! This class provides a generic search
4
+ # interface - which you can use to search all your indexed models at once.
5
+ # Most times, you will just want a specific model's results - to search and
6
+ # search_for_ids methods will do the job in exactly the same manner when
7
+ # called from a model.
8
+ #
9
+ 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
+ def search_for_ids(*args)
16
+ results, client = search_results(*args.clone)
17
+
18
+ options = args.extract_options!
19
+ page = options[:page] ? options[:page].to_i : 1
20
+
21
+ ThinkingSphinx::Collection.ids_from_results(results, page, client.limit, options)
22
+ end
23
+
24
+ # Searches through the Sphinx indexes for relevant matches. There's
25
+ # various ways to search, sort, group and filter - which are covered
26
+ # below.
27
+ #
28
+ # Also, if you have WillPaginate installed, the search method can be used
29
+ # just like paginate. The same parameters - :page and :per_page - work as
30
+ # expected, and the returned result set can be used by the will_paginate
31
+ # helper.
32
+ #
33
+ # == Basic Searching
34
+ #
35
+ # The simplest way of searching is straight text.
36
+ #
37
+ # ThinkingSphinx::Search.search "pat"
38
+ # ThinkingSphinx::Search.search "google"
39
+ # User.search "pat", :page => (params[:page] || 1)
40
+ # Article.search "relevant news issue of the day"
41
+ #
42
+ # If you specify :include, like in an #find call, this will be respected
43
+ # when loading the relevant models from the search results.
44
+ #
45
+ # User.search "pat", :include => :posts
46
+ #
47
+ # == Advanced Searching
48
+ #
49
+ # Sphinx supports 5 different matching modes. By default Thinking Sphinx
50
+ # uses :all, which unsurprisingly requires all the supplied search terms
51
+ # to match a result.
52
+ #
53
+ # Alternative modes include:
54
+ #
55
+ # User.search "pat allan", :match_mode => :any
56
+ # User.search "pat allan", :match_mode => :phrase
57
+ # User.search "pat | allan", :match_mode => :boolean
58
+ # User.search "@name pat | @username pat", :match_mode => :extended
59
+ #
60
+ # Any will find results with any of the search terms. Phrase treats the search
61
+ # terms a single phrase instead of individual words. Boolean and extended allow
62
+ # for more complex query syntax, refer to the sphinx documentation for further
63
+ # details.
64
+ #
65
+ # == Searching by Fields
66
+ #
67
+ # If you want to step it up a level, you can limit your search terms to
68
+ # specific fields:
69
+ #
70
+ # User.search :conditions => {:name => "pat"}
71
+ #
72
+ # This uses Sphinx's extended match mode, unless you specify a different
73
+ # match mode explicitly (but then this way of searching won't work). Also
74
+ # note that you don't need to put in a search string.
75
+ #
76
+ # == Searching by Attributes
77
+ #
78
+ # Also known as filters, you can limit your searches to documents that
79
+ # have specific values for their attributes. There are two ways to do
80
+ # this. The first is one that works in all scenarios - using the :with
81
+ # option.
82
+ #
83
+ # ThinkingSphinx::Search.search :with => {:parent_id => 10}
84
+ #
85
+ # The second is only viable if you're searching with a specific model
86
+ # (not multi-model searching). With a single model, Thinking Sphinx
87
+ # can figure out what attributes and fields are available, so you can
88
+ # put it all in the :conditions hash, and it will sort it out.
89
+ #
90
+ # Node.search :conditions => {:parent_id => 10}
91
+ #
92
+ # Filters can be single values, arrays of values, or ranges.
93
+ #
94
+ # Article.search "East Timor", :conditions => {:rating => 3..5}
95
+ #
96
+ # == Excluding by Attributes
97
+ #
98
+ # Sphinx also supports negative filtering - where the filters are of
99
+ # attribute values to exclude. This is done with the :without option:
100
+ #
101
+ # User.search :without => {:role_id => 1}
102
+ #
103
+ # == Sorting
104
+ #
105
+ # Sphinx can only sort by attributes, so generally you will need to avoid
106
+ # using field names in your :order option. However, if you're searching
107
+ # on a single model, and have specified some fields as sortable, you can
108
+ # use those field names and Thinking Sphinx will interpret accordingly.
109
+ # Remember: this will only happen for single-model searches, and only
110
+ # through the :order option.
111
+ #
112
+ # Location.search "Melbourne", :order => :state
113
+ # User.search :conditions => {:role_id => 2}, :order => "name ASC"
114
+ #
115
+ # Keep in mind that if you use a string, you *must* specify the direction
116
+ # (ASC or DESC) else Sphinx won't return any results. If you use a symbol
117
+ # then Thinking Sphinx assumes ASC, but if you wish to state otherwise,
118
+ # use the :sort_mode option:
119
+ #
120
+ # Location.search "Melbourne", :order => :state, :sort_mode => :desc
121
+ #
122
+ # Of course, there are other sort modes - check out the Sphinx
123
+ # documentation[http://sphinxsearch.com/doc.html] for that level of
124
+ # detail though.
125
+ #
126
+ # == Grouping
127
+ #
128
+ # For this you can use the group_by, group_clause and group_function
129
+ # options - which are all directly linked to Sphinx's expectations. No
130
+ # magic from Thinking Sphinx. It can get a little tricky, so make sure
131
+ # you read all the relevant
132
+ # documentation[http://sphinxsearch.com/doc.html#clustering] first.
133
+ #
134
+ # Yes this section will be expanded, but this is a start.
135
+ #
136
+ # == Geo/Location Searching
137
+ #
138
+ # Sphinx - and therefore Thinking Sphinx - has the facility to search
139
+ # around a geographical point, using a given latitude and longitude. To
140
+ # take advantage of this, you will need to have both of those values in
141
+ # attributes. To search with that point, you can then use one of the
142
+ # following syntax examples:
143
+ #
144
+ # Address.search "Melbourne", :geo => [1.4, -2.217], :order => "@geodist asc"
145
+ # Address.search "Australia", :geo => [-0.55, 3.108], :order => "@geodist asc"
146
+ # :latitude_attr => "latit", :longitude_attr => "longit"
147
+ #
148
+ # The first example applies when your latitude and longitude attributes
149
+ # are named any of lat, latitude, lon, long or longitude. If that's not
150
+ # the case, you will need to explicitly state them in your search, _or_
151
+ # you can do so in your model:
152
+ #
153
+ # define_index do
154
+ # has :latit # Float column, stored in radians
155
+ # has :longit # Float column, stored in radians
156
+ #
157
+ # set_property :latitude_attr => "latit"
158
+ # set_property :longitude_attr => "longit"
159
+ # end
160
+ #
161
+ # Now, geo-location searching really only has an affect if you have a
162
+ # filter, sort or grouping clause related to it - otherwise it's just a
163
+ # normal search, and _will not_ return a distance value otherwise. To
164
+ # make use of the positioning difference, use the special attribute
165
+ # "@geodist" in any of your filters or sorting or grouping clauses.
166
+ #
167
+ # And don't forget - both the latitude and longitude you use in your
168
+ # search, and the values in your indexes, need to be stored as a float in radians,
169
+ # _not_ degrees. Keep in mind that if you do this conversion in SQL
170
+ # you will need to explicitly declare a column type of :float.
171
+ #
172
+ # define_index do
173
+ # has 'RADIANS(lat)', :as => :lat, :type => :float
174
+ # # ...
175
+ # end
176
+ #
177
+ # Once you've got your results set, you can access the distances as
178
+ # follows:
179
+ #
180
+ # @results.each_with_geodist do |result, distance|
181
+ # # ...
182
+ # end
183
+ #
184
+ # The distance value is returned as a float, representing the distance in
185
+ # metres.
186
+ #
187
+ def search(*args)
188
+ results, client = search_results(*args.clone)
189
+
190
+ ::ActiveRecord::Base.logger.error(
191
+ "Sphinx Error: #{results[:error]}"
192
+ ) if results[:error]
193
+
194
+ options = args.extract_options!
195
+ klass = options[:class]
196
+ page = options[:page] ? options[:page].to_i : 1
197
+
198
+ ThinkingSphinx::Collection.create_from_results(results, page, client.limit, options)
199
+ end
200
+
201
+ def count(*args)
202
+ results, client = search_results(*args.clone)
203
+ results[:total] || 0
204
+ end
205
+
206
+ # Checks if a document with the given id exists within a specific index.
207
+ # Expected parameters:
208
+ #
209
+ # - ID of the document
210
+ # - Index to check within
211
+ # - Options hash (defaults to {})
212
+ #
213
+ # Example:
214
+ #
215
+ # ThinkingSphinx::Search.search_for_id(10, "user_core", :class => User)
216
+ #
217
+ def search_for_id(*args)
218
+ options = args.extract_options!
219
+ client = client_from_options options
220
+
221
+ query, filters = search_conditions(
222
+ options[:class], options[:conditions] || {}
223
+ )
224
+ client.filters += filters
225
+ client.match_mode = :extended unless query.empty?
226
+ client.id_range = args.first..args.first
227
+
228
+ begin
229
+ return client.query(query, args[1])[:matches].length > 0
230
+ rescue Errno::ECONNREFUSED => err
231
+ raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
232
+ end
233
+ end
234
+
235
+ private
236
+
237
+ # This method handles the common search functionality, and returns both
238
+ # the result hash and the client. Not super elegant, but it'll do for
239
+ # the moment.
240
+ #
241
+ def search_results(*args)
242
+ options = args.extract_options!
243
+ client = client_from_options options
244
+
245
+ query, filters = search_conditions(
246
+ options[:class], options[:conditions] || {}
247
+ )
248
+ client.filters += filters
249
+ client.match_mode = :extended unless query.empty?
250
+ query = args.join(" ") + query
251
+
252
+ set_sort_options! client, options
253
+
254
+ client.limit = options[:per_page].to_i if options[:per_page]
255
+ page = options[:page] ? options[:page].to_i : 1
256
+ client.offset = (page - 1) * client.limit
257
+
258
+ begin
259
+ ::ActiveRecord::Base.logger.debug "Sphinx: #{query}"
260
+ results = client.query query
261
+ ::ActiveRecord::Base.logger.debug "Sphinx Result: #{results[:matches].collect{|m| m[:attributes]["sphinx_internal_id"]}.inspect}"
262
+ rescue Errno::ECONNREFUSED => err
263
+ raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
264
+ end
265
+
266
+ return results, client
267
+ end
268
+
269
+ # Set all the appropriate settings for the client, using the provided
270
+ # options hash.
271
+ #
272
+ def client_from_options(options = {})
273
+ config = ThinkingSphinx::Configuration.instance
274
+ client = Riddle::Client.new config.address, config.port
275
+ klass = options[:class]
276
+ index_options = klass ? klass.sphinx_indexes.last.options : {}
277
+
278
+ [
279
+ :max_matches, :match_mode, :sort_mode, :sort_by, :id_range,
280
+ :group_by, :group_function, :group_clause, :group_distinct, :cut_off,
281
+ :retry_count, :retry_delay, :index_weights, :rank_mode,
282
+ :max_query_time, :field_weights, :filters, :anchor, :limit
283
+ ].each do |key|
284
+ client.send(
285
+ key.to_s.concat("=").to_sym,
286
+ options[key] || index_options[key] || client.send(key)
287
+ )
288
+ end
289
+
290
+ options[:classes] = [klass] if klass
291
+
292
+ client.anchor = anchor_conditions(klass, options) || {} if client.anchor.empty?
293
+
294
+ client.filters << Riddle::Client::Filter.new(
295
+ "sphinx_deleted", [0]
296
+ )
297
+
298
+ # class filters
299
+ client.filters << Riddle::Client::Filter.new(
300
+ "class_crc", options[:classes].collect { |k| k.to_crc32s }.flatten
301
+ ) if options[:classes]
302
+
303
+ # normal attribute filters
304
+ client.filters += options[:with].collect { |attr,val|
305
+ Riddle::Client::Filter.new attr.to_s, filter_value(val)
306
+ } if options[:with]
307
+
308
+ # exclusive attribute filters
309
+ client.filters += options[:without].collect { |attr,val|
310
+ Riddle::Client::Filter.new attr.to_s, filter_value(val), true
311
+ } if options[:without]
312
+
313
+ client
314
+ end
315
+
316
+ def filter_value(value)
317
+ case value
318
+ when Range
319
+ value.first.is_a?(Time) ? value.first.to_i..value.last.to_i : value
320
+ when Array
321
+ value.collect { |val| val.is_a?(Time) ? val.to_i : val }
322
+ else
323
+ Array(value)
324
+ end
325
+ end
326
+
327
+ # Translate field and attribute conditions to the relevant search string
328
+ # and filters.
329
+ #
330
+ def search_conditions(klass, conditions={})
331
+ attributes = klass ? klass.sphinx_indexes.collect { |index|
332
+ index.attributes.collect { |attrib| attrib.unique_name }
333
+ }.flatten : []
334
+
335
+ search_string = ""
336
+ filters = []
337
+
338
+ conditions.each do |key,val|
339
+ if attributes.include?(key.to_sym)
340
+ filters << Riddle::Client::Filter.new(
341
+ key.to_s, filter_value(val)
342
+ )
343
+ else
344
+ search_string << "@#{key} #{val} "
345
+ end
346
+ end
347
+
348
+ return search_string, filters
349
+ end
350
+
351
+ # Return the appropriate latitude and longitude values, depending on
352
+ # whether the relevant attributes have been defined, and also whether
353
+ # there's actually any values.
354
+ #
355
+ def anchor_conditions(klass, options)
356
+ attributes = klass ? klass.sphinx_indexes.collect { |index|
357
+ index.attributes.collect { |attrib| attrib.unique_name }
358
+ }.flatten : []
359
+
360
+ lat_attr = klass ? klass.sphinx_indexes.collect { |index|
361
+ index.options[:latitude_attr]
362
+ }.compact.first : nil
363
+
364
+ lon_attr = klass ? klass.sphinx_indexes.collect { |index|
365
+ index.options[:longitude_attr]
366
+ }.compact.first : nil
367
+
368
+ lat_attr = options[:latitude_attr] if options[:latitude_attr]
369
+ lat_attr ||= :lat if attributes.include?(:lat)
370
+ lat_attr ||= :latitude if attributes.include?(:latitude)
371
+
372
+ lon_attr = options[:longitude_attr] if options[:longitude_attr]
373
+ lon_attr ||= :lng if attributes.include?(:lng)
374
+ lon_attr ||= :lon if attributes.include?(:lon)
375
+ lon_attr ||= :long if attributes.include?(:long)
376
+ lon_attr ||= :longitude if attributes.include?(:longitude)
377
+
378
+ lat = options[:lat]
379
+ lon = options[:lon]
380
+
381
+ if options[:geo]
382
+ lat = options[:geo].first
383
+ lon = options[:geo].last
384
+ end
385
+
386
+ lat && lon ? {
387
+ :latitude_attribute => lat_attr.to_s,
388
+ :latitude => lat,
389
+ :longitude_attribute => lon_attr.to_s,
390
+ :longitude => lon
391
+ } : nil
392
+ end
393
+
394
+ # Set the sort options using the :order key as well as the appropriate
395
+ # Riddle settings.
396
+ #
397
+ def set_sort_options!(client, options)
398
+ klass = options[:class]
399
+ fields = klass ? klass.sphinx_indexes.collect { |index|
400
+ index.fields.collect { |field| field.unique_name }
401
+ }.flatten : []
402
+
403
+ case order = options[:order]
404
+ when Symbol
405
+ client.sort_mode = :attr_asc if client.sort_mode == :relevance || client.sort_mode.nil?
406
+ if fields.include?(order)
407
+ client.sort_by = order.to_s.concat("_sort")
408
+ else
409
+ client.sort_by = order.to_s
410
+ end
411
+ when String
412
+ client.sort_mode = :extended
413
+ client.sort_by = sorted_fields_to_attributes(order, fields)
414
+ else
415
+ # do nothing
416
+ end
417
+
418
+ client.sort_mode = :attr_asc if client.sort_mode == :asc
419
+ client.sort_mode = :attr_desc if client.sort_mode == :desc
420
+ end
421
+
422
+ # Search through a collection of fields and translate any appearances
423
+ # of them in a string to their attribute equivalent for sorting.
424
+ #
425
+ def sorted_fields_to_attributes(string, fields)
426
+ fields.each { |field|
427
+ string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
428
+ match.gsub field.to_s, field.to_s.concat("_sort")
429
+ }
430
+ }
431
+
432
+ string
433
+ end
434
+ end
435
+ end
436
+ end