ryanb-thinking_sphinx 0.9.8

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