dpickett-thinking-sphinx 1.1.4

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 (79) hide show
  1. data/LICENCE +20 -0
  2. data/README +107 -0
  3. data/lib/thinking_sphinx/active_record/delta.rb +74 -0
  4. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  5. data/lib/thinking_sphinx/active_record/search.rb +57 -0
  6. data/lib/thinking_sphinx/active_record.rb +245 -0
  7. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +34 -0
  8. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +53 -0
  9. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +129 -0
  10. data/lib/thinking_sphinx/association.rb +144 -0
  11. data/lib/thinking_sphinx/attribute.rb +254 -0
  12. data/lib/thinking_sphinx/class_facet.rb +20 -0
  13. data/lib/thinking_sphinx/collection.rb +142 -0
  14. data/lib/thinking_sphinx/configuration.rb +236 -0
  15. data/lib/thinking_sphinx/core/string.rb +22 -0
  16. data/lib/thinking_sphinx/deltas/datetime_delta.rb +50 -0
  17. data/lib/thinking_sphinx/deltas/default_delta.rb +65 -0
  18. data/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb +24 -0
  19. data/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb +27 -0
  20. data/lib/thinking_sphinx/deltas/delayed_delta/job.rb +26 -0
  21. data/lib/thinking_sphinx/deltas/delayed_delta.rb +25 -0
  22. data/lib/thinking_sphinx/deltas.rb +22 -0
  23. data/lib/thinking_sphinx/facet.rb +58 -0
  24. data/lib/thinking_sphinx/facet_collection.rb +45 -0
  25. data/lib/thinking_sphinx/field.rb +172 -0
  26. data/lib/thinking_sphinx/index/builder.rb +233 -0
  27. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  28. data/lib/thinking_sphinx/index.rb +432 -0
  29. data/lib/thinking_sphinx/rails_additions.rb +133 -0
  30. data/lib/thinking_sphinx/search.rb +654 -0
  31. data/lib/thinking_sphinx/tasks.rb +128 -0
  32. data/lib/thinking_sphinx.rb +145 -0
  33. data/spec/unit/thinking_sphinx/active_record/delta_spec.rb +136 -0
  34. data/spec/unit/thinking_sphinx/active_record/has_many_association_spec.rb +53 -0
  35. data/spec/unit/thinking_sphinx/active_record/search_spec.rb +107 -0
  36. data/spec/unit/thinking_sphinx/active_record_spec.rb +256 -0
  37. data/spec/unit/thinking_sphinx/association_spec.rb +247 -0
  38. data/spec/unit/thinking_sphinx/attribute_spec.rb +212 -0
  39. data/spec/unit/thinking_sphinx/collection_spec.rb +14 -0
  40. data/spec/unit/thinking_sphinx/configuration_spec.rb +136 -0
  41. data/spec/unit/thinking_sphinx/core/string_spec.rb +9 -0
  42. data/spec/unit/thinking_sphinx/field_spec.rb +145 -0
  43. data/spec/unit/thinking_sphinx/index/builder_spec.rb +5 -0
  44. data/spec/unit/thinking_sphinx/index/faux_column_spec.rb +30 -0
  45. data/spec/unit/thinking_sphinx/index_spec.rb +54 -0
  46. data/spec/unit/thinking_sphinx/search_spec.rb +59 -0
  47. data/spec/unit/thinking_sphinx_spec.rb +129 -0
  48. data/tasks/distribution.rb +48 -0
  49. data/tasks/rails.rake +1 -0
  50. data/tasks/testing.rb +86 -0
  51. data/vendor/after_commit/LICENSE +20 -0
  52. data/vendor/after_commit/README +16 -0
  53. data/vendor/after_commit/Rakefile +22 -0
  54. data/vendor/after_commit/init.rb +5 -0
  55. data/vendor/after_commit/lib/after_commit/active_record.rb +91 -0
  56. data/vendor/after_commit/lib/after_commit/connection_adapters.rb +103 -0
  57. data/vendor/after_commit/lib/after_commit.rb +42 -0
  58. data/vendor/after_commit/test/after_commit_test.rb +53 -0
  59. data/vendor/delayed_job/lib/delayed/job.rb +251 -0
  60. data/vendor/delayed_job/lib/delayed/message_sending.rb +7 -0
  61. data/vendor/delayed_job/lib/delayed/performable_method.rb +55 -0
  62. data/vendor/delayed_job/lib/delayed/worker.rb +54 -0
  63. data/vendor/riddle/lib/riddle/client/filter.rb +53 -0
  64. data/vendor/riddle/lib/riddle/client/message.rb +65 -0
  65. data/vendor/riddle/lib/riddle/client/response.rb +84 -0
  66. data/vendor/riddle/lib/riddle/client.rb +619 -0
  67. data/vendor/riddle/lib/riddle/configuration/distributed_index.rb +48 -0
  68. data/vendor/riddle/lib/riddle/configuration/index.rb +142 -0
  69. data/vendor/riddle/lib/riddle/configuration/indexer.rb +19 -0
  70. data/vendor/riddle/lib/riddle/configuration/remote_index.rb +17 -0
  71. data/vendor/riddle/lib/riddle/configuration/searchd.rb +25 -0
  72. data/vendor/riddle/lib/riddle/configuration/section.rb +37 -0
  73. data/vendor/riddle/lib/riddle/configuration/source.rb +23 -0
  74. data/vendor/riddle/lib/riddle/configuration/sql_source.rb +34 -0
  75. data/vendor/riddle/lib/riddle/configuration/xml_source.rb +28 -0
  76. data/vendor/riddle/lib/riddle/configuration.rb +33 -0
  77. data/vendor/riddle/lib/riddle/controller.rb +44 -0
  78. data/vendor/riddle/lib/riddle.rb +30 -0
  79. metadata +158 -0
@@ -0,0 +1,236 @@
1
+ require 'erb'
2
+ require 'singleton'
3
+
4
+ module ThinkingSphinx
5
+ # This class both keeps track of the configuration settings for Sphinx and
6
+ # also generates the resulting file for Sphinx to use.
7
+ #
8
+ # Here are the default settings, relative to RAILS_ROOT where relevant:
9
+ #
10
+ # config file:: config/#{environment}.sphinx.conf
11
+ # searchd log file:: log/searchd.log
12
+ # query log file:: log/searchd.query.log
13
+ # pid file:: log/searchd.#{environment}.pid
14
+ # searchd files:: db/sphinx/#{environment}/
15
+ # address:: 127.0.0.1
16
+ # port:: 3312
17
+ # allow star:: false
18
+ # min prefix length:: 1
19
+ # min infix length:: 1
20
+ # mem limit:: 64M
21
+ # max matches:: 1000
22
+ # morphology:: stem_en
23
+ # charset type:: utf-8
24
+ # charset table:: nil
25
+ # ignore chars:: nil
26
+ # html strip:: false
27
+ # html remove elements:: ''
28
+ #
29
+ # If you want to change these settings, create a YAML file at
30
+ # config/sphinx.yml with settings for each environment, in a similar
31
+ # fashion to database.yml - using the following keys: config_file,
32
+ # searchd_log_file, query_log_file, pid_file, searchd_file_path, port,
33
+ # allow_star, enable_star, min_prefix_len, min_infix_len, mem_limit,
34
+ # max_matches, # morphology, charset_type, charset_table, ignore_chars,
35
+ # html_strip, # html_remove_elements. I think you've got the idea.
36
+ #
37
+ # Each setting in the YAML file is optional - so only put in the ones you
38
+ # want to change.
39
+ #
40
+ # Keep in mind, if for some particular reason you're using a version of
41
+ # Sphinx older than 0.9.8 r871 (that's prior to the proper 0.9.8 release),
42
+ # don't set allow_star to true.
43
+ #
44
+ class Configuration
45
+ include Singleton
46
+
47
+ SourceOptions = %w( mysql_connect_flags sql_range_step sql_query_pre
48
+ sql_query_post sql_ranged_throttle sql_query_post_index )
49
+
50
+ IndexOptions = %w( charset_table charset_type docinfo enable_star
51
+ exceptions html_index_attrs html_remove_elements html_strip ignore_chars
52
+ min_infix_len min_prefix_len min_word_len mlock morphology ngram_chars
53
+ ngram_len phrase_boundary phrase_boundary_step preopen stopwords
54
+ wordforms )
55
+
56
+ attr_accessor :config_file, :searchd_log_file, :query_log_file,
57
+ :pid_file, :searchd_file_path, :address, :port, :allow_star,
58
+ :database_yml_file, :app_root, :bin_path, :model_directories
59
+
60
+ attr_accessor :source_options, :index_options
61
+
62
+ attr_reader :environment, :configuration
63
+
64
+ # Load in the configuration settings - this will look for config/sphinx.yml
65
+ # and parse it according to the current environment.
66
+ #
67
+ def initialize(app_root = Dir.pwd)
68
+ self.reset
69
+ end
70
+
71
+ def reset
72
+ self.app_root = RAILS_ROOT if defined?(RAILS_ROOT)
73
+ self.app_root = Merb.root if defined?(Merb)
74
+ self.app_root ||= app_root
75
+
76
+ @configuration = Riddle::Configuration.new
77
+ @configuration.searchd.address = "127.0.0.1"
78
+ @configuration.searchd.port = 3312
79
+ @configuration.searchd.pid_file = "#{self.app_root}/log/searchd.#{environment}.pid"
80
+ @configuration.searchd.log = "#{self.app_root}/log/searchd.log"
81
+ @configuration.searchd.query_log = "#{self.app_root}/log/searchd.query.log"
82
+
83
+ self.database_yml_file = "#{self.app_root}/config/database.yml"
84
+ self.config_file = "#{self.app_root}/config/#{environment}.sphinx.conf"
85
+ self.searchd_file_path = "#{self.app_root}/db/sphinx/#{environment}"
86
+ self.allow_star = false
87
+ self.bin_path = ""
88
+ self.model_directories = ["#{app_root}/app/models/"]
89
+
90
+ self.source_options = {}
91
+ self.index_options = {
92
+ :charset_type => "utf-8",
93
+ :morphology => "stem_en"
94
+ }
95
+
96
+ parse_config
97
+
98
+ self
99
+ end
100
+
101
+ def self.environment
102
+ @@environment ||= (
103
+ defined?(Merb) ? Merb.environment : ENV['RAILS_ENV']
104
+ ) || "development"
105
+ end
106
+
107
+ def environment
108
+ self.class.environment
109
+ end
110
+
111
+ def controller
112
+ @controller ||= Riddle::Controller.new(@configuration, self.config_file)
113
+ end
114
+
115
+ # Generate the config file for Sphinx by using all the settings defined and
116
+ # looping through all the models with indexes to build the relevant
117
+ # indexer and searchd configuration, and sources and indexes details.
118
+ #
119
+ def build(file_path=nil)
120
+ load_models
121
+ file_path ||= "#{self.config_file}"
122
+
123
+ @configuration.indexes.clear
124
+
125
+ ThinkingSphinx.indexed_models.each_with_index do |model, model_index|
126
+ @configuration.indexes.concat model.constantize.to_riddle(model_index)
127
+ end
128
+
129
+ open(file_path, "w") do |file|
130
+ file.write @configuration.render
131
+ end
132
+ end
133
+
134
+ # Make sure all models are loaded - without reloading any that
135
+ # ActiveRecord::Base is already aware of (otherwise we start to hit some
136
+ # messy dependencies issues).
137
+ #
138
+ def load_models
139
+ self.model_directories.each do |base|
140
+ Dir["#{base}**/*.rb"].each do |file|
141
+ model_name = file.gsub(/^#{base}([\w_\/\\]+)\.rb/, '\1')
142
+
143
+ next if model_name.nil?
144
+ next if ::ActiveRecord::Base.send(:subclasses).detect { |model|
145
+ model.name == model_name
146
+ }
147
+
148
+ begin
149
+ model_name.camelize.constantize
150
+ rescue LoadError
151
+ model_name.gsub!(/.*[\/\\]/, '').nil? ? next : retry
152
+ rescue NameError
153
+ next
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ def address
160
+ @configuration.searchd.address
161
+ end
162
+
163
+ def address=(address)
164
+ @configuration.searchd.address = address
165
+ end
166
+
167
+ def port
168
+ @configuration.searchd.port
169
+ end
170
+
171
+ def port=(port)
172
+ @configuration.searchd.port = port
173
+ end
174
+
175
+ def pid_file
176
+ @configuration.searchd.pid_file
177
+ end
178
+
179
+ def pid_file=(pid_file)
180
+ @configuration.searchd.pid_file = pid_file
181
+ end
182
+
183
+ def searchd_log_file
184
+ @configuration.searchd.log
185
+ end
186
+
187
+ def searchd_log_file=(file)
188
+ @configuration.searchd.log = file
189
+ end
190
+
191
+ def query_log_file
192
+ @configuration.searchd.query_log
193
+ end
194
+
195
+ def query_log_file=(file)
196
+ @configuration.searchd.query_log = file
197
+ end
198
+
199
+ private
200
+
201
+ # Parse the config/sphinx.yml file - if it exists - then use the attribute
202
+ # accessors to set the appropriate values. Nothing too clever.
203
+ #
204
+ def parse_config
205
+ path = "#{app_root}/config/sphinx.yml"
206
+ return unless File.exists?(path)
207
+
208
+ conf = YAML::load(ERB.new(IO.read(path)).result)[environment]
209
+
210
+ conf.each do |key,value|
211
+ self.send("#{key}=", value) if self.methods.include?("#{key}=")
212
+
213
+ set_sphinx_setting self.source_options, key, value, SourceOptions
214
+ set_sphinx_setting self.index_options, key, value, IndexOptions
215
+ set_sphinx_setting @configuration.searchd, key, value
216
+ set_sphinx_setting @configuration.indexer, key, value
217
+ end unless conf.nil?
218
+
219
+ self.bin_path += '/' unless self.bin_path.blank?
220
+
221
+ if self.allow_star
222
+ self.index_options[:enable_star] = true
223
+ self.index_options[:min_prefix_len] = 1
224
+ end
225
+ end
226
+
227
+ def set_sphinx_setting(object, key, value, allowed = {})
228
+ if object.is_a?(Hash)
229
+ object[key.to_sym] = value if allowed.include?(key.to_s)
230
+ else
231
+ object.send("#{key}=", value) if object.methods.include?("#{key}")
232
+ send("#{key}=", value) if self.methods.include?("#{key}")
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,22 @@
1
+ module ThinkingSphinx
2
+ module Core
3
+ module String
4
+
5
+ def to_crc32
6
+ result = 0xFFFFFFFF
7
+ self.each_byte do |byte|
8
+ result ^= byte
9
+ 8.times do
10
+ result = (result >> 1) ^ (0xEDB88320 * (result & 1))
11
+ end
12
+ end
13
+ result ^ 0xFFFFFFFF
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+
20
+ class String
21
+ include ThinkingSphinx::Core::String
22
+ end
@@ -0,0 +1,50 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class DatetimeDelta < ThinkingSphinx::Deltas::DefaultDelta
4
+ attr_accessor :column, :threshold
5
+
6
+ def initialize(index, options)
7
+ @index = index
8
+ @column = options.delete(:delta_column) || :updated_at
9
+ @threshold = options.delete(:threshold) || 1.day
10
+ end
11
+
12
+ def index(model, instance = nil)
13
+ # do nothing
14
+ true
15
+ end
16
+
17
+ def delayed_index(model)
18
+ config = ThinkingSphinx::Configuration.instance
19
+ rotate = ThinkingSphinx.sphinx_running? ? "--rotate" : ""
20
+
21
+ output = `#{config.bin_path}indexer --config #{config.config_file} #{rotate} #{delta_index_name model}`
22
+ output += `#{config.bin_path}indexer --config #{config.config_file} #{rotate} --merge #{core_index_name model} #{delta_index_name model} --merge-dst-range sphinx_deleted 0 0`
23
+ puts output unless ThinkingSphinx.suppress_delta_output?
24
+
25
+ true
26
+ end
27
+
28
+ def toggle(instance)
29
+ # do nothing
30
+ end
31
+
32
+ def toggled(instance)
33
+ instance.send(@column) > @threshold.ago
34
+ end
35
+
36
+ def reset_query(model)
37
+ nil
38
+ end
39
+
40
+ def clause(model, toggled)
41
+ if toggled
42
+ "#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" +
43
+ " > #{adapter.time_difference(@threshold)}"
44
+ else
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,65 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class DefaultDelta
4
+ attr_accessor :column
5
+
6
+ def initialize(index, options)
7
+ @index = index
8
+ @column = options.delete(:delta_column) || :delta
9
+ end
10
+
11
+ def index(model, instance = nil)
12
+ return true unless ThinkingSphinx.updates_enabled? &&
13
+ ThinkingSphinx.deltas_enabled?
14
+
15
+ config = ThinkingSphinx::Configuration.instance
16
+ client = Riddle::Client.new config.address, config.port
17
+
18
+ client.update(
19
+ core_index_name(model),
20
+ ['sphinx_deleted'],
21
+ {instance.sphinx_document_id => [1]}
22
+ ) if instance && ThinkingSphinx.sphinx_running? && instance.in_core_index?
23
+
24
+ output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{delta_index_name model}`
25
+ puts output unless ThinkingSphinx.suppress_delta_output?
26
+
27
+ true
28
+ end
29
+
30
+ def toggle(instance)
31
+ instance.delta = true
32
+ end
33
+
34
+ def toggled(instance)
35
+ instance.delta
36
+ end
37
+
38
+ def reset_query(model)
39
+ "UPDATE #{model.quoted_table_name} SET " +
40
+ "#{@index.quote_column(@column.to_s)} = #{adapter.boolean(false)}"
41
+ end
42
+
43
+ def clause(model, toggled)
44
+ "#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" +
45
+ " = #{adapter.boolean(toggled)}"
46
+ end
47
+
48
+ protected
49
+
50
+ def core_index_name(model)
51
+ "#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_core"
52
+ end
53
+
54
+ def delta_index_name(model)
55
+ "#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_delta"
56
+ end
57
+
58
+ private
59
+
60
+ def adapter
61
+ @adapter = @index.model.sphinx_database_adapter
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,24 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class DeltaJob
4
+ attr_accessor :index
5
+
6
+ def initialize(index)
7
+ @index = index
8
+ end
9
+
10
+ def perform
11
+ return true unless ThinkingSphinx.updates_enabled? &&
12
+ ThinkingSphinx.deltas_enabled?
13
+
14
+ config = ThinkingSphinx::Configuration.instance
15
+ client = Riddle::Client.new config.address, config.port
16
+
17
+ output = `#{config.bin_path}indexer --config #{config.config_file} --rotate #{index}`
18
+ puts output unless ThinkingSphinx.suppress_delta_output?
19
+
20
+ true
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class FlagAsDeletedJob
4
+ attr_accessor :index, :document_id
5
+
6
+ def initialize(index, document_id)
7
+ @index, @document_id = index, document_id
8
+ end
9
+
10
+ def perform
11
+ return true unless ThinkingSphinx.updates_enabled?
12
+
13
+ config = ThinkingSphinx::Configuration.instance
14
+ client = Riddle::Client.new config.address, config.port
15
+
16
+ client.update(
17
+ @index,
18
+ ['sphinx_deleted'],
19
+ {@document_id => [1]}
20
+ ) if ThinkingSphinx.sphinx_running? &&
21
+ ThinkingSphinx::Search.search_for_id(@document_id, @index)
22
+
23
+ true
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ module ThinkingSphinx
2
+ module Deltas
3
+ class Job < Delayed::Job
4
+ def self.enqueue(object, priority = 0)
5
+ super unless duplicates_exist(object)
6
+ end
7
+
8
+ def self.cancel_thinking_sphinx_jobs
9
+ if connection.tables.include?("delayed_jobs")
10
+ delete_all("handler LIKE '--- !ruby/object:ThinkingSphinx::Deltas::%'")
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def self.duplicates_exist(object)
17
+ count(
18
+ :conditions => {
19
+ :handler => object.to_yaml,
20
+ :locked_at => nil
21
+ }
22
+ ) > 0
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ require 'delayed/job'
2
+
3
+ require 'thinking_sphinx/deltas/delayed_delta/delta_job'
4
+ require 'thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job'
5
+ require 'thinking_sphinx/deltas/delayed_delta/job'
6
+
7
+ module ThinkingSphinx
8
+ module Deltas
9
+ class DelayedDelta < ThinkingSphinx::Deltas::DefaultDelta
10
+ def index(model, instance = nil)
11
+ ThinkingSphinx::Deltas::Job.enqueue(
12
+ ThinkingSphinx::Deltas::DeltaJob.new(delta_index_name(model))
13
+ )
14
+
15
+ Delayed::Job.enqueue(
16
+ ThinkingSphinx::Deltas::FlagAsDeletedJob.new(
17
+ core_index_name(model), instance.sphinx_document_id
18
+ )
19
+ ) if instance
20
+
21
+ true
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ require 'thinking_sphinx/deltas/default_delta'
2
+ require 'thinking_sphinx/deltas/delayed_delta'
3
+ require 'thinking_sphinx/deltas/datetime_delta'
4
+
5
+ module ThinkingSphinx
6
+ module Deltas
7
+ def self.parse(index, options)
8
+ case options.delete(:delta)
9
+ when TrueClass, :default
10
+ DefaultDelta.new index, options
11
+ when :delayed
12
+ DelayedDelta.new index, options
13
+ when :datetime
14
+ DatetimeDelta.new index, options
15
+ when FalseClass, nil
16
+ nil
17
+ else
18
+ raise "Unknown delta type"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,58 @@
1
+ module ThinkingSphinx
2
+ class Facet
3
+ attr_reader :reference
4
+
5
+ def initialize(reference)
6
+ @reference = reference
7
+
8
+ if reference.columns.length != 1
9
+ raise "Can't translate Facets on multiple-column field or attribute"
10
+ end
11
+ end
12
+
13
+ def name
14
+ reference.unique_name
15
+ end
16
+
17
+ def attribute_name
18
+ @attribute_name ||= case @reference
19
+ when Attribute
20
+ @reference.unique_name.to_s
21
+ when Field
22
+ @reference.unique_name.to_s + "_sort"
23
+ end
24
+ end
25
+
26
+ def value(object, attribute_value)
27
+ return translate(object, attribute_value) if @reference.is_a?(Field)
28
+
29
+ case @reference.type
30
+ when :string, :multi
31
+ translate(object, attribute_value)
32
+ when :datetime
33
+ Time.at(attribute_value)
34
+ when :boolean
35
+ attribute_value > 0
36
+ else
37
+ attribute_value
38
+ end
39
+ end
40
+
41
+ def to_s
42
+ name
43
+ end
44
+
45
+ protected
46
+
47
+ def translate(object, attribute_value)
48
+ column.__stack.each { |method|
49
+ object = object.send(method)
50
+ }
51
+ object.send(column.__name)
52
+ end
53
+
54
+ def column
55
+ @reference.columns.first
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,45 @@
1
+ module ThinkingSphinx
2
+ class FacetCollection < Hash
3
+ attr_accessor :arguments
4
+
5
+ def initialize(arguments)
6
+ @arguments = arguments.clone
7
+ @attribute_values = {}
8
+ @facets = []
9
+ end
10
+
11
+ def add_from_results(facet, results)
12
+ self[facet.name] ||= {}
13
+ @attribute_values[facet.name] = {}
14
+ @facets << facet
15
+
16
+ results.each_with_groupby_and_count { |result, group, count|
17
+ facet_value = facet.value(result, group)
18
+
19
+ self[facet.name][facet_value] ||= 0
20
+ self[facet.name][facet_value] += count
21
+ @attribute_values[facet.name][facet_value] ||= group
22
+ }
23
+ end
24
+
25
+ def for(hash = {})
26
+ arguments = @arguments.clone
27
+ options = arguments.extract_options!
28
+ options[:with] ||= {}
29
+
30
+ hash.each do |key, value|
31
+ attrib = facet_for_key(key).attribute_name
32
+ options[:with][attrib] = @attribute_values[key][value]
33
+ end
34
+
35
+ arguments << options
36
+ ThinkingSphinx::Search.search *arguments
37
+ end
38
+
39
+ private
40
+
41
+ def facet_for_key(key)
42
+ @facets.detect { |facet| facet.name == key }
43
+ end
44
+ end
45
+ end