summaryrefslogtreecommitdiff
path: root/vendor/plugins/thinking-sphinx/lib
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/plugins/thinking-sphinx/lib')
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx.rb180
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record.rb260
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/delta.rb78
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/has_many_association.rb29
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/search.rb57
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/abstract_adapter.rb42
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/mysql_adapter.rb54
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/postgresql_adapter.rb130
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/association.rb161
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/attribute.rb358
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/class_facet.rb15
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/collection.rb147
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/configuration.rb237
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/core/string.rb15
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas.rb27
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/datetime_delta.rb50
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/default_delta.rb67
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta.rb25
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb24
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb27
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/job.rb26
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet.rb58
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet_collection.rb60
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/field.rb172
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index.rb423
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/builder.rb264
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/faux_column.rb110
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/rails_additions.rb136
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/search.rb780
-rw-r--r--vendor/plugins/thinking-sphinx/lib/thinking_sphinx/tasks.rb128
30 files changed, 0 insertions, 4140 deletions
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx.rb
deleted file mode 100644
index 745643f..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx.rb
+++ /dev/null
@@ -1,180 +0,0 @@
1Dir[File.join(File.dirname(__FILE__), '../vendor/*/lib')].each do |path|
2 $LOAD_PATH.unshift path
3end
4
5require 'active_record'
6require 'riddle'
7require 'after_commit'
8
9require 'thinking_sphinx/core/string'
10require 'thinking_sphinx/active_record'
11require 'thinking_sphinx/association'
12require 'thinking_sphinx/attribute'
13require 'thinking_sphinx/collection'
14require 'thinking_sphinx/configuration'
15require 'thinking_sphinx/facet'
16require 'thinking_sphinx/class_facet'
17require 'thinking_sphinx/facet_collection'
18require 'thinking_sphinx/field'
19require 'thinking_sphinx/index'
20require 'thinking_sphinx/rails_additions'
21require 'thinking_sphinx/search'
22require 'thinking_sphinx/deltas'
23
24require 'thinking_sphinx/adapters/abstract_adapter'
25require 'thinking_sphinx/adapters/mysql_adapter'
26require 'thinking_sphinx/adapters/postgresql_adapter'
27
28ActiveRecord::Base.send(:include, ThinkingSphinx::ActiveRecord)
29
30Merb::Plugins.add_rakefiles(
31 File.join(File.dirname(__FILE__), "thinking_sphinx", "tasks")
32) if defined?(Merb)
33
34module ThinkingSphinx
35 module Version #:nodoc:
36 Major = 1
37 Minor = 1
38 Tiny = 6
39
40 String = [Major, Minor, Tiny].join('.')
41 end
42
43 # A ConnectionError will get thrown when a connection to Sphinx can't be
44 # made.
45 class ConnectionError < StandardError
46 end
47
48 # A StaleIdsException is thrown by Collection.instances_from_matches if there
49 # are records in Sphinx but not in the database, so the search can be retried.
50 class StaleIdsException < StandardError
51 attr_accessor :ids
52 def initialize(ids)
53 self.ids = ids
54 end
55 end
56
57 # The collection of indexed models. Keep in mind that Rails lazily loads
58 # its classes, so this may not actually be populated with _all_ the models
59 # that have Sphinx indexes.
60 def self.indexed_models
61 @@indexed_models ||= []
62 end
63
64 def self.unique_id_expression(offset = nil)
65 "* #{ThinkingSphinx.indexed_models.size} + #{offset || 0}"
66 end
67
68 # Check if index definition is disabled.
69 #
70 def self.define_indexes?
71 @@define_indexes = true unless defined?(@@define_indexes)
72 @@define_indexes == true
73 end
74
75 # Enable/disable indexes - you may want to do this while migrating data.
76 #
77 # ThinkingSphinx.define_indexes = false
78 #
79 def self.define_indexes=(value)
80 @@define_indexes = value
81 end
82
83 @@deltas_enabled = nil
84
85 # Check if delta indexing is enabled.
86 #
87 def self.deltas_enabled?
88 @@deltas_enabled = (ThinkingSphinx::Configuration.environment != 'test') if @@deltas_enabled.nil?
89 @@deltas_enabled
90 end
91
92 # Enable/disable all delta indexing.
93 #
94 # ThinkingSphinx.deltas_enabled = false
95 #
96 def self.deltas_enabled=(value)
97 @@deltas_enabled = value
98 end
99
100 @@updates_enabled = nil
101
102 # Check if updates are enabled. True by default, unless within the test
103 # environment.
104 #
105 def self.updates_enabled?
106 @@updates_enabled = (ThinkingSphinx::Configuration.environment != 'test') if @@updates_enabled.nil?
107 @@updates_enabled
108 end
109
110 # Enable/disable updates to Sphinx
111 #
112 # ThinkingSphinx.updates_enabled = false
113 #
114 def self.updates_enabled=(value)
115 @@updates_enabled = value
116 end
117
118 @@suppress_delta_output = false
119
120 def self.suppress_delta_output?
121 @@suppress_delta_output
122 end
123
124 def self.suppress_delta_output=(value)
125 @@suppress_delta_output = value
126 end
127
128 # Checks to see if MySQL will allow simplistic GROUP BY statements. If not,
129 # or if not using MySQL, this will return false.
130 #
131 def self.use_group_by_shortcut?
132 !!(
133 mysql? && ::ActiveRecord::Base.connection.select_all(
134 "SELECT @@global.sql_mode, @@session.sql_mode;"
135 ).all? { |key,value| value.nil? || value[/ONLY_FULL_GROUP_BY/].nil? }
136 )
137 end
138
139 def self.sphinx_running?
140 !!sphinx_pid && pid_active?(sphinx_pid)
141 end
142
143 def self.sphinx_pid
144 pid_file = ThinkingSphinx::Configuration.instance.pid_file
145 cat_command = 'cat'
146 return nil unless File.exists?(pid_file)
147
148 if microsoft?
149 pid_file.gsub!('/', '\\')
150 cat_command = 'type'
151 end
152
153 `#{cat_command} #{pid_file}`[/\d+/]
154 end
155
156 def self.pid_active?(pid)
157 return true if microsoft?
158
159 begin
160 # In JRuby this returns -1 if the process doesn't exist
161 Process.getpgid(pid.to_i) != -1
162 rescue Exception => e
163 false
164 end
165 end
166
167 def self.microsoft?
168 RUBY_PLATFORM =~ /mswin/
169 end
170
171 def self.jruby?
172 defined?(JRUBY_VERSION)
173 end
174
175 def self.mysql?
176 ::ActiveRecord::Base.connection.class.name.demodulize == "MysqlAdapter" || (
177 jruby? && ::ActiveRecord::Base.connection.config[:adapter] == "jdbcmysql"
178 )
179 end
180end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record.rb
deleted file mode 100644
index 3e85b50..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record.rb
+++ /dev/null
@@ -1,260 +0,0 @@
1require 'thinking_sphinx/active_record/delta'
2require 'thinking_sphinx/active_record/search'
3require 'thinking_sphinx/active_record/has_many_association'
4
5module ThinkingSphinx
6 # Core additions to ActiveRecord models - define_index for creating indexes
7 # for models. If you want to interrogate the index objects created for the
8 # model, you can use the class-level accessor :sphinx_indexes.
9 #
10 module ActiveRecord
11 def self.included(base)
12 base.class_eval do
13 class_inheritable_array :sphinx_indexes, :sphinx_facets
14 class << self
15 # Allows creation of indexes for Sphinx. If you don't do this, there
16 # isn't much point trying to search (or using this plugin at all,
17 # really).
18 #
19 # An example or two:
20 #
21 # define_index
22 # indexes :id, :as => :model_id
23 # indexes name
24 # end
25 #
26 # You can also grab fields from associations - multiple levels deep
27 # if necessary.
28 #
29 # define_index do
30 # indexes tags.name, :as => :tag
31 # indexes articles.content
32 # indexes orders.line_items.product.name, :as => :product
33 # end
34 #
35 # And it will automatically concatenate multiple fields:
36 #
37 # define_index do
38 # indexes [author.first_name, author.last_name], :as => :author
39 # end
40 #
41 # The #indexes method is for fields - if you want attributes, use
42 # #has instead. All the same rules apply - but keep in mind that
43 # attributes are for sorting, grouping and filtering, not searching.
44 #
45 # define_index do
46 # # fields ...
47 #
48 # has created_at, updated_at
49 # end
50 #
51 # One last feature is the delta index. This requires the model to
52 # have a boolean field named 'delta', and is enabled as follows:
53 #
54 # define_index do
55 # # fields ...
56 # # attributes ...
57 #
58 # set_property :delta => true
59 # end
60 #
61 # Check out the more detailed documentation for each of these methods
62 # at ThinkingSphinx::Index::Builder.
63 #
64 def define_index(&block)
65 return unless ThinkingSphinx.define_indexes?
66
67 self.sphinx_indexes ||= []
68 index = Index.new(self, &block)
69
70 self.sphinx_indexes << index
71 unless ThinkingSphinx.indexed_models.include?(self.name)
72 ThinkingSphinx.indexed_models << self.name
73 end
74
75 if index.delta?
76 before_save :toggle_delta
77 after_commit :index_delta
78 end
79
80 after_destroy :toggle_deleted
81
82 index
83 end
84 alias_method :sphinx_index, :define_index
85
86 def sphinx_index_options
87 sphinx_indexes.last.options
88 end
89
90 # Generate a unique CRC value for the model's name, to use to
91 # determine which Sphinx documents belong to which AR records.
92 #
93 # Really only written for internal use - but hey, if it's useful to
94 # you in some other way, awesome.
95 #
96 def to_crc32
97 self.name.to_crc32
98 end
99
100 def to_crc32s
101 (subclasses << self).collect { |klass| klass.to_crc32 }
102 end
103
104 def source_of_sphinx_index
105 possible_models = self.sphinx_indexes.collect { |index| index.model }
106 return self if possible_models.include?(self)
107
108 parent = self.superclass
109 while !possible_models.include?(parent) && parent != ::ActiveRecord::Base
110 parent = parent.superclass
111 end
112
113 return parent
114 end
115
116 def to_riddle(offset)
117 sphinx_database_adapter.setup
118
119 indexes = [to_riddle_for_core(offset)]
120 indexes << to_riddle_for_delta(offset) if sphinx_delta?
121 indexes << to_riddle_for_distributed
122 end
123
124 def sphinx_database_adapter
125 @sphinx_database_adapter ||=
126 ThinkingSphinx::AbstractAdapter.detect(self)
127 end
128
129 private
130
131 def sphinx_name
132 self.name.underscore.tr(':/\\', '_')
133 end
134
135 def sphinx_delta?
136 self.sphinx_indexes.any? { |index| index.delta? }
137 end
138
139 def to_riddle_for_core(offset)
140 index = Riddle::Configuration::Index.new("#{sphinx_name}_core")
141 index.path = File.join(
142 ThinkingSphinx::Configuration.instance.searchd_file_path, index.name
143 )
144
145 set_configuration_options_for_indexes index
146 set_field_settings_for_indexes index
147
148 self.sphinx_indexes.select { |ts_index|
149 ts_index.model == self
150 }.each_with_index do |ts_index, i|
151 index.sources << ts_index.to_riddle_for_core(offset, i)
152 end
153
154 index
155 end
156
157 def to_riddle_for_delta(offset)
158 index = Riddle::Configuration::Index.new("#{sphinx_name}_delta")
159 index.parent = "#{sphinx_name}_core"
160 index.path = File.join(ThinkingSphinx::Configuration.instance.searchd_file_path, index.name)
161
162 self.sphinx_indexes.each_with_index do |ts_index, i|
163 index.sources << ts_index.to_riddle_for_delta(offset, i) if ts_index.delta?
164 end
165
166 index
167 end
168
169 def to_riddle_for_distributed
170 index = Riddle::Configuration::DistributedIndex.new(sphinx_name)
171 index.local_indexes << "#{sphinx_name}_core"
172 index.local_indexes.unshift "#{sphinx_name}_delta" if sphinx_delta?
173 index
174 end
175
176 def set_configuration_options_for_indexes(index)
177 ThinkingSphinx::Configuration.instance.index_options.each do |key, value|
178 index.send("#{key}=".to_sym, value)
179 end
180
181 self.sphinx_indexes.each do |ts_index|
182 ts_index.options.each do |key, value|
183 index.send("#{key}=".to_sym, value) if ThinkingSphinx::Configuration::IndexOptions.include?(key.to_s) && !value.nil?
184 end
185 end
186 end
187
188 def set_field_settings_for_indexes(index)
189 field_names = lambda { |field| field.unique_name.to_s }
190
191 self.sphinx_indexes.each do |ts_index|
192 index.prefix_field_names += ts_index.prefix_fields.collect(&field_names)
193 index.infix_field_names += ts_index.infix_fields.collect(&field_names)
194 end
195 end
196 end
197 end
198
199 base.send(:include, ThinkingSphinx::ActiveRecord::Delta)
200 base.send(:include, ThinkingSphinx::ActiveRecord::Search)
201
202 ::ActiveRecord::Associations::HasManyAssociation.send(
203 :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
204 )
205 ::ActiveRecord::Associations::HasManyThroughAssociation.send(
206 :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
207 )
208 end
209
210 def in_index?(suffix)
211 self.class.search_for_id self.sphinx_document_id, sphinx_index_name(suffix)
212 end
213
214 def in_core_index?
215 in_index? "core"
216 end
217
218 def in_delta_index?
219 in_index? "delta"
220 end
221
222 def in_both_indexes?
223 in_core_index? && in_delta_index?
224 end
225
226 def toggle_deleted
227 return unless ThinkingSphinx.updates_enabled? && ThinkingSphinx.sphinx_running?
228
229 config = ThinkingSphinx::Configuration.instance
230 client = Riddle::Client.new config.address, config.port
231
232 client.update(
233 "#{self.class.sphinx_indexes.first.name}_core",
234 ['sphinx_deleted'],
235 {self.sphinx_document_id => 1}
236 ) if self.in_core_index?
237
238 client.update(
239 "#{self.class.sphinx_indexes.first.name}_delta",
240 ['sphinx_deleted'],
241 {self.sphinx_document_id => 1}
242 ) if ThinkingSphinx.deltas_enabled? &&
243 self.class.sphinx_indexes.any? { |index| index.delta? } &&
244 self.toggled_delta?
245 rescue ::ThinkingSphinx::ConnectionError
246 # nothing
247 end
248
249 def sphinx_document_id
250 (self.id * ThinkingSphinx.indexed_models.size) +
251 ThinkingSphinx.indexed_models.index(self.class.source_of_sphinx_index.name)
252 end
253
254 private
255
256 def sphinx_index_name(suffix)
257 "#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_#{suffix}"
258 end
259 end
260end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/delta.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/delta.rb
deleted file mode 100644
index f21aba2..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/delta.rb
+++ /dev/null
@@ -1,78 +0,0 @@
1module ThinkingSphinx
2 module ActiveRecord
3 # This module contains all the delta-related code for models. There isn't
4 # really anything you need to call manually in here - except perhaps
5 # index_delta, but not sure what reason why.
6 #
7 module Delta
8 # Code for after_commit callback is written by Eli Miller:
9 # http://elimiller.blogspot.com/2007/06/proper-cache-expiry-with-aftercommit.html
10 # with slight modification from Joost Hietbrink.
11 #
12 def self.included(base)
13 base.class_eval do
14 class << self
15 # Temporarily disable delta indexing inside a block, then perform a single
16 # rebuild of index at the end.
17 #
18 # Useful when performing updates to batches of models to prevent
19 # the delta index being rebuilt after each individual update.
20 #
21 # In the following example, the delta index will only be rebuilt once,
22 # not 10 times.
23 #
24 # SomeModel.suspended_delta do
25 # 10.times do
26 # SomeModel.create( ... )
27 # end
28 # end
29 #
30 def suspended_delta(reindex_after = true, &block)
31 original_setting = ThinkingSphinx.deltas_enabled?
32 ThinkingSphinx.deltas_enabled = false
33 begin
34 yield
35 ensure
36 ThinkingSphinx.deltas_enabled = original_setting
37 self.index_delta if reindex_after
38 end
39 end
40
41 # Build the delta index for the related model. This won't be called
42 # if running in the test environment.
43 #
44 def index_delta(instance = nil)
45 delta_object.index(self, instance)
46 end
47
48 def delta_object
49 self.sphinx_indexes.first.delta_object
50 end
51 end
52
53 def toggled_delta?
54 self.class.delta_object.toggled(self)
55 end
56
57 private
58
59 # Set the delta value for the model to be true.
60 def toggle_delta
61 self.class.delta_object.toggle(self) if should_toggle_delta?
62 end
63
64 # Build the delta index for the related model. This won't be called
65 # if running in the test environment.
66 #
67 def index_delta
68 self.class.index_delta(self) if self.class.delta_object.toggled(self)
69 end
70
71 def should_toggle_delta?
72 !self.respond_to?(:changed?) || self.changed? || self.new_record?
73 end
74 end
75 end
76 end
77 end
78end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/has_many_association.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/has_many_association.rb
deleted file mode 100644
index 44b25c0..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/has_many_association.rb
+++ /dev/null
@@ -1,29 +0,0 @@
1module ThinkingSphinx
2 module ActiveRecord
3 module HasManyAssociation
4 def search(*args)
5 foreign_key = @reflection.primary_key_name
6 stack = [@reflection.options[:through]].compact
7
8 attribute = nil
9 (@reflection.klass.sphinx_indexes || []).each do |index|
10 attribute = index.attributes.detect { |attrib|
11 attrib.columns.length == 1 &&
12 attrib.columns.first.__name == foreign_key.to_sym &&
13 attrib.columns.first.__stack == stack
14 }
15 break if attribute
16 end
17
18 raise "Missing Attribute for Foreign Key #{foreign_key}" unless attribute
19
20 options = args.extract_options!
21 options[:with] ||= {}
22 options[:with][attribute.unique_name] = @owner.id
23
24 args << options
25 @reflection.klass.search(*args)
26 end
27 end
28 end
29end \ No newline at end of file
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/search.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/search.rb
deleted file mode 100644
index fc3f2b4..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/active_record/search.rb
+++ /dev/null
@@ -1,57 +0,0 @@
1module ThinkingSphinx
2 module ActiveRecord
3 # This module covers the specific model searches - but the syntax is
4 # exactly the same as the core Search class - so use that as your refence
5 # point.
6 #
7 module Search
8 def self.included(base)
9 base.class_eval do
10 class << self
11 # Searches for results that match the parameters provided. Will only
12 # return the ids for the matching objects. See
13 # ThinkingSphinx::Search#search for syntax examples.
14 #
15 def search_for_ids(*args)
16 options = args.extract_options!
17 options[:class] = self
18 args << options
19 ThinkingSphinx::Search.search_for_ids(*args)
20 end
21
22 # Searches for results limited to a single model. See
23 # ThinkingSphinx::Search#search for syntax examples.
24 #
25 def search(*args)
26 options = args.extract_options!
27 options[:class] = self
28 args << options
29 ThinkingSphinx::Search.search(*args)
30 end
31
32 def search_count(*args)
33 options = args.extract_options!
34 options[:class] = self
35 args << options
36 ThinkingSphinx::Search.count(*args)
37 end
38
39 def search_for_id(*args)
40 options = args.extract_options!
41 options[:class] = self
42 args << options
43 ThinkingSphinx::Search.search_for_id(*args)
44 end
45
46 def facets(*args)
47 options = args.extract_options!
48 options[:class] = self
49 args << options
50 ThinkingSphinx::Search.facets(*args)
51 end
52 end
53 end
54 end
55 end
56 end
57end \ No newline at end of file
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/abstract_adapter.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/abstract_adapter.rb
deleted file mode 100644
index b68b75e..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/abstract_adapter.rb
+++ /dev/null
@@ -1,42 +0,0 @@
1module ThinkingSphinx
2 class AbstractAdapter
3 def initialize(model)
4 @model = model
5 end
6
7 def setup
8 # Deliberately blank - subclasses should do something though. Well, if
9 # they need to.
10 end
11
12 def self.detect(model)
13 case model.connection.class.name
14 when "ActiveRecord::ConnectionAdapters::MysqlAdapter",
15 "ActiveRecord::ConnectionAdapters::MysqlplusAdapter"
16 ThinkingSphinx::MysqlAdapter.new model
17 when "ActiveRecord::ConnectionAdapters::PostgreSQLAdapter"
18 ThinkingSphinx::PostgreSQLAdapter.new model
19 when "ActiveRecord::ConnectionAdapters::JdbcAdapter"
20 if model.connection.config[:adapter] == "jdbcmysql"
21 ThinkingSphinx::MysqlAdapter.new model
22 elsif model.connection.config[:adapter] == "jdbcpostgresql"
23 ThinkingSphinx::PostgreSQLAdapter.new model
24 else
25 raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL"
26 end
27 else
28 raise "Invalid Database Adapter: Sphinx only supports MySQL and PostgreSQL, not #{model.connection.class.name}"
29 end
30 end
31
32 def quote_with_table(column)
33 "#{@model.quoted_table_name}.#{@model.connection.quote_column_name(column)}"
34 end
35
36 protected
37
38 def connection
39 @connection ||= @model.connection
40 end
41 end
42end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/mysql_adapter.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/mysql_adapter.rb
deleted file mode 100644
index 597d4b6..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/mysql_adapter.rb
+++ /dev/null
@@ -1,54 +0,0 @@
1module ThinkingSphinx
2 class MysqlAdapter < AbstractAdapter
3 def setup
4 # Does MySQL actually need to do anything?
5 end
6
7 def sphinx_identifier
8 "mysql"
9 end
10
11 def concatenate(clause, separator = ' ')
12 "CONCAT_WS('#{separator}', #{clause})"
13 end
14
15 def group_concatenate(clause, separator = ' ')
16 "GROUP_CONCAT(DISTINCT #{clause} SEPARATOR '#{separator}')"
17 end
18
19 def cast_to_string(clause)
20 "CAST(#{clause} AS CHAR)"
21 end
22
23 def cast_to_datetime(clause)
24 "UNIX_TIMESTAMP(#{clause})"
25 end
26
27 def cast_to_unsigned(clause)
28 "CAST(#{clause} AS UNSIGNED)"
29 end
30
31 def convert_nulls(clause, default = '')
32 default = "'#{default}'" if default.is_a?(String)
33
34 "IFNULL(#{clause}, #{default})"
35 end
36
37 def boolean(value)
38 value ? 1 : 0
39 end
40
41 def crc(clause, blank_to_null = false)
42 clause = "NULLIF(#{clause},'')" if blank_to_null
43 "CRC32(#{clause})"
44 end
45
46 def utf8_query_pre
47 "SET NAMES utf8"
48 end
49
50 def time_difference(diff)
51 "DATE_SUB(NOW(), INTERVAL #{diff} SECOND)"
52 end
53 end
54end \ No newline at end of file
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/postgresql_adapter.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/postgresql_adapter.rb
deleted file mode 100644
index 625971d..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/adapters/postgresql_adapter.rb
+++ /dev/null
@@ -1,130 +0,0 @@
1module ThinkingSphinx
2 class PostgreSQLAdapter < AbstractAdapter
3 def setup
4 create_array_accum_function
5 create_crc32_function
6 end
7
8 def sphinx_identifier
9 "pgsql"
10 end
11
12 def concatenate(clause, separator = ' ')
13 clause.split(', ').collect { |field|
14 "COALESCE(CAST(#{field} as varchar), '')"
15 }.join(" || '#{separator}' || ")
16 end
17
18 def group_concatenate(clause, separator = ' ')
19 "array_to_string(array_accum(#{clause}), '#{separator}')"
20 end
21
22 def cast_to_string(clause)
23 clause
24 end
25
26 def cast_to_datetime(clause)
27 "cast(extract(epoch from #{clause}) as int)"
28 end
29
30 def cast_to_unsigned(clause)
31 clause
32 end
33
34 def convert_nulls(clause, default = '')
35 default = "'#{default}'" if default.is_a?(String)
36
37 "COALESCE(#{clause}, #{default})"
38 end
39
40 def boolean(value)
41 value ? 'TRUE' : 'FALSE'
42 end
43
44 def crc(clause, blank_to_null = false)
45 clause = "NULLIF(#{clause},'')" if blank_to_null
46 "crc32(#{clause})"
47 end
48
49 def utf8_query_pre
50 nil
51 end
52
53 def time_difference(diff)
54 "current_timestamp - interval '#{diff} seconds'"
55 end
56
57 private
58
59 def execute(command, output_error = false)
60 connection.execute "begin"
61 connection.execute "savepoint ts"
62 begin
63 connection.execute command
64 rescue StandardError => err
65 puts err if output_error
66 connection.execute "rollback to savepoint ts"
67 end
68 connection.execute "release savepoint ts"
69 connection.execute "commit"
70 end
71
72 def create_array_accum_function
73 if connection.raw_connection.respond_to?(:server_version) && connection.raw_connection.server_version > 80200
74 execute <<-SQL
75 CREATE AGGREGATE array_accum (anyelement)
76 (
77 sfunc = array_append,
78 stype = anyarray,
79 initcond = '{}'
80 );
81 SQL
82 else
83 execute <<-SQL
84 CREATE AGGREGATE array_accum
85 (
86 basetype = anyelement,
87 sfunc = array_append,
88 stype = anyarray,
89 initcond = '{}'
90 );
91 SQL
92 end
93 end
94
95 def create_crc32_function
96 execute "CREATE LANGUAGE 'plpgsql';"
97 function = <<-SQL
98 CREATE OR REPLACE FUNCTION crc32(word text)
99 RETURNS bigint AS $$
100 DECLARE tmp bigint;
101 DECLARE i int;
102 DECLARE j int;
103 DECLARE word_array bytea;
104 BEGIN
105 i = 0;
106 tmp = 4294967295;
107 word_array = decode(replace(word, E'\\\\', E'\\\\\\\\'), 'escape');
108 LOOP
109 tmp = (tmp # get_byte(word_array, i))::bigint;
110 i = i + 1;
111 j = 0;
112 LOOP
113 tmp = ((tmp >> 1) # (3988292384 * (tmp & 1)))::bigint;
114 j = j + 1;
115 IF j >= 8 THEN
116 EXIT;
117 END IF;
118 END LOOP;
119 IF i >= char_length(word) THEN
120 EXIT;
121 END IF;
122 END LOOP;
123 return (tmp # 4294967295);
124 END
125 $$ IMMUTABLE STRICT LANGUAGE plpgsql;
126 SQL
127 execute function, true
128 end
129 end
130end \ No newline at end of file
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/association.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/association.rb
deleted file mode 100644
index b057035..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/association.rb
+++ /dev/null
@@ -1,161 +0,0 @@
1module ThinkingSphinx
2 # Association tracks a specific reflection and join to reference data that
3 # isn't in the base model. Very much an internal class for Thinking Sphinx -
4 # perhaps because I feel it's not as strong (or simple) as most of the rest.
5 #
6 class Association
7 attr_accessor :parent, :reflection, :join
8
9 # Create a new association by passing in the parent association, and the
10 # corresponding reflection instance. If there is no parent, pass in nil.
11 #
12 # top = Association.new nil, top_reflection
13 # child = Association.new top, child_reflection
14 #
15 def initialize(parent, reflection)
16 @parent, @reflection = parent, reflection
17 @children = {}
18 end
19
20 # Get the children associations for a given association name. The only time
21 # that there'll actually be more than one association is when the
22 # relationship is polymorphic. To keep things simple though, it will always
23 # be an Array that gets returned (an empty one if no matches).
24 #
25 # # where pages is an association on the class tied to the reflection.
26 # association.children(:pages)
27 #
28 def children(assoc)
29 @children[assoc] ||= Association.children(@reflection.klass, assoc, self)
30 end
31
32 # Get the children associations for a given class, association name and
33 # parent association. Much like the instance method of the same name, it
34 # will return an empty array if no associations have the name, and only
35 # have multiple association instances if the underlying relationship is
36 # polymorphic.
37 #
38 # Association.children(User, :pages, user_association)
39 #
40 def self.children(klass, assoc, parent=nil)
41 ref = klass.reflect_on_association(assoc)
42
43 return [] if ref.nil?
44 return [Association.new(parent, ref)] unless ref.options[:polymorphic]
45
46 # association is polymorphic - create associations for each
47 # non-polymorphic reflection.
48 polymorphic_classes(ref).collect { |klass|
49 Association.new parent, ::ActiveRecord::Reflection::AssociationReflection.new(
50 ref.macro,
51 "#{ref.name}_#{klass.name}".to_sym,
52 casted_options(klass, ref),
53 ref.active_record
54 )
55 }
56 end
57
58 # Link up the join for this model from a base join - and set parent
59 # associations' joins recursively.
60 #
61 def join_to(base_join)
62 parent.join_to(base_join) if parent && parent.join.nil?
63
64 @join ||= ::ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.new(
65 @reflection, base_join, parent ? parent.join : base_join.joins.first
66 )
67 end
68
69 # Returns the association's join SQL statements - and it replaces
70 # ::ts_join_alias:: with the aliased table name so the generated reflection
71 # join conditions avoid column name collisions.
72 #
73 def to_sql
74 @join.association_join.gsub(/::ts_join_alias::/,
75 "#{@reflection.klass.connection.quote_table_name(@join.parent.aliased_table_name)}"
76 )
77 end
78
79 # Returns true if the association - or a parent - is a has_many or
80 # has_and_belongs_to_many.
81 #
82 def is_many?
83 case @reflection.macro
84 when :has_many, :has_and_belongs_to_many
85 true
86 else
87 @parent ? @parent.is_many? : false
88 end
89 end
90
91 # Returns an array of all the associations that lead to this one - starting
92 # with the top level all the way to the current association object.
93 #
94 def ancestors
95 (parent ? parent.ancestors : []) << self
96 end
97
98 def has_column?(column)
99 @reflection.klass.column_names.include?(column.to_s)
100 end
101
102 def primary_key_from_reflection
103 if @reflection.options[:through]
104 @reflection.source_reflection.options[:foreign_key] ||
105 @reflection.source_reflection.primary_key_name
106 else
107 nil
108 end
109 end
110
111 def table
112 if @reflection.options[:through]
113 @join.aliased_join_table_name
114 else
115 @join.aliased_table_name
116 end
117 end
118
119 private
120
121 # Returns all the objects that could be currently instantiated from a
122 # polymorphic association. This is pretty damn fast if there's an index on
123 # the foreign type column - but if there isn't, it can take a while if you
124 # have a lot of data.
125 #
126 def self.polymorphic_classes(ref)
127 ref.active_record.connection.select_all(
128 "SELECT DISTINCT #{ref.options[:foreign_type]} " +
129 "FROM #{ref.active_record.table_name} " +
130 "WHERE #{ref.options[:foreign_type]} IS NOT NULL"
131 ).collect { |row|
132 row[ref.options[:foreign_type]].constantize
133 }
134 end
135
136 # Returns a new set of options for an association that mimics an existing
137 # polymorphic relationship for a specific class. It adds a condition to
138 # filter by the appropriate object.
139 #
140 def self.casted_options(klass, ref)
141 options = ref.options.clone
142 options[:polymorphic] = nil
143 options[:class_name] = klass.name
144 options[:foreign_key] ||= "#{ref.name}_id"
145
146 quoted_foreign_type = klass.connection.quote_column_name ref.options[:foreign_type]
147 case options[:conditions]
148 when nil
149 options[:conditions] = "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
150 when Array
151 options[:conditions] << "::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
152 when Hash
153 options[:conditions].merge!(ref.options[:foreign_type] => klass.name)
154 else
155 options[:conditions] << " AND ::ts_join_alias::.#{quoted_foreign_type} = '#{klass.name}'"
156 end
157
158 options
159 end
160 end
161end \ No newline at end of file
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/attribute.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/attribute.rb
deleted file mode 100644
index 1d45b2e..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/attribute.rb
+++ /dev/null
@@ -1,358 +0,0 @@
1module ThinkingSphinx
2 # Attributes - eternally useful when it comes to filtering, sorting or
3 # grouping. This class isn't really useful to you unless you're hacking
4 # around with the internals of Thinking Sphinx - but hey, don't let that
5 # stop you.
6 #
7 # One key thing to remember - if you're using the attribute manually to
8 # generate SQL statements, you'll need to set the base model, and all the
9 # associations. Which can get messy. Use Index.link!, it really helps.
10 #
11 class Attribute
12 attr_accessor :alias, :columns, :associations, :model, :faceted, :source
13
14 # To create a new attribute, you'll need to pass in either a single Column
15 # or an array of them, and some (optional) options.
16 #
17 # Valid options are:
18 # - :as => :alias_name
19 # - :type => :attribute_type
20 # - :source => :field, :query, :ranged_query
21 #
22 # Alias is only required in three circumstances: when there's
23 # another attribute or field with the same name, when the column name is
24 # 'id', or when there's more than one column.
25 #
26 # Type is not required, unless you want to force a column to be a certain
27 # type (but keep in mind the value will not be CASTed in the SQL
28 # statements). The only time you really need to use this is when the type
29 # can't be figured out by the column - ie: when not actually using a
30 # database column as your source.
31 #
32 # Source is only used for multi-value attributes (MVA). By default this will
33 # use a left-join and a group_concat to obtain the values. For better performance
34 # during indexing it can be beneficial to let Sphinx use a separate query to retrieve
35 # all document,value-pairs.
36 # Either :query or :ranged_query will enable this feature, where :ranged_query will cause
37 # the query to be executed incremental.
38 #
39 # Example usage:
40 #
41 # Attribute.new(
42 # Column.new(:created_at)
43 # )
44 #
45 # Attribute.new(
46 # Column.new(:posts, :id),
47 # :as => :post_ids
48 # )
49 #
50 # Attribute.new(
51 # Column.new(:posts, :id),
52 # :as => :post_ids,
53 # :source => :ranged_query
54 # )
55 #
56 # Attribute.new(
57 # [Column.new(:pages, :id), Column.new(:articles, :id)],
58 # :as => :content_ids
59 # )
60 #
61 # Attribute.new(
62 # Column.new("NOW()"),
63 # :as => :indexed_at,
64 # :type => :datetime
65 # )
66 #
67 # If you're creating attributes for latitude and longitude, don't forget
68 # that Sphinx expects these values to be in radians.
69 #
70 def initialize(columns, options = {})
71 @columns = Array(columns)
72 @associations = {}
73
74 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) }
75
76 @alias = options[:as]
77 @type = options[:type]
78 @faceted = options[:facet]
79 @source = options[:source]
80 @crc = options[:crc]
81
82 @type ||= :multi unless @source.nil?
83 @type = :integer if @type == :string && @crc
84 end
85
86 # Get the part of the SELECT clause related to this attribute. Don't forget
87 # to set your model and associations first though.
88 #
89 # This will concatenate strings and arrays of integers, and convert
90 # datetimes to timestamps, as needed.
91 #
92 def to_select_sql
93 return nil unless include_as_association?
94
95 clause = @columns.collect { |column|
96 column_with_prefix(column)
97 }.join(', ')
98
99 separator = all_ints? ? ',' : ' '
100
101 clause = adapter.concatenate(clause, separator) if concat_ws?
102 clause = adapter.group_concatenate(clause, separator) if is_many?
103 clause = adapter.cast_to_datetime(clause) if type == :datetime
104 clause = adapter.convert_nulls(clause) if type == :string
105 clause = adapter.crc(clause) if @crc
106
107 "#{clause} AS #{quote_column(unique_name)}"
108 end
109
110 # Get the part of the GROUP BY clause related to this attribute - if one is
111 # needed. If not, all you'll get back is nil. The latter will happen if
112 # there isn't actually a real column to get data from, or if there's
113 # multiple data values (read: a has_many or has_and_belongs_to_many
114 # association).
115 #
116 def to_group_sql
117 case
118 when is_many?, is_string?, ThinkingSphinx.use_group_by_shortcut?
119 nil
120 else
121 @columns.collect { |column|
122 column_with_prefix(column)
123 }
124 end
125 end
126
127 def type_to_config
128 {
129 :multi => :sql_attr_multi,
130 :datetime => :sql_attr_timestamp,
131 :string => :sql_attr_str2ordinal,
132 :float => :sql_attr_float,
133 :boolean => :sql_attr_bool,
134 :integer => :sql_attr_uint
135 }[type]
136 end
137
138 def include_as_association?
139 ! (type == :multi && (source == :query || source == :ranged_query))
140 end
141
142 # Returns the configuration value that should be used for
143 # the attribute.
144 # Special case is the multi-valued attribute that needs some
145 # extra configuration.
146 #
147 def config_value(offset = nil)
148 if type == :multi
149 multi_config = include_as_association? ? "field" :
150 source_value(offset).gsub(/\n\s*/, " ")
151 "uint #{unique_name} from #{multi_config}"
152 else
153 unique_name
154 end
155 end
156
157 # Returns the unique name of the attribute - which is either the alias of
158 # the attribute, or the name of the only column - if there is only one. If
159 # there isn't, there should be an alias. Else things probably won't work.
160 # Consider yourself warned.
161 #
162 def unique_name
163 if @columns.length == 1
164 @alias || @columns.first.__name
165 else
166 @alias
167 end
168 end
169
170 # Returns the type of the column. If that's not already set, it returns
171 # :multi if there's the possibility of more than one value, :string if
172 # there's more than one association, otherwise it figures out what the
173 # actual column's datatype is and returns that.
174 #
175 def type
176 @type ||= begin
177 base_type = case
178 when is_many?, is_many_ints?
179 :multi
180 when @associations.values.flatten.length > 1
181 :string
182 else
183 translated_type_from_database
184 end
185
186 if base_type == :string && @crc
187 :integer
188 else
189 @crc = false
190 base_type
191 end
192 end
193 end
194
195 def to_facet
196 return nil unless @faceted
197
198 ThinkingSphinx::Facet.new(self)
199 end
200
201 private
202
203 def source_value(offset)
204 if is_string?
205 "#{source.to_s.dasherize}; #{columns.first.__name}"
206 elsif source == :ranged_query
207 "ranged-query; #{query offset} #{query_clause}; #{range_query}"
208 else
209 "query; #{query offset}"
210 end
211 end
212
213 def query(offset)
214 assoc = association_for_mva
215 raise "Could not determine SQL for MVA" if assoc.nil?
216
217 <<-SQL
218SELECT #{foreign_key_for_mva assoc}
219 #{ThinkingSphinx.unique_id_expression(offset)} AS #{quote_column('id')},
220 #{primary_key_for_mva(assoc)} AS #{quote_column(unique_name)}
221FROM #{quote_table_name assoc.table}
222 SQL
223 end
224
225 def query_clause
226 foreign_key = foreign_key_for_mva association_for_mva
227 "WHERE #{foreign_key} >= $start AND #{foreign_key} <= $end"
228 end
229
230 def range_query
231 assoc = association_for_mva
232 foreign_key = foreign_key_for_mva assoc
233 "SELECT MIN(#{foreign_key}), MAX(#{foreign_key}) FROM #{quote_table_name assoc.table}"
234 end
235
236 def primary_key_for_mva(assoc)
237 quote_with_table(
238 assoc.table, assoc.primary_key_from_reflection || columns.first.__name
239 )
240 end
241
242 def foreign_key_for_mva(assoc)
243 quote_with_table assoc.table, assoc.reflection.primary_key_name
244 end
245
246 def association_for_mva
247 @association_for_mva ||= associations[columns.first].detect { |assoc|
248 assoc.has_column?(columns.first.__name)
249 }
250 end
251
252 def adapter
253 @adapter ||= @model.sphinx_database_adapter
254 end
255
256 def quote_with_table(table, column)
257 "#{quote_table_name(table)}.#{quote_column(column)}"
258 end
259
260 def quote_column(column)
261 @model.connection.quote_column_name(column)
262 end
263
264 def quote_table_name(table_name)
265 @model.connection.quote_table_name(table_name)
266 end
267
268 # Indication of whether the columns should be concatenated with a space
269 # between each value. True if there's either multiple sources or multiple
270 # associations.
271 #
272 def concat_ws?
273 multiple_associations? || @columns.length > 1
274 end
275
276 # Checks whether any column requires multiple associations (which only
277 # happens for polymorphic situations).
278 #
279 def multiple_associations?
280 associations.any? { |col,assocs| assocs.length > 1 }
281 end
282
283 # Builds a column reference tied to the appropriate associations. This
284 # dives into the associations hash and their corresponding joins to
285 # figure out how to correctly reference a column in SQL.
286 #
287 def column_with_prefix(column)
288 if column.is_string?
289 column.__name
290 elsif associations[column].empty?
291 "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
292 else
293 associations[column].collect { |assoc|
294 assoc.has_column?(column.__name) ?
295 "#{quote_table_name(assoc.join.aliased_table_name)}" +
296 ".#{quote_column(column.__name)}" :
297 nil
298 }.compact.join(', ')
299 end
300 end
301
302 # Could there be more than one value related to the parent record? If so,
303 # then this will return true. If not, false. It's that simple.
304 #
305 def is_many?
306 associations.values.flatten.any? { |assoc| assoc.is_many? }
307 end
308
309 def is_many_ints?
310 concat_ws? && all_ints?
311 end
312
313 # Returns true if any of the columns are string values, instead of database
314 # column references.
315 def is_string?
316 columns.all? { |col| col.is_string? }
317 end
318
319 def all_ints?
320 @columns.all? { |col|
321 klasses = @associations[col].empty? ? [@model] :
322 @associations[col].collect { |assoc| assoc.reflection.klass }
323 klasses.all? { |klass|
324 column = klass.columns.detect { |column| column.name == col.__name.to_s }
325 !column.nil? && column.type == :integer
326 }
327 }
328 end
329
330 def type_from_database
331 klass = @associations.values.flatten.first ?
332 @associations.values.flatten.first.reflection.klass : @model
333
334 klass.columns.detect { |col|
335 @columns.collect { |c| c.__name.to_s }.include? col.name
336 }.type
337 end
338
339 def translated_type_from_database
340 case type_from_db = type_from_database
341 when :datetime, :string, :float, :boolean, :integer
342 type_from_db
343 when :decimal
344 :float
345 when :timestamp, :date
346 :datetime
347 else
348 raise <<-MESSAGE
349
350Cannot automatically map column type #{type_from_db} to an equivalent Sphinx
351type (integer, float, boolean, datetime, string as ordinal). You could try to
352explicitly convert the column's value in your define_index block:
353 has "CAST(column AS INT)", :type => :integer, :as => :column
354 MESSAGE
355 end
356 end
357 end
358end \ No newline at end of file
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/class_facet.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/class_facet.rb
deleted file mode 100644
index cb301b8..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/class_facet.rb
+++ /dev/null
@@ -1,15 +0,0 @@
1module ThinkingSphinx
2 class ClassFacet < ThinkingSphinx::Facet
3 def name
4 :class
5 end
6
7 def attribute_name
8 "class_crc"
9 end
10
11 def value(object, attribute_value)
12 object.class.name
13 end
14 end
15end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/collection.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/collection.rb
deleted file mode 100644
index 406c2ae..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/collection.rb
+++ /dev/null
@@ -1,147 +0,0 @@
1module ThinkingSphinx
2 class Collection < ::Array
3 attr_reader :total_entries, :total_pages, :current_page, :per_page
4 attr_accessor :results
5
6 # Compatibility with older versions of will_paginate
7 alias_method :page_count, :total_pages
8
9 def initialize(page, per_page, entries, total_entries)
10 @current_page, @per_page, @total_entries = page, per_page, total_entries
11
12 @total_pages = (entries / @per_page.to_f).ceil
13 end
14
15 def self.ids_from_results(results, page, limit, options)
16 collection = self.new(page, limit,
17 results[:total] || 0, results[:total_found] || 0
18 )
19 collection.results = results
20 collection.replace results[:matches].collect { |match|
21 match[:attributes]["sphinx_internal_id"]
22 }
23 return collection
24 end
25
26 def self.create_from_results(results, page, limit, options)
27 collection = self.new(page, limit,
28 results[:total] || 0, results[:total_found] || 0
29 )
30 collection.results = results
31 collection.replace instances_from_matches(results[:matches], options)
32 return collection
33 end
34
35 def self.instances_from_matches(matches, options = {})
36 if klass = options[:class]
37 instances_from_class klass, matches, options
38 else
39 instances_from_classes matches, options
40 end
41 end
42
43 def self.instances_from_class(klass, matches, options = {})
44 index_options = klass.sphinx_index_options
45
46 ids = matches.collect { |match| match[:attributes]["sphinx_internal_id"] }
47 instances = ids.length > 0 ? klass.find(
48 :all,
49 :conditions => {klass.primary_key.to_sym => ids},
50 :include => (options[:include] || index_options[:include]),
51 :select => (options[:select] || index_options[:select]),
52 :order => (options[:sql_order] || index_options[:sql_order])
53 ) : []
54
55 # Raise an exception if we find records in Sphinx but not in the DB, so
56 # the search method can retry without them. See
57 # ThinkingSphinx::Search.retry_search_on_stale_index.
58 if options[:raise_on_stale] && instances.length < ids.length
59 stale_ids = ids - instances.map {|i| i.id }
60 raise StaleIdsException, stale_ids
61 end
62
63 # if the user has specified an SQL order, return the collection
64 # without rearranging it into the Sphinx order
65 return instances if options[:sql_order]
66
67 ids.collect { |obj_id|
68 instances.detect { |obj| obj.id == obj_id }
69 }
70 end
71
72 # Group results by class and call #find(:all) once for each group to reduce
73 # the number of #find's in multi-model searches.
74 #
75 def self.instances_from_classes(matches, options = {})
76 groups = matches.group_by { |match| match[:attributes]["class_crc"] }
77 groups.each do |crc, group|
78 group.replace(
79 instances_from_class(class_from_crc(crc), group, options)
80 )
81 end
82
83 matches.collect do |match|
84 groups.detect { |crc, group|
85 crc == match[:attributes]["class_crc"]
86 }[1].detect { |obj|
87 obj.id == match[:attributes]["sphinx_internal_id"]
88 }
89 end
90 end
91
92 def self.class_from_crc(crc)
93 @@models_by_crc ||= ThinkingSphinx.indexed_models.inject({}) do |hash, model|
94 hash[model.constantize.to_crc32] = model
95 model.constantize.subclasses.each { |subclass|
96 hash[subclass.to_crc32] = subclass.name
97 }
98 hash
99 end
100 @@models_by_crc[crc].constantize
101 end
102
103 def previous_page
104 current_page > 1 ? (current_page - 1) : nil
105 end
106
107 def next_page
108 current_page < total_pages ? (current_page + 1): nil
109 end
110
111 def offset
112 (current_page - 1) * @per_page
113 end
114
115 def method_missing(method, *args, &block)
116 super unless method.to_s[/^each_with_.*/]
117
118 each_with_attribute method.to_s.gsub(/^each_with_/, ''), &block
119 end
120
121 def each_with_groupby_and_count(&block)
122 results[:matches].each_with_index do |match, index|
123 yield self[index], match[:attributes]["@groupby"], match[:attributes]["@count"]
124 end
125 end
126
127 def each_with_attribute(attribute, &block)
128 results[:matches].each_with_index do |match, index|
129 yield self[index], (match[:attributes][attribute] || match[:attributes]["@#{attribute}"])
130 end
131 end
132
133 def each_with_weighting(&block)
134 results[:matches].each_with_index do |match, index|
135 yield self[index], match[:weight]
136 end
137 end
138
139 def inject_with_groupby_and_count(initial = nil, &block)
140 index = -1
141 results[:matches].inject(initial) do |memo, match|
142 index += 1
143 yield memo, self[index], match[:attributes]["@groupby"], match[:attributes]["@count"]
144 end
145 end
146 end
147end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/configuration.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/configuration.rb
deleted file mode 100644
index 31543f5..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/configuration.rb
+++ /dev/null
@@ -1,237 +0,0 @@
1require 'erb'
2require 'singleton'
3
4module 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 Dir.glob("#{app_root}/vendor/plugins/*/app/models/")
90
91 self.source_options = {}
92 self.index_options = {
93 :charset_type => "utf-8",
94 :morphology => "stem_en"
95 }
96
97 parse_config
98
99 self
100 end
101
102 def self.environment
103 @@environment ||= (
104 defined?(Merb) ? Merb.environment : ENV['RAILS_ENV']
105 ) || "development"
106 end
107
108 def environment
109 self.class.environment
110 end
111
112 def controller
113 @controller ||= Riddle::Controller.new(@configuration, self.config_file)
114 end
115
116 # Generate the config file for Sphinx by using all the settings defined and
117 # looping through all the models with indexes to build the relevant
118 # indexer and searchd configuration, and sources and indexes details.
119 #
120 def build(file_path=nil)
121 load_models
122 file_path ||= "#{self.config_file}"
123
124 @configuration.indexes.clear
125
126 ThinkingSphinx.indexed_models.each_with_index do |model, model_index|
127 @configuration.indexes.concat model.constantize.to_riddle(model_index)
128 end
129
130 open(file_path, "w") do |file|
131 file.write @configuration.render
132 end
133 end
134
135 # Make sure all models are loaded - without reloading any that
136 # ActiveRecord::Base is already aware of (otherwise we start to hit some
137 # messy dependencies issues).
138 #
139 def load_models
140 self.model_directories.each do |base|
141 Dir["#{base}**/*.rb"].each do |file|
142 model_name = file.gsub(/^#{base}([\w_\/\\]+)\.rb/, '\1')
143
144 next if model_name.nil?
145 next if ::ActiveRecord::Base.send(:subclasses).detect { |model|
146 model.name == model_name
147 }
148
149 begin
150 model_name.camelize.constantize
151 rescue LoadError
152 model_name.gsub!(/.*[\/\\]/, '').nil? ? next : retry
153 rescue NameError
154 next
155 end
156 end
157 end
158 end
159
160 def address
161 @configuration.searchd.address
162 end
163
164 def address=(address)
165 @configuration.searchd.address = address
166 end
167
168 def port
169 @configuration.searchd.port
170 end
171
172 def port=(port)
173 @configuration.searchd.port = port
174 end
175
176 def pid_file
177 @configuration.searchd.pid_file
178 end
179
180 def pid_file=(pid_file)
181 @configuration.searchd.pid_file = pid_file
182 end
183
184 def searchd_log_file
185 @configuration.searchd.log
186 end
187
188 def searchd_log_file=(file)
189 @configuration.searchd.log = file
190 end
191
192 def query_log_file
193 @configuration.searchd.query_log
194 end
195
196 def query_log_file=(file)
197 @configuration.searchd.query_log = file
198 end
199
200 private
201
202 # Parse the config/sphinx.yml file - if it exists - then use the attribute
203 # accessors to set the appropriate values. Nothing too clever.
204 #
205 def parse_config
206 path = "#{app_root}/config/sphinx.yml"
207 return unless File.exists?(path)
208
209 conf = YAML::load(ERB.new(IO.read(path)).result)[environment]
210
211 conf.each do |key,value|
212 self.send("#{key}=", value) if self.methods.include?("#{key}=")
213
214 set_sphinx_setting self.source_options, key, value, SourceOptions
215 set_sphinx_setting self.index_options, key, value, IndexOptions
216 set_sphinx_setting @configuration.searchd, key, value
217 set_sphinx_setting @configuration.indexer, key, value
218 end unless conf.nil?
219
220 self.bin_path += '/' unless self.bin_path.blank?
221
222 if self.allow_star
223 self.index_options[:enable_star] = true
224 self.index_options[:min_prefix_len] = 1
225 end
226 end
227
228 def set_sphinx_setting(object, key, value, allowed = {})
229 if object.is_a?(Hash)
230 object[key.to_sym] = value if allowed.include?(key.to_s)
231 else
232 object.send("#{key}=", value) if object.methods.include?("#{key}")
233 send("#{key}=", value) if self.methods.include?("#{key}")
234 end
235 end
236 end
237end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/core/string.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/core/string.rb
deleted file mode 100644
index 7ab3e62..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/core/string.rb
+++ /dev/null
@@ -1,15 +0,0 @@
1require 'zlib'
2
3module ThinkingSphinx
4 module Core
5 module String
6 def to_crc32
7 Zlib.crc32 self
8 end
9 end
10 end
11end
12
13class String
14 include ThinkingSphinx::Core::String
15end \ No newline at end of file
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas.rb
deleted file mode 100644
index 57c396e..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas.rb
+++ /dev/null
@@ -1,27 +0,0 @@
1require 'thinking_sphinx/deltas/default_delta'
2require 'thinking_sphinx/deltas/delayed_delta'
3require 'thinking_sphinx/deltas/datetime_delta'
4
5module ThinkingSphinx
6 module Deltas
7 def self.parse(index, options)
8 delta_option = options.delete(:delta)
9 case delta_option
10 when TrueClass, :default
11 DefaultDelta.new index, options
12 when :delayed
13 DelayedDelta.new index, options
14 when :datetime
15 DatetimeDelta.new index, options
16 when FalseClass, nil
17 nil
18 else
19 if delta_option.ancestors.include?(ThinkingSphinx::Deltas::DefaultDelta)
20 delta_option.new index, options
21 else
22 raise "Unknown delta type"
23 end
24 end
25 end
26 end
27end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/datetime_delta.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/datetime_delta.rb
deleted file mode 100644
index 2ee46d4..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/datetime_delta.rb
+++ /dev/null
@@ -1,50 +0,0 @@
1module 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
50end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/default_delta.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/default_delta.rb
deleted file mode 100644
index c973612..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/default_delta.rb
+++ /dev/null
@@ -1,67 +0,0 @@
1module 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 return true if instance && !toggled(instance)
15
16 config = ThinkingSphinx::Configuration.instance
17 client = Riddle::Client.new config.address, config.port
18 rotate = ThinkingSphinx.sphinx_running? ? "--rotate" : ""
19
20 output = `#{config.bin_path}indexer --config #{config.config_file} #{rotate} #{delta_index_name model}`
21 puts(output) unless ThinkingSphinx.suppress_delta_output?
22
23 client.update(
24 core_index_name(model),
25 ['sphinx_deleted'],
26 {instance.sphinx_document_id => [1]}
27 ) if instance && ThinkingSphinx.sphinx_running? && instance.in_both_indexes?
28
29 true
30 end
31
32 def toggle(instance)
33 instance.delta = true
34 end
35
36 def toggled(instance)
37 instance.delta
38 end
39
40 def reset_query(model)
41 "UPDATE #{model.quoted_table_name} SET " +
42 "#{@index.quote_column(@column.to_s)} = #{adapter.boolean(false)}"
43 end
44
45 def clause(model, toggled)
46 "#{model.quoted_table_name}.#{@index.quote_column(@column.to_s)}" +
47 " = #{adapter.boolean(toggled)}"
48 end
49
50 protected
51
52 def core_index_name(model)
53 "#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_core"
54 end
55
56 def delta_index_name(model)
57 "#{model.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_delta"
58 end
59
60 private
61
62 def adapter
63 @adapter = @index.model.sphinx_database_adapter
64 end
65 end
66 end
67end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta.rb
deleted file mode 100644
index e95298b..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta.rb
+++ /dev/null
@@ -1,25 +0,0 @@
1require 'delayed/job'
2
3require 'thinking_sphinx/deltas/delayed_delta/delta_job'
4require 'thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job'
5require 'thinking_sphinx/deltas/delayed_delta/job'
6
7module 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
25end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb
deleted file mode 100644
index f9511ec..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/delta_job.rb
+++ /dev/null
@@ -1,24 +0,0 @@
1module 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
24end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb
deleted file mode 100644
index d6afd27..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/flag_as_deleted_job.rb
+++ /dev/null
@@ -1,27 +0,0 @@
1module 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
27end \ No newline at end of file
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/job.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/job.rb
deleted file mode 100644
index de0a7cb..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/deltas/delayed_delta/job.rb
+++ /dev/null
@@ -1,26 +0,0 @@
1module 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
26end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet.rb
deleted file mode 100644
index e2db449..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet.rb
+++ /dev/null
@@ -1,58 +0,0 @@
1module 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 @attribute_name ||= @reference.unique_name.to_s + "_facet"
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
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 private
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
58end \ No newline at end of file
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet_collection.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet_collection.rb
deleted file mode 100644
index 1ad9d1a..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/facet_collection.rb
+++ /dev/null
@@ -1,60 +0,0 @@
1module 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 facet = facet_from_object(results.first, facet) if facet.is_a?(String)
13
14 self[facet.name] ||= {}
15 @attribute_values[facet.name] ||= {}
16 @facets << facet
17
18 results.each_with_groupby_and_count { |result, group, count|
19 facet_value = facet.value(result, group)
20
21 self[facet.name][facet_value] ||= 0
22 self[facet.name][facet_value] += count
23 @attribute_values[facet.name][facet_value] = group
24 }
25 end
26
27 def for(hash = {})
28 arguments = @arguments.clone
29 options = arguments.extract_options!
30 options[:with] ||= {}
31
32 hash.each do |key, value|
33 attrib = facet_for_key(key).attribute_name
34 options[:with][attrib] = underlying_value key, value
35 end
36
37 arguments << options
38 ThinkingSphinx::Search.search *arguments
39 end
40
41 private
42
43 def underlying_value(key, value)
44 case value
45 when Array
46 value.collect { |item| underlying_value(key, item) }
47 else
48 @attribute_values[key][value]
49 end
50 end
51
52 def facet_for_key(key)
53 @facets.detect { |facet| facet.name == key }
54 end
55
56 def facet_from_object(object, name)
57 object.sphinx_facets.detect { |facet| facet.attribute_name == name }
58 end
59 end
60end \ No newline at end of file
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/field.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/field.rb
deleted file mode 100644
index 9edaede..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/field.rb
+++ /dev/null
@@ -1,172 +0,0 @@
1module ThinkingSphinx
2 # Fields - holding the string data which Sphinx indexes for your searches.
3 # This class isn't really useful to you unless you're hacking around with the
4 # internals of Thinking Sphinx - but hey, don't let that stop you.
5 #
6 # One key thing to remember - if you're using the field manually to
7 # generate SQL statements, you'll need to set the base model, and all the
8 # associations. Which can get messy. Use Index.link!, it really helps.
9 #
10 class Field
11 attr_accessor :alias, :columns, :sortable, :associations, :model, :infixes,
12 :prefixes, :faceted
13
14 # To create a new field, you'll need to pass in either a single Column
15 # or an array of them, and some (optional) options. The columns are
16 # references to the data that will make up the field.
17 #
18 # Valid options are:
19 # - :as => :alias_name
20 # - :sortable => true
21 # - :infixes => true
22 # - :prefixes => true
23 #
24 # Alias is only required in three circumstances: when there's
25 # another attribute or field with the same name, when the column name is
26 # 'id', or when there's more than one column.
27 #
28 # Sortable defaults to false - but is quite useful when set to true, as
29 # it creates an attribute with the same string value (which Sphinx converts
30 # to an integer value), which can be sorted by. Thinking Sphinx is smart
31 # enough to realise that when you specify fields in sort statements, you
32 # mean their respective attributes.
33 #
34 # If you have partial matching enabled (ie: enable_star), then you can
35 # specify certain fields to have their prefixes and infixes indexed. Keep
36 # in mind, though, that Sphinx's default is _all_ fields - so once you
37 # highlight a particular field, no other fields in the index will have
38 # these partial indexes.
39 #
40 # Here's some examples:
41 #
42 # Field.new(
43 # Column.new(:name)
44 # )
45 #
46 # Field.new(
47 # [Column.new(:first_name), Column.new(:last_name)],
48 # :as => :name, :sortable => true
49 # )
50 #
51 # Field.new(
52 # [Column.new(:posts, :subject), Column.new(:posts, :content)],
53 # :as => :posts, :prefixes => true
54 # )
55 #
56 def initialize(columns, options = {})
57 @columns = Array(columns)
58 @associations = {}
59
60 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) }
61
62 @alias = options[:as]
63 @sortable = options[:sortable] || false
64 @infixes = options[:infixes] || false
65 @prefixes = options[:prefixes] || false
66 @faceted = options[:facet] || false
67 end
68
69 # Get the part of the SELECT clause related to this field. Don't forget
70 # to set your model and associations first though.
71 #
72 # This will concatenate strings if there's more than one data source or
73 # multiple data values (has_many or has_and_belongs_to_many associations).
74 #
75 def to_select_sql
76 clause = @columns.collect { |column|
77 column_with_prefix(column)
78 }.join(', ')
79
80 clause = adapter.concatenate(clause) if concat_ws?
81 clause = adapter.group_concatenate(clause) if is_many?
82
83 "#{adapter.cast_to_string clause } AS #{quote_column(unique_name)}"
84 end
85
86 # Get the part of the GROUP BY clause related to this field - if one is
87 # needed. If not, all you'll get back is nil. The latter will happen if
88 # there's multiple data values (read: a has_many or has_and_belongs_to_many
89 # association).
90 #
91 def to_group_sql
92 case
93 when is_many?, ThinkingSphinx.use_group_by_shortcut?
94 nil
95 else
96 @columns.collect { |column|
97 column_with_prefix(column)
98 }
99 end
100 end
101
102 # Returns the unique name of the field - which is either the alias of
103 # the field, or the name of the only column - if there is only one. If
104 # there isn't, there should be an alias. Else things probably won't work.
105 # Consider yourself warned.
106 #
107 def unique_name
108 if @columns.length == 1
109 @alias || @columns.first.__name
110 else
111 @alias
112 end
113 end
114
115 def to_facet
116 return nil unless @faceted
117
118 ThinkingSphinx::Facet.new(self)
119 end
120
121 private
122
123 def adapter
124 @adapter ||= @model.sphinx_database_adapter
125 end
126
127 def quote_column(column)
128 @model.connection.quote_column_name(column)
129 end
130
131 # Indication of whether the columns should be concatenated with a space
132 # between each value. True if there's either multiple sources or multiple
133 # associations.
134 #
135 def concat_ws?
136 @columns.length > 1 || multiple_associations?
137 end
138
139 # Checks whether any column requires multiple associations (which only
140 # happens for polymorphic situations).
141 #
142 def multiple_associations?
143 associations.any? { |col,assocs| assocs.length > 1 }
144 end
145
146 # Builds a column reference tied to the appropriate associations. This
147 # dives into the associations hash and their corresponding joins to
148 # figure out how to correctly reference a column in SQL.
149 #
150 def column_with_prefix(column)
151 if column.is_string?
152 column.__name
153 elsif associations[column].empty?
154 "#{@model.quoted_table_name}.#{quote_column(column.__name)}"
155 else
156 associations[column].collect { |assoc|
157 assoc.has_column?(column.__name) ?
158 "#{@model.connection.quote_table_name(assoc.join.aliased_table_name)}" +
159 ".#{quote_column(column.__name)}" :
160 nil
161 }.compact.join(', ')
162 end
163 end
164
165 # Could there be more than one value related to the parent record? If so,
166 # then this will return true. If not, false. It's that simple.
167 #
168 def is_many?
169 associations.values.flatten.any? { |assoc| assoc.is_many? }
170 end
171 end
172end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index.rb
deleted file mode 100644
index 30cfe38..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index.rb
+++ /dev/null
@@ -1,423 +0,0 @@
1require 'thinking_sphinx/index/builder'
2require 'thinking_sphinx/index/faux_column'
3
4module ThinkingSphinx
5 # The Index class is a ruby representation of a Sphinx source (not a Sphinx
6 # index - yes, I know it's a little confusing. You'll manage). This is
7 # another 'internal' Thinking Sphinx class - if you're using it directly,
8 # you either know what you're doing, or messing with things beyond your ken.
9 # Enjoy.
10 #
11 class Index
12 attr_accessor :model, :fields, :attributes, :conditions, :groupings,
13 :delta_object, :options
14
15 # Create a new index instance by passing in the model it is tied to, and
16 # a block to build it with (optional but recommended). For documentation
17 # on the syntax for inside the block, the Builder class is what you want.
18 #
19 # Quick Example:
20 #
21 # Index.new(User) do
22 # indexes login, email
23 #
24 # has created_at
25 #
26 # set_property :delta => true
27 # end
28 #
29 def initialize(model, &block)
30 @model = model
31 @associations = {}
32 @fields = []
33 @attributes = []
34 @conditions = []
35 @groupings = []
36 @options = {}
37 @delta_object = nil
38
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
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", "Java::JavaSql::SQLException", "ActiveRecord::StatementInvalid"
185 return
186 else
187 raise err
188 end
189 end
190
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
209 end
210
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
230 end
231
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
247 end
248
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
256 end
257
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 )
267 end
268
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))
273 end
274
275 def attribute_by_alias(attr_alias)
276 @attributes.detect { |attrib| attrib.alias == attr_alias }
277 end
278
279 def facet_by_alias(name)
280 @model.sphinx_facets.detect { |facet| facet.name == name }
281 end
282
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]
298 end
299
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
305
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?
318 end
319
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
328 end
329
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
336 end
337
338 def sql_query_pre_for_delta
339 [""]
340 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
372SELECT #{ (
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(", ") }
377FROM #{ @model.table_name }
378 #{ assocs.collect { |assoc| assoc.to_sql }.join(' ') }
379WHERE #{@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 }
382GROUP 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 end
423end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/builder.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/builder.rb
deleted file mode 100644
index dbd2ba0..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/builder.rb
+++ /dev/null
@@ -1,264 +0,0 @@
1module ThinkingSphinx
2 class Index
3 # The Builder class is the core for the index definition block processing.
4 # There are four methods you really need to pay attention to:
5 # - indexes (aliased to includes and attribute)
6 # - has (aliased to attribute)
7 # - where
8 # - set_property (aliased to set_properties)
9 #
10 # The first two of these methods allow you to define what data makes up
11 # your indexes. #where provides a method to add manual SQL conditions, and
12 # set_property allows you to set some settings on a per-index basis. Check
13 # out each method's documentation for better ideas of usage.
14 #
15 class Builder
16 class << self
17 # No idea where this is coming from - haven't found it in any ruby or
18 # rails documentation. It's not needed though, so it gets undef'd.
19 # Hopefully the list of methods that get in the way doesn't get too
20 # long.
21 HiddenMethods = [:parent, :name, :id, :type].each { |method|
22 define_method(method) {
23 caller.grep(/irb.completion/).empty? ? method_missing(method) : super
24 }
25 }
26
27 attr_accessor :fields, :attributes, :properties, :conditions,
28 :groupings
29
30 # Set up all the collections. Consider this the equivalent of an
31 # instance's initialize method.
32 #
33 def setup
34 @fields = []
35 @attributes = []
36 @properties = {}
37 @conditions = []
38 @groupings = []
39 end
40
41 # This is how you add fields - the strings Sphinx looks at - to your
42 # index. Technically, to use this method, you need to pass in some
43 # columns and options - but there's some neat method_missing stuff
44 # happening, so lets stick to the expected syntax within a define_index
45 # block.
46 #
47 # Expected options are :as, which points to a column alias in symbol
48 # form, and :sortable, which indicates whether you want to sort by this
49 # field.
50 #
51 # Adding Single-Column Fields:
52 #
53 # You can use symbols or methods - and can chain methods together to
54 # get access down the associations tree.
55 #
56 # indexes :id, :as => :my_id
57 # indexes :name, :sortable => true
58 # indexes first_name, last_name, :sortable => true
59 # indexes users.posts.content, :as => :post_content
60 # indexes users(:id), :as => :user_ids
61 #
62 # Keep in mind that if any keywords for Ruby methods - such as id or
63 # name - clash with your column names, you need to use the symbol
64 # version (see the first, second and last examples above).
65 #
66 # If you specify multiple columns (example #2), a field will be created
67 # for each. Don't use the :as option in this case. If you want to merge
68 # those columns together, continue reading.
69 #
70 # Adding Multi-Column Fields:
71 #
72 # indexes [first_name, last_name], :as => :name
73 # indexes [location, parent.location], :as => :location
74 #
75 # To combine multiple columns into a single field, you need to wrap
76 # them in an Array, as shown by the above examples. There's no
77 # limitations on whether they're symbols or methods or what level of
78 # associations they come from.
79 #
80 # Adding SQL Fragment Fields
81 #
82 # You can also define a field using an SQL fragment, useful for when
83 # you would like to index a calculated value.
84 #
85 # indexes "age < 18", :as => :minor
86 #
87 def indexes(*args)
88 options = args.extract_options!
89 args.each do |columns|
90 field = Field.new(FauxColumn.coerce(columns), options)
91 fields << field
92
93 add_sort_attribute field, options if field.sortable
94 add_facet_attribute field, options if field.faceted
95 end
96 end
97 alias_method :field, :indexes
98 alias_method :includes, :indexes
99
100 # This is the method to add attributes to your index (hence why it is
101 # aliased as 'attribute'). The syntax is the same as #indexes, so use
102 # that as starting point, but keep in mind the following points.
103 #
104 # An attribute can have an alias (the :as option), but it is always
105 # sortable - so you don't need to explicitly request that. You _can_
106 # specify the data type of the attribute (the :type option), but the
107 # code's pretty good at figuring that out itself from peering into the
108 # database.
109 #
110 # Attributes are limited to the following types: integers, floats,
111 # datetimes (converted to timestamps), booleans and strings. Don't
112 # forget that Sphinx converts string attributes to integers, which are
113 # useful for sorting, but that's about it.
114 #
115 # You can also have a collection of integers for multi-value attributes
116 # (MVAs). Generally these would be through a has_many relationship,
117 # like in this example:
118 #
119 # has posts(:id), :as => :post_ids
120 #
121 # This allows you to filter on any of the values tied to a specific
122 # record. Might be best to read through the Sphinx documentation to get
123 # a better idea of that though.
124 #
125 # Adding SQL Fragment Attributes
126 #
127 # You can also define an attribute using an SQL fragment, useful for
128 # when you would like to index a calculated value. Don't forget to set
129 # the type of the attribute though:
130 #
131 # has "age < 18", :as => :minor, :type => :boolean
132 #
133 # If you're creating attributes for latitude and longitude, don't
134 # forget that Sphinx expects these values to be in radians.
135 #
136 def has(*args)
137 options = args.extract_options!
138 args.each do |columns|
139 attribute = Attribute.new(FauxColumn.coerce(columns), options)
140 attributes << attribute
141
142 add_facet_attribute attribute, options if attribute.faceted
143 end
144 end
145 alias_method :attribute, :has
146
147 def facet(*args)
148 options = args.extract_options!
149 options[:facet] = true
150
151 args.each do |columns|
152 attribute = Attribute.new(FauxColumn.coerce(columns), options)
153 attributes << attribute
154
155 add_facet_attribute attribute, options
156 end
157 end
158
159 # Use this method to add some manual SQL conditions for your index
160 # request. You can pass in as many strings as you like, they'll get
161 # joined together with ANDs later on.
162 #
163 # where "user_id = 10"
164 # where "parent_type = 'Article'", "created_at < NOW()"
165 #
166 def where(*args)
167 @conditions += args
168 end
169
170 # Use this method to add some manual SQL strings to the GROUP BY
171 # clause. You can pass in as many strings as you'd like, they'll get
172 # joined together with commas later on.
173 #
174 # group_by "lat", "lng"
175 #
176 def group_by(*args)
177 @groupings += args
178 end
179
180 # This is what to use to set properties on the index. Chief amongst
181 # those is the delta property - to allow automatic updates to your
182 # indexes as new models are added and edited - but also you can
183 # define search-related properties which will be the defaults for all
184 # searches on the model.
185 #
186 # set_property :delta => true
187 # set_property :field_weights => {"name" => 100}
188 # set_property :order => "name ASC"
189 # set_property :include => :picture
190 # set_property :select => 'name'
191 #
192 # Also, the following two properties are particularly relevant for
193 # geo-location searching - latitude_attr and longitude_attr. If your
194 # attributes for these two values are named something other than
195 # lat/latitude or lon/long/longitude, you can dictate what they are
196 # when defining the index, so you don't need to specify them for every
197 # geo-related search.
198 #
199 # set_property :latitude_attr => "lt", :longitude_attr => "lg"
200 #
201 # Please don't forget to add a boolean field named 'delta' to your
202 # model's database table if enabling the delta index for it.
203 # Valid options for the delta property are:
204 #
205 # true
206 # false
207 # :default
208 # :delayed
209 # :datetime
210 #
211 # You can also extend ThinkingSphinx::Deltas::DefaultDelta to implement
212 # your own handling for delta indexing.
213
214 def set_property(*args)
215 options = args.extract_options!
216 if options.empty?
217 @properties[args[0]] = args[1]
218 else
219 @properties.merge!(options)
220 end
221 end
222 alias_method :set_properties, :set_property
223
224 # Handles the generation of new columns for the field and attribute
225 # definitions.
226 #
227 def method_missing(method, *args)
228 FauxColumn.new(method, *args)
229 end
230
231 # A method to allow adding fields from associations which have names
232 # that clash with method names in the Builder class (ie: properties,
233 # fields, attributes).
234 #
235 # Example: indexes assoc(:properties).column
236 #
237 def assoc(assoc, *args)
238 FauxColumn.new(assoc, *args)
239 end
240
241 private
242
243 def add_sort_attribute(field, options)
244 add_internal_attribute field, options, "_sort"
245 end
246
247 def add_facet_attribute(resource, options)
248 add_internal_attribute resource, options, "_facet", true
249 end
250
251 def add_internal_attribute(resource, options, suffix, crc = false)
252 @attributes << Attribute.new(
253 resource.columns.collect { |col| col.clone },
254 options.merge(
255 :type => resource.is_a?(Field) ? :string : nil,
256 :as => resource.unique_name.to_s.concat(suffix).to_sym,
257 :crc => crc
258 ).except(:facet)
259 )
260 end
261 end
262 end
263 end
264end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/faux_column.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/faux_column.rb
deleted file mode 100644
index 84068de..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/index/faux_column.rb
+++ /dev/null
@@ -1,110 +0,0 @@
1module ThinkingSphinx
2 class Index
3 # Instances of this class represent database columns and the stack of
4 # associations that lead from the base model to them.
5 #
6 # The name and stack are accessible through methods starting with __ to
7 # avoid conflicting with the method_missing calls that build the stack.
8 #
9 class FauxColumn
10 # Create a new column with a pre-defined stack. The top element in the
11 # stack will get shifted to be the name value.
12 #
13 def initialize(*stack)
14 @name = stack.pop
15 @stack = stack
16 end
17
18 def self.coerce(columns)
19 case columns
20 when Symbol, String
21 FauxColumn.new(columns)
22 when Array
23 columns.collect { |col| FauxColumn.coerce(col) }
24 when FauxColumn
25 columns
26 else
27 nil
28 end
29 end
30
31 # Can't use normal method name, as that could be an association or
32 # column name.
33 #
34 def __name
35 @name
36 end
37
38 # Can't use normal method name, as that could be an association or
39 # column name.
40 #
41 def __stack
42 @stack
43 end
44
45 # Returns true if the stack is empty *and* if the name is a string -
46 # which is an indication that of raw SQL, as opposed to a value from a
47 # table's column.
48 #
49 def is_string?
50 @name.is_a?(String) && @stack.empty?
51 end
52
53 # This handles any 'invalid' method calls and sets them as the name,
54 # and pushing the previous name into the stack. The object returns
55 # itself.
56 #
57 # If there's a single argument, it becomes the name, and the method
58 # symbol goes into the stack as well. Multiple arguments means new
59 # columns with the original stack and new names (from each argument) gets
60 # returned.
61 #
62 # Easier to explain with examples:
63 #
64 # col = FauxColumn.new :a, :b, :c
65 # col.__name #=> :c
66 # col.__stack #=> [:a, :b]
67 #
68 # col.whatever #=> col
69 # col.__name #=> :whatever
70 # col.__stack #=> [:a, :b, :c]
71 #
72 # col.something(:id) #=> col
73 # col.__name #=> :id
74 # col.__stack #=> [:a, :b, :c, :whatever, :something]
75 #
76 # cols = col.short(:x, :y, :z)
77 # cols[0].__name #=> :x
78 # cols[0].__stack #=> [:a, :b, :c, :whatever, :something, :short]
79 # cols[1].__name #=> :y
80 # cols[1].__stack #=> [:a, :b, :c, :whatever, :something, :short]
81 # cols[2].__name #=> :z
82 # cols[2].__stack #=> [:a, :b, :c, :whatever, :something, :short]
83 #
84 # Also, this allows method chaining to build up a relevant stack:
85 #
86 # col = FauxColumn.new :a, :b
87 # col.__name #=> :b
88 # col.__stack #=> [:a]
89 #
90 # col.one.two.three #=> col
91 # col.__name #=> :three
92 # col.__stack #=> [:a, :b, :one, :two]
93 #
94 def method_missing(method, *args)
95 @stack << @name
96 @name = method
97
98 if (args.empty?)
99 self
100 elsif (args.length == 1)
101 method_missing(args.first)
102 else
103 args.collect { |arg|
104 FauxColumn.new(@stack + [@name, arg])
105 }
106 end
107 end
108 end
109 end
110end \ No newline at end of file
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/rails_additions.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/rails_additions.rb
deleted file mode 100644
index c7db0a1..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/rails_additions.rb
+++ /dev/null
@@ -1,136 +0,0 @@
1module ThinkingSphinx
2 module HashExcept
3 # Returns a new hash without the given keys.
4 def except(*keys)
5 rejected = Set.new(respond_to?(:convert_key) ? keys.map { |key| convert_key(key) } : keys)
6 reject { |key,| rejected.include?(key) }
7 end
8
9 # Replaces the hash without only the given keys.
10 def except!(*keys)
11 replace(except(*keys))
12 end
13 end
14end
15
16Hash.send(
17 :include, ThinkingSphinx::HashExcept
18) unless Hash.instance_methods.include?("except")
19
20module ThinkingSphinx
21 module ArrayExtractOptions
22 def extract_options!
23 last.is_a?(::Hash) ? pop : {}
24 end
25 end
26end
27
28Array.send(
29 :include, ThinkingSphinx::ArrayExtractOptions
30) unless Array.instance_methods.include?("extract_options!")
31
32module ThinkingSphinx
33 module AbstractQuotedTableName
34 def quote_table_name(name)
35 quote_column_name(name)
36 end
37 end
38end
39
40ActiveRecord::ConnectionAdapters::AbstractAdapter.send(
41 :include, ThinkingSphinx::AbstractQuotedTableName
42) unless ActiveRecord::ConnectionAdapters::AbstractAdapter.instance_methods.include?("quote_table_name")
43
44module ThinkingSphinx
45 module MysqlQuotedTableName
46 def quote_table_name(name) #:nodoc:
47 quote_column_name(name).gsub('.', '`.`')
48 end
49 end
50end
51
52if 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
59end
60
61module ThinkingSphinx
62 module ActiveRecordQuotedName
63 def quoted_table_name
64 self.connection.quote_table_name(self.table_name)
65 end
66 end
67end
68
69ActiveRecord::Base.extend(
70 ThinkingSphinx::ActiveRecordQuotedName
71) unless ActiveRecord::Base.respond_to?("quoted_table_name")
72
73module ThinkingSphinx
74 module ActiveRecordStoreFullSTIClass
75 def store_full_sti_class
76 false
77 end
78 end
79end
80
81ActiveRecord::Base.extend(
82 ThinkingSphinx::ActiveRecordStoreFullSTIClass
83) unless ActiveRecord::Base.respond_to?(:store_full_sti_class)
84
85module ThinkingSphinx
86 module ClassAttributeMethods
87 def cattr_reader(*syms)
88 syms.flatten.each do |sym|
89 next if sym.is_a?(Hash)
90 class_eval(<<-EOS, __FILE__, __LINE__)
91 unless defined? @@#{sym}
92 @@#{sym} = nil
93 end
94
95 def self.#{sym}
96 @@#{sym}
97 end
98
99 def #{sym}
100 @@#{sym}
101 end
102 EOS
103 end
104 end
105
106 def cattr_writer(*syms)
107 options = syms.extract_options!
108 syms.flatten.each do |sym|
109 class_eval(<<-EOS, __FILE__, __LINE__)
110 unless defined? @@#{sym}
111 @@#{sym} = nil
112 end
113
114 def self.#{sym}=(obj)
115 @@#{sym} = obj
116 end
117
118 #{"
119 def #{sym}=(obj)
120 @@#{sym} = obj
121 end
122 " unless options[:instance_writer] == false }
123 EOS
124 end
125 end
126
127 def cattr_accessor(*syms)
128 cattr_reader(*syms)
129 cattr_writer(*syms)
130 end
131 end
132end
133
134Class.extend(
135 ThinkingSphinx::ClassAttributeMethods
136) unless Class.respond_to?(:cattr_reader)
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/search.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/search.rb
deleted file mode 100644
index d476787..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/search.rb
+++ /dev/null
@@ -1,780 +0,0 @@
1module ThinkingSphinx
2 # Once you've got those indexes in and built, this is the stuff that
3 # matters - how to search! This class provides a generic search
4 # interface - which you can use to search all your indexed models at once.
5 # Most times, you will just want a specific model's results - to search and
6 # search_for_ids methods will do the job in exactly the same manner when
7 # called from a model.
8 #
9 class Search
10 GlobalFacetOptions = {
11 :all_attributes => false,
12 :class_facet => true
13 }
14
15 class << self
16 # Searches for results that match the parameters provided. Will only
17 # return the ids for the matching objects. See #search for syntax
18 # examples.
19 #
20 # Note that this only searches the Sphinx index, with no ActiveRecord
21 # queries. Thus, if your index is not in sync with the database, this
22 # method may return ids that no longer exist there.
23 #
24 def search_for_ids(*args)
25 results, client = search_results(*args.clone)
26
27 options = args.extract_options!
28 page = options[:page] ? options[:page].to_i : 1
29
30 ThinkingSphinx::Collection.ids_from_results(results, page, client.limit, options)
31 end
32
33 # Searches through the Sphinx indexes for relevant matches. There's
34 # various ways to search, sort, group and filter - which are covered
35 # below.
36 #
37 # Also, if you have WillPaginate installed, the search method can be used
38 # just like paginate. The same parameters - :page and :per_page - work as
39 # expected, and the returned result set can be used by the will_paginate
40 # helper.
41 #
42 # == Basic Searching
43 #
44 # The simplest way of searching is straight text.
45 #
46 # ThinkingSphinx::Search.search "pat"
47 # ThinkingSphinx::Search.search "google"
48 # User.search "pat", :page => (params[:page] || 1)
49 # Article.search "relevant news issue of the day"
50 #
51 # If you specify :include, like in an #find call, this will be respected
52 # when loading the relevant models from the search results.
53 #
54 # User.search "pat", :include => :posts
55 #
56 # == Match Modes
57 #
58 # Sphinx supports 5 different matching modes. By default Thinking Sphinx
59 # uses :all, which unsurprisingly requires all the supplied search terms
60 # to match a result.
61 #
62 # Alternative modes include:
63 #
64 # User.search "pat allan", :match_mode => :any
65 # User.search "pat allan", :match_mode => :phrase
66 # User.search "pat | allan", :match_mode => :boolean
67 # User.search "@name pat | @username pat", :match_mode => :extended
68 #
69 # Any will find results with any of the search terms. Phrase treats the search
70 # terms a single phrase instead of individual words. Boolean and extended allow
71 # for more complex query syntax, refer to the sphinx documentation for further
72 # details.
73 #
74 # == Weighting
75 #
76 # Sphinx has support for weighting, where matches in one field can be considered
77 # more important than in another. Weights are integers, with 1 as the default.
78 # They can be set per-search like this:
79 #
80 # User.search "pat allan", :field_weights => { :alias => 4, :aka => 2 }
81 #
82 # If you're searching multiple models, you can set per-index weights:
83 #
84 # ThinkingSphinx::Search.search "pat", :index_weights => { User => 10 }
85 #
86 # See http://sphinxsearch.com/doc.html#weighting for further details.
87 #
88 # == Searching by Fields
89 #
90 # If you want to step it up a level, you can limit your search terms to
91 # specific fields:
92 #
93 # User.search :conditions => {:name => "pat"}
94 #
95 # This uses Sphinx's extended match mode, unless you specify a different
96 # match mode explicitly (but then this way of searching won't work). Also
97 # note that you don't need to put in a search string.
98 #
99 # == Searching by Attributes
100 #
101 # Also known as filters, you can limit your searches to documents that
102 # have specific values for their attributes. There are three ways to do
103 # this. The first two techniques work in all scenarios - using the :with
104 # or :with_all options.
105 #
106 # ThinkingSphinx::Search.search :with => {:tag_ids => 10}
107 # ThinkingSphinx::Search.search :with => {:tag_ids => [10,12]}
108 # ThinkingSphinx::Search.search :with_all => {:tag_ids => [10,12]}
109 #
110 # The first :with search will match records with a tag_id attribute of 10.
111 # The second :with will match records with a tag_id attribute of 10 OR 12.
112 # If you need to find records that are tagged with ids 10 AND 12, you
113 # will need to use the :with_all search parameter. This is particuarly
114 # useful in conjunction with Multi Value Attributes (MVAs).
115 #
116 # The third filtering technique is only viable if you're searching with a
117 # specific model (not multi-model searching). With a single model,
118 # Thinking Sphinx can figure out what attributes and fields are available,
119 # so you can put it all in the :conditions hash, and it will sort it out.
120 #
121 # Node.search :conditions => {:parent_id => 10}
122 #
123 # Filters can be single values, arrays of values, or ranges.
124 #
125 # Article.search "East Timor", :conditions => {:rating => 3..5}
126 #
127 # == Excluding by Attributes
128 #
129 # Sphinx also supports negative filtering - where the filters are of
130 # attribute values to exclude. This is done with the :without option:
131 #
132 # User.search :without => {:role_id => 1}
133 #
134 # == Excluding by Primary Key
135 #
136 # There is a shortcut to exclude records by their ActiveRecord primary key:
137 #
138 # User.search :without_ids => 1
139 #
140 # Pass an array or a single value.
141 #
142 # The primary key must be an integer as a negative filter is used. Note
143 # that for multi-model search, an id may occur in more than one model.
144 #
145 # == Infix (Star) Searching
146 #
147 # By default, Sphinx uses English stemming, e.g. matching "shoes" if you
148 # search for "shoe". It won't find "Melbourne" if you search for
149 # "elbourn", though.
150 #
151 # Enable infix searching by something like this in config/sphinx.yml:
152 #
153 # development:
154 # enable_star: 1
155 # min_infix_length: 2
156 #
157 # Note that this will make indexing take longer.
158 #
159 # With those settings (and after reindexing), wildcard asterisks can be used
160 # in queries:
161 #
162 # Location.search "*elbourn*"
163 #
164 # To automatically add asterisks around every token (but not operators),
165 # pass the :star option:
166 #
167 # Location.search "elbourn -ustrali", :star => true, :match_mode => :boolean
168 #
169 # This would become "*elbourn* -*ustrali*". The :star option only adds the
170 # asterisks. You need to make the config/sphinx.yml changes yourself.
171 #
172 # By default, the tokens are assumed to match the regular expression /\w+/u.
173 # If you've modified the charset_table, pass another regular expression, e.g.
174 #
175 # User.search("oo@bar.c", :star => /[\w@.]+/u)
176 #
177 # to search for "*oo@bar.c*" and not "*oo*@*bar*.*c*".
178 #
179 # == Sorting
180 #
181 # Sphinx can only sort by attributes, so generally you will need to avoid
182 # using field names in your :order option. However, if you're searching
183 # on a single model, and have specified some fields as sortable, you can
184 # use those field names and Thinking Sphinx will interpret accordingly.
185 # Remember: this will only happen for single-model searches, and only
186 # through the :order option.
187 #
188 # Location.search "Melbourne", :order => :state
189 # User.search :conditions => {:role_id => 2}, :order => "name ASC"
190 #
191 # Keep in mind that if you use a string, you *must* specify the direction
192 # (ASC or DESC) else Sphinx won't return any results. If you use a symbol
193 # then Thinking Sphinx assumes ASC, but if you wish to state otherwise,
194 # use the :sort_mode option:
195 #
196 # Location.search "Melbourne", :order => :state, :sort_mode => :desc
197 #
198 # Of course, there are other sort modes - check out the Sphinx
199 # documentation[http://sphinxsearch.com/doc.html] for that level of
200 # detail though.
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 #
208 # == Grouping
209 #
210 # For this you can use the group_by, group_clause and group_function
211 # options - which are all directly linked to Sphinx's expectations. No
212 # magic from Thinking Sphinx. It can get a little tricky, so make sure
213 # you read all the relevant
214 # documentation[http://sphinxsearch.com/doc.html#clustering] first.
215 #
216 # Grouping is done via three parameters within the options hash
217 # * <tt>:group_function</tt> determines the way grouping is done
218 # * <tt>:group_by</tt> determines the field which is used for grouping
219 # * <tt>:group_clause</tt> determines the sorting order
220 #
221 # === group_function
222 #
223 # Valid values for :group_function are
224 # * <tt>:day</tt>, <tt>:week</tt>, <tt>:month</tt>, <tt>:year</tt> - Grouping is done by the respective timeframes.
225 # * <tt>:attr</tt>, <tt>:attrpair</tt> - Grouping is done by the specified attributes(s)
226 #
227 # === group_by
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 #
276 #
277 # == Geo/Location Searching
278 #
279 # Sphinx - and therefore Thinking Sphinx - has the facility to search
280 # around a geographical point, using a given latitude and longitude. To
281 # take advantage of this, you will need to have both of those values in
282 # attributes. To search with that point, you can then use one of the
283 # following syntax examples:
284 #
285 # Address.search "Melbourne", :geo => [1.4, -2.217], :order => "@geodist asc"
286 # Address.search "Australia", :geo => [-0.55, 3.108], :order => "@geodist asc"
287 # :latitude_attr => "latit", :longitude_attr => "longit"
288 #
289 # The first example applies when your latitude and longitude attributes
290 # are named any of lat, latitude, lon, long or longitude. If that's not
291 # the case, you will need to explicitly state them in your search, _or_
292 # you can do so in your model:
293 #
294 # define_index do
295 # has :latit # Float column, stored in radians
296 # has :longit # Float column, stored in radians
297 #
298 # set_property :latitude_attr => "latit"
299 # set_property :longitude_attr => "longit"
300 # end
301 #
302 # Now, geo-location searching really only has an affect if you have a
303 # filter, sort or grouping clause related to it - otherwise it's just a
304 # normal search, and _will not_ return a distance value otherwise. To
305 # make use of the positioning difference, use the special attribute
306 # "@geodist" in any of your filters or sorting or grouping clauses.
307 #
308 # And don't forget - both the latitude and longitude you use in your
309 # search, and the values in your indexes, need to be stored as a float in radians,
310 # _not_ degrees. Keep in mind that if you do this conversion in SQL
311 # you will need to explicitly declare a column type of :float.
312 #
313 # define_index do
314 # has 'RADIANS(lat)', :as => :lat, :type => :float
315 # # ...
316 # end
317 #
318 # Once you've got your results set, you can access the distances as
319 # follows:
320 #
321 # @results.each_with_geodist do |result, distance|
322 # # ...
323 # end
324 #
325 # The distance value is returned as a float, representing the distance in
326 # metres.
327 #
328 # == Handling a Stale Index
329 #
330 # Especially if you don't use delta indexing, you risk having records in the
331 # Sphinx index that are no longer in the database. By default, those will simply
332 # come back as nils:
333 #
334 # >> pat_user.delete
335 # >> User.search("pat")
336 # Sphinx Result: [1,2]
337 # => [nil, <#User id: 2>]
338 #
339 # (If you search across multiple models, you'll get ActiveRecord::RecordNotFound.)
340 #
341 # You can simply Array#compact these results or handle the nils in some other way, but
342 # Sphinx will still report two results, and the missing records may upset your layout.
343 #
344 # If you pass :retry_stale => true to a single-model search, missing records will
345 # cause Thinking Sphinx to retry the query but excluding those records. Since search
346 # is paginated, the new search could potentially include missing records as well, so by
347 # default Thinking Sphinx will retry three times. Pass :retry_stale => 5 to retry five
348 # times, and so on. If there are still missing ids on the last retry, they are
349 # shown as nils.
350 #
351 def search(*args)
352 query = args.clone # an array
353 options = query.extract_options!
354
355 retry_search_on_stale_index(query, options) do
356 results, client = search_results(*(query + [options]))
357
358 ::ActiveRecord::Base.logger.error(
359 "Sphinx Error: #{results[:error]}"
360 ) if results[:error]
361
362 klass = options[:class]
363 page = options[:page] ? options[:page].to_i : 1
364
365 ThinkingSphinx::Collection.create_from_results(results, page, client.limit, options)
366 end
367 end
368
369 def retry_search_on_stale_index(query, options, &block)
370 stale_ids = []
371 stale_retries_left = case options[:retry_stale]
372 when true
373 3 # default to three retries
374 when nil, false
375 0 # no retries
376 else options[:retry_stale].to_i
377 end
378 begin
379 # Passing this in an option so Collection.create_from_results can see it.
380 # It should only raise on stale records if there are any retries left.
381 options[:raise_on_stale] = stale_retries_left > 0
382 block.call
383 # If ThinkingSphinx::Collection.create_from_results found records in Sphinx but not
384 # in the DB and the :raise_on_stale option is set, this exception is raised. We retry
385 # a limited number of times, excluding the stale ids from the search.
386 rescue StaleIdsException => e
387 stale_retries_left -= 1
388
389 stale_ids |= e.ids # For logging
390 options[:without_ids] = Array(options[:without_ids]) | e.ids # Actual exclusion
391
392 tries = stale_retries_left
393 ::ActiveRecord::Base.logger.debug("Sphinx Stale Ids (%s %s left): %s" % [
394 tries, (tries==1 ? 'try' : 'tries'), stale_ids.join(', ')
395 ])
396
397 retry
398 end
399 end
400
401 def count(*args)
402 results, client = search_results(*args.clone)
403 results[:total_found] || 0
404 end
405
406 # Checks if a document with the given id exists within a specific index.
407 # Expected parameters:
408 #
409 # - ID of the document
410 # - Index to check within
411 # - Options hash (defaults to {})
412 #
413 # Example:
414 #
415 # ThinkingSphinx::Search.search_for_id(10, "user_core", :class => User)
416 #
417 def search_for_id(*args)
418 options = args.extract_options!
419 client = client_from_options options
420
421 query, filters = search_conditions(
422 options[:class], options[:conditions] || {}
423 )
424 client.filters += filters
425 client.match_mode = :extended unless query.empty?
426 client.id_range = args.first..args.first
427
428 begin
429 return client.query(query, args[1])[:matches].length > 0
430 rescue Errno::ECONNREFUSED => err
431 raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
432 end
433 end
434
435 # Model.facets *args
436 # ThinkingSphinx::Search.facets *args
437 # ThinkingSphinx::Search.facets *args, :all_attributes => true
438 # ThinkingSphinx::Search.facets *args, :class_facet => false
439 #
440 def facets(*args)
441 options = args.extract_options!
442
443 if options[:class]
444 facets_for_model options[:class], args, options
445 else
446 facets_for_all_models args, options
447 end
448 end
449
450 private
451
452 # This method handles the common search functionality, and returns both
453 # the result hash and the client. Not super elegant, but it'll do for
454 # the moment.
455 #
456 def search_results(*args)
457 options = args.extract_options!
458 query = args.join(' ')
459 client = client_from_options options
460
461 query = star_query(query, options[:star]) if options[:star]
462
463 extra_query, filters = search_conditions(
464 options[:class], options[:conditions] || {}
465 )
466 client.filters += filters
467 client.match_mode = :extended unless extra_query.empty?
468 query = [query, extra_query].join(' ')
469 query.strip! # Because "" and " " are not equivalent
470
471 set_sort_options! client, options
472
473 client.limit = options[:per_page].to_i if options[:per_page]
474 page = options[:page] ? options[:page].to_i : 1
475 page = 1 if page <= 0
476 client.offset = (page - 1) * client.limit
477
478 begin
479 ::ActiveRecord::Base.logger.debug "Sphinx: #{query}"
480 results = client.query query
481 ::ActiveRecord::Base.logger.debug "Sphinx Result: #{results[:matches].collect{|m| m[:attributes]["sphinx_internal_id"]}.inspect}"
482 rescue Errno::ECONNREFUSED => err
483 raise ThinkingSphinx::ConnectionError, "Connection to Sphinx Daemon (searchd) failed."
484 end
485
486 return results, client
487 end
488
489 # Set all the appropriate settings for the client, using the provided
490 # options hash.
491 #
492 def client_from_options(options = {})
493 config = ThinkingSphinx::Configuration.instance
494 client = Riddle::Client.new config.address, config.port
495 klass = options[:class]
496 index_options = klass ? klass.sphinx_index_options : {}
497
498 # The Riddle default is per-query max_matches=1000. If we set the
499 # per-server max to a smaller value in sphinx.yml, we need to override
500 # the Riddle default or else we get search errors like
501 # "per-query max_matches=1000 out of bounds (per-server max_matches=200)"
502 if per_server_max_matches = config.configuration.searchd.max_matches
503 options[:max_matches] ||= per_server_max_matches
504 end
505
506 # Turn :index_weights => { "foo" => 2, User => 1 }
507 # into :index_weights => { "foo" => 2, "user_core" => 1, "user_delta" => 1 }
508 if iw = options[:index_weights]
509 options[:index_weights] = iw.inject({}) do |hash, (index,weight)|
510 if index.is_a?(Class)
511 name = ThinkingSphinx::Index.name(index)
512 hash["#{name}_core"] = weight
513 hash["#{name}_delta"] = weight
514 else
515 hash[index] = weight
516 end
517 hash
518 end
519 end
520
521 [
522 :max_matches, :match_mode, :sort_mode, :sort_by, :id_range,
523 :group_by, :group_function, :group_clause, :group_distinct, :cut_off,
524 :retry_count, :retry_delay, :index_weights, :rank_mode,
525 :max_query_time, :field_weights, :filters, :anchor, :limit
526 ].each do |key|
527 client.send(
528 key.to_s.concat("=").to_sym,
529 options[key] || index_options[key] || client.send(key)
530 )
531 end
532
533 options[:classes] = [klass] if klass
534
535 client.anchor = anchor_conditions(klass, options) || {} if client.anchor.empty?
536
537 client.filters << Riddle::Client::Filter.new(
538 "sphinx_deleted", [0]
539 )
540
541 # class filters
542 client.filters << Riddle::Client::Filter.new(
543 "class_crc", options[:classes].collect { |k| k.to_crc32s }.flatten
544 ) if options[:classes]
545
546 # normal attribute filters
547 client.filters += options[:with].collect { |attr,val|
548 Riddle::Client::Filter.new attr.to_s, filter_value(val)
549 } if options[:with]
550
551 # exclusive attribute filters
552 client.filters += options[:without].collect { |attr,val|
553 Riddle::Client::Filter.new attr.to_s, filter_value(val), true
554 } if options[:without]
555
556 # every-match attribute filters
557 client.filters += options[:with_all].collect { |attr,vals|
558 Array(vals).collect { |val|
559 Riddle::Client::Filter.new attr.to_s, filter_value(val)
560 }
561 }.flatten if options[:with_all]
562
563 # exclusive attribute filter on primary key
564 client.filters += Array(options[:without_ids]).collect { |id|
565 Riddle::Client::Filter.new 'sphinx_internal_id', filter_value(id), true
566 } if options[:without_ids]
567
568 client
569 end
570
571 def star_query(query, custom_token = nil)
572 token = custom_token.is_a?(Regexp) ? custom_token : /\w+/u
573
574 query.gsub(/("#{token}(.*?#{token})?"|(?![!-])#{token})/u) do
575 pre, proper, post = $`, $&, $'
576 is_operator = pre.match(%r{(\W|^)[@~/]\Z}) # E.g. "@foo", "/2", "~3", but not as part of a token
577 is_quote = proper.starts_with?('"') && proper.ends_with?('"') # E.g. "foo bar", with quotes
578 has_star = pre.ends_with?("*") || post.starts_with?("*")
579 if is_operator || is_quote || has_star
580 proper
581 else
582 "*#{proper}*"
583 end
584 end
585 end
586
587 def filter_value(value)
588 case value
589 when Range
590 value.first.is_a?(Time) ? timestamp(value.first)..timestamp(value.last) : value
591 when Array
592 value.collect { |val| val.is_a?(Time) ? timestamp(val) : val }
593 else
594 Array(value)
595 end
596 end
597
598 # Returns the integer timestamp for a Time object.
599 #
600 # If using Rails 2.1+, need to handle timezones to translate them back to
601 # UTC, as that's what datetimes will be stored as by MySQL.
602 #
603 # in_time_zone is a method that was added for the timezone support in
604 # Rails 2.1, which is why it's used for testing. I'm sure there's better
605 # ways, but this does the job.
606 #
607 def timestamp(value)
608 value.respond_to?(:in_time_zone) ? value.utc.to_i : value.to_i
609 end
610
611 # Translate field and attribute conditions to the relevant search string
612 # and filters.
613 #
614 def search_conditions(klass, conditions={})
615 attributes = klass ? klass.sphinx_indexes.collect { |index|
616 index.attributes.collect { |attrib| attrib.unique_name }
617 }.flatten : []
618
619 search_string = []
620 filters = []
621
622 conditions.each do |key,val|
623 if attributes.include?(key.to_sym)
624 filters << Riddle::Client::Filter.new(
625 key.to_s, filter_value(val)
626 )
627 else
628 search_string << "@#{key} #{val}"
629 end
630 end
631
632 return search_string.join(' '), filters
633 end
634
635 # Return the appropriate latitude and longitude values, depending on
636 # whether the relevant attributes have been defined, and also whether
637 # there's actually any values.
638 #
639 def anchor_conditions(klass, options)
640 attributes = klass ? klass.sphinx_indexes.collect { |index|
641 index.attributes.collect { |attrib| attrib.unique_name }
642 }.flatten : []
643
644 lat_attr = klass ? klass.sphinx_indexes.collect { |index|
645 index.options[:latitude_attr]
646 }.compact.first : nil
647
648 lon_attr = klass ? klass.sphinx_indexes.collect { |index|
649 index.options[:longitude_attr]
650 }.compact.first : nil
651
652 lat_attr = options[:latitude_attr] if options[:latitude_attr]
653 lat_attr ||= :lat if attributes.include?(:lat)
654 lat_attr ||= :latitude if attributes.include?(:latitude)
655
656 lon_attr = options[:longitude_attr] if options[:longitude_attr]
657 lon_attr ||= :lng if attributes.include?(:lng)
658 lon_attr ||= :lon if attributes.include?(:lon)
659 lon_attr ||= :long if attributes.include?(:long)
660 lon_attr ||= :longitude if attributes.include?(:longitude)
661
662 lat = options[:lat]
663 lon = options[:lon]
664
665 if options[:geo]
666 lat = options[:geo].first
667 lon = options[:geo].last
668 end
669
670 lat && lon ? {
671 :latitude_attribute => lat_attr.to_s,
672 :latitude => lat,
673 :longitude_attribute => lon_attr.to_s,
674 :longitude => lon
675 } : nil
676 end
677
678 # Set the sort options using the :order key as well as the appropriate
679 # Riddle settings.
680 #
681 def set_sort_options!(client, options)
682 klass = options[:class]
683 fields = klass ? klass.sphinx_indexes.collect { |index|
684 index.fields.collect { |field| field.unique_name }
685 }.flatten : []
686 index_options = klass ? klass.sphinx_index_options : {}
687
688 order = options[:order] || index_options[:order]
689 case order
690 when Symbol
691 client.sort_mode = :attr_asc if client.sort_mode == :relevance || client.sort_mode.nil?
692 if fields.include?(order)
693 client.sort_by = order.to_s.concat("_sort")
694 else
695 client.sort_by = order.to_s
696 end
697 when String
698 client.sort_mode = :extended
699 client.sort_by = sorted_fields_to_attributes(order, fields)
700 else
701 # do nothing
702 end
703
704 client.sort_mode = :attr_asc if client.sort_mode == :asc
705 client.sort_mode = :attr_desc if client.sort_mode == :desc
706 end
707
708 # Search through a collection of fields and translate any appearances
709 # of them in a string to their attribute equivalent for sorting.
710 #
711 def sorted_fields_to_attributes(string, fields)
712 fields.each { |field|
713 string.gsub!(/(^|\s)#{field}(,?\s|$)/) { |match|
714 match.gsub field.to_s, field.to_s.concat("_sort")
715 }
716 }
717
718 string
719 end
720
721 def facets_for_model(klass, args, options)
722 hash = ThinkingSphinx::FacetCollection.new args + [options]
723 options = options.clone.merge! :group_function => :attr
724
725 klass.sphinx_facets.inject(hash) do |hash, facet|
726 unless facet.name == :class && !options[:class_facet]
727 options[:group_by] = facet.attribute_name
728 hash.add_from_results facet, search(*(args + [options]))
729 end
730
731 hash
732 end
733 end
734
735 def facets_for_all_models(args, options)
736 options = GlobalFacetOptions.merge(options)
737 hash = ThinkingSphinx::FacetCollection.new args + [options]
738 options = options.merge! :group_function => :attr
739
740 facet_names(options).inject(hash) do |hash, name|
741 options[:group_by] = name
742 hash.add_from_results name, search(*(args + [options]))
743 hash
744 end
745 end
746
747 def facet_classes(options)
748 options[:classes] || ThinkingSphinx.indexed_models.collect { |model|
749 model.constantize
750 }
751 end
752
753 def facet_names(options)
754 classes = facet_classes(options)
755 names = options[:all_attributes] ?
756 facet_names_for_all_classes(classes) :
757 facet_names_common_to_all_classes(classes)
758
759 names.delete "class_crc" unless options[:class_facet]
760 names
761 end
762
763 def facet_names_for_all_classes(classes)
764 classes.collect { |klass|
765 klass.sphinx_facets.collect { |facet| facet.attribute_name }
766 }.flatten.uniq
767 end
768
769 def facet_names_common_to_all_classes(classes)
770 facet_names_for_all_classes(classes).select { |name|
771 classes.all? { |klass|
772 klass.sphinx_facets.detect { |facet|
773 facet.attribute_name == name
774 }
775 }
776 }
777 end
778 end
779 end
780end
diff --git a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/tasks.rb b/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/tasks.rb
deleted file mode 100644
index ab8990e..0000000
--- a/vendor/plugins/thinking-sphinx/lib/thinking_sphinx/tasks.rb
+++ /dev/null
@@ -1,128 +0,0 @@
1require 'fileutils'
2
3namespace :thinking_sphinx do
4 task :app_env do
5 Rake::Task[:environment].invoke if defined?(RAILS_ROOT)
6 Rake::Task[:merb_env].invoke if defined?(Merb)
7 end
8
9 desc "Stop if running, then start a Sphinx searchd daemon using Thinking Sphinx's settings"
10 task :running_start => :app_env do
11 Rake::Task["thinking_sphinx:stop"].invoke if sphinx_running?
12 Rake::Task["thinking_sphinx:start"].invoke
13 end
14
15 desc "Start a Sphinx searchd daemon using Thinking Sphinx's settings"
16 task :start => :app_env do
17 config = ThinkingSphinx::Configuration.instance
18
19 FileUtils.mkdir_p config.searchd_file_path
20 raise RuntimeError, "searchd is already running." if sphinx_running?
21
22 Dir["#{config.searchd_file_path}/*.spl"].each { |file| File.delete(file) }
23
24 cmd = "#{config.bin_path}searchd --pidfile --config #{config.config_file}"
25 puts cmd
26 system cmd
27
28 sleep(2)
29
30 if sphinx_running?
31 puts "Started successfully (pid #{sphinx_pid})."
32 else
33 puts "Failed to start searchd daemon. Check #{config.searchd_log_file}."
34 end
35 end
36
37 desc "Stop Sphinx using Thinking Sphinx's settings"
38 task :stop => :app_env do
39 raise RuntimeError, "searchd is not running." unless sphinx_running?
40 config = ThinkingSphinx::Configuration.instance
41 pid = sphinx_pid
42 system "#{config.bin_path}searchd --stop --config #{config.config_file}"
43 puts "Stopped search daemon (pid #{pid})."
44 end
45
46 desc "Restart Sphinx"
47 task :restart => [:app_env, :stop, :start]
48
49 desc "Generate the Sphinx configuration file using Thinking Sphinx's settings"
50 task :configure => :app_env do
51 config = ThinkingSphinx::Configuration.instance
52 puts "Generating Configuration to #{config.config_file}"
53 config.build
54 end
55
56 desc "Index data for Sphinx using Thinking Sphinx's settings"
57 task :index => :app_env do
58 ThinkingSphinx::Deltas::Job.cancel_thinking_sphinx_jobs
59
60 config = ThinkingSphinx::Configuration.instance
61 unless ENV["INDEX_ONLY"] == "true"
62 puts "Generating Configuration to #{config.config_file}"
63 config.build
64 end
65
66 FileUtils.mkdir_p config.searchd_file_path
67 cmd = "#{config.bin_path}indexer --config #{config.config_file} --all"
68 cmd << " --rotate" if sphinx_running?
69 puts cmd
70 system cmd
71 end
72
73 namespace :index do
74 task :delta => :app_env do
75 ThinkingSphinx.indexed_models.select { |model|
76 model.constantize.sphinx_indexes.any? { |index| index.delta? }
77 }.each do |model|
78 model.constantize.sphinx_indexes.select { |index|
79 index.delta? && index.delta_object.respond_to?(:delayed_index)
80 }.each { |index|
81 index.delta_object.delayed_index(index.model)
82 }
83 end
84 end
85 end
86
87 desc "Process stored delta index requests"
88 task :delayed_delta => :app_env do
89 require 'delayed/worker'
90
91 Delayed::Worker.new(
92 :min_priority => ENV['MIN_PRIORITY'],
93 :max_priority => ENV['MAX_PRIORITY']
94 ).start
95 end
96end
97
98namespace :ts do
99 desc "Stop if running, then start a Sphinx searchd daemon using Thinking Sphinx's settings"
100 task :run => "thinking_sphinx:running_start"
101 desc "Start a Sphinx searchd daemon using Thinking Sphinx's settings"
102 task :start => "thinking_sphinx:start"
103 desc "Stop Sphinx using Thinking Sphinx's settings"
104 task :stop => "thinking_sphinx:stop"
105 desc "Index data for Sphinx using Thinking Sphinx's settings"
106 task :in => "thinking_sphinx:index"
107 namespace :in do
108 desc "Index Thinking Sphinx datetime delta indexes"
109 task :delta => "thinking_sphinx:index:delta"
110 end
111 task :index => "thinking_sphinx:index"
112 desc "Restart Sphinx"
113 task :restart => "thinking_sphinx:restart"
114 desc "Generate the Sphinx configuration file using Thinking Sphinx's settings"
115 task :conf => "thinking_sphinx:configure"
116 desc "Generate the Sphinx configuration file using Thinking Sphinx's settings"
117 task :config => "thinking_sphinx:configure"
118 desc "Process stored delta index requests"
119 task :dd => "thinking_sphinx:delayed_delta"
120end
121
122def sphinx_pid
123 ThinkingSphinx.sphinx_pid
124end
125
126def sphinx_running?
127 ThinkingSphinx.sphinx_running?
128end