DrMark-thinking-sphinx 1.1.6 → 1.1.14

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 (40) hide show
  1. data/{README → README.textile} +84 -84
  2. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  3. data/lib/thinking_sphinx/active_record/delta.rb +10 -1
  4. data/lib/thinking_sphinx/active_record.rb +10 -3
  5. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +1 -1
  6. data/lib/thinking_sphinx/attribute.rb +44 -134
  7. data/lib/thinking_sphinx/class_facet.rb +15 -0
  8. data/lib/thinking_sphinx/collection.rb +1 -0
  9. data/lib/thinking_sphinx/configuration.rb +7 -3
  10. data/lib/thinking_sphinx/deltas/datetime_delta.rb +1 -1
  11. data/lib/thinking_sphinx/deltas/default_delta.rb +3 -2
  12. data/lib/thinking_sphinx/deltas/delayed_delta.rb +4 -2
  13. data/lib/thinking_sphinx/deltas.rb +9 -6
  14. data/lib/thinking_sphinx/deploy/capistrano.rb +82 -0
  15. data/lib/thinking_sphinx/facet.rb +68 -18
  16. data/lib/thinking_sphinx/facet_collection.rb +16 -17
  17. data/lib/thinking_sphinx/field.rb +7 -97
  18. data/lib/thinking_sphinx/index/builder.rb +255 -232
  19. data/lib/thinking_sphinx/index.rb +37 -349
  20. data/lib/thinking_sphinx/property.rb +160 -0
  21. data/lib/thinking_sphinx/search/facets.rb +98 -0
  22. data/lib/thinking_sphinx/search.rb +4 -73
  23. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  24. data/lib/thinking_sphinx/source/sql.rb +124 -0
  25. data/lib/thinking_sphinx/source.rb +150 -0
  26. data/lib/thinking_sphinx/tasks.rb +1 -1
  27. data/lib/thinking_sphinx.rb +3 -1
  28. data/spec/unit/thinking_sphinx/active_record_spec.rb +14 -12
  29. data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -11
  30. data/spec/unit/thinking_sphinx/facet_collection_spec.rb +64 -0
  31. data/spec/unit/thinking_sphinx/facet_spec.rb +278 -0
  32. data/spec/unit/thinking_sphinx/field_spec.rb +18 -9
  33. data/spec/unit/thinking_sphinx/index/builder_spec.rb +347 -1
  34. data/spec/unit/thinking_sphinx/index_spec.rb +22 -27
  35. data/spec/unit/thinking_sphinx/rails_additions_spec.rb +183 -0
  36. data/spec/unit/thinking_sphinx/search_spec.rb +71 -0
  37. data/spec/unit/thinking_sphinx/source_spec.rb +156 -0
  38. data/tasks/distribution.rb +1 -1
  39. data/tasks/testing.rb +7 -15
  40. metadata +19 -3
@@ -9,8 +9,7 @@ module ThinkingSphinx
9
9
  # Enjoy.
10
10
  #
11
11
  class Index
12
- attr_accessor :model, :fields, :attributes, :conditions, :groupings,
13
- :delta_object, :options
12
+ attr_accessor :model, :sources, :delta_object
14
13
 
15
14
  # Create a new index instance by passing in the model it is tied to, and
16
15
  # a block to build it with (optional but recommended). For documentation
@@ -28,153 +27,11 @@ module ThinkingSphinx
28
27
  #
29
28
  def initialize(model, &block)
30
29
  @model = model
31
- @associations = {}
32
- @fields = []
33
- @attributes = []
34
- @conditions = []
35
- @groupings = []
30
+ @sources = []
36
31
  @options = {}
37
32
  @delta_object = nil
38
33
 
39
- initialize_from_builder(&block) if block_given?
40
- end
41
-
42
- def name
43
- self.class.name(@model)
44
- end
45
-
46
- def self.name(model)
47
- model.name.underscore.tr(':/\\', '_')
48
- end
49
-
50
- def to_riddle_for_core(offset, index)
51
- add_internal_attributes_and_facets
52
- link!
53
-
54
- source = Riddle::Configuration::SQLSource.new(
55
- "#{name}_core_#{index}", adapter.sphinx_identifier
56
- )
57
-
58
- set_source_database_settings source
59
- set_source_attributes source, offset
60
- set_source_sql source, offset
61
- set_source_settings source
62
-
63
- source
64
- end
65
-
66
- def to_riddle_for_delta(offset, index)
67
- add_internal_attributes_and_facets
68
- link!
69
-
70
- source = Riddle::Configuration::SQLSource.new(
71
- "#{name}_delta_#{index}", adapter.sphinx_identifier
72
- )
73
- source.parent = "#{name}_core_#{index}"
74
-
75
- set_source_database_settings source
76
- set_source_attributes source, offset
77
- set_source_sql source, offset, true
78
-
79
- source
80
- end
81
-
82
- # Link all the fields and associations to their corresponding
83
- # associations and joins. This _must_ be called before interrogating
84
- # the index's fields and associations for anything that may reference
85
- # their SQL structure.
86
- #
87
- def link!
88
- base = ::ActiveRecord::Associations::ClassMethods::JoinDependency.new(
89
- @model, [], nil
90
- )
91
-
92
- @fields.each { |field|
93
- field.model ||= @model
94
- field.columns.each { |col|
95
- field.associations[col] = associations(col.__stack.clone)
96
- field.associations[col].each { |assoc| assoc.join_to(base) }
97
- }
98
- }
99
-
100
- @attributes.each { |attribute|
101
- attribute.model ||= @model
102
- attribute.columns.each { |col|
103
- attribute.associations[col] = associations(col.__stack.clone)
104
- attribute.associations[col].each { |assoc| assoc.join_to(base) }
105
- }
106
- }
107
- end
108
-
109
- # Flag to indicate whether this index has a corresponding delta index.
110
- #
111
- def delta?
112
- !@delta_object.nil?
113
- end
114
-
115
- def adapter
116
- @adapter ||= @model.sphinx_database_adapter
117
- end
118
-
119
- def prefix_fields
120
- @fields.select { |field| field.prefixes }
121
- end
122
-
123
- def infix_fields
124
- @fields.select { |field| field.infixes }
125
- end
126
-
127
- def index_options
128
- all_index_options = ThinkingSphinx::Configuration.instance.index_options.clone
129
- @options.keys.select { |key|
130
- ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s)
131
- }.each { |key| all_index_options[key.to_sym] = @options[key] }
132
- all_index_options
133
- end
134
-
135
- def quote_column(column)
136
- @model.connection.quote_column_name(column)
137
- end
138
-
139
- private
140
-
141
- def utf8?
142
- self.index_options[:charset_type] == "utf-8"
143
- end
144
-
145
- # Does all the magic with the block provided to the base #initialize.
146
- # Creates a new class subclassed from Builder, and evaluates the block
147
- # on it, then pulls all relevant settings - fields, attributes, conditions,
148
- # properties - into the new index.
149
- #
150
- # Also creates a CRC attribute for the model.
151
- #
152
- def initialize_from_builder(&block)
153
- builder = Class.new(Builder)
154
- builder.setup
155
-
156
- builder.instance_eval &block
157
-
158
- unless @model.descends_from_active_record?
159
- stored_class = @model.store_full_sti_class ? @model.name : @model.name.demodulize
160
- builder.where("#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)} = '#{stored_class}'")
161
- end
162
-
163
- set_model = Proc.new { |item| item.model = @model }
164
-
165
- @fields = builder.fields &set_model
166
- @attributes = builder.attributes.each &set_model
167
- @conditions = builder.conditions
168
- @groupings = builder.groupings
169
- @delta_object = ThinkingSphinx::Deltas.parse self, builder.properties
170
- @options = builder.properties
171
-
172
- is_faceted = Proc.new { |item| item.faceted }
173
- add_facet = Proc.new { |item| @model.sphinx_facets << item.to_facet }
174
-
175
- @model.sphinx_facets ||= []
176
- @fields.select( &is_faceted).each &add_facet
177
- @attributes.select(&is_faceted).each &add_facet
34
+ # add_internal_attributes_and_facets
178
35
 
179
36
  # We want to make sure that if the database doesn't exist, then Thinking
180
37
  # Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
@@ -188,236 +45,67 @@ module ThinkingSphinx
188
45
  end
189
46
  end
190
47
 
191
- # Returns all associations used amongst all the fields and attributes.
192
- # This includes all associations between the model and what the actual
193
- # columns are from.
194
- #
195
- def all_associations
196
- @all_associations ||= (
197
- # field associations
198
- @fields.collect { |field|
199
- field.associations.values
200
- }.flatten +
201
- # attribute associations
202
- @attributes.collect { |attrib|
203
- attrib.associations.values if attrib.include_as_association?
204
- }.compact.flatten
205
- ).uniq.collect { |assoc|
206
- # get ancestors as well as column-level associations
207
- assoc.ancestors
208
- }.flatten.uniq
48
+ def fields
49
+ @sources.collect { |source| source.fields }.flatten
209
50
  end
210
51
 
211
- # Gets a stack of associations for a specific path.
212
- #
213
- def associations(path, parent = nil)
214
- assocs = []
215
-
216
- if parent.nil?
217
- assocs = association(path.shift)
218
- else
219
- assocs = parent.children(path.shift)
220
- end
221
-
222
- until path.empty?
223
- point = path.shift
224
- assocs = assocs.collect { |assoc|
225
- assoc.children(point)
226
- }.flatten
227
- end
228
-
229
- assocs
52
+ def attributes
53
+ @sources.collect { |source| source.attributes }.flatten
230
54
  end
231
55
 
232
- # Gets the association stack for a specific key.
233
- #
234
- def association(key)
235
- @associations[key] ||= Association.children(@model, key)
236
- end
237
-
238
- def crc_column
239
- if @model.column_names.include?(@model.inheritance_column)
240
- adapter.cast_to_unsigned(adapter.convert_nulls(
241
- adapter.crc(adapter.quote_with_table(@model.inheritance_column), true),
242
- @model.to_crc32
243
- ))
244
- else
245
- @model.to_crc32.to_s
246
- end
56
+ def name
57
+ self.class.name(@model)
247
58
  end
248
59
 
249
- def add_internal_attributes_and_facets
250
- add_internal_attribute :sphinx_internal_id, :integer, @model.primary_key.to_sym
251
- add_internal_attribute :class_crc, :integer, crc_column, true
252
- add_internal_attribute :subclass_crcs, :multi, subclasses_to_s
253
- add_internal_attribute :sphinx_deleted, :integer, "0"
254
-
255
- add_internal_facet :class_crc
60
+ def self.name(model)
61
+ model.name.underscore.tr(':/\\', '_')
256
62
  end
257
63
 
258
- def add_internal_attribute(name, type, contents, facet = false)
259
- return unless attribute_by_alias(name).nil?
260
-
261
- @attributes << Attribute.new(
262
- FauxColumn.new(contents),
263
- :type => type,
264
- :as => name,
265
- :facet => facet
266
- )
64
+ def prefix_fields
65
+ fields.select { |field| field.prefixes }
267
66
  end
268
67
 
269
- def add_internal_facet(name)
270
- return unless facet_by_alias(name).nil?
271
-
272
- @model.sphinx_facets << ClassFacet.new(attribute_by_alias(name))
68
+ def infix_fields
69
+ fields.select { |field| field.infixes }
273
70
  end
274
71
 
275
- def attribute_by_alias(attr_alias)
276
- @attributes.detect { |attrib| attrib.alias == attr_alias }
72
+ def local_options
73
+ @options
277
74
  end
278
75
 
279
- def facet_by_alias(name)
280
- @model.sphinx_facets.detect { |facet| facet.name == name }
76
+ def options
77
+ all_index_options = ThinkingSphinx::Configuration.instance.index_options.clone
78
+ @options.keys.select { |key|
79
+ ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s)
80
+ }.each { |key| all_index_options[key.to_sym] = @options[key] }
81
+ all_index_options
281
82
  end
282
83
 
283
- def subclasses_to_s
284
- "'" + (@model.send(:subclasses).collect { |klass|
285
- klass.to_crc32.to_s
286
- } << @model.to_crc32.to_s).join(",") + "'"
287
- end
288
-
289
- def set_source_database_settings(source)
290
- config = @model.connection.instance_variable_get(:@config)
291
-
292
- source.sql_host = config[:host] || "localhost"
293
- source.sql_user = config[:username] || config[:user] || ""
294
- source.sql_pass = (config[:password].to_s || "").gsub('#', '\#')
295
- source.sql_db = config[:database]
296
- source.sql_port = config[:port]
297
- source.sql_sock = config[:socket]
84
+ def delta?
85
+ !@delta_object.nil?
298
86
  end
299
87
 
300
- def set_source_attributes(source, offset = nil)
301
- attributes.each do |attrib|
302
- source.send(attrib.type_to_config) << attrib.config_value(offset)
303
- end
304
- end
88
+ private
305
89
 
306
- def set_source_sql(source, offset, delta = false)
307
- source.sql_query = to_sql(:offset => offset, :delta => delta).gsub(/\n/, ' ')
308
- source.sql_query_range = to_sql_query_range(:delta => delta)
309
- source.sql_query_info = to_sql_query_info(offset)
310
-
311
- source.sql_query_pre += send(!delta ? :sql_query_pre_for_core : :sql_query_pre_for_delta)
312
-
313
- if @options[:group_concat_max_len]
314
- source.sql_query_pre << "SET SESSION group_concat_max_len = #{@options[:group_concat_max_len]}"
315
- end
316
-
317
- source.sql_query_pre += [adapter.utf8_query_pre].compact if utf8?
90
+ def adapter
91
+ @adapter ||= @model.sphinx_database_adapter
318
92
  end
319
93
 
320
- def set_source_settings(source)
321
- ThinkingSphinx::Configuration.instance.source_options.each do |key, value|
322
- source.send("#{key}=".to_sym, value)
323
- end
324
-
325
- @options.each do |key, value|
326
- source.send("#{key}=".to_sym, value) if ThinkingSphinx::Configuration::SourceOptions.include?(key.to_s) && !value.nil?
327
- end
94
+ def utf8?
95
+ options[:charset_type] == "utf-8"
328
96
  end
329
97
 
330
- def sql_query_pre_for_core
331
- if self.delta? && !@delta_object.reset_query(@model).blank?
332
- [@delta_object.reset_query(@model)]
333
- else
334
- []
335
- end
98
+ # Does all the magic with the block provided to the base #initialize.
99
+ # Creates a new class subclassed from Builder, and evaluates the block
100
+ # on it, then pulls all relevant settings - fields, attributes, conditions,
101
+ # properties - into the new index.
102
+ #
103
+ def initialize_from_builder(&block)
104
+ #
336
105
  end
337
106
 
338
107
  def sql_query_pre_for_delta
339
108
  [""]
340
109
  end
341
-
342
- # Generates the big SQL statement to get the data back for all the fields
343
- # and attributes, using all the relevant association joins. If you want
344
- # the version filtered for delta values, send through :delta => true in the
345
- # options. Won't do much though if the index isn't set up to support a
346
- # delta sibling.
347
- #
348
- # Examples:
349
- #
350
- # index.to_sql
351
- # index.to_sql(:delta => true)
352
- #
353
- def to_sql(options={})
354
- assocs = all_associations
355
-
356
- where_clause = ""
357
- if self.delta? && !@delta_object.clause(@model, options[:delta]).blank?
358
- where_clause << " AND #{@delta_object.clause(@model, options[:delta])}"
359
- end
360
- unless @conditions.empty?
361
- where_clause << " AND " << @conditions.join(" AND ")
362
- end
363
-
364
- internal_groupings = []
365
- if @model.column_names.include?(@model.inheritance_column)
366
- internal_groupings << "#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)}"
367
- end
368
-
369
- unique_id_expr = ThinkingSphinx.unique_id_expression(options[:offset])
370
-
371
- sql = <<-SQL
372
- SELECT #{ (
373
- ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)} #{unique_id_expr} AS #{quote_column(@model.primary_key)} "] +
374
- @fields.collect { |field| field.to_select_sql } +
375
- @attributes.collect { |attribute| attribute.to_select_sql }
376
- ).compact.join(", ") }
377
- FROM #{ @model.table_name }
378
- #{ assocs.collect { |assoc| assoc.to_sql }.join(' ') }
379
- WHERE #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} >= $start
380
- AND #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} <= $end
381
- #{ where_clause }
382
- GROUP BY #{ (
383
- ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)}"] +
384
- @fields.collect { |field| field.to_group_sql }.compact +
385
- @attributes.collect { |attribute| attribute.to_group_sql }.compact +
386
- @groupings + internal_groupings
387
- ).join(", ") }
388
- SQL
389
-
390
- sql += " ORDER BY NULL" if adapter.sphinx_identifier == "mysql"
391
- sql
392
- end
393
-
394
- # Simple helper method for the query info SQL - which is a statement that
395
- # returns the single row for a corresponding id.
396
- #
397
- def to_sql_query_info(offset)
398
- "SELECT * FROM #{@model.quoted_table_name} WHERE " +
399
- " #{quote_column(@model.primary_key)} = (($id - #{offset}) / #{ThinkingSphinx.indexed_models.size})"
400
- end
401
-
402
- # Simple helper method for the query range SQL - which is a statement that
403
- # returns minimum and maximum id values. These can be filtered by delta -
404
- # so pass in :delta => true to get the delta version of the SQL.
405
- #
406
- def to_sql_query_range(options={})
407
- min_statement = adapter.convert_nulls(
408
- "MIN(#{quote_column(@model.primary_key)})", 1
409
- )
410
- max_statement = adapter.convert_nulls(
411
- "MAX(#{quote_column(@model.primary_key)})", 1
412
- )
413
-
414
- sql = "SELECT #{min_statement}, #{max_statement} " +
415
- "FROM #{@model.quoted_table_name} "
416
- if self.delta? && !@delta_object.clause(@model, options[:delta]).blank?
417
- sql << "WHERE #{@delta_object.clause(@model, options[:delta])}"
418
- end
419
-
420
- sql
421
- end
422
110
  end
423
111
  end
@@ -0,0 +1,160 @@
1
+ module ThinkingSphinx
2
+ class Property
3
+ attr_accessor :alias, :columns, :associations, :model, :faceted, :admin
4
+
5
+ def initialize(source, columns, options = {})
6
+ @source = source
7
+ @model = source.model
8
+ @columns = Array(columns)
9
+ @associations = {}
10
+
11
+ raise "Cannot define a field or attribute in #{source.model.name} with no columns. Maybe you are trying to index a field with a reserved name (id, name). You can fix this error by using a symbol rather than a bare name (:id instead of id)." if @columns.empty? || @columns.any? { |column| !column.respond_to?(:__stack) }
12
+
13
+ @alias = options[:as]
14
+ @faceted = options[:facet]
15
+ @admin = options[:admin]
16
+
17
+ @columns.each { |col|
18
+ @associations[col] = association_stack(col.__stack.clone).each { |assoc|
19
+ assoc.join_to(source.base)
20
+ }
21
+ }
22
+ end
23
+
24
+ # Returns the unique name of the attribute - which is either the alias of
25
+ # the attribute, or the name of the only column - if there is only one. If
26
+ # there isn't, there should be an alias. Else things probably won't work.
27
+ # Consider yourself warned.
28
+ #
29
+ def unique_name
30
+ if @columns.length == 1
31
+ @alias || @columns.first.__name
32
+ else
33
+ @alias
34
+ end
35
+ end
36
+
37
+ def to_facet
38
+ return nil unless @faceted
39
+
40
+ ThinkingSphinx::Facet.new(self)
41
+ end
42
+
43
+ # Get the part of the GROUP BY clause related to this attribute - if one is
44
+ # needed. If not, all you'll get back is nil. The latter will happen if
45
+ # there isn't actually a real column to get data from, or if there's
46
+ # multiple data values (read: a has_many or has_and_belongs_to_many
47
+ # association).
48
+ #
49
+ def to_group_sql
50
+ case
51
+ when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
52
+ nil
53
+ else
54
+ @columns.collect { |column|
55
+ column_with_prefix(column)
56
+ }
57
+ end
58
+ end
59
+
60
+ def changed?(instance)
61
+ return true if is_string? || @columns.any? { |col| !col.__stack.empty? }
62
+
63
+ !@columns.all? { |col|
64
+ instance.respond_to?("#{col.__name.to_s}_changed?") &&
65
+ !instance.send("#{col.__name.to_s}_changed?")
66
+ }
67
+ end
68
+
69
+ def admin?
70
+ admin
71
+ end
72
+
73
+ def public?
74
+ !admin
75
+ end
76
+
77
+ private
78
+
79
+ # Could there be more than one value related to the parent record? If so,
80
+ # then this will return true. If not, false. It's that simple.
81
+ #
82
+ def is_many?
83
+ associations.values.flatten.any? { |assoc| assoc.is_many? }
84
+ end
85
+
86
+ # Returns true if any of the columns are string values, instead of database
87
+ # column references.
88
+ def is_string?
89
+ columns.all? { |col| col.is_string? }
90
+ end
91
+
92
+ def adapter
93
+ @adapter ||= @model.sphinx_database_adapter
94
+ end
95
+
96
+ def quote_with_table(table, column)
97
+ "#{quote_table_name(table)}.#{quote_column(column)}"
98
+ end
99
+
100
+ def quote_column(column)
101
+ @model.connection.quote_column_name(column)
102
+ end
103
+
104
+ def quote_table_name(table_name)
105
+ @model.connection.quote_table_name(table_name)
106
+ end
107
+
108
+ # Indication of whether the columns should be concatenated with a space
109
+ # between each value. True if there's either multiple sources or multiple
110
+ # associations.
111
+ #
112
+ def concat_ws?
113
+ multiple_associations? || @columns.length > 1
114
+ end
115
+
116
+ # Checks whether any column requires multiple associations (which only
117
+ # happens for polymorphic situations).
118
+ #
119
+ def multiple_associations?
120
+ associations.any? { |col,assocs| assocs.length > 1 }
121
+ end
122
+
123
+ # Builds a column reference tied to the appropriate associations. This
124
+ # dives into the associations hash and their corresponding joins to
125
+ # figure out how to correctly reference a column in SQL.
126
+ #
127
+ def column_with_prefix(column)
128
+ if column.is_string?
129
+ column.__name
130
+ elsif associations[column].empty?
131
+ "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
132
+ else
133
+ associations[column].collect { |assoc|
134
+ assoc.has_column?(column.__name) ?
135
+ "#{quote_with_table(assoc.join.aliased_table_name, column.__name)}" :
136
+ nil
137
+ }.compact.join(', ')
138
+ end
139
+ end
140
+
141
+ # Gets a stack of associations for a specific path.
142
+ #
143
+ def association_stack(path, parent = nil)
144
+ assocs = []
145
+
146
+ if parent.nil?
147
+ assocs = @source.association(path.shift)
148
+ else
149
+ assocs = parent.children(path.shift)
150
+ end
151
+
152
+ until path.empty?
153
+ point = path.shift
154
+ assocs = assocs.collect { |assoc| assoc.children(point) }.flatten
155
+ end
156
+
157
+ assocs
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,98 @@
1
+ module ThinkingSphinx
2
+ class Search
3
+ module Facets
4
+ # Model.facets *args
5
+ # ThinkingSphinx::Search.facets *args
6
+ # ThinkingSphinx::Search.facets *args, :all_attributes => true
7
+ # ThinkingSphinx::Search.facets *args, :class_facet => false
8
+ #
9
+ def facets(*args)
10
+ options = args.extract_options!
11
+
12
+ if options[:class]
13
+ facets_for_model options[:class], args, options
14
+ else
15
+ facets_for_all_models args, options
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def facets_for_model(klass, args, options)
22
+ hash = ThinkingSphinx::FacetCollection.new args + [options]
23
+ options = options.clone.merge! facet_query_options
24
+
25
+ klass.sphinx_facets.inject(hash) do |hash, facet|
26
+ unless facet.name == :class && !options[:class_facet]
27
+ options[:group_by] = facet.attribute_name
28
+ hash.add_from_results facet, search(*(args + [options]))
29
+ end
30
+
31
+ hash
32
+ end
33
+ end
34
+
35
+ def facets_for_all_models(args, options)
36
+ options = GlobalFacetOptions.merge(options)
37
+ hash = ThinkingSphinx::FacetCollection.new args + [options]
38
+ options = options.merge! facet_query_options
39
+
40
+ facet_names(options).inject(hash) do |hash, name|
41
+ options[:group_by] = name
42
+ hash.add_from_results name, search(*(args + [options]))
43
+ hash
44
+ end
45
+ end
46
+
47
+ def facet_query_options
48
+ config = ThinkingSphinx::Configuration.instance
49
+ max = config.configuration.searchd.max_matches || 1000
50
+
51
+ {
52
+ :group_function => :attr,
53
+ :limit => max,
54
+ :max_matches => max
55
+ }
56
+ end
57
+
58
+ def facet_classes(options)
59
+ options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
60
+ model.constantize
61
+ }
62
+ end
63
+
64
+ def facet_names(options)
65
+ classes = facet_classes(options)
66
+ names = options[:all_attributes] ?
67
+ facet_names_for_all_classes(classes) :
68
+ facet_names_common_to_all_classes(classes)
69
+
70
+ names.delete "class_crc" unless options[:class_facet]
71
+ names
72
+ end
73
+
74
+ def facet_names_for_all_classes(classes)
75
+ all_facets = classes.collect { |klass| klass.sphinx_facets }.flatten
76
+
77
+ all_facets.group_by { |facet|
78
+ facet.name
79
+ }.collect { |name, facets|
80
+ if facets.collect { |facet| facet.type }.uniq.length > 1
81
+ raise "Facet #{name} exists in more than one model with different types"
82
+ end
83
+ facets.first.attribute_name
84
+ }
85
+ end
86
+
87
+ def facet_names_common_to_all_classes(classes)
88
+ facet_names_for_all_classes(classes).select { |name|
89
+ classes.all? { |klass|
90
+ klass.sphinx_facets.detect { |facet|
91
+ facet.attribute_name == name
92
+ }
93
+ }
94
+ }
95
+ end
96
+ end
97
+ end
98
+ end