dpickett-thinking-sphinx 1.1.4 → 1.1.12

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