zipme-thinking-sphinx 1.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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