DrMark-thinking-sphinx 0.9.9 → 1.1.6

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 (77) hide show
  1. data/README +64 -2
  2. data/lib/thinking_sphinx.rb +88 -11
  3. data/lib/thinking_sphinx/active_record.rb +136 -21
  4. data/lib/thinking_sphinx/active_record/delta.rb +43 -62
  5. data/lib/thinking_sphinx/active_record/has_many_association.rb +1 -1
  6. data/lib/thinking_sphinx/active_record/search.rb +7 -0
  7. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  8. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  9. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +130 -0
  10. data/lib/thinking_sphinx/association.rb +17 -0
  11. data/lib/thinking_sphinx/attribute.rb +171 -97
  12. data/lib/thinking_sphinx/collection.rb +126 -2
  13. data/lib/thinking_sphinx/configuration.rb +120 -171
  14. data/lib/thinking_sphinx/core/string.rb +15 -0
  15. data/lib/thinking_sphinx/deltas.rb +27 -0
  16. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  17. data/lib/thinking_sphinx/deltas/default_delta.rb +67 -0
  18. data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
  19. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  20. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  21. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  22. data/lib/thinking_sphinx/facet.rb +58 -0
  23. data/lib/thinking_sphinx/facet_collection.rb +60 -0
  24. data/lib/thinking_sphinx/field.rb +18 -52
  25. data/lib/thinking_sphinx/index.rb +246 -199
  26. data/lib/thinking_sphinx/index/builder.rb +85 -16
  27. data/lib/thinking_sphinx/rails_additions.rb +85 -5
  28. data/lib/thinking_sphinx/search.rb +459 -190
  29. data/lib/thinking_sphinx/tasks.rb +128 -0
  30. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +53 -124
  31. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +2 -2
  32. data/spec/unit/thinking_sphinx/active_record_spec.rb +110 -30
  33. data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -149
  34. data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
  35. data/spec/unit/thinking_sphinx/configuration_spec.rb +54 -412
  36. data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
  37. data/spec/unit/thinking_sphinx/field_spec.rb +0 -79
  38. data/spec/unit/thinking_sphinx/index/builder_spec.rb +1 -29
  39. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +1 -39
  40. data/spec/unit/thinking_sphinx/index_spec.rb +78 -226
  41. data/spec/unit/thinking_sphinx/search_spec.rb +29 -228
  42. data/spec/unit/thinking_sphinx_spec.rb +23 -19
  43. data/tasks/distribution.rb +48 -0
  44. data/tasks/rails.rake +1 -0
  45. data/tasks/testing.rb +86 -0
  46. data/vendor/after_commit/LICENSE +20 -0
  47. data/vendor/after_commit/README +16 -0
  48. data/vendor/after_commit/Rakefile +22 -0
  49. data/vendor/after_commit/init.rb +8 -0
  50. data/vendor/after_commit/lib/after_commit.rb +45 -0
  51. data/vendor/after_commit/lib/after_commit/active_record.rb +114 -0
  52. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  53. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  54. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  55. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  56. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  57. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  58. data/{lib → vendor/riddle/lib}/riddle.rb +9 -5
  59. data/{lib → vendor/riddle/lib}/riddle/client.rb +6 -26
  60. data/{lib → vendor/riddle/lib}/riddle/client/filter.rb +10 -1
  61. data/{lib → vendor/riddle/lib}/riddle/client/message.rb +0 -0
  62. data/{lib → vendor/riddle/lib}/riddle/client/response.rb +0 -0
  63. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  64. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  65. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  66. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  67. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  68. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  69. data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
  70. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  71. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  72. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  73. data/vendor/riddle/lib/riddle/controller.rb +44 -0
  74. metadata +63 -10
  75. data/lib/test.rb +0 -46
  76. data/tasks/thinking_sphinx_tasks.rake +0 -1
  77. data/tasks/thinking_sphinx_tasks.rb +0 -86
@@ -18,9 +18,14 @@ module ThinkingSphinx
18
18
  # rails documentation. It's not needed though, so it gets undef'd.
19
19
  # Hopefully the list of methods that get in the way doesn't get too
20
20
  # long.
21
- undef_method :parent
21
+ HiddenMethods = [:parent, :name, :id, :type].each { |method|
22
+ define_method(method) {
23
+ caller.grep(/irb.completion/).empty? ? method_missing(method) : super
24
+ }
25
+ }
22
26
 
23
- attr_accessor :fields, :attributes, :properties, :conditions
27
+ attr_accessor :fields, :attributes, :properties, :conditions,
28
+ :groupings
24
29
 
25
30
  # Set up all the collections. Consider this the equivalent of an
26
31
  # instance's initialize method.
@@ -30,6 +35,7 @@ module ThinkingSphinx
30
35
  @attributes = []
31
36
  @properties = {}
32
37
  @conditions = []
38
+ @groupings = []
33
39
  end
34
40
 
35
41
  # This is how you add fields - the strings Sphinx looks at - to your
@@ -81,22 +87,16 @@ module ThinkingSphinx
81
87
  def indexes(*args)
82
88
  options = args.extract_options!
83
89
  args.each do |columns|
84
- fields << Field.new(FauxColumn.coerce(columns), options)
90
+ field = Field.new(FauxColumn.coerce(columns), options)
91
+ fields << field
85
92
 
86
- if fields.last.sortable
87
- attributes << Attribute.new(
88
- fields.last.columns.collect { |col| col.clone },
89
- options.merge(
90
- :type => :string,
91
- :as => fields.last.unique_name.to_s.concat("_sort").to_sym
92
- )
93
- )
94
- end
93
+ add_sort_attribute field, options if field.sortable
94
+ add_facet_attribute field, options if field.faceted
95
95
  end
96
96
  end
97
97
  alias_method :field, :indexes
98
98
  alias_method :includes, :indexes
99
-
99
+
100
100
  # This is the method to add attributes to your index (hence why it is
101
101
  # aliased as 'attribute'). The syntax is the same as #indexes, so use
102
102
  # that as starting point, but keep in mind the following points.
@@ -128,7 +128,7 @@ module ThinkingSphinx
128
128
  # when you would like to index a calculated value. Don't forget to set
129
129
  # the type of the attribute though:
130
130
  #
131
- # indexes "age < 18", :as => :minor, :type => :boolean
131
+ # has "age < 18", :as => :minor, :type => :boolean
132
132
  #
133
133
  # If you're creating attributes for latitude and longitude, don't
134
134
  # forget that Sphinx expects these values to be in radians.
@@ -136,11 +136,26 @@ module ThinkingSphinx
136
136
  def has(*args)
137
137
  options = args.extract_options!
138
138
  args.each do |columns|
139
- attributes << Attribute.new(FauxColumn.coerce(columns), options)
139
+ attribute = Attribute.new(FauxColumn.coerce(columns), options)
140
+ attributes << attribute
141
+
142
+ add_facet_attribute attribute, options if attribute.faceted
140
143
  end
141
144
  end
142
145
  alias_method :attribute, :has
143
146
 
147
+ def facet(*args)
148
+ options = args.extract_options!
149
+ options[:facet] = true
150
+
151
+ args.each do |columns|
152
+ attribute = Attribute.new(FauxColumn.coerce(columns), options)
153
+ attributes << attribute
154
+
155
+ add_facet_attribute attribute, options
156
+ end
157
+ end
158
+
144
159
  # Use this method to add some manual SQL conditions for your index
145
160
  # request. You can pass in as many strings as you like, they'll get
146
161
  # joined together with ANDs later on.
@@ -152,6 +167,16 @@ module ThinkingSphinx
152
167
  @conditions += args
153
168
  end
154
169
 
170
+ # Use this method to add some manual SQL strings to the GROUP BY
171
+ # clause. You can pass in as many strings as you'd like, they'll get
172
+ # joined together with commas later on.
173
+ #
174
+ # group_by "lat", "lng"
175
+ #
176
+ def group_by(*args)
177
+ @groupings += args
178
+ end
179
+
155
180
  # This is what to use to set properties on the index. Chief amongst
156
181
  # those is the delta property - to allow automatic updates to your
157
182
  # indexes as new models are added and edited - but also you can
@@ -160,6 +185,9 @@ module ThinkingSphinx
160
185
  #
161
186
  # set_property :delta => true
162
187
  # set_property :field_weights => {"name" => 100}
188
+ # set_property :order => "name ASC"
189
+ # set_property :include => :picture
190
+ # set_property :select => 'name'
163
191
  #
164
192
  # Also, the following two properties are particularly relevant for
165
193
  # geo-location searching - latitude_attr and longitude_attr. If your
@@ -168,11 +196,21 @@ module ThinkingSphinx
168
196
  # when defining the index, so you don't need to specify them for every
169
197
  # geo-related search.
170
198
  #
171
- # set_property :latitude_attr => "lt", :longitude => "lg"
199
+ # set_property :latitude_attr => "lt", :longitude_attr => "lg"
172
200
  #
173
201
  # Please don't forget to add a boolean field named 'delta' to your
174
202
  # model's database table if enabling the delta index for it.
203
+ # Valid options for the delta property are:
204
+ #
205
+ # true
206
+ # false
207
+ # :default
208
+ # :delayed
209
+ # :datetime
175
210
  #
211
+ # You can also extend ThinkingSphinx::Deltas::DefaultDelta to implement
212
+ # your own handling for delta indexing.
213
+
176
214
  def set_property(*args)
177
215
  options = args.extract_options!
178
216
  if options.empty?
@@ -189,6 +227,37 @@ module ThinkingSphinx
189
227
  def method_missing(method, *args)
190
228
  FauxColumn.new(method, *args)
191
229
  end
230
+
231
+ # A method to allow adding fields from associations which have names
232
+ # that clash with method names in the Builder class (ie: properties,
233
+ # fields, attributes).
234
+ #
235
+ # Example: indexes assoc(:properties).column
236
+ #
237
+ def assoc(assoc, *args)
238
+ FauxColumn.new(assoc, *args)
239
+ end
240
+
241
+ private
242
+
243
+ def add_sort_attribute(field, options)
244
+ add_internal_attribute field, options, "_sort"
245
+ end
246
+
247
+ def add_facet_attribute(resource, options)
248
+ add_internal_attribute resource, options, "_facet", true
249
+ end
250
+
251
+ def add_internal_attribute(resource, options, suffix, crc = false)
252
+ @attributes << Attribute.new(
253
+ resource.columns.collect { |col| col.clone },
254
+ options.merge(
255
+ :type => resource.is_a?(Field) ? :string : nil,
256
+ :as => resource.unique_name.to_s.concat(suffix).to_sym,
257
+ :crc => crc
258
+ ).except(:facet)
259
+ )
260
+ end
192
261
  end
193
262
  end
194
263
  end
@@ -29,6 +29,18 @@ Array.send(
29
29
  :include, ThinkingSphinx::ArrayExtractOptions
30
30
  ) unless Array.instance_methods.include?("extract_options!")
31
31
 
32
+ module ThinkingSphinx
33
+ module AbstractQuotedTableName
34
+ def quote_table_name(name)
35
+ quote_column_name(name)
36
+ end
37
+ end
38
+ end
39
+
40
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.send(
41
+ :include, ThinkingSphinx::AbstractQuotedTableName
42
+ ) unless ActiveRecord::ConnectionAdapters::AbstractAdapter.instance_methods.include?("quote_table_name")
43
+
32
44
  module ThinkingSphinx
33
45
  module MysqlQuotedTableName
34
46
  def quote_table_name(name) #:nodoc:
@@ -37,10 +49,13 @@ module ThinkingSphinx
37
49
  end
38
50
  end
39
51
 
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")
52
+ if ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter") or ActiveRecord::Base.respond_to?(:jdbcmysql_connection)
53
+ adapter = ActiveRecord::ConnectionAdapters.const_get(
54
+ defined?(JRUBY_VERSION) ? :JdbcAdapter : :MysqlAdapter
55
+ )
56
+ unless adapter.instance_methods.include?("quote_table_name")
57
+ adapter.send(:include, ThinkingSphinx::MysqlQuotedTableName)
58
+ end
44
59
  end
45
60
 
46
61
  module ThinkingSphinx
@@ -53,4 +68,69 @@ end
53
68
 
54
69
  ActiveRecord::Base.extend(
55
70
  ThinkingSphinx::ActiveRecordQuotedName
56
- ) unless ActiveRecord::Base.respond_to?("quoted_table_name")
71
+ ) unless ActiveRecord::Base.respond_to?("quoted_table_name")
72
+
73
+ module ThinkingSphinx
74
+ module ActiveRecordStoreFullSTIClass
75
+ def store_full_sti_class
76
+ false
77
+ end
78
+ end
79
+ end
80
+
81
+ ActiveRecord::Base.extend(
82
+ ThinkingSphinx::ActiveRecordStoreFullSTIClass
83
+ ) unless ActiveRecord::Base.respond_to?(:store_full_sti_class)
84
+
85
+ module ThinkingSphinx
86
+ module ClassAttributeMethods
87
+ def cattr_reader(*syms)
88
+ syms.flatten.each do |sym|
89
+ next if sym.is_a?(Hash)
90
+ class_eval(<<-EOS, __FILE__, __LINE__)
91
+ unless defined? @@#{sym}
92
+ @@#{sym} = nil
93
+ end
94
+
95
+ def self.#{sym}
96
+ @@#{sym}
97
+ end
98
+
99
+ def #{sym}
100
+ @@#{sym}
101
+ end
102
+ EOS
103
+ end
104
+ end
105
+
106
+ def cattr_writer(*syms)
107
+ options = syms.extract_options!
108
+ syms.flatten.each do |sym|
109
+ class_eval(<<-EOS, __FILE__, __LINE__)
110
+ unless defined? @@#{sym}
111
+ @@#{sym} = nil
112
+ end
113
+
114
+ def self.#{sym}=(obj)
115
+ @@#{sym} = obj
116
+ end
117
+
118
+ #{"
119
+ def #{sym}=(obj)
120
+ @@#{sym} = obj
121
+ end
122
+ " unless options[:instance_writer] == false }
123
+ EOS
124
+ end
125
+ end
126
+
127
+ def cattr_accessor(*syms)
128
+ cattr_reader(*syms)
129
+ cattr_writer(*syms)
130
+ end
131
+ end
132
+ end
133
+
134
+ Class.extend(
135
+ ThinkingSphinx::ClassAttributeMethods
136
+ ) unless Class.respond_to?(:cattr_reader)
@@ -5,35 +5,29 @@ module ThinkingSphinx
5
5
  # Most times, you will just want a specific model's results - to search and
6
6
  # search_for_ids methods will do the job in exactly the same manner when
7
7
  # called from a model.
8
- #
8
+ #
9
9
  class Search
10
+ GlobalFacetOptions = {
11
+ :all_attributes => false,
12
+ :class_facet => true
13
+ }
14
+
10
15
  class << self
11
16
  # Searches for results that match the parameters provided. Will only
12
17
  # return the ids for the matching objects. See #search for syntax
13
18
  # examples.
14
19
  #
20
+ # Note that this only searches the Sphinx index, with no ActiveRecord
21
+ # queries. Thus, if your index is not in sync with the database, this
22
+ # method may return ids that no longer exist there.
23
+ #
15
24
  def search_for_ids(*args)
16
25
  results, client = search_results(*args.clone)
17
-
26
+
18
27
  options = args.extract_options!
19
28
  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|
25
- match[:attributes]["sphinx_internal_id"]
26
- }
27
- collection.instance_variable_set :@total_entries, results[:total_found]
28
- end
29
- return (options[:include_raw] ? [pager, results] : pager)
30
- rescue
31
- if options[:include_raw]
32
- results[:matches].collect { |match| match[:attributes]["sphinx_internal_id"] }, results
33
- else
34
- results[:matches].collect { |match| match[:attributes]["sphinx_internal_id"] }
35
- end
36
- end
29
+
30
+ ThinkingSphinx::Collection.ids_from_results(results, page, client.limit, options)
37
31
  end
38
32
 
39
33
  # Searches through the Sphinx indexes for relevant matches. There's
@@ -44,11 +38,11 @@ module ThinkingSphinx
44
38
  # just like paginate. The same parameters - :page and :per_page - work as
45
39
  # expected, and the returned result set can be used by the will_paginate
46
40
  # helper.
47
- #
41
+ #
48
42
  # == Basic Searching
49
43
  #
50
44
  # The simplest way of searching is straight text.
51
- #
45
+ #
52
46
  # ThinkingSphinx::Search.search "pat"
53
47
  # ThinkingSphinx::Search.search "google"
54
48
  # User.search "pat", :page => (params[:page] || 1)
@@ -56,10 +50,10 @@ module ThinkingSphinx
56
50
  #
57
51
  # If you specify :include, like in an #find call, this will be respected
58
52
  # when loading the relevant models from the search results.
59
- #
53
+ #
60
54
  # User.search "pat", :include => :posts
61
55
  #
62
- # == Advanced Searching
56
+ # == Match Modes
63
57
  #
64
58
  # Sphinx supports 5 different matching modes. By default Thinking Sphinx
65
59
  # uses :all, which unsurprisingly requires all the supplied search terms
@@ -77,11 +71,25 @@ module ThinkingSphinx
77
71
  # for more complex query syntax, refer to the sphinx documentation for further
78
72
  # details.
79
73
  #
80
- # == Searching by Fields
74
+ # == Weighting
75
+ #
76
+ # Sphinx has support for weighting, where matches in one field can be considered
77
+ # more important than in another. Weights are integers, with 1 as the default.
78
+ # They can be set per-search like this:
79
+ #
80
+ # User.search "pat allan", :field_weights => { :alias => 4, :aka => 2 }
81
+ #
82
+ # If you're searching multiple models, you can set per-index weights:
83
+ #
84
+ # ThinkingSphinx::Search.search "pat", :index_weights => { User => 10 }
85
+ #
86
+ # See http://sphinxsearch.com/doc.html#weighting for further details.
81
87
  #
88
+ # == Searching by Fields
89
+ #
82
90
  # If you want to step it up a level, you can limit your search terms to
83
91
  # specific fields:
84
- #
92
+ #
85
93
  # User.search :conditions => {:name => "pat"}
86
94
  #
87
95
  # This uses Sphinx's extended match mode, unless you specify a different
@@ -91,21 +99,29 @@ module ThinkingSphinx
91
99
  # == Searching by Attributes
92
100
  #
93
101
  # Also known as filters, you can limit your searches to documents that
94
- # have specific values for their attributes. There are two ways to do
95
- # this. The first is one that works in all scenarios - using the :with
96
- # option.
97
- #
98
- # ThinkingSphinx::Search.search :with => {:parent_id => 10}
99
- #
100
- # The second is only viable if you're searching with a specific model
101
- # (not multi-model searching). With a single model, Thinking Sphinx
102
- # can figure out what attributes and fields are available, so you can
103
- # put it all in the :conditions hash, and it will sort it out.
104
- #
102
+ # have specific values for their attributes. There are three ways to do
103
+ # this. The first two techniques work in all scenarios - using the :with
104
+ # or :with_all options.
105
+ #
106
+ # ThinkingSphinx::Search.search :with => {:tag_ids => 10}
107
+ # ThinkingSphinx::Search.search :with => {:tag_ids => [10,12]}
108
+ # ThinkingSphinx::Search.search :with_all => {:tag_ids => [10,12]}
109
+ #
110
+ # The first :with search will match records with a tag_id attribute of 10.
111
+ # The second :with will match records with a tag_id attribute of 10 OR 12.
112
+ # If you need to find records that are tagged with ids 10 AND 12, you
113
+ # will need to use the :with_all search parameter. This is particuarly
114
+ # useful in conjunction with Multi Value Attributes (MVAs).
115
+ #
116
+ # The third filtering technique is only viable if you're searching with a
117
+ # specific model (not multi-model searching). With a single model,
118
+ # Thinking Sphinx can figure out what attributes and fields are available,
119
+ # so you can put it all in the :conditions hash, and it will sort it out.
120
+ #
105
121
  # Node.search :conditions => {:parent_id => 10}
106
- #
122
+ #
107
123
  # Filters can be single values, arrays of values, or ranges.
108
- #
124
+ #
109
125
  # Article.search "East Timor", :conditions => {:rating => 3..5}
110
126
  #
111
127
  # == Excluding by Attributes
@@ -115,6 +131,51 @@ module ThinkingSphinx
115
131
  #
116
132
  # User.search :without => {:role_id => 1}
117
133
  #
134
+ # == Excluding by Primary Key
135
+ #
136
+ # There is a shortcut to exclude records by their ActiveRecord primary key:
137
+ #
138
+ # User.search :without_ids => 1
139
+ #
140
+ # Pass an array or a single value.
141
+ #
142
+ # The primary key must be an integer as a negative filter is used. Note
143
+ # that for multi-model search, an id may occur in more than one model.
144
+ #
145
+ # == Infix (Star) Searching
146
+ #
147
+ # By default, Sphinx uses English stemming, e.g. matching "shoes" if you
148
+ # search for "shoe". It won't find "Melbourne" if you search for
149
+ # "elbourn", though.
150
+ #
151
+ # Enable infix searching by something like this in config/sphinx.yml:
152
+ #
153
+ # development:
154
+ # enable_star: 1
155
+ # min_infix_length: 2
156
+ #
157
+ # Note that this will make indexing take longer.
158
+ #
159
+ # With those settings (and after reindexing), wildcard asterisks can be used
160
+ # in queries:
161
+ #
162
+ # Location.search "*elbourn*"
163
+ #
164
+ # To automatically add asterisks around every token (but not operators),
165
+ # pass the :star option:
166
+ #
167
+ # Location.search "elbourn -ustrali", :star => true, :match_mode => :boolean
168
+ #
169
+ # This would become "*elbourn* -*ustrali*". The :star option only adds the
170
+ # asterisks. You need to make the config/sphinx.yml changes yourself.
171
+ #
172
+ # By default, the tokens are assumed to match the regular expression /\w+/u.
173
+ # If you've modified the charset_table, pass another regular expression, e.g.
174
+ #
175
+ # User.search("oo@bar.c", :star => /[\w@.]+/u)
176
+ #
177
+ # to search for "*oo@bar.c*" and not "*oo*@*bar*.*c*".
178
+ #
118
179
  # == Sorting
119
180
  #
120
181
  # Sphinx can only sort by attributes, so generally you will need to avoid
@@ -138,15 +199,80 @@ module ThinkingSphinx
138
199
  # documentation[http://sphinxsearch.com/doc.html] for that level of
139
200
  # detail though.
140
201
  #
141
- # == Grouping
202
+ # If desired, you can sort by a column in your model instead of a sphinx
203
+ # field or attribute. This sort only applies to the current page, so is
204
+ # most useful when performing a search with a single page of results.
142
205
  #
206
+ # User.search("pat", :sql_order => "name")
207
+ #
208
+ # == Grouping
209
+ #
143
210
  # For this you can use the group_by, group_clause and group_function
144
211
  # options - which are all directly linked to Sphinx's expectations. No
145
212
  # magic from Thinking Sphinx. It can get a little tricky, so make sure
146
213
  # you read all the relevant
147
214
  # documentation[http://sphinxsearch.com/doc.html#clustering] first.
215
+ #
216
+ # Grouping is done via three parameters within the options hash
217
+ # * <tt>:group_function</tt> determines the way grouping is done
218
+ # * <tt>:group_by</tt> determines the field which is used for grouping
219
+ # * <tt>:group_clause</tt> determines the sorting order
220
+ #
221
+ # === group_function
222
+ #
223
+ # Valid values for :group_function are
224
+ # * <tt>:day</tt>, <tt>:week</tt>, <tt>:month</tt>, <tt>:year</tt> - Grouping is done by the respective timeframes.
225
+ # * <tt>:attr</tt>, <tt>:attrpair</tt> - Grouping is done by the specified attributes(s)
226
+ #
227
+ # === group_by
148
228
  #
149
- # Yes this section will be expanded, but this is a start.
229
+ # This parameter denotes the field by which grouping is done. Note that the
230
+ # specified field must be a sphinx attribute or index.
231
+ #
232
+ # === group_clause
233
+ #
234
+ # This determines the sorting order of the groups. In a grouping search,
235
+ # the matches within a group will sorted by the <tt>:sort_mode</tt> and <tt>:order</tt> parameters.
236
+ # The group matches themselves however, will be sorted by <tt>:group_clause</tt>.
237
+ #
238
+ # The syntax for this is the same as an order parameter in extended sort mode.
239
+ # Namely, you can specify an SQL-like sort expression with up to 5 attributes
240
+ # (including internal attributes), eg: "@relevance DESC, price ASC, @id DESC"
241
+ #
242
+ # === Grouping by timestamp
243
+ #
244
+ # Timestamp grouping groups off items by the day, week, month or year of the
245
+ # attribute given. In order to do this you need to define a timestamp attribute,
246
+ # which pretty much looks like the standard defintion for any attribute.
247
+ #
248
+ # define_index do
249
+ # #
250
+ # # All your other stuff
251
+ # #
252
+ # has :created_at
253
+ # end
254
+ #
255
+ # When you need to fire off your search, it'll go something to the tune of
256
+ #
257
+ # Fruit.search "apricot", :group_function => :day, :group_by => 'created_at'
258
+ #
259
+ # The <tt>@groupby</tt> special attribute will contain the date for that group.
260
+ # Depending on the <tt>:group_function</tt> parameter, the date format will be
261
+ #
262
+ # * <tt>:day</tt> - YYYYMMDD
263
+ # * <tt>:week</tt> - YYYYNNN (NNN is the first day of the week in question,
264
+ # counting from the start of the year )
265
+ # * <tt>:month</tt> - YYYYMM
266
+ # * <tt>:year</tt> - YYYY
267
+ #
268
+ #
269
+ # === Grouping by attribute
270
+ #
271
+ # The syntax is the same as grouping by timestamp, except for the fact that the
272
+ # <tt>:group_function</tt> parameter is changed
273
+ #
274
+ # Fruit.search "apricot", :group_function => :attr, :group_by => 'size'
275
+ #
150
276
  #
151
277
  # == Geo/Location Searching
152
278
  #
@@ -155,11 +281,11 @@ module ThinkingSphinx
155
281
  # take advantage of this, you will need to have both of those values in
156
282
  # attributes. To search with that point, you can then use one of the
157
283
  # following syntax examples:
158
- #
159
- # Address.search "Melbourne", :geo => [1.4, -2.217]
160
- # Address.search "Australia", :geo => [-0.55, 3.108],
284
+ #
285
+ # Address.search "Melbourne", :geo => [1.4, -2.217], :order => "@geodist asc"
286
+ # Address.search "Australia", :geo => [-0.55, 3.108], :order => "@geodist asc"
161
287
  # :latitude_attr => "latit", :longitude_attr => "longit"
162
- #
288
+ #
163
289
  # The first example applies when your latitude and longitude attributes
164
290
  # are named any of lat, latitude, lon, long or longitude. If that's not
165
291
  # the case, you will need to explicitly state them in your search, _or_
@@ -168,17 +294,17 @@ module ThinkingSphinx
168
294
  # define_index do
169
295
  # has :latit # Float column, stored in radians
170
296
  # has :longit # Float column, stored in radians
171
- #
297
+ #
172
298
  # set_property :latitude_attr => "latit"
173
299
  # set_property :longitude_attr => "longit"
174
300
  # end
175
- #
301
+ #
176
302
  # Now, geo-location searching really only has an affect if you have a
177
303
  # filter, sort or grouping clause related to it - otherwise it's just a
178
- # normal search. To make use of the positioning difference, use the
179
- # special attribute "@geodist" in any of your filters or sorting or grouping
180
- # clauses.
181
- #
304
+ # normal search, and _will not_ return a distance value otherwise. To
305
+ # make use of the positioning difference, use the special attribute
306
+ # "@geodist" in any of your filters or sorting or grouping clauses.
307
+ #
182
308
  # And don't forget - both the latitude and longitude you use in your
183
309
  # search, and the values in your indexes, need to be stored as a float in radians,
184
310
  # _not_ degrees. Keep in mind that if you do this conversion in SQL
@@ -188,40 +314,93 @@ module ThinkingSphinx
188
314
  # has 'RADIANS(lat)', :as => :lat, :type => :float
189
315
  # # ...
190
316
  # end
317
+ #
318
+ # Once you've got your results set, you can access the distances as
319
+ # follows:
320
+ #
321
+ # @results.each_with_geodist do |result, distance|
322
+ # # ...
323
+ # end
324
+ #
325
+ # The distance value is returned as a float, representing the distance in
326
+ # metres.
327
+ #
328
+ # == Handling a Stale Index
329
+ #
330
+ # Especially if you don't use delta indexing, you risk having records in the
331
+ # Sphinx index that are no longer in the database. By default, those will simply
332
+ # come back as nils:
191
333
  #
334
+ # >> pat_user.delete
335
+ # >> User.search("pat")
336
+ # Sphinx Result: [1,2]
337
+ # => [nil, <#User id: 2>]
338
+ #
339
+ # (If you search across multiple models, you'll get ActiveRecord::RecordNotFound.)
340
+ #
341
+ # You can simply Array#compact these results or handle the nils in some other way, but
342
+ # Sphinx will still report two results, and the missing records may upset your layout.
343
+ #
344
+ # If you pass :retry_stale => true to a single-model search, missing records will
345
+ # cause Thinking Sphinx to retry the query but excluding those records. Since search
346
+ # is paginated, the new search could potentially include missing records as well, so by
347
+ # default Thinking Sphinx will retry three times. Pass :retry_stale => 5 to retry five
348
+ # times, and so on. If there are still missing ids on the last retry, they are
349
+ # shown as nils.
350
+ #
192
351
  def search(*args)
193
- results, client = search_results(*args.clone)
194
-
195
- ::ActiveRecord::Base.logger.error(
196
- "Sphinx Error: #{results[:error]}"
197
- ) if results[:error]
198
-
199
- options = args.extract_options!
200
- klass = options[:class]
201
- page = options[:page] ? options[:page].to_i : 1
202
-
203
- # begin
204
- pager = ThinkingSphinx::Collection.new(page, client.limit,
205
- results[:total] || 0, results[:total_found] || 0)
206
- pager.replace instances_from_results(results[:matches], options, klass)
207
- # pager = WillPaginate::Collection.create(page,
208
- # client.limit, results[:total] || 0) do |collection|
209
- # collection.replace instances_from_results(results[:matches], options, klass)
210
- # collection.instance_variable_set :@total_entries, results[:total_found]
211
- # end
212
- return (options[:include_raw] ? [pager, results] : pager)
213
- # rescue StandardError => err
214
- # if options[:include_raw]
215
- # return instances_from_results(results[:matches], options, klass), results
216
- # else
217
- # return instances_from_results(results[:matches], options, klass)
218
- # end
352
+ query = args.clone # an array
353
+ options = query.extract_options!
354
+
355
+ retry_search_on_stale_index(query, options) do
356
+ results, client = search_results(*(query + [options]))
357
+
358
+ ::ActiveRecord::Base.logger.error(
359
+ "Sphinx Error: #{results[:error]}"
360
+ ) if results[:error]
361
+
362
+ klass = options[:class]
363
+ page = options[:page] ? options[:page].to_i : 1
364
+
365
+ ThinkingSphinx::Collection.create_from_results(results, page, client.limit, options)
366
+ end
367
+ end
368
+
369
+ def retry_search_on_stale_index(query, options, &block)
370
+ stale_ids = []
371
+ stale_retries_left = case options[:retry_stale]
372
+ when true
373
+ 3 # default to three retries
374
+ when nil, false
375
+ 0 # no retries
376
+ else options[:retry_stale].to_i
377
+ end
378
+ begin
379
+ # Passing this in an option so Collection.create_from_results can see it.
380
+ # It should only raise on stale records if there are any retries left.
381
+ options[:raise_on_stale] = stale_retries_left > 0
382
+ block.call
383
+ # If ThinkingSphinx::Collection.create_from_results found records in Sphinx but not
384
+ # in the DB and the :raise_on_stale option is set, this exception is raised. We retry
385
+ # a limited number of times, excluding the stale ids from the search.
386
+ rescue StaleIdsException => e
387
+ stale_retries_left -= 1
388
+
389
+ stale_ids |= e.ids # For logging
390
+ options[:without_ids] = Array(options[:without_ids]) | e.ids # Actual exclusion
391
+
392
+ tries = stale_retries_left
393
+ ::ActiveRecord::Base.logger.debug("Sphinx Stale Ids (%s %s left): %s" % [
394
+ tries, (tries==1 ? 'try' : 'tries'), stale_ids.join(', ')
395
+ ])
396
+
397
+ retry
219
398
  end
220
399
  end
221
400
 
222
401
  def count(*args)
223
402
  results, client = search_results(*args.clone)
224
- results[:total] || 0
403
+ results[:total_found] || 0
225
404
  end
226
405
 
227
406
  # Checks if a document with the given id exists within a specific index.
@@ -230,50 +409,70 @@ module ThinkingSphinx
230
409
  # - ID of the document
231
410
  # - Index to check within
232
411
  # - Options hash (defaults to {})
233
- #
412
+ #
234
413
  # Example:
235
- #
414
+ #
236
415
  # ThinkingSphinx::Search.search_for_id(10, "user_core", :class => User)
237
- #
416
+ #
238
417
  def search_for_id(*args)
239
418
  options = args.extract_options!
240
419
  client = client_from_options options
241
-
420
+
242
421
  query, filters = search_conditions(
243
422
  options[:class], options[:conditions] || {}
244
423
  )
245
424
  client.filters += filters
246
425
  client.match_mode = :extended unless query.empty?
247
426
  client.id_range = args.first..args.first
248
-
427
+
249
428
  begin
250
429
  return client.query(query, args[1])[:matches].length > 0
251
430
  rescue Errno::ECONNREFUSED => err
252
431
  raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
253
432
  end
254
433
  end
255
-
434
+
435
+ # Model.facets *args
436
+ # ThinkingSphinx::Search.facets *args
437
+ # ThinkingSphinx::Search.facets *args, :all_attributes => true
438
+ # ThinkingSphinx::Search.facets *args, :class_facet => false
439
+ #
440
+ def facets(*args)
441
+ options = args.extract_options!
442
+
443
+ if options[:class]
444
+ facets_for_model options[:class], args, options
445
+ else
446
+ facets_for_all_models args, options
447
+ end
448
+ end
449
+
256
450
  private
257
-
451
+
258
452
  # This method handles the common search functionality, and returns both
259
453
  # the result hash and the client. Not super elegant, but it'll do for
260
454
  # the moment.
261
- #
455
+ #
262
456
  def search_results(*args)
263
457
  options = args.extract_options!
458
+ query = args.join(' ')
264
459
  client = client_from_options options
265
-
266
- query, filters = search_conditions(
460
+
461
+ query = star_query(query, options[:star]) if options[:star]
462
+
463
+ extra_query, filters = search_conditions(
267
464
  options[:class], options[:conditions] || {}
268
465
  )
269
466
  client.filters += filters
270
- client.match_mode = :extended unless query.empty?
271
- query = args.join(" ") + query
272
-
467
+ client.match_mode = :extended unless extra_query.empty?
468
+ query = [query, extra_query].join(' ')
469
+ query.strip! # Because "" and " " are not equivalent
470
+
273
471
  set_sort_options! client, options
274
-
472
+
275
473
  client.limit = options[:per_page].to_i if options[:per_page]
276
474
  page = options[:page] ? options[:page].to_i : 1
475
+ page = 1 if page <= 0
277
476
  client.offset = (page - 1) * client.limit
278
477
 
279
478
  begin
@@ -283,74 +482,42 @@ module ThinkingSphinx
283
482
  rescue Errno::ECONNREFUSED => err
284
483
  raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
285
484
  end
286
-
485
+
287
486
  return results, client
288
487
  end
289
-
290
- # This function loops over the records and appends a 'distance' variable to each one with
291
- # the value from Sphinx
292
- def append_distances(instances, results, distance_name)
293
- instances.each_with_index do |record, index|
294
- if record
295
- distance = (results[index][:attributes]['@geodist'] or nil)
296
- record.instance_variable_get('@attributes')["#{distance_name}"] = distance
297
- end
298
- end
299
- end
300
-
301
- def instances_from_results(results, options = {}, klass = nil)
302
- if klass.nil?
303
- results.collect { |result| instance_from_result result, options }
304
- else
305
- ids = results.collect { |result| result[:attributes]["sphinx_internal_id"] }
306
- instances = ids.length > 0 ? klass.find(
307
- :all,
308
- :conditions => {klass.primary_key.to_sym => ids},
309
- :include => options[:include],
310
- :select => options[:select]
311
- ) : []
312
- final_instances = ids.collect { |obj_id| instances.detect { |obj| obj.id == obj_id } }
313
-
314
- final_instances = append_distances(final_instances, results, options[:distance_name]) if options[:distance_name] && (results.collect { |result| result[:attributes]['@geodist'] }.length > 0)
315
-
316
- return final_instances
317
- end
318
- end
319
-
320
- # Either use the provided class to instantiate a result from a model, or
321
- # get the result's CRC value and determine the class from that.
322
- #
323
- def instance_from_result(result, options)
324
- class_from_crc(result[:attributes]["class_crc"]).find(
325
- result[:attributes]["sphinx_internal_id"],
326
- :include => options[:include], :select => options[:select]
327
- )
328
- end
329
-
330
- # Convert a CRC value to the corresponding class.
331
- #
332
- def class_from_crc(crc)
333
- unless @models_by_crc
334
- Configuration.new.load_models
335
-
336
- @models_by_crc = ThinkingSphinx.indexed_models.inject({}) do |hash, model|
337
- hash[model.constantize.to_crc32] = model
338
- hash
339
- end
340
- end
341
-
342
- @models_by_crc[crc].constantize
343
- end
344
-
488
+
345
489
  # Set all the appropriate settings for the client, using the provided
346
490
  # options hash.
347
491
  #
348
492
  def client_from_options(options = {})
349
- config = ThinkingSphinx::Configuration.new
493
+ config = ThinkingSphinx::Configuration.instance
350
494
  client = Riddle::Client.new config.address, config.port
351
495
  klass = options[:class]
352
- index_options = klass ? klass.indexes.last.options : {}
353
-
496
+ index_options = klass ? klass.sphinx_index_options : {}
497
+
498
+ # The Riddle default is per-query max_matches=1000. If we set the
499
+ # per-server max to a smaller value in sphinx.yml, we need to override
500
+ # the Riddle default or else we get search errors like
501
+ # "per-query max_matches=1000 out of bounds (per-server max_matches=200)"
502
+ if per_server_max_matches = config.configuration.searchd.max_matches
503
+ options[:max_matches] ||= per_server_max_matches
504
+ end
505
+
506
+ # Turn :index_weights => { "foo" => 2, User => 1 }
507
+ # into :index_weights => { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
508
+ if iw = options[:index_weights]
509
+ options[:index_weights] = iw.inject({}) do |hash, (index,weight)|
510
+ if index.is_a?(Class)
511
+ name = ThinkingSphinx::Index.name(index)
512
+ hash["#{name}_core"] = weight
513
+ hash["#{name}_delta"] = weight
514
+ else
515
+ hash[index] = weight
516
+ end
517
+ hash
518
+ end
519
+ end
520
+
354
521
  [
355
522
  :max_matches, :match_mode, :sort_mode, :sort_by, :id_range,
356
523
  :group_by, :group_function, :group_clause, :group_distinct, :cut_off,
@@ -366,116 +533,160 @@ module ThinkingSphinx
366
533
  options[:classes] = [klass] if klass
367
534
 
368
535
  client.anchor = anchor_conditions(klass, options) || {} if client.anchor.empty?
369
-
536
+
370
537
  client.filters << Riddle::Client::Filter.new(
371
538
  "sphinx_deleted", [0]
372
539
  )
373
540
 
374
541
  # class filters
375
542
  client.filters << Riddle::Client::Filter.new(
376
- "subclass_crcs", options[:classes].collect { |k| k.to_crc32s }.flatten
543
+ "class_crc", options[:classes].collect { |k| k.to_crc32s }.flatten
377
544
  ) if options[:classes]
378
-
545
+
379
546
  # normal attribute filters
380
547
  client.filters += options[:with].collect { |attr,val|
381
548
  Riddle::Client::Filter.new attr.to_s, filter_value(val)
382
549
  } if options[:with]
383
-
550
+
384
551
  # exclusive attribute filters
385
552
  client.filters += options[:without].collect { |attr,val|
386
553
  Riddle::Client::Filter.new attr.to_s, filter_value(val), true
387
554
  } if options[:without]
388
-
555
+
556
+ # every-match attribute filters
557
+ client.filters += options[:with_all].collect { |attr,vals|
558
+ Array(vals).collect { |val|
559
+ Riddle::Client::Filter.new attr.to_s, filter_value(val)
560
+ }
561
+ }.flatten if options[:with_all]
562
+
563
+ # exclusive attribute filter on primary key
564
+ client.filters += Array(options[:without_ids]).collect { |id|
565
+ Riddle::Client::Filter.new 'sphinx_internal_id', filter_value(id), true
566
+ } if options[:without_ids]
567
+
389
568
  client
390
569
  end
391
-
570
+
571
+ def star_query(query, custom_token = nil)
572
+ token = custom_token.is_a?(Regexp) ? custom_token : /\w+/u
573
+
574
+ query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
575
+ pre, proper, post = $`, $&, $'
576
+ is_operator = pre.match(%r{(\W|^)[@~/]\Z}) # E.g. "@foo", "/2", "~3", but not as part of a token
577
+ is_quote = proper.starts_with?('"') && proper.ends_with?('"') # E.g. "foo bar", with quotes
578
+ has_star = pre.ends_with?("*") || post.starts_with?("*")
579
+ if is_operator || is_quote || has_star
580
+ proper
581
+ else
582
+ "*#{proper}*"
583
+ end
584
+ end
585
+ end
586
+
392
587
  def filter_value(value)
393
588
  case value
394
589
  when Range
395
- value.first.is_a?(Time) ? value.first.to_i..value.last.to_i : value
590
+ value.first.is_a?(Time) ? timestamp(value.first)..timestamp(value.last) : value
396
591
  when Array
397
- value.collect { |val| val.is_a?(Time) ? val.to_i : val }
592
+ value.collect { |val| val.is_a?(Time) ? timestamp(val) : val }
398
593
  else
399
594
  Array(value)
400
595
  end
401
596
  end
402
-
597
+
598
+ # Returns the integer timestamp for a Time object.
599
+ #
600
+ # If using Rails 2.1+, need to handle timezones to translate them back to
601
+ # UTC, as that's what datetimes will be stored as by MySQL.
602
+ #
603
+ # in_time_zone is a method that was added for the timezone support in
604
+ # Rails 2.1, which is why it's used for testing. I'm sure there's better
605
+ # ways, but this does the job.
606
+ #
607
+ def timestamp(value)
608
+ value.respond_to?(:in_time_zone) ? value.utc.to_i : value.to_i
609
+ end
610
+
403
611
  # Translate field and attribute conditions to the relevant search string
404
612
  # and filters.
405
- #
613
+ #
406
614
  def search_conditions(klass, conditions={})
407
- attributes = klass ? klass.indexes.collect { |index|
615
+ attributes = klass ? klass.sphinx_indexes.collect { |index|
408
616
  index.attributes.collect { |attrib| attrib.unique_name }
409
617
  }.flatten : []
410
-
411
- search_string = ""
618
+
619
+ search_string = []
412
620
  filters = []
413
-
621
+
414
622
  conditions.each do |key,val|
415
623
  if attributes.include?(key.to_sym)
416
624
  filters << Riddle::Client::Filter.new(
417
625
  key.to_s, filter_value(val)
418
626
  )
419
627
  else
420
- search_string << "@#{key} #{val} "
628
+ search_string << "@#{key} #{val}"
421
629
  end
422
630
  end
423
631
 
424
- return search_string, filters
632
+ return search_string.join(' '), filters
425
633
  end
426
-
634
+
427
635
  # Return the appropriate latitude and longitude values, depending on
428
636
  # whether the relevant attributes have been defined, and also whether
429
637
  # there's actually any values.
430
- #
638
+ #
431
639
  def anchor_conditions(klass, options)
432
- attributes = klass ? klass.indexes.collect { |index|
640
+ attributes = klass ? klass.sphinx_indexes.collect { |index|
433
641
  index.attributes.collect { |attrib| attrib.unique_name }
434
642
  }.flatten : []
435
-
436
- lat_attr = klass ? klass.indexes.collect { |index|
643
+
644
+ lat_attr = klass ? klass.sphinx_indexes.collect { |index|
437
645
  index.options[:latitude_attr]
438
646
  }.compact.first : nil
439
-
440
- lon_attr = klass ? klass.indexes.collect { |index|
647
+
648
+ lon_attr = klass ? klass.sphinx_indexes.collect { |index|
441
649
  index.options[:longitude_attr]
442
650
  }.compact.first : nil
443
-
651
+
444
652
  lat_attr = options[:latitude_attr] if options[:latitude_attr]
445
653
  lat_attr ||= :lat if attributes.include?(:lat)
446
654
  lat_attr ||= :latitude if attributes.include?(:latitude)
447
-
655
+
448
656
  lon_attr = options[:longitude_attr] if options[:longitude_attr]
657
+ lon_attr ||= :lng if attributes.include?(:lng)
449
658
  lon_attr ||= :lon if attributes.include?(:lon)
450
659
  lon_attr ||= :long if attributes.include?(:long)
451
660
  lon_attr ||= :longitude if attributes.include?(:longitude)
452
-
661
+
453
662
  lat = options[:lat]
454
663
  lon = options[:lon]
455
-
664
+
456
665
  if options[:geo]
457
666
  lat = options[:geo].first
458
667
  lon = options[:geo].last
459
668
  end
460
-
669
+
461
670
  lat && lon ? {
462
- :latitude_attribute => lat_attr,
671
+ :latitude_attribute => lat_attr.to_s,
463
672
  :latitude => lat,
464
- :longitude_attribute => lon_attr,
673
+ :longitude_attribute => lon_attr.to_s,
465
674
  :longitude => lon
466
675
  } : nil
467
676
  end
468
-
677
+
469
678
  # Set the sort options using the :order key as well as the appropriate
470
679
  # Riddle settings.
471
- #
680
+ #
472
681
  def set_sort_options!(client, options)
473
682
  klass = options[:class]
474
- fields = klass ? klass.indexes.collect { |index|
683
+ fields = klass ? klass.sphinx_indexes.collect { |index|
475
684
  index.fields.collect { |field| field.unique_name }
476
685
  }.flatten : []
686
+ index_options = klass ? klass.sphinx_index_options : {}
477
687
 
478
- case order = options[:order]
688
+ order = options[:order] || index_options[:order]
689
+ case order
479
690
  when Symbol
480
691
  client.sort_mode = :attr_asc if client.sort_mode == :relevance || client.sort_mode.nil?
481
692
  if fields.include?(order)
@@ -489,23 +700,81 @@ module ThinkingSphinx
489
700
  else
490
701
  # do nothing
491
702
  end
492
-
703
+
493
704
  client.sort_mode = :attr_asc if client.sort_mode == :asc
494
705
  client.sort_mode = :attr_desc if client.sort_mode == :desc
495
706
  end
496
-
707
+
497
708
  # Search through a collection of fields and translate any appearances
498
709
  # of them in a string to their attribute equivalent for sorting.
499
- #
710
+ #
500
711
  def sorted_fields_to_attributes(string, fields)
501
712
  fields.each { |field|
502
713
  string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
503
714
  match.gsub field.to_s, field.to_s.concat("_sort")
504
715
  }
505
716
  }
506
-
717
+
507
718
  string
508
719
  end
720
+
721
+ def facets_for_model(klass, args, options)
722
+ hash = ThinkingSphinx::FacetCollection.new args + [options]
723
+ options = options.clone.merge! :group_function => :attr
724
+
725
+ klass.sphinx_facets.inject(hash) do |hash, facet|
726
+ unless facet.name == :class && !options[:class_facet]
727
+ options[:group_by] = facet.attribute_name
728
+ hash.add_from_results facet, search(*(args + [options]))
729
+ end
730
+
731
+ hash
732
+ end
733
+ end
734
+
735
+ def facets_for_all_models(args, options)
736
+ options = GlobalFacetOptions.merge(options)
737
+ hash = ThinkingSphinx::FacetCollection.new args + [options]
738
+ options = options.merge! :group_function => :attr
739
+
740
+ facet_names(options).inject(hash) do |hash, name|
741
+ options[:group_by] = name
742
+ hash.add_from_results name, search(*(args + [options]))
743
+ hash
744
+ end
745
+ end
746
+
747
+ def facet_classes(options)
748
+ options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
749
+ model.constantize
750
+ }
751
+ end
752
+
753
+ def facet_names(options)
754
+ classes = facet_classes(options)
755
+ names = options[:all_attributes] ?
756
+ facet_names_for_all_classes(classes) :
757
+ facet_names_common_to_all_classes(classes)
758
+
759
+ names.delete "class_crc" unless options[:class_facet]
760
+ names
761
+ end
762
+
763
+ def facet_names_for_all_classes(classes)
764
+ classes.collect { |klass|
765
+ klass.sphinx_facets.collect { |facet| facet.attribute_name }
766
+ }.flatten.uniq
767
+ end
768
+
769
+ def facet_names_common_to_all_classes(classes)
770
+ facet_names_for_all_classes(classes).select { |name|
771
+ classes.all? { |klass|
772
+ klass.sphinx_facets.detect { |facet|
773
+ facet.attribute_name == name
774
+ }
775
+ }
776
+ }
777
+ end
509
778
  end
510
779
  end
511
- end
780
+ end