dpickett-thinking-sphinx 1.1.4 → 1.1.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/README.textile +126 -0
  2. data/lib/thinking_sphinx/active_record/attribute_updates.rb +48 -0
  3. data/lib/thinking_sphinx/active_record/delta.rb +14 -1
  4. data/lib/thinking_sphinx/active_record.rb +23 -5
  5. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +9 -1
  6. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +3 -2
  7. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +4 -3
  8. data/lib/thinking_sphinx/association.rb +17 -0
  9. data/lib/thinking_sphinx/attribute.rb +106 -95
  10. data/lib/thinking_sphinx/class_facet.rb +0 -5
  11. data/lib/thinking_sphinx/collection.rb +7 -1
  12. data/lib/thinking_sphinx/configuration.rb +9 -4
  13. data/lib/thinking_sphinx/core/string.rb +3 -10
  14. data/lib/thinking_sphinx/deltas/default_delta.rb +8 -5
  15. data/lib/thinking_sphinx/deltas/delayed_delta.rb +4 -2
  16. data/lib/thinking_sphinx/deltas.rb +7 -2
  17. data/lib/thinking_sphinx/deploy/capistrano.rb +80 -0
  18. data/lib/thinking_sphinx/facet.rb +22 -9
  19. data/lib/thinking_sphinx/facet_collection.rb +27 -12
  20. data/lib/thinking_sphinx/field.rb +4 -96
  21. data/lib/thinking_sphinx/index/builder.rb +46 -15
  22. data/lib/thinking_sphinx/index.rb +58 -66
  23. data/lib/thinking_sphinx/property.rb +133 -0
  24. data/lib/thinking_sphinx/rails_additions.rb +7 -4
  25. data/lib/thinking_sphinx/search.rb +181 -44
  26. data/lib/thinking_sphinx/tasks.rb +4 -4
  27. data/lib/thinking_sphinx.rb +47 -11
  28. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +2 -2
  29. data/spec/unit/thinking_sphinx/active_record_spec.rb +64 -4
  30. data/spec/unit/thinking_sphinx/attribute_spec.rb +16 -1
  31. data/spec/unit/thinking_sphinx/facet_collection_spec.rb +64 -0
  32. data/spec/unit/thinking_sphinx/facet_spec.rb +46 -0
  33. data/spec/unit/thinking_sphinx/index_spec.rb +90 -0
  34. data/spec/unit/thinking_sphinx/rails_additions_spec.rb +183 -0
  35. data/spec/unit/thinking_sphinx/search_spec.rb +44 -0
  36. data/spec/unit/thinking_sphinx_spec.rb +10 -6
  37. data/tasks/distribution.rb +1 -1
  38. data/tasks/testing.rb +7 -15
  39. data/vendor/after_commit/init.rb +3 -0
  40. data/vendor/after_commit/lib/after_commit/active_record.rb +27 -4
  41. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +1 -1
  42. data/vendor/after_commit/lib/after_commit.rb +4 -1
  43. metadata +12 -3
  44. data/README +0 -107
@@ -37,6 +37,19 @@ module ThinkingSphinx
37
37
  @delta_object = nil
38
38
 
39
39
  initialize_from_builder(&block) if block_given?
40
+
41
+ add_internal_attributes_and_facets
42
+
43
+ # We want to make sure that if the database doesn't exist, then Thinking
44
+ # Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
45
+ # and db:migrate). It's a bit hacky, but I can't think of a better way.
46
+ rescue StandardError => err
47
+ case err.class.name
48
+ when "Mysql::Error", "Java::JavaSql::SQLException", "ActiveRecord::StatementInvalid"
49
+ return
50
+ else
51
+ raise err
52
+ end
40
53
  end
41
54
 
42
55
  def name
@@ -48,7 +61,6 @@ module ThinkingSphinx
48
61
  end
49
62
 
50
63
  def to_riddle_for_core(offset, index)
51
- add_internal_attributes
52
64
  link!
53
65
 
54
66
  source = Riddle::Configuration::SQLSource.new(
@@ -56,7 +68,7 @@ module ThinkingSphinx
56
68
  )
57
69
 
58
70
  set_source_database_settings source
59
- set_source_attributes source
71
+ set_source_attributes source, offset
60
72
  set_source_sql source, offset
61
73
  set_source_settings source
62
74
 
@@ -64,7 +76,6 @@ module ThinkingSphinx
64
76
  end
65
77
 
66
78
  def to_riddle_for_delta(offset, index)
67
- add_internal_attributes
68
79
  link!
69
80
 
70
81
  source = Riddle::Configuration::SQLSource.new(
@@ -73,7 +84,7 @@ module ThinkingSphinx
73
84
  source.parent = "#{name}_core_#{index}"
74
85
 
75
86
  set_source_database_settings source
76
- set_source_attributes source
87
+ set_source_attributes source, offset
77
88
  set_source_sql source, offset, true
78
89
 
79
90
  source
@@ -175,17 +186,6 @@ module ThinkingSphinx
175
186
  @model.sphinx_facets ||= []
176
187
  @fields.select( &is_faceted).each &add_facet
177
188
  @attributes.select(&is_faceted).each &add_facet
178
-
179
- # We want to make sure that if the database doesn't exist, then Thinking
180
- # Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
181
- # and db:migrate). It's a bit hacky, but I can't think of a better way.
182
- rescue StandardError => err
183
- case err.class.name
184
- when "Mysql::Error", "ActiveRecord::StatementInvalid"
185
- return
186
- else
187
- raise err
188
- end
189
189
  end
190
190
 
191
191
  # Returns all associations used amongst all the fields and attributes.
@@ -200,8 +200,8 @@ module ThinkingSphinx
200
200
  }.flatten +
201
201
  # attribute associations
202
202
  @attributes.collect { |attrib|
203
- attrib.associations.values
204
- }.flatten
203
+ attrib.associations.values if attrib.include_as_association?
204
+ }.compact.flatten
205
205
  ).uniq.collect { |assoc|
206
206
  # get ancestors as well as column-level associations
207
207
  assoc.ancestors
@@ -238,7 +238,7 @@ module ThinkingSphinx
238
238
  def crc_column
239
239
  if @model.column_names.include?(@model.inheritance_column)
240
240
  adapter.cast_to_unsigned(adapter.convert_nulls(
241
- adapter.crc(adapter.quote_with_table(@model.inheritance_column)),
241
+ adapter.crc(adapter.quote_with_table(@model.inheritance_column), true),
242
242
  @model.to_crc32
243
243
  ))
244
244
  else
@@ -246,50 +246,45 @@ module ThinkingSphinx
246
246
  end
247
247
  end
248
248
 
249
- def add_internal_attributes
250
- @attributes << Attribute.new(
251
- FauxColumn.new(@model.primary_key.to_sym),
252
- :type => :integer,
253
- :as => :sphinx_internal_id
254
- ) unless @attributes.detect { |attr| attr.alias == :sphinx_internal_id }
255
-
256
- unless @attributes.detect { |attr| attr.alias == :class_crc }
257
- @attributes << Attribute.new(
258
- FauxColumn.new(crc_column),
259
- :type => :integer,
260
- :as => :class_crc,
261
- :facet => true
262
- )
263
-
264
- @model.sphinx_facets << ThinkingSphinx::ClassFacet.new(@attributes.last)
265
- end
266
-
267
- if @model.column_names.include?(@model.inheritance_column)
268
- class_col = FauxColumn.new(
269
- adapter.convert_nulls(adapter.quote_with_table(@model.inheritance_column), @model.to_s)
270
- )
271
- else
272
- class_col = FauxColumn.new("'#{@model.to_s}'")
273
- end
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"
274
254
 
275
- @attributes << Attribute.new(class_col,
276
- :type => :string,
277
- :as => :class
278
- )
255
+ add_internal_facet :class_crc
256
+ end
257
+
258
+ def add_internal_attribute(name, type, contents, facet = false)
259
+ return unless attribute_by_alias(name).nil?
279
260
 
280
261
  @attributes << Attribute.new(
281
- FauxColumn.new("'" + (@model.send(:subclasses).collect { |klass|
282
- klass.to_crc32.to_s
283
- } << @model.to_crc32.to_s).join(",") + "'"),
284
- :type => :multi,
285
- :as => :subclass_crcs
286
- ) unless @attributes.detect { |attr| attr.alias == :subclass_crcs }
262
+ FauxColumn.new(contents),
263
+ :type => type,
264
+ :as => name,
265
+ :facet => facet,
266
+ :admin => true
267
+ )
268
+ end
269
+
270
+ def add_internal_facet(name)
271
+ return unless facet_by_alias(name).nil?
287
272
 
288
- @attributes << Attribute.new(
289
- FauxColumn.new("0"),
290
- :type => :integer,
291
- :as => :sphinx_deleted
292
- ) unless @attributes.detect { |attr| attr.alias == :sphinx_deleted }
273
+ @model.sphinx_facets << ClassFacet.new(attribute_by_alias(name))
274
+ end
275
+
276
+ def attribute_by_alias(attr_alias)
277
+ @attributes.detect { |attrib| attrib.alias == attr_alias }
278
+ end
279
+
280
+ def facet_by_alias(name)
281
+ @model.sphinx_facets.detect { |facet| facet.name == name }
282
+ end
283
+
284
+ def subclasses_to_s
285
+ "'" + (@model.send(:subclasses).collect { |klass|
286
+ klass.to_crc32.to_s
287
+ } << @model.to_crc32.to_s).join(",") + "'"
293
288
  end
294
289
 
295
290
  def set_source_database_settings(source)
@@ -303,9 +298,9 @@ module ThinkingSphinx
303
298
  source.sql_sock = config[:socket]
304
299
  end
305
300
 
306
- def set_source_attributes(source)
301
+ def set_source_attributes(source, offset = nil)
307
302
  attributes.each do |attrib|
308
- source.send(attrib.type_to_config) << attrib.config_value
303
+ source.send(attrib.type_to_config) << attrib.config_value(offset)
309
304
  end
310
305
  end
311
306
 
@@ -372,14 +367,14 @@ module ThinkingSphinx
372
367
  internal_groupings << "#{@model.quoted_table_name}.#{quote_column(@model.inheritance_column)}"
373
368
  end
374
369
 
375
- unique_id_expr = "* #{ThinkingSphinx.indexed_models.size} + #{options[:offset] || 0}"
370
+ unique_id_expr = ThinkingSphinx.unique_id_expression(options[:offset])
376
371
 
377
372
  sql = <<-SQL
378
373
  SELECT #{ (
379
374
  ["#{@model.quoted_table_name}.#{quote_column(@model.primary_key)} #{unique_id_expr} AS #{quote_column(@model.primary_key)} "] +
380
375
  @fields.collect { |field| field.to_select_sql } +
381
376
  @attributes.collect { |attribute| attribute.to_select_sql }
382
- ).join(", ") }
377
+ ).compact.join(", ") }
383
378
  FROM #{ @model.table_name }
384
379
  #{ assocs.collect { |assoc| assoc.to_sql }.join(' ') }
385
380
  WHERE #{@model.quoted_table_name}.#{quote_column(@model.primary_key)} >= $start
@@ -393,10 +388,7 @@ GROUP BY #{ (
393
388
  ).join(", ") }
394
389
  SQL
395
390
 
396
- if @model.connection.class.name == "ActiveRecord::ConnectionAdapters::MysqlAdapter"
397
- sql += " ORDER BY NULL"
398
- end
399
-
391
+ sql += " ORDER BY NULL" if adapter.sphinx_identifier == "mysql"
400
392
  sql
401
393
  end
402
394
 
@@ -0,0 +1,133 @@
1
+ module ThinkingSphinx
2
+ class Property
3
+ attr_accessor :alias, :columns, :associations, :model, :faceted, :admin
4
+
5
+ def initialize(columns, options = {})
6
+ @columns = Array(columns)
7
+ @associations = {}
8
+
9
+ raise "Cannot define a field 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) }
10
+
11
+ @alias = options[:as]
12
+ @faceted = options[:facet]
13
+ @admin = options[:admin]
14
+ end
15
+
16
+ # Returns the unique name of the attribute - which is either the alias of
17
+ # the attribute, or the name of the only column - if there is only one. If
18
+ # there isn't, there should be an alias. Else things probably won't work.
19
+ # Consider yourself warned.
20
+ #
21
+ def unique_name
22
+ if @columns.length == 1
23
+ @alias || @columns.first.__name
24
+ else
25
+ @alias
26
+ end
27
+ end
28
+
29
+ def to_facet
30
+ return nil unless @faceted
31
+
32
+ ThinkingSphinx::Facet.new(self)
33
+ end
34
+
35
+ # Get the part of the GROUP BY clause related to this attribute - if one is
36
+ # needed. If not, all you'll get back is nil. The latter will happen if
37
+ # there isn't actually a real column to get data from, or if there's
38
+ # multiple data values (read: a has_many or has_and_belongs_to_many
39
+ # association).
40
+ #
41
+ def to_group_sql
42
+ case
43
+ when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
44
+ nil
45
+ else
46
+ @columns.collect { |column|
47
+ column_with_prefix(column)
48
+ }
49
+ end
50
+ end
51
+
52
+ def changed?(instance)
53
+ return true if is_string? || @columns.any? { |col| !col.__stack.empty? }
54
+
55
+ !@columns.all? { |col|
56
+ instance.respond_to?("#{col.__name.to_s}_changed?") &&
57
+ !instance.send("#{col.__name.to_s}_changed?")
58
+ }
59
+ end
60
+
61
+ def admin?
62
+ admin
63
+ end
64
+
65
+ def public?
66
+ !admin
67
+ end
68
+
69
+ private
70
+
71
+ # Could there be more than one value related to the parent record? If so,
72
+ # then this will return true. If not, false. It's that simple.
73
+ #
74
+ def is_many?
75
+ associations.values.flatten.any? { |assoc| assoc.is_many? }
76
+ end
77
+
78
+ # Returns true if any of the columns are string values, instead of database
79
+ # column references.
80
+ def is_string?
81
+ columns.all? { |col| col.is_string? }
82
+ end
83
+
84
+ def adapter
85
+ @adapter ||= @model.sphinx_database_adapter
86
+ end
87
+
88
+ def quote_with_table(table, column)
89
+ "#{quote_table_name(table)}.#{quote_column(column)}"
90
+ end
91
+
92
+ def quote_column(column)
93
+ @model.connection.quote_column_name(column)
94
+ end
95
+
96
+ def quote_table_name(table_name)
97
+ @model.connection.quote_table_name(table_name)
98
+ end
99
+
100
+ # Indication of whether the columns should be concatenated with a space
101
+ # between each value. True if there's either multiple sources or multiple
102
+ # associations.
103
+ #
104
+ def concat_ws?
105
+ multiple_associations? || @columns.length > 1
106
+ end
107
+
108
+ # Checks whether any column requires multiple associations (which only
109
+ # happens for polymorphic situations).
110
+ #
111
+ def multiple_associations?
112
+ associations.any? { |col,assocs| assocs.length > 1 }
113
+ end
114
+
115
+ # Builds a column reference tied to the appropriate associations. This
116
+ # dives into the associations hash and their corresponding joins to
117
+ # figure out how to correctly reference a column in SQL.
118
+ #
119
+ def column_with_prefix(column)
120
+ if column.is_string?
121
+ column.__name
122
+ elsif associations[column].empty?
123
+ "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
124
+ else
125
+ associations[column].collect { |assoc|
126
+ assoc.has_column?(column.__name) ?
127
+ "#{quote_with_table(assoc.join.aliased_table_name, column.__name)}" :
128
+ nil
129
+ }.compact.join(', ')
130
+ end
131
+ end
132
+ end
133
+ end
@@ -49,10 +49,13 @@ module ThinkingSphinx
49
49
  end
50
50
  end
51
51
 
52
- if ActiveRecord::ConnectionAdapters.constants.include?("MysqlAdapter")
53
- ActiveRecord::ConnectionAdapters::MysqlAdapter.send(
54
- :include, ThinkingSphinx::MysqlQuotedTableName
55
- ) 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
56
59
  end
57
60
 
58
61
  module ThinkingSphinx
@@ -1,4 +1,4 @@
1
- module ThinkingSphinx
1
+ module ThinkingSphinx
2
2
  # Once you've got those indexes in and built, this is the stuff that
3
3
  # matters - how to search! This class provides a generic search
4
4
  # interface - which you can use to search all your indexed models at once.
@@ -7,6 +7,11 @@ module ThinkingSphinx
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
@@ -94,16 +99,24 @@ module ThinkingSphinx
94
99
  # == Searching by Attributes
95
100
  #
96
101
  # Also known as filters, you can limit your searches to documents that
97
- # have specific values for their attributes. There are two ways to do
98
- # this. The first is one that works in all scenarios - using the :with
99
- # option.
100
- #
101
- # ThinkingSphinx::Search.search :with => {:parent_id => 10}
102
- #
103
- # The second is only viable if you're searching with a specific model
104
- # (not multi-model searching). With a single model, Thinking Sphinx
105
- # can figure out what attributes and fields are available, so you can
106
- # put it all in the :conditions hash, and it will sort it out.
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.
107
120
  #
108
121
  # Node.search :conditions => {:parent_id => 10}
109
122
  #
@@ -186,6 +199,12 @@ module ThinkingSphinx
186
199
  # documentation[http://sphinxsearch.com/doc.html] for that level of
187
200
  # detail though.
188
201
  #
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.
205
+ #
206
+ # User.search("pat", :sql_order => "name")
207
+ #
189
208
  # == Grouping
190
209
  #
191
210
  # For this you can use the group_by, group_clause and group_function
@@ -194,7 +213,66 @@ module ThinkingSphinx
194
213
  # you read all the relevant
195
214
  # documentation[http://sphinxsearch.com/doc.html#clustering] first.
196
215
  #
197
- # Yes this section will be expanded, but this is a start.
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
228
+ #
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
+ #
198
276
  #
199
277
  # == Geo/Location Searching
200
278
  #
@@ -291,8 +369,10 @@ module ThinkingSphinx
291
369
  def retry_search_on_stale_index(query, options, &block)
292
370
  stale_ids = []
293
371
  stale_retries_left = case options[:retry_stale]
294
- when true: 3 # default to three retries
295
- when nil, false: 0 # no retries
372
+ when true
373
+ 3 # default to three retries
374
+ when nil, false
375
+ 0 # no retries
296
376
  else options[:retry_stale].to_i
297
377
  end
298
378
  begin
@@ -352,43 +432,23 @@ module ThinkingSphinx
352
432
  end
353
433
  end
354
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
+ #
355
440
  def facets(*args)
356
- hash = ThinkingSphinx::FacetCollection.new args
357
- options = args.extract_options!.clone.merge! :group_function => :attr
358
-
359
- klasses = options[:classes] || [options[:class]]
360
- klasses = [] if options[:class].nil?
361
-
362
- #no classes specified so get classes from resultset
363
- if klasses.empty?
364
- options[:group_by] = "class_crc"
365
- results = search(*(args + [options]))
366
-
367
- hash[:class] = {}
368
- results.each_with_groupby_and_count do |result, group, count|
369
- hash[:class][result.class.name] = count
370
- klasses << result.class
371
- end
372
- end
441
+ options = args.extract_options!
373
442
 
374
- klasses.each do |klass|
375
- klass.sphinx_facets.inject(hash) do |hash, facet|
376
- if facet.name != :class || options[:include_class_facets]
377
- hash.add_from_results facet,
378
- search(*(args +
379
- [options.merge(:group_by => facet.attribute_name)]))
380
- end
381
-
382
- hash
383
- end
443
+ if options[:class]
444
+ facets_for_model options[:class], args, options
445
+ else
446
+ facets_for_all_models args, options
384
447
  end
385
-
386
- hash
387
448
  end
388
449
 
389
450
  private
390
451
 
391
-
392
452
  # This method handles the common search functionality, and returns both
393
453
  # the result hash and the client. Not super elegant, but it'll do for
394
454
  # the moment.
@@ -412,6 +472,7 @@ module ThinkingSphinx
412
472
 
413
473
  client.limit = options[:per_page].to_i if options[:per_page]
414
474
  page = options[:page] ? options[:page].to_i : 1
475
+ page = 1 if page <= 0
415
476
  client.offset = (page - 1) * client.limit
416
477
 
417
478
  begin
@@ -492,6 +553,13 @@ module ThinkingSphinx
492
553
  Riddle::Client::Filter.new attr.to_s, filter_value(val), true
493
554
  } if options[:without]
494
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
+
495
563
  # exclusive attribute filter on primary key
496
564
  client.filters += Array(options[:without_ids]).collect { |id|
497
565
  Riddle::Client::Filter.new 'sphinx_internal_id', filter_value(id), true
@@ -649,6 +717,75 @@ module ThinkingSphinx
649
717
 
650
718
  string
651
719
  end
720
+
721
+ def facets_for_model(klass, args, options)
722
+ hash = ThinkingSphinx::FacetCollection.new args + [options]
723
+ options = options.clone.merge! facet_query_options
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! facet_query_options
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_query_options
748
+ config = ThinkingSphinx::Configuration.instance
749
+ max = config.configuration.searchd.max_matches || 1000
750
+
751
+ {
752
+ :group_function => :attr,
753
+ :limit => max,
754
+ :max_matches => max
755
+ }
756
+ end
757
+
758
+ def facet_classes(options)
759
+ options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
760
+ model.constantize
761
+ }
762
+ end
763
+
764
+ def facet_names(options)
765
+ classes = facet_classes(options)
766
+ names = options[:all_attributes] ?
767
+ facet_names_for_all_classes(classes) :
768
+ facet_names_common_to_all_classes(classes)
769
+
770
+ names.delete "class_crc" unless options[:class_facet]
771
+ names
772
+ end
773
+
774
+ def facet_names_for_all_classes(classes)
775
+ classes.collect { |klass|
776
+ klass.sphinx_facets.collect { |facet| facet.attribute_name }
777
+ }.flatten.uniq
778
+ end
779
+
780
+ def facet_names_common_to_all_classes(classes)
781
+ facet_names_for_all_classes(classes).select { |name|
782
+ classes.all? { |klass|
783
+ klass.sphinx_facets.detect { |facet|
784
+ facet.attribute_name == name
785
+ }
786
+ }
787
+ }
788
+ end
652
789
  end
653
790
  end
654
791
  end
@@ -8,8 +8,8 @@ namespace :thinking_sphinx do
8
8
 
9
9
  desc "Stop if running, then start a Sphinx searchd daemon using Thinking Sphinx's settings"
10
10
  task :running_start => :app_env do
11
- Rake::Task["thinking_sphinx:stop"].invoke if sphinx_running?
12
- Rake::Task["thinking_sphinx:start"].invoke
11
+ Rake::Task["thinking_sphinx:stop"].invoke if sphinx_running?
12
+ Rake::Task["thinking_sphinx:start"].invoke
13
13
  end
14
14
 
15
15
  desc "Start a Sphinx searchd daemon using Thinking Sphinx's settings"
@@ -30,7 +30,7 @@ namespace :thinking_sphinx do
30
30
  if sphinx_running?
31
31
  puts "Started successfully (pid #{sphinx_pid})."
32
32
  else
33
- puts "Failed to start searchd daemon. Check #{config.searchd_log_file}."
33
+ puts "Failed to start searchd daemon. Check #{config.searchd_log_file}"
34
34
  end
35
35
  end
36
36
 
@@ -39,7 +39,7 @@ namespace :thinking_sphinx do
39
39
  raise RuntimeError, "searchd is not running." unless sphinx_running?
40
40
  config = ThinkingSphinx::Configuration.instance
41
41
  pid = sphinx_pid
42
- system "searchd --stop --config #{config.config_file}"
42
+ system "#{config.bin_path}searchd --stop --config #{config.config_file}"
43
43
  puts "Stopped search daemon (pid #{pid})."
44
44
  end
45
45