angelf-thinking-sphinx 1.3.18

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 (159) hide show
  1. data/LICENCE +20 -0
  2. data/README.textile +170 -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 +90 -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 +127 -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 +47 -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 +380 -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 +146 -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 +164 -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 +55 -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. data/tasks/testing.rb +76 -0
  159. metadata +342 -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