jaikoo-thinking-sphinx 0.9.10

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