joshcutler-thinking-sphinx 1.3.18

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 +167 -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 +77 -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 +193 -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/env.rb +21 -0
  34. data/features/support/lib/generic_delta_handler.rb +8 -0
  35. data/features/thinking_sphinx/database.example.yml +3 -0
  36. data/features/thinking_sphinx/db/fixtures/alphas.rb +10 -0
  37. data/features/thinking_sphinx/db/fixtures/authors.rb +1 -0
  38. data/features/thinking_sphinx/db/fixtures/betas.rb +11 -0
  39. data/features/thinking_sphinx/db/fixtures/boxes.rb +9 -0
  40. data/features/thinking_sphinx/db/fixtures/categories.rb +1 -0
  41. data/features/thinking_sphinx/db/fixtures/cats.rb +3 -0
  42. data/features/thinking_sphinx/db/fixtures/comments.rb +24 -0
  43. data/features/thinking_sphinx/db/fixtures/developers.rb +31 -0
  44. data/features/thinking_sphinx/db/fixtures/dogs.rb +3 -0
  45. data/features/thinking_sphinx/db/fixtures/extensible_betas.rb +10 -0
  46. data/features/thinking_sphinx/db/fixtures/foxes.rb +3 -0
  47. data/features/thinking_sphinx/db/fixtures/gammas.rb +10 -0
  48. data/features/thinking_sphinx/db/fixtures/music.rb +4 -0
  49. data/features/thinking_sphinx/db/fixtures/people.rb +1001 -0
  50. data/features/thinking_sphinx/db/fixtures/posts.rb +6 -0
  51. data/features/thinking_sphinx/db/fixtures/robots.rb +14 -0
  52. data/features/thinking_sphinx/db/fixtures/tags.rb +27 -0
  53. data/features/thinking_sphinx/db/migrations/create_alphas.rb +8 -0
  54. data/features/thinking_sphinx/db/migrations/create_animals.rb +5 -0
  55. data/features/thinking_sphinx/db/migrations/create_authors.rb +3 -0
  56. data/features/thinking_sphinx/db/migrations/create_authors_posts.rb +6 -0
  57. data/features/thinking_sphinx/db/migrations/create_betas.rb +5 -0
  58. data/features/thinking_sphinx/db/migrations/create_boxes.rb +5 -0
  59. data/features/thinking_sphinx/db/migrations/create_categories.rb +3 -0
  60. data/features/thinking_sphinx/db/migrations/create_comments.rb +10 -0
  61. data/features/thinking_sphinx/db/migrations/create_developers.rb +7 -0
  62. data/features/thinking_sphinx/db/migrations/create_extensible_betas.rb +5 -0
  63. data/features/thinking_sphinx/db/migrations/create_gammas.rb +3 -0
  64. data/features/thinking_sphinx/db/migrations/create_genres.rb +3 -0
  65. data/features/thinking_sphinx/db/migrations/create_music.rb +6 -0
  66. data/features/thinking_sphinx/db/migrations/create_people.rb +13 -0
  67. data/features/thinking_sphinx/db/migrations/create_posts.rb +5 -0
  68. data/features/thinking_sphinx/db/migrations/create_robots.rb +4 -0
  69. data/features/thinking_sphinx/db/migrations/create_taggings.rb +5 -0
  70. data/features/thinking_sphinx/db/migrations/create_tags.rb +4 -0
  71. data/features/thinking_sphinx/models/alpha.rb +22 -0
  72. data/features/thinking_sphinx/models/animal.rb +5 -0
  73. data/features/thinking_sphinx/models/author.rb +3 -0
  74. data/features/thinking_sphinx/models/beta.rb +8 -0
  75. data/features/thinking_sphinx/models/box.rb +8 -0
  76. data/features/thinking_sphinx/models/cat.rb +3 -0
  77. data/features/thinking_sphinx/models/category.rb +4 -0
  78. data/features/thinking_sphinx/models/comment.rb +10 -0
  79. data/features/thinking_sphinx/models/developer.rb +16 -0
  80. data/features/thinking_sphinx/models/dog.rb +3 -0
  81. data/features/thinking_sphinx/models/extensible_beta.rb +9 -0
  82. data/features/thinking_sphinx/models/fox.rb +5 -0
  83. data/features/thinking_sphinx/models/gamma.rb +5 -0
  84. data/features/thinking_sphinx/models/genre.rb +3 -0
  85. data/features/thinking_sphinx/models/medium.rb +5 -0
  86. data/features/thinking_sphinx/models/music.rb +8 -0
  87. data/features/thinking_sphinx/models/person.rb +23 -0
  88. data/features/thinking_sphinx/models/post.rb +21 -0
  89. data/features/thinking_sphinx/models/robot.rb +12 -0
  90. data/features/thinking_sphinx/models/tag.rb +3 -0
  91. data/features/thinking_sphinx/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 +242 -0
  96. data/lib/thinking_sphinx/active_record.rb +380 -0
  97. data/lib/thinking_sphinx/active_record/attribute_updates.rb +50 -0
  98. data/lib/thinking_sphinx/active_record/delta.rb +61 -0
  99. data/lib/thinking_sphinx/active_record/has_many_association.rb +51 -0
  100. data/lib/thinking_sphinx/active_record/scopes.rb +75 -0
  101. data/lib/thinking_sphinx/adapters/abstract_adapter.rb +46 -0
  102. data/lib/thinking_sphinx/adapters/mysql_adapter.rb +58 -0
  103. data/lib/thinking_sphinx/adapters/postgresql_adapter.rb +147 -0
  104. data/lib/thinking_sphinx/association.rb +164 -0
  105. data/lib/thinking_sphinx/attribute.rb +390 -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 +292 -0
  109. data/lib/thinking_sphinx/context.rb +74 -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 +80 -0
  119. data/lib/thinking_sphinx/index.rb +157 -0
  120. data/lib/thinking_sphinx/index/builder.rb +302 -0
  121. data/lib/thinking_sphinx/index/faux_column.rb +118 -0
  122. data/lib/thinking_sphinx/join.rb +37 -0
  123. data/lib/thinking_sphinx/property.rb +168 -0
  124. data/lib/thinking_sphinx/rails_additions.rb +150 -0
  125. data/lib/thinking_sphinx/search.rb +785 -0
  126. data/lib/thinking_sphinx/search_methods.rb +439 -0
  127. data/lib/thinking_sphinx/source.rb +159 -0
  128. data/lib/thinking_sphinx/source/internal_properties.rb +46 -0
  129. data/lib/thinking_sphinx/source/sql.rb +130 -0
  130. data/lib/thinking_sphinx/tasks.rb +121 -0
  131. data/lib/thinking_sphinx/test.rb +52 -0
  132. data/rails/init.rb +16 -0
  133. data/spec/thinking_sphinx/active_record/delta_spec.rb +128 -0
  134. data/spec/thinking_sphinx/active_record/has_many_association_spec.rb +71 -0
  135. data/spec/thinking_sphinx/active_record/scopes_spec.rb +177 -0
  136. data/spec/thinking_sphinx/active_record_spec.rb +618 -0
  137. data/spec/thinking_sphinx/association_spec.rb +239 -0
  138. data/spec/thinking_sphinx/attribute_spec.rb +548 -0
  139. data/spec/thinking_sphinx/auto_version_spec.rb +39 -0
  140. data/spec/thinking_sphinx/configuration_spec.rb +271 -0
  141. data/spec/thinking_sphinx/context_spec.rb +126 -0
  142. data/spec/thinking_sphinx/core/array_spec.rb +9 -0
  143. data/spec/thinking_sphinx/core/string_spec.rb +9 -0
  144. data/spec/thinking_sphinx/excerpter_spec.rb +49 -0
  145. data/spec/thinking_sphinx/facet_search_spec.rb +176 -0
  146. data/spec/thinking_sphinx/facet_spec.rb +333 -0
  147. data/spec/thinking_sphinx/field_spec.rb +113 -0
  148. data/spec/thinking_sphinx/index/builder_spec.rb +495 -0
  149. data/spec/thinking_sphinx/index/faux_column_spec.rb +36 -0
  150. data/spec/thinking_sphinx/index_spec.rb +183 -0
  151. data/spec/thinking_sphinx/rails_additions_spec.rb +203 -0
  152. data/spec/thinking_sphinx/search_methods_spec.rb +152 -0
  153. data/spec/thinking_sphinx/search_spec.rb +1206 -0
  154. data/spec/thinking_sphinx/source_spec.rb +243 -0
  155. data/spec/thinking_sphinx_spec.rb +204 -0
  156. data/tasks/distribution.rb +46 -0
  157. data/tasks/rails.rake +1 -0
  158. metadata +475 -0
@@ -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,242 @@
1
+ require 'active_record'
2
+ require 'after_commit'
3
+ require 'yaml'
4
+ require 'riddle'
5
+
6
+ require 'thinking_sphinx/auto_version'
7
+ require 'thinking_sphinx/core/array'
8
+ require 'thinking_sphinx/core/string'
9
+ require 'thinking_sphinx/property'
10
+ require 'thinking_sphinx/active_record'
11
+ require 'thinking_sphinx/association'
12
+ require 'thinking_sphinx/attribute'
13
+ require 'thinking_sphinx/configuration'
14
+ require 'thinking_sphinx/context'
15
+ require 'thinking_sphinx/excerpter'
16
+ require 'thinking_sphinx/facet'
17
+ require 'thinking_sphinx/class_facet'
18
+ require 'thinking_sphinx/facet_search'
19
+ require 'thinking_sphinx/field'
20
+ require 'thinking_sphinx/index'
21
+ require 'thinking_sphinx/join'
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
+ @@sphinx_mutex = Mutex.new
67
+ @@context = nil
68
+
69
+ def self.context
70
+ if @@context.nil?
71
+ @@sphinx_mutex.synchronize do
72
+ if @@context.nil?
73
+ @@context = ThinkingSphinx::Context.new
74
+ @@context.prepare
75
+ end
76
+ end
77
+ end
78
+
79
+ @@context
80
+ end
81
+
82
+ def self.reset_context!
83
+ @@sphinx_mutex.synchronize do
84
+ @@context = nil
85
+ end
86
+ end
87
+
88
+ def self.unique_id_expression(offset = nil)
89
+ "* #{context.indexed_models.size} + #{offset || 0}"
90
+ end
91
+
92
+ # Check if index definition is disabled.
93
+ #
94
+ def self.define_indexes?
95
+ if Thread.current[:thinking_sphinx_define_indexes].nil?
96
+ Thread.current[:thinking_sphinx_define_indexes] = true
97
+ end
98
+
99
+ Thread.current[:thinking_sphinx_define_indexes]
100
+ end
101
+
102
+ # Enable/disable indexes - you may want to do this while migrating data.
103
+ #
104
+ # ThinkingSphinx.define_indexes = false
105
+ #
106
+ def self.define_indexes=(value)
107
+ Thread.current[:thinking_sphinx_define_indexes] = value
108
+ end
109
+
110
+ # Check if delta indexing is enabled.
111
+ #
112
+ def self.deltas_enabled?
113
+ if Thread.current[:thinking_sphinx_deltas_enabled].nil?
114
+ Thread.current[:thinking_sphinx_deltas_enabled] = (
115
+ ThinkingSphinx::Configuration.environment != "test"
116
+ )
117
+ end
118
+
119
+ Thread.current[:thinking_sphinx_deltas_enabled]
120
+ end
121
+
122
+ # Enable/disable all delta indexing.
123
+ #
124
+ # ThinkingSphinx.deltas_enabled = false
125
+ #
126
+ def self.deltas_enabled=(value)
127
+ Thread.current[:thinking_sphinx_deltas_enabled] = value
128
+ end
129
+
130
+ # Check if updates are enabled. True by default, unless within the test
131
+ # environment.
132
+ #
133
+ def self.updates_enabled?
134
+ if Thread.current[:thinking_sphinx_updates_enabled].nil?
135
+ Thread.current[:thinking_sphinx_updates_enabled] = (
136
+ ThinkingSphinx::Configuration.environment != "test"
137
+ )
138
+ end
139
+
140
+ Thread.current[:thinking_sphinx_updates_enabled]
141
+ end
142
+
143
+ # Enable/disable updates to Sphinx
144
+ #
145
+ # ThinkingSphinx.updates_enabled = false
146
+ #
147
+ def self.updates_enabled=(value)
148
+ Thread.current[:thinking_sphinx_updates_enabled] = value
149
+ end
150
+
151
+ def self.suppress_delta_output?
152
+ Thread.current[:thinking_sphinx_suppress_delta_output] ||= false
153
+ end
154
+
155
+ def self.suppress_delta_output=(value)
156
+ Thread.current[:thinking_sphinx_suppress_delta_output] = value
157
+ end
158
+
159
+ # Checks to see if MySQL will allow simplistic GROUP BY statements. If not,
160
+ # or if not using MySQL, this will return false.
161
+ #
162
+ def self.use_group_by_shortcut?
163
+ if Thread.current[:thinking_sphinx_use_group_by_shortcut].nil?
164
+ Thread.current[:thinking_sphinx_use_group_by_shortcut] = !!(
165
+ mysql? && ::ActiveRecord::Base.connection.select_all(
166
+ "SELECT @@global.sql_mode, @@session.sql_mode;"
167
+ ).all? { |key,value| value.nil? || value[/ONLY_FULL_GROUP_BY/].nil? }
168
+ )
169
+ end
170
+
171
+ Thread.current[:thinking_sphinx_use_group_by_shortcut]
172
+ end
173
+
174
+ # An indication of whether Sphinx is running on a remote machine instead of
175
+ # the same machine.
176
+ #
177
+ def self.remote_sphinx?
178
+ Thread.current[:thinking_sphinx_remote_sphinx] ||= false
179
+ end
180
+
181
+ # Tells Thinking Sphinx that Sphinx is running on a different machine, and
182
+ # thus it can't reliably guess whether it is running or not (ie: the
183
+ # #sphinx_running? method), and so just assumes it is.
184
+ #
185
+ # Useful for multi-machine deployments. Set it in your production.rb file.
186
+ #
187
+ # ThinkingSphinx.remote_sphinx = true
188
+ #
189
+ def self.remote_sphinx=(value)
190
+ Thread.current[:thinking_sphinx_remote_sphinx] = value
191
+ end
192
+
193
+ # Check if Sphinx is running. If remote_sphinx is set to true (indicating
194
+ # Sphinx is on a different machine), this will always return true, and you
195
+ # will have to handle any connection errors yourself.
196
+ #
197
+ def self.sphinx_running?
198
+ remote_sphinx? || sphinx_running_by_pid?
199
+ end
200
+
201
+ # Check if Sphinx is actually running, provided the pid is on the same
202
+ # machine as this code.
203
+ #
204
+ def self.sphinx_running_by_pid?
205
+ !!sphinx_pid && pid_active?(sphinx_pid)
206
+ end
207
+
208
+ def self.sphinx_pid
209
+ if File.exists?(ThinkingSphinx::Configuration.instance.pid_file)
210
+ File.read(ThinkingSphinx::Configuration.instance.pid_file)[/\d+/]
211
+ else
212
+ nil
213
+ end
214
+ end
215
+
216
+ def self.pid_active?(pid)
217
+ !!Process.kill(0, pid.to_i)
218
+ rescue Errno::EPERM => e
219
+ true
220
+ rescue Exception => e
221
+ false
222
+ end
223
+
224
+ def self.microsoft?
225
+ RUBY_PLATFORM =~ /mswin/
226
+ end
227
+
228
+ def self.jruby?
229
+ defined?(JRUBY_VERSION)
230
+ end
231
+
232
+ def self.mysql?
233
+ ::ActiveRecord::Base.connection.class.name.demodulize == "MysqlAdapter" ||
234
+ ::ActiveRecord::Base.connection.class.name.demodulize == "MysqlplusAdapter" || (
235
+ jruby? && ::ActiveRecord::Base.connection.config[:adapter] == "jdbcmysql"
236
+ )
237
+ end
238
+
239
+ extend ThinkingSphinx::SearchMethods::ClassMethods
240
+ end
241
+
242
+ ThinkingSphinx::AutoVersion.detect
@@ -0,0 +1,380 @@
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
+ add_sphinx_index name, &block
158
+ }
159
+
160
+ include ThinkingSphinx::ActiveRecord::Scopes
161
+ include ThinkingSphinx::SearchMethods
162
+ end
163
+
164
+ def define_indexes
165
+ superclass.define_indexes unless superclass == ::ActiveRecord::Base
166
+
167
+ return if sphinx_index_blocks.nil? ||
168
+ defined_indexes? ||
169
+ !ThinkingSphinx.define_indexes?
170
+
171
+ sphinx_index_blocks.each do |block|
172
+ block.call
173
+ end
174
+
175
+ self.defined_indexes = true
176
+
177
+ # We want to make sure that if the database doesn't exist, then Thinking
178
+ # Sphinx doesn't mind when running non-TS tasks (like db:create, db:drop
179
+ # and db:migrate). It's a bit hacky, but I can't think of a better way.
180
+ rescue StandardError => err
181
+ case err.class.name
182
+ when "Mysql::Error", "Java::JavaSql::SQLException", "ActiveRecord::StatementInvalid"
183
+ return
184
+ else
185
+ raise err
186
+ end
187
+ end
188
+
189
+ def add_sphinx_index(name, &block)
190
+ index = ThinkingSphinx::Index::Builder.generate self, name, &block
191
+
192
+ unless sphinx_indexes.any? { |i| i.name == index.name }
193
+ add_sphinx_callbacks_and_extend(index.delta?)
194
+ insert_sphinx_index index
195
+ end
196
+ end
197
+
198
+ def insert_sphinx_index(index)
199
+ self.sphinx_indexes << index
200
+ subclasses.each { |klass| klass.insert_sphinx_index(index) }
201
+ end
202
+
203
+ def has_sphinx_indexes?
204
+ sphinx_indexes &&
205
+ sphinx_index_blocks &&
206
+ (sphinx_indexes.length > 0 || sphinx_index_blocks.length > 0)
207
+ end
208
+
209
+ def indexed_by_sphinx?
210
+ sphinx_indexes && sphinx_indexes.length > 0
211
+ end
212
+
213
+ def delta_indexed_by_sphinx?
214
+ sphinx_indexes && sphinx_indexes.any? { |index| index.delta? }
215
+ end
216
+
217
+ def sphinx_index_names
218
+ define_indexes
219
+ sphinx_indexes.collect(&:all_names).flatten
220
+ end
221
+
222
+ def core_index_names
223
+ define_indexes
224
+ sphinx_indexes.collect(&:core_name)
225
+ end
226
+
227
+ def delta_index_names
228
+ define_indexes
229
+ sphinx_indexes.select(&:delta?).collect(&:delta_name)
230
+ end
231
+
232
+ def to_riddle
233
+ define_indexes
234
+ sphinx_database_adapter.setup
235
+
236
+ local_sphinx_indexes.collect { |index|
237
+ index.to_riddle(sphinx_offset)
238
+ }.flatten
239
+ end
240
+
241
+ def source_of_sphinx_index
242
+ define_indexes
243
+ possible_models = self.sphinx_indexes.collect { |index| index.model }
244
+ return self if possible_models.include?(self)
245
+
246
+ parent = self.superclass
247
+ while !possible_models.include?(parent) && parent != ::ActiveRecord::Base
248
+ parent = parent.superclass
249
+ end
250
+
251
+ return parent
252
+ end
253
+
254
+ def delete_in_index(index, document_id)
255
+ return unless ThinkingSphinx.sphinx_running? &&
256
+ search_for_id(document_id, index)
257
+
258
+ ThinkingSphinx::Configuration.instance.client.update(
259
+ index, ['sphinx_deleted'], {document_id => [1]}
260
+ )
261
+ end
262
+
263
+ def sphinx_offset
264
+ ThinkingSphinx.context.superclass_indexed_models.
265
+ index eldest_indexed_ancestor
266
+ end
267
+
268
+ # Temporarily disable delta indexing inside a block, then perform a single
269
+ # rebuild of index at the end.
270
+ #
271
+ # Useful when performing updates to batches of models to prevent
272
+ # the delta index being rebuilt after each individual update.
273
+ #
274
+ # In the following example, the delta index will only be rebuilt once,
275
+ # not 10 times.
276
+ #
277
+ # SomeModel.suspended_delta do
278
+ # 10.times do
279
+ # SomeModel.create( ... )
280
+ # end
281
+ # end
282
+ #
283
+ def suspended_delta(reindex_after = true, &block)
284
+ define_indexes
285
+ original_setting = ThinkingSphinx.deltas_enabled?
286
+ ThinkingSphinx.deltas_enabled = false
287
+ begin
288
+ yield
289
+ ensure
290
+ ThinkingSphinx.deltas_enabled = original_setting
291
+ self.index_delta if reindex_after
292
+ end
293
+ end
294
+
295
+ private
296
+
297
+ def local_sphinx_indexes
298
+ sphinx_indexes.select { |index|
299
+ index.model == self
300
+ }
301
+ end
302
+
303
+ def add_sphinx_callbacks_and_extend(delta = false)
304
+ unless indexed_by_sphinx?
305
+ after_destroy :toggle_deleted
306
+
307
+ include ThinkingSphinx::ActiveRecord::AttributeUpdates
308
+ end
309
+
310
+ if delta && !delta_indexed_by_sphinx?
311
+ include ThinkingSphinx::ActiveRecord::Delta
312
+
313
+ before_save :toggle_delta
314
+ after_commit :index_delta
315
+ end
316
+ end
317
+
318
+ def eldest_indexed_ancestor
319
+ ancestors.reverse.detect { |ancestor|
320
+ ThinkingSphinx.context.indexed_models.include?(ancestor.name)
321
+ }.name
322
+ end
323
+ end
324
+
325
+ def in_index?(suffix)
326
+ self.class.search_for_id self.sphinx_document_id, sphinx_index_name(suffix)
327
+ end
328
+
329
+ def in_core_index?
330
+ in_index? "core"
331
+ end
332
+
333
+ def in_delta_index?
334
+ in_index? "delta"
335
+ end
336
+
337
+ def in_both_indexes?
338
+ in_core_index? && in_delta_index?
339
+ end
340
+
341
+ def toggle_deleted
342
+ return unless ThinkingSphinx.updates_enabled?
343
+
344
+ self.class.core_index_names.each do |index_name|
345
+ self.class.delete_in_index index_name, self.sphinx_document_id
346
+ end
347
+ self.class.delta_index_names.each do |index_name|
348
+ self.class.delete_in_index index_name, self.sphinx_document_id
349
+ end if self.class.delta_indexed_by_sphinx? && toggled_delta?
350
+
351
+ rescue ::ThinkingSphinx::ConnectionError
352
+ # nothing
353
+ end
354
+
355
+ # Returns the unique integer id for the object. This method uses the
356
+ # attribute hash to get around ActiveRecord always mapping the #id method
357
+ # to whatever the real primary key is (which may be a unique string hash).
358
+ #
359
+ # @return [Integer] Unique record id for the purposes of Sphinx.
360
+ #
361
+ def primary_key_for_sphinx
362
+ @primary_key_for_sphinx ||= read_attribute(self.class.primary_key_for_sphinx)
363
+ end
364
+
365
+ def sphinx_document_id
366
+ primary_key_for_sphinx * ThinkingSphinx.context.indexed_models.size +
367
+ self.class.sphinx_offset
368
+ end
369
+
370
+ private
371
+
372
+ def sphinx_index_name(suffix)
373
+ "#{self.class.source_of_sphinx_index.name.underscore.tr(':/\\', '_')}_#{suffix}"
374
+ end
375
+
376
+ def define_indexes
377
+ self.class.define_indexes
378
+ end
379
+ end
380
+ end