zipme-thinking-sphinx 1.3.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (158) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +168 -0
  3. data/VERSION +1 -0
  4. data/features/abstract_inheritance.feature +10 -0
  5. data/features/alternate_primary_key.feature +27 -0
  6. data/features/attribute_transformation.feature +22 -0
  7. data/features/attribute_updates.feature +51 -0
  8. data/features/deleting_instances.feature +67 -0
  9. data/features/direct_attributes.feature +11 -0
  10. data/features/excerpts.feature +13 -0
  11. data/features/extensible_delta_indexing.feature +9 -0
  12. data/features/facets.feature +82 -0
  13. data/features/facets_across_model.feature +29 -0
  14. data/features/handling_edits.feature +92 -0
  15. data/features/retry_stale_indexes.feature +24 -0
  16. data/features/searching_across_models.feature +20 -0
  17. data/features/searching_by_index.feature +40 -0
  18. data/features/searching_by_model.feature +175 -0
  19. data/features/searching_with_find_arguments.feature +56 -0
  20. data/features/sphinx_detection.feature +25 -0
  21. data/features/sphinx_scopes.feature +42 -0
  22. data/features/step_definitions/alpha_steps.rb +16 -0
  23. data/features/step_definitions/beta_steps.rb +7 -0
  24. data/features/step_definitions/common_steps.rb +188 -0
  25. data/features/step_definitions/extensible_delta_indexing_steps.rb +7 -0
  26. data/features/step_definitions/facet_steps.rb +96 -0
  27. data/features/step_definitions/find_arguments_steps.rb +36 -0
  28. data/features/step_definitions/gamma_steps.rb +15 -0
  29. data/features/step_definitions/scope_steps.rb +15 -0
  30. data/features/step_definitions/search_steps.rb +89 -0
  31. data/features/step_definitions/sphinx_steps.rb +35 -0
  32. data/features/sti_searching.feature +19 -0
  33. data/features/support/database.example.yml +3 -0
  34. data/features/support/db/fixtures/alphas.rb +10 -0
  35. data/features/support/db/fixtures/authors.rb +1 -0
  36. data/features/support/db/fixtures/betas.rb +10 -0
  37. data/features/support/db/fixtures/boxes.rb +9 -0
  38. data/features/support/db/fixtures/categories.rb +1 -0
  39. data/features/support/db/fixtures/cats.rb +3 -0
  40. data/features/support/db/fixtures/comments.rb +24 -0
  41. data/features/support/db/fixtures/developers.rb +29 -0
  42. data/features/support/db/fixtures/dogs.rb +3 -0
  43. data/features/support/db/fixtures/extensible_betas.rb +10 -0
  44. data/features/support/db/fixtures/foxes.rb +3 -0
  45. data/features/support/db/fixtures/gammas.rb +10 -0
  46. data/features/support/db/fixtures/music.rb +4 -0
  47. data/features/support/db/fixtures/people.rb +1001 -0
  48. data/features/support/db/fixtures/posts.rb +6 -0
  49. data/features/support/db/fixtures/robots.rb +14 -0
  50. data/features/support/db/fixtures/tags.rb +27 -0
  51. data/features/support/db/migrations/create_alphas.rb +8 -0
  52. data/features/support/db/migrations/create_animals.rb +5 -0
  53. data/features/support/db/migrations/create_authors.rb +3 -0
  54. data/features/support/db/migrations/create_authors_posts.rb +6 -0
  55. data/features/support/db/migrations/create_betas.rb +5 -0
  56. data/features/support/db/migrations/create_boxes.rb +5 -0
  57. data/features/support/db/migrations/create_categories.rb +3 -0
  58. data/features/support/db/migrations/create_comments.rb +10 -0
  59. data/features/support/db/migrations/create_developers.rb +9 -0
  60. data/features/support/db/migrations/create_extensible_betas.rb +5 -0
  61. data/features/support/db/migrations/create_gammas.rb +3 -0
  62. data/features/support/db/migrations/create_genres.rb +3 -0
  63. data/features/support/db/migrations/create_music.rb +6 -0
  64. data/features/support/db/migrations/create_people.rb +13 -0
  65. data/features/support/db/migrations/create_posts.rb +5 -0
  66. data/features/support/db/migrations/create_robots.rb +4 -0
  67. data/features/support/db/migrations/create_taggings.rb +5 -0
  68. data/features/support/db/migrations/create_tags.rb +4 -0
  69. data/features/support/env.rb +21 -0
  70. data/features/support/lib/generic_delta_handler.rb +8 -0
  71. data/features/support/models/alpha.rb +22 -0
  72. data/features/support/models/animal.rb +5 -0
  73. data/features/support/models/author.rb +3 -0
  74. data/features/support/models/beta.rb +8 -0
  75. data/features/support/models/box.rb +8 -0
  76. data/features/support/models/cat.rb +3 -0
  77. data/features/support/models/category.rb +4 -0
  78. data/features/support/models/comment.rb +10 -0
  79. data/features/support/models/developer.rb +16 -0
  80. data/features/support/models/dog.rb +3 -0
  81. data/features/support/models/extensible_beta.rb +9 -0
  82. data/features/support/models/fox.rb +5 -0
  83. data/features/support/models/gamma.rb +5 -0
  84. data/features/support/models/genre.rb +3 -0
  85. data/features/support/models/medium.rb +5 -0
  86. data/features/support/models/music.rb +8 -0
  87. data/features/support/models/person.rb +23 -0
  88. data/features/support/models/post.rb +21 -0
  89. data/features/support/models/robot.rb +12 -0
  90. data/features/support/models/tag.rb +3 -0
  91. data/features/support/models/tagging.rb +4 -0
  92. data/lib/cucumber/thinking_sphinx/external_world.rb +8 -0
  93. data/lib/cucumber/thinking_sphinx/internal_world.rb +126 -0
  94. data/lib/cucumber/thinking_sphinx/sql_logger.rb +20 -0
  95. data/lib/thinking_sphinx.rb +231 -0
  96. data/lib/thinking_sphinx/active_record.rb +373 -0
  97. data/lib/thinking_sphinx/active_record/attribute_updates.rb +46 -0
  98. data/lib/thinking_sphinx/active_record/delta.rb +61 -0
  99. data/lib/thinking_sphinx/active_record/has_many_association.rb +29 -0
  100. data/lib/thinking_sphinx/active_record/scopes.rb +75 -0
  101. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +42 -0
  102. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +54 -0
  103. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +143 -0
  104. data/lib/thinking_sphinx/association.rb +164 -0
  105. data/lib/thinking_sphinx/attribute.rb +362 -0
  106. data/lib/thinking_sphinx/auto_version.rb +22 -0
  107. data/lib/thinking_sphinx/class_facet.rb +15 -0
  108. data/lib/thinking_sphinx/configuration.rb +283 -0
  109. data/lib/thinking_sphinx/context.rb +68 -0
  110. data/lib/thinking_sphinx/core/array.rb +7 -0
  111. data/lib/thinking_sphinx/core/string.rb +15 -0
  112. data/lib/thinking_sphinx/deltas.rb +28 -0
  113. data/lib/thinking_sphinx/deltas/default_delta.rb +62 -0
  114. data/lib/thinking_sphinx/deploy/capistrano.rb +100 -0
  115. data/lib/thinking_sphinx/excerpter.rb +22 -0
  116. data/lib/thinking_sphinx/facet.rb +125 -0
  117. data/lib/thinking_sphinx/facet_search.rb +136 -0
  118. data/lib/thinking_sphinx/field.rb +82 -0
  119. data/lib/thinking_sphinx/index.rb +157 -0
  120. data/lib/thinking_sphinx/index/builder.rb +296 -0
  121. data/lib/thinking_sphinx/index/faux_column.rb +110 -0
  122. data/lib/thinking_sphinx/property.rb +162 -0
  123. data/lib/thinking_sphinx/rails_additions.rb +150 -0
  124. data/lib/thinking_sphinx/search.rb +775 -0
  125. data/lib/thinking_sphinx/search_methods.rb +439 -0
  126. data/lib/thinking_sphinx/source.rb +153 -0
  127. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  128. data/lib/thinking_sphinx/source/sql.rb +130 -0
  129. data/lib/thinking_sphinx/tasks.rb +121 -0
  130. data/lib/thinking_sphinx/test.rb +52 -0
  131. data/rails/init.rb +16 -0
  132. data/spec/thinking_sphinx/active_record/delta_spec.rb +128 -0
  133. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +55 -0
  134. data/spec/thinking_sphinx/active_record/scopes_spec.rb +177 -0
  135. data/spec/thinking_sphinx/active_record_spec.rb +622 -0
  136. data/spec/thinking_sphinx/association_spec.rb +239 -0
  137. data/spec/thinking_sphinx/attribute_spec.rb +570 -0
  138. data/spec/thinking_sphinx/auto_version_spec.rb +39 -0
  139. data/spec/thinking_sphinx/configuration_spec.rb +234 -0
  140. data/spec/thinking_sphinx/context_spec.rb +119 -0
  141. data/spec/thinking_sphinx/core/array_spec.rb +9 -0
  142. data/spec/thinking_sphinx/core/string_spec.rb +9 -0
  143. data/spec/thinking_sphinx/excerpter_spec.rb +57 -0
  144. data/spec/thinking_sphinx/facet_search_spec.rb +176 -0
  145. data/spec/thinking_sphinx/facet_spec.rb +333 -0
  146. data/spec/thinking_sphinx/field_spec.rb +154 -0
  147. data/spec/thinking_sphinx/index/builder_spec.rb +479 -0
  148. data/spec/thinking_sphinx/index/faux_column_spec.rb +30 -0
  149. data/spec/thinking_sphinx/index_spec.rb +183 -0
  150. data/spec/thinking_sphinx/rails_additions_spec.rb +203 -0
  151. data/spec/thinking_sphinx/search_methods_spec.rb +152 -0
  152. data/spec/thinking_sphinx/search_spec.rb +1181 -0
  153. data/spec/thinking_sphinx/source_spec.rb +235 -0
  154. data/spec/thinking_sphinx_spec.rb +204 -0
  155. data/tasks/distribution.rb +39 -0
  156. data/tasks/rails.rake +1 -0
  157. data/tasks/testing.rb +72 -0
  158. metadata +242 -0
@@ -0,0 +1,4 @@
1
+ class Tagging < ActiveRecord::Base
2
+ belongs_to :tag
3
+ belongs_to :taggable, :polymorphic => true
4
+ end
@@ -0,0 +1,8 @@
1
+ require 'thinking_sphinx/test'
2
+
3
+ class Cucumber::ThinkingSphinx::ExternalWorld
4
+ def initialize(suppress_delta_output = true)
5
+ ::ThinkingSphinx::Test.init
6
+ ::ThinkingSphinx::Test.start_with_autostop
7
+ end
8
+ end
@@ -0,0 +1,126 @@
1
+ require 'cucumber/thinking_sphinx/sql_logger'
2
+
3
+ module Cucumber
4
+ module ThinkingSphinx
5
+ class InternalWorld
6
+ attr_accessor :temporary_directory, :migrations_directory,
7
+ :models_directory, :fixtures_directory, :database_file
8
+ attr_accessor :adapter, :database, :username,
9
+ :password, :host
10
+
11
+ def initialize
12
+ @temporary_directory = "#{Dir.pwd}/tmp"
13
+ @migrations_directory = "features/support/db/migrations"
14
+ @models_directory = "features/support/models"
15
+ @fixtures_directory = "features/support/db/fixtures"
16
+ @database_file = "features/support/database.yml"
17
+
18
+ @adapter = ENV['DATABASE'] || 'mysql'
19
+ @database = 'thinking_sphinx'
20
+ @username = 'thinking_sphinx'
21
+ # @password = 'thinking_sphinx'
22
+ @host = 'localhost'
23
+ end
24
+
25
+ def setup
26
+ make_temporary_directory
27
+
28
+ configure_cleanup
29
+ configure_thinking_sphinx
30
+ configure_active_record
31
+
32
+ prepare_data
33
+ setup_sphinx
34
+
35
+ self
36
+ end
37
+
38
+ def configure_database
39
+ ActiveRecord::Base.establish_connection database_settings
40
+ self
41
+ end
42
+
43
+ private
44
+
45
+ def config
46
+ @config ||= ::ThinkingSphinx::Configuration.instance
47
+ end
48
+
49
+ def make_temporary_directory
50
+ FileUtils.mkdir_p temporary_directory
51
+ Dir["#{temporary_directory}/*"].each do |file|
52
+ FileUtils.rm_rf file
53
+ end
54
+ end
55
+
56
+ def configure_thinking_sphinx
57
+ config.config_file = "#{temporary_directory}/sphinx.conf"
58
+ config.searchd_log_file = "#{temporary_directory}/searchd.log"
59
+ config.query_log_file = "#{temporary_directory}/searchd.query.log"
60
+ config.pid_file = "#{temporary_directory}/searchd.pid"
61
+ config.searchd_file_path = "#{temporary_directory}/indexes/"
62
+
63
+ ::ThinkingSphinx.suppress_delta_output = true
64
+ end
65
+
66
+ def configure_cleanup
67
+ Kernel.at_exit do
68
+ ::ThinkingSphinx::Configuration.instance.controller.stop
69
+ sleep(0.5) # Ensure Sphinx has shut down completely
70
+ ActiveRecord::Base.logger.close
71
+ end
72
+ end
73
+
74
+ def yaml_database_settings
75
+ return {} unless File.exist?(@database_file)
76
+
77
+ YAML.load open(@database_file)
78
+ end
79
+
80
+ def database_settings
81
+ {
82
+ 'adapter' => @adapter,
83
+ 'database' => @database,
84
+ 'username' => @username,
85
+ 'password' => @password,
86
+ 'host' => @host
87
+ }.merge yaml_database_settings
88
+ end
89
+
90
+ def configure_active_record
91
+ ActiveRecord::Base.logger = Logger.new(
92
+ open("#{temporary_directory}/active_record.log", "a")
93
+ )
94
+
95
+ ActiveRecord::Base.connection.class.send(
96
+ :include, Cucumber::ThinkingSphinx::SqlLogger
97
+ )
98
+ end
99
+
100
+ def prepare_data
101
+ ::ThinkingSphinx.deltas_enabled = false
102
+
103
+ load_files migrations_directory
104
+ load_files models_directory
105
+ load_files fixtures_directory
106
+
107
+ ::ThinkingSphinx.deltas_enabled = true
108
+ end
109
+
110
+ def load_files(path)
111
+ files = Dir["#{path}/*.rb"].sort!
112
+ files.each do |file|
113
+ require file.gsub(/\.rb$/, '')
114
+ end
115
+ end
116
+
117
+ def setup_sphinx
118
+ FileUtils.mkdir_p config.searchd_file_path
119
+
120
+ config.build
121
+ config.controller.index
122
+ config.controller.start
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,20 @@
1
+ module Cucumber
2
+ module ThinkingSphinx
3
+ module SqlLogger
4
+ def self.included(base)
5
+ base.send :alias_method_chain, :execute, :query_record
6
+ end
7
+
8
+ IGNORED_SQL = [
9
+ /^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/,
10
+ /^SELECT @@ROWCOUNT/, /^SHOW FIELDS/
11
+ ]
12
+
13
+ def execute_with_query_record(sql, name = nil, &block)
14
+ $queries_executed ||= []
15
+ $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r }
16
+ execute_without_query_record(sql, name, &block)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,231 @@
1
+ require 'active_record'
2
+ require 'after_commit'
3
+ require 'yaml'
4
+ require 'cgi'
5
+ require 'riddle'
6
+
7
+ require 'thinking_sphinx/auto_version'
8
+ require 'thinking_sphinx/core/array'
9
+ require 'thinking_sphinx/core/string'
10
+ require 'thinking_sphinx/property'
11
+ require 'thinking_sphinx/active_record'
12
+ require 'thinking_sphinx/association'
13
+ require 'thinking_sphinx/attribute'
14
+ require 'thinking_sphinx/configuration'
15
+ require 'thinking_sphinx/context'
16
+ require 'thinking_sphinx/excerpter'
17
+ require 'thinking_sphinx/facet'
18
+ require 'thinking_sphinx/class_facet'
19
+ require 'thinking_sphinx/facet_search'
20
+ require 'thinking_sphinx/field'
21
+ require 'thinking_sphinx/index'
22
+ require 'thinking_sphinx/source'
23
+ require 'thinking_sphinx/rails_additions'
24
+ require 'thinking_sphinx/search'
25
+ require 'thinking_sphinx/search_methods'
26
+ require 'thinking_sphinx/deltas'
27
+
28
+ require 'thinking_sphinx/adapters/abstract_adapter'
29
+ require 'thinking_sphinx/adapters/mysql_adapter'
30
+ require 'thinking_sphinx/adapters/postgresql_adapter'
31
+
32
+ ActiveRecord::Base.send(:include, ThinkingSphinx::ActiveRecord)
33
+
34
+ Merb::Plugins.add_rakefiles(
35
+ File.join(File.dirname(__FILE__), "thinking_sphinx", "tasks")
36
+ ) if defined?(Merb)
37
+
38
+ module ThinkingSphinx
39
+ # A ConnectionError will get thrown when a connection to Sphinx can't be
40
+ # made.
41
+ class ConnectionError < StandardError
42
+ end
43
+
44
+ # A StaleIdsException is thrown by Collection.instances_from_matches if there
45
+ # are records in Sphinx but not in the database, so the search can be retried.
46
+ class StaleIdsException < StandardError
47
+ attr_accessor :ids
48
+ def initialize(ids)
49
+ self.ids = ids
50
+ end
51
+ end
52
+
53
+ # The current version of Thinking Sphinx.
54
+ #
55
+ # @return [String] The version number as a string
56
+ #
57
+ def self.version
58
+ open(File.join(File.dirname(__FILE__), '../VERSION')) { |f|
59
+ f.read.strip
60
+ }
61
+ end
62
+
63
+ # The collection of indexed models. Keep in mind that Rails lazily loads
64
+ # its classes, so this may not actually be populated with _all_ the models
65
+ # that have Sphinx indexes.
66
+ def self.context
67
+ if Thread.current[:thinking_sphinx_context].nil?
68
+ Thread.current[:thinking_sphinx_context] = ThinkingSphinx::Context.new
69
+ Thread.current[:thinking_sphinx_context].prepare
70
+ end
71
+
72
+ Thread.current[:thinking_sphinx_context]
73
+ end
74
+
75
+ def self.reset_context!
76
+ Thread.current[:thinking_sphinx_context] = nil
77
+ end
78
+
79
+ def self.unique_id_expression(offset = nil)
80
+ "* #{context.indexed_models.size} + #{offset || 0}"
81
+ end
82
+
83
+ # Check if index definition is disabled.
84
+ #
85
+ def self.define_indexes?
86
+ if Thread.current[:thinking_sphinx_define_indexes].nil?
87
+ Thread.current[:thinking_sphinx_define_indexes] = true
88
+ end
89
+
90
+ Thread.current[:thinking_sphinx_define_indexes]
91
+ end
92
+
93
+ # Enable/disable indexes - you may want to do this while migrating data.
94
+ #
95
+ # ThinkingSphinx.define_indexes = false
96
+ #
97
+ def self.define_indexes=(value)
98
+ Thread.current[:thinking_sphinx_define_indexes] = value
99
+ end
100
+
101
+ # Check if delta indexing is enabled.
102
+ #
103
+ def self.deltas_enabled?
104
+ if Thread.current[:thinking_sphinx_deltas_enabled].nil?
105
+ Thread.current[:thinking_sphinx_deltas_enabled] = (
106
+ ThinkingSphinx::Configuration.environment != "test"
107
+ )
108
+ end
109
+
110
+ Thread.current[:thinking_sphinx_deltas_enabled]
111
+ end
112
+
113
+ # Enable/disable all delta indexing.
114
+ #
115
+ # ThinkingSphinx.deltas_enabled = false
116
+ #
117
+ def self.deltas_enabled=(value)
118
+ Thread.current[:thinking_sphinx_deltas_enabled] = value
119
+ end
120
+
121
+ # Check if updates are enabled. True by default, unless within the test
122
+ # environment.
123
+ #
124
+ def self.updates_enabled?
125
+ if Thread.current[:thinking_sphinx_updates_enabled].nil?
126
+ Thread.current[:thinking_sphinx_updates_enabled] = (
127
+ ThinkingSphinx::Configuration.environment != "test"
128
+ )
129
+ end
130
+
131
+ Thread.current[:thinking_sphinx_updates_enabled]
132
+ end
133
+
134
+ # Enable/disable updates to Sphinx
135
+ #
136
+ # ThinkingSphinx.updates_enabled = false
137
+ #
138
+ def self.updates_enabled=(value)
139
+ Thread.current[:thinking_sphinx_updates_enabled] = value
140
+ end
141
+
142
+ def self.suppress_delta_output?
143
+ Thread.current[:thinking_sphinx_suppress_delta_output] ||= false
144
+ end
145
+
146
+ def self.suppress_delta_output=(value)
147
+ Thread.current[:thinking_sphinx_suppress_delta_output] = value
148
+ end
149
+
150
+ # Checks to see if MySQL will allow simplistic GROUP BY statements. If not,
151
+ # or if not using MySQL, this will return false.
152
+ #
153
+ def self.use_group_by_shortcut?
154
+ if Thread.current[:thinking_sphinx_use_group_by_shortcut].nil?
155
+ Thread.current[:thinking_sphinx_use_group_by_shortcut] = !!(
156
+ mysql? && ::ActiveRecord::Base.connection.select_all(
157
+ "SELECT @@global.sql_mode, @@session.sql_mode;"
158
+ ).all? { |key,value| value.nil? || value[/ONLY_FULL_GROUP_BY/].nil? }
159
+ )
160
+ end
161
+
162
+ Thread.current[:thinking_sphinx_use_group_by_shortcut]
163
+ end
164
+
165
+ # An indication of whether Sphinx is running on a remote machine instead of
166
+ # the same machine.
167
+ #
168
+ def self.remote_sphinx?
169
+ Thread.current[:thinking_sphinx_remote_sphinx] ||= false
170
+ end
171
+
172
+ # Tells Thinking Sphinx that Sphinx is running on a different machine, and
173
+ # thus it can't reliably guess whether it is running or not (ie: the
174
+ # #sphinx_running? method), and so just assumes it is.
175
+ #
176
+ # Useful for multi-machine deployments. Set it in your production.rb file.
177
+ #
178
+ # ThinkingSphinx.remote_sphinx = true
179
+ #
180
+ def self.remote_sphinx=(value)
181
+ Thread.current[:thinking_sphinx_remote_sphinx] = value
182
+ end
183
+
184
+ # Check if Sphinx is running. If remote_sphinx is set to true (indicating
185
+ # Sphinx is on a different machine), this will always return true, and you
186
+ # will have to handle any connection errors yourself.
187
+ #
188
+ def self.sphinx_running?
189
+ remote_sphinx? || sphinx_running_by_pid?
190
+ end
191
+
192
+ # Check if Sphinx is actually running, provided the pid is on the same
193
+ # machine as this code.
194
+ #
195
+ def self.sphinx_running_by_pid?
196
+ !!sphinx_pid && pid_active?(sphinx_pid)
197
+ end
198
+
199
+ def self.sphinx_pid
200
+ if File.exists?(ThinkingSphinx::Configuration.instance.pid_file)
201
+ File.read(ThinkingSphinx::Configuration.instance.pid_file)[/\d+/]
202
+ else
203
+ nil
204
+ end
205
+ end
206
+
207
+ def self.pid_active?(pid)
208
+ !!Process.kill(0, pid.to_i)
209
+ rescue Exception => e
210
+ false
211
+ end
212
+
213
+ def self.microsoft?
214
+ RUBY_PLATFORM =~ /mswin/
215
+ end
216
+
217
+ def self.jruby?
218
+ defined?(JRUBY_VERSION)
219
+ end
220
+
221
+ def self.mysql?
222
+ ::ActiveRecord::Base.connection.class.name.demodulize == "MysqlAdapter" ||
223
+ ::ActiveRecord::Base.connection.class.name.demodulize == "MysqlplusAdapter" || (
224
+ jruby? && ::ActiveRecord::Base.connection.config[:adapter] == "jdbcmysql"
225
+ )
226
+ end
227
+
228
+ extend ThinkingSphinx::SearchMethods::ClassMethods
229
+ end
230
+
231
+ ThinkingSphinx::AutoVersion.detect
@@ -0,0 +1,373 @@
1
+ require 'thinking_sphinx/active_record/attribute_updates'
2
+ require 'thinking_sphinx/active_record/delta'
3
+ require 'thinking_sphinx/active_record/has_many_association'
4
+ require 'thinking_sphinx/active_record/scopes'
5
+
6
+ module ThinkingSphinx
7
+ # Core additions to ActiveRecord models - define_index for creating indexes
8
+ # for models. If you want to interrogate the index objects created for the
9
+ # model, you can use the class-level accessor :sphinx_indexes.
10
+ #
11
+ module ActiveRecord
12
+ def self.included(base)
13
+ base.class_eval do
14
+ class_inheritable_array :sphinx_indexes, :sphinx_facets
15
+
16
+ extend ThinkingSphinx::ActiveRecord::ClassMethods
17
+
18
+ class << self
19
+ attr_accessor :sphinx_index_blocks
20
+
21
+ def set_sphinx_primary_key(attribute)
22
+ @sphinx_primary_key_attribute = attribute
23
+ end
24
+
25
+ def primary_key_for_sphinx
26
+ @sphinx_primary_key_attribute || primary_key
27
+ end
28
+
29
+ def sphinx_index_options
30
+ sphinx_indexes.last.options
31
+ end
32
+
33
+ # Generate a unique CRC value for the model's name, to use to
34
+ # determine which Sphinx documents belong to which AR records.
35
+ #
36
+ # Really only written for internal use - but hey, if it's useful to
37
+ # you in some other way, awesome.
38
+ #
39
+ def to_crc32
40
+ self.name.to_crc32
41
+ end
42
+
43
+ def to_crc32s
44
+ (subclasses << self).collect { |klass| klass.to_crc32 }
45
+ end
46
+
47
+ def sphinx_database_adapter
48
+ @sphinx_database_adapter ||=
49
+ ThinkingSphinx::AbstractAdapter.detect(self)
50
+ end
51
+
52
+ def sphinx_name
53
+ self.name.underscore.tr(':/\\', '_')
54
+ end
55
+
56
+ #
57
+ # The above method to_crc32s is dependant on the subclasses being loaded consistently
58
+ # After a reset_subclasses is called (during a Dispatcher.cleanup_application in development)
59
+ # Our subclasses will be lost but our context will not reload them for us.
60
+ #
61
+ # We reset the context which causes the subclasses to be reloaded next time the context is called.
62
+ #
63
+ def reset_subclasses_with_thinking_sphinx
64
+ reset_subclasses_without_thinking_sphinx
65
+ ThinkingSphinx.reset_context!
66
+ end
67
+
68
+ alias_method_chain :reset_subclasses, :thinking_sphinx
69
+
70
+ private
71
+
72
+ def defined_indexes?
73
+ @defined_indexes
74
+ end
75
+
76
+ def defined_indexes=(value)
77
+ @defined_indexes = value
78
+ end
79
+
80
+ def sphinx_delta?
81
+ self.sphinx_indexes.any? { |index| index.delta? }
82
+ end
83
+ end
84
+ end
85
+
86
+ ::ActiveRecord::Associations::HasManyAssociation.send(
87
+ :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
88
+ )
89
+ ::ActiveRecord::Associations::HasManyThroughAssociation.send(
90
+ :include, ThinkingSphinx::ActiveRecord::HasManyAssociation
91
+ )
92
+ end
93
+
94
+ module ClassMethods
95
+ # Allows creation of indexes for Sphinx. If you don't do this, there
96
+ # isn't much point trying to search (or using this plugin at all,
97
+ # really).
98
+ #
99
+ # An example or two:
100
+ #
101
+ # define_index
102
+ # indexes :id, :as => :model_id
103
+ # indexes name
104
+ # end
105
+ #
106
+ # You can also grab fields from associations - multiple levels deep
107
+ # if necessary.
108
+ #
109
+ # define_index do
110
+ # indexes tags.name, :as => :tag
111
+ # indexes articles.content
112
+ # indexes orders.line_items.product.name, :as => :product
113
+ # end
114
+ #
115
+ # And it will automatically concatenate multiple fields:
116
+ #
117
+ # define_index do
118
+ # indexes [author.first_name, author.last_name], :as => :author
119
+ # end
120
+ #
121
+ # The #indexes method is for fields - if you want attributes, use
122
+ # #has instead. All the same rules apply - but keep in mind that
123
+ # attributes are for sorting, grouping and filtering, not searching.
124
+ #
125
+ # define_index do
126
+ # # fields ...
127
+ #
128
+ # has created_at, updated_at
129
+ # end
130
+ #
131
+ # One last feature is the delta index. This requires the model to
132
+ # have a boolean field named 'delta', and is enabled as follows:
133
+ #
134
+ # define_index do
135
+ # # fields ...
136
+ # # attributes ...
137
+ #
138
+ # set_property :delta => true
139
+ # end
140
+ #
141
+ # Check out the more detailed documentation for each of these methods
142
+ # at ThinkingSphinx::Index::Builder.
143
+ #
144
+ def define_index(name = nil, &block)
145
+ self.sphinx_index_blocks ||= []
146
+ self.sphinx_indexes ||= []
147
+ self.sphinx_facets ||= []
148
+
149
+ ThinkingSphinx.context.add_indexed_model self
150
+
151
+ if sphinx_index_blocks.empty?
152
+ before_validation :define_indexes
153
+ before_destroy :define_indexes
154
+ end
155
+
156
+ self.sphinx_index_blocks << lambda {
157
+ index = ThinkingSphinx::Index::Builder.generate self, name, &block
158
+ add_sphinx_callbacks_and_extend(index.delta?)
159
+ add_sphinx_index index
160
+ }
161
+
162
+ include ThinkingSphinx::ActiveRecord::Scopes
163
+ include ThinkingSphinx::SearchMethods
164
+ end
165
+
166
+ def define_indexes
167
+ superclass.define_indexes unless superclass == ::ActiveRecord::Base
168
+
169
+ return if sphinx_index_blocks.nil? ||
170
+ defined_indexes? ||
171
+ !ThinkingSphinx.define_indexes?
172
+
173
+ sphinx_index_blocks.each do |block|
174
+ block.call
175
+ end
176
+
177
+ self.defined_indexes = true
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
+ def add_sphinx_index(index)
192
+ self.sphinx_indexes << index
193
+ subclasses.each { |klass| klass.add_sphinx_index(index) }
194
+ end
195
+
196
+ def has_sphinx_indexes?
197
+ sphinx_indexes &&
198
+ sphinx_index_blocks &&
199
+ (sphinx_indexes.length > 0 || sphinx_index_blocks.length > 0)
200
+ end
201
+
202
+ def indexed_by_sphinx?
203
+ sphinx_indexes && sphinx_indexes.length > 0
204
+ end
205
+
206
+ def delta_indexed_by_sphinx?
207
+ sphinx_indexes && sphinx_indexes.any? { |index| index.delta? }
208
+ end
209
+
210
+ def sphinx_index_names
211
+ define_indexes
212
+ sphinx_indexes.collect(&:all_names).flatten
213
+ end
214
+
215
+ def core_index_names
216
+ define_indexes
217
+ sphinx_indexes.collect(&:core_name)
218
+ end
219
+
220
+ def delta_index_names
221
+ define_indexes
222
+ sphinx_indexes.select(&:delta?).collect(&:delta_name)
223
+ end
224
+
225
+ def to_riddle
226
+ define_indexes
227
+ sphinx_database_adapter.setup
228
+
229
+ local_sphinx_indexes.collect { |index|
230
+ index.to_riddle(sphinx_offset)
231
+ }.flatten
232
+ end
233
+
234
+ def source_of_sphinx_index
235
+ define_indexes
236
+ possible_models = self.sphinx_indexes.collect { |index| index.model }
237
+ return self if possible_models.include?(self)
238
+
239
+ parent = self.superclass
240
+ while !possible_models.include?(parent) && parent != ::ActiveRecord::Base
241
+ parent = parent.superclass
242
+ end
243
+
244
+ return parent
245
+ end
246
+
247
+ def delete_in_index(index, document_id)
248
+ return unless ThinkingSphinx.sphinx_running? &&
249
+ search_for_id(document_id, index)
250
+
251
+ ThinkingSphinx::Configuration.instance.client.update(
252
+ index, ['sphinx_deleted'], {document_id => [1]}
253
+ )
254
+ end
255
+
256
+ def sphinx_offset
257
+ ThinkingSphinx.context.superclass_indexed_models.
258
+ index eldest_indexed_ancestor
259
+ end
260
+
261
+ # Temporarily disable delta indexing inside a block, then perform a single
262
+ # rebuild of index at the end.
263
+ #
264
+ # Useful when performing updates to batches of models to prevent
265
+ # the delta index being rebuilt after each individual update.
266
+ #
267
+ # In the following example, the delta index will only be rebuilt once,
268
+ # not 10 times.
269
+ #
270
+ # SomeModel.suspended_delta do
271
+ # 10.times do
272
+ # SomeModel.create( ... )
273
+ # end
274
+ # end
275
+ #
276
+ def suspended_delta(reindex_after = true, &block)
277
+ define_indexes
278
+ original_setting = ThinkingSphinx.deltas_enabled?
279
+ ThinkingSphinx.deltas_enabled = false
280
+ begin
281
+ yield
282
+ ensure
283
+ ThinkingSphinx.deltas_enabled = original_setting
284
+ self.index_delta if reindex_after
285
+ end
286
+ end
287
+
288
+ private
289
+
290
+ def local_sphinx_indexes
291
+ sphinx_indexes.select { |index|
292
+ index.model == self
293
+ }
294
+ end
295
+
296
+ def add_sphinx_callbacks_and_extend(delta = false)
297
+ unless indexed_by_sphinx?
298
+ after_destroy :toggle_deleted
299
+
300
+ include ThinkingSphinx::ActiveRecord::AttributeUpdates
301
+ end
302
+
303
+ if delta && !delta_indexed_by_sphinx?
304
+ include ThinkingSphinx::ActiveRecord::Delta
305
+
306
+ before_save :toggle_delta
307
+ after_commit :index_delta
308
+ end
309
+ end
310
+
311
+ def eldest_indexed_ancestor
312
+ ancestors.reverse.detect { |ancestor|
313
+ ThinkingSphinx.context.indexed_models.include?(ancestor.name)
314
+ }.name
315
+ end
316
+ end
317
+
318
+ def in_index?(suffix)
319
+ self.class.search_for_id self.sphinx_document_id, sphinx_index_name(suffix)
320
+ end
321
+
322
+ def in_core_index?
323
+ in_index? "core"
324
+ end
325
+
326
+ def in_delta_index?
327
+ in_index? "delta"
328
+ end
329
+
330
+ def in_both_indexes?
331
+ in_core_index? && in_delta_index?
332
+ end
333
+
334
+ def toggle_deleted
335
+ return unless ThinkingSphinx.updates_enabled?
336
+
337
+ self.class.core_index_names.each do |index_name|
338
+ self.class.delete_in_index index_name, self.sphinx_document_id
339
+ end
340
+ self.class.delta_index_names.each do |index_name|
341
+ self.class.delete_in_index index_name, self.sphinx_document_id
342
+ end if self.class.delta_indexed_by_sphinx? && toggled_delta?
343
+
344
+ rescue ::ThinkingSphinx::ConnectionError
345
+ # nothing
346
+ end
347
+
348
+ # Returns the unique integer id for the object. This method uses the
349
+ # attribute hash to get around ActiveRecord always mapping the #id method
350
+ # to whatever the real primary key is (which may be a unique string hash).
351
+ #
352
+ # @return [Integer] Unique record id for the purposes of Sphinx.
353
+ #
354
+ def primary_key_for_sphinx
355
+ @primary_key_for_sphinx ||= read_attribute(self.class.primary_key_for_sphinx)
356
+ end
357
+
358
+ def sphinx_document_id
359
+ primary_key_for_sphinx * ThinkingSphinx.context.indexed_models.size +
360
+ self.class.sphinx_offset
361
+ end
362
+
363
+ private
364
+
365
+ def sphinx_index_name(suffix)
366
+ "#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_#{suffix}"
367
+ end
368
+
369
+ def define_indexes
370
+ self.class.define_indexes
371
+ end
372
+ end
373
+ end