substantial-sunspot_rails 2.0.0.pre.111215

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 (66) hide show
  1. data/.gitignore +7 -0
  2. data/.rspec +1 -0
  3. data/History.txt +66 -0
  4. data/LICENSE +18 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.rdoc +265 -0
  7. data/Rakefile +12 -0
  8. data/TODO +8 -0
  9. data/dev_tasks/rdoc.rake +24 -0
  10. data/dev_tasks/release.rake +4 -0
  11. data/dev_tasks/spec.rake +107 -0
  12. data/dev_tasks/todo.rake +4 -0
  13. data/gemfiles/rails-2.3.14 +15 -0
  14. data/gemfiles/rails-3.0.11 +15 -0
  15. data/gemfiles/rails-3.1.3 +15 -0
  16. data/generators/sunspot/sunspot_generator.rb +9 -0
  17. data/generators/sunspot/templates/sunspot.yml +18 -0
  18. data/install.rb +1 -0
  19. data/lib/generators/sunspot_rails.rb +9 -0
  20. data/lib/generators/sunspot_rails/install/install_generator.rb +13 -0
  21. data/lib/generators/sunspot_rails/install/templates/config/sunspot.yml +17 -0
  22. data/lib/substantial-sunspot_rails.rb +1 -0
  23. data/lib/sunspot/rails.rb +65 -0
  24. data/lib/sunspot/rails/adapters.rb +83 -0
  25. data/lib/sunspot/rails/configuration.rb +340 -0
  26. data/lib/sunspot/rails/init.rb +5 -0
  27. data/lib/sunspot/rails/log_subscriber.rb +33 -0
  28. data/lib/sunspot/rails/railtie.rb +36 -0
  29. data/lib/sunspot/rails/railties/controller_runtime.rb +36 -0
  30. data/lib/sunspot/rails/request_lifecycle.rb +36 -0
  31. data/lib/sunspot/rails/searchable.rb +480 -0
  32. data/lib/sunspot/rails/server.rb +106 -0
  33. data/lib/sunspot/rails/solr_instrumentation.rb +18 -0
  34. data/lib/sunspot/rails/solr_logging.rb +62 -0
  35. data/lib/sunspot/rails/spec_helper.rb +26 -0
  36. data/lib/sunspot/rails/stub_session_proxy.rb +142 -0
  37. data/lib/sunspot/rails/tasks.rb +84 -0
  38. data/lib/sunspot_rails.rb +12 -0
  39. data/spec/configuration_spec.rb +195 -0
  40. data/spec/model_lifecycle_spec.rb +63 -0
  41. data/spec/model_spec.rb +595 -0
  42. data/spec/rails_template/app/controllers/application_controller.rb +10 -0
  43. data/spec/rails_template/app/controllers/posts_controller.rb +6 -0
  44. data/spec/rails_template/app/models/author.rb +8 -0
  45. data/spec/rails_template/app/models/blog.rb +12 -0
  46. data/spec/rails_template/app/models/location.rb +2 -0
  47. data/spec/rails_template/app/models/photo_post.rb +2 -0
  48. data/spec/rails_template/app/models/post.rb +11 -0
  49. data/spec/rails_template/app/models/post_with_auto.rb +10 -0
  50. data/spec/rails_template/app/models/post_with_default_scope.rb +11 -0
  51. data/spec/rails_template/config/boot.rb +127 -0
  52. data/spec/rails_template/config/preinitializer.rb +22 -0
  53. data/spec/rails_template/config/routes.rb +9 -0
  54. data/spec/rails_template/config/sunspot.yml +22 -0
  55. data/spec/rails_template/db/schema.rb +27 -0
  56. data/spec/request_lifecycle_spec.rb +61 -0
  57. data/spec/schema.rb +27 -0
  58. data/spec/searchable_spec.rb +12 -0
  59. data/spec/server_spec.rb +33 -0
  60. data/spec/session_spec.rb +57 -0
  61. data/spec/shared_examples/indexed_after_save.rb +8 -0
  62. data/spec/shared_examples/not_indexed_after_save.rb +8 -0
  63. data/spec/spec_helper.rb +48 -0
  64. data/spec/stub_session_proxy_spec.rb +122 -0
  65. data/substantial-sunspot_rails.gemspec +43 -0
  66. metadata +228 -0
@@ -0,0 +1,5 @@
1
+ Sunspot.session = Sunspot::Rails.build_session
2
+ Sunspot::Adapters::InstanceAdapter.register(Sunspot::Rails::Adapters::ActiveRecordInstanceAdapter, ActiveRecord::Base)
3
+ Sunspot::Adapters::DataAccessor.register(Sunspot::Rails::Adapters::ActiveRecordDataAccessor, ActiveRecord::Base)
4
+ ActiveRecord::Base.module_eval { include(Sunspot::Rails::Searchable) }
5
+ ActionController::Base.module_eval { include(Sunspot::Rails::RequestLifecycle) }
@@ -0,0 +1,33 @@
1
+ module Sunspot
2
+ module Rails
3
+ class LogSubscriber < ActiveSupport::LogSubscriber
4
+ def self.runtime=(value)
5
+ Thread.current["sorl_runtime"] = value
6
+ end
7
+
8
+ def self.runtime
9
+ Thread.current["sorl_runtime"] ||= 0
10
+ end
11
+
12
+ def self.reset_runtime
13
+ rt, self.runtime = runtime, 0
14
+ rt
15
+ end
16
+
17
+ def request(event)
18
+ self.class.runtime += event.duration
19
+ return unless logger.debug?
20
+
21
+ name = '%s (%.1fms)' % ["SOLR Request", event.duration]
22
+
23
+ # produces: path=/select parameters={fq: ["type:Tag"], q: rossi, fl: * score, qf: tag_name_text, defType: dismax, start: 0, rows: 20}
24
+ parameters = event.payload[:parameters].map { |k, v| "#{k}: #{color(v, BOLD, true)}" }.join(', ')
25
+ request = "path=#{event.payload[:path]} parameters={#{parameters}}"
26
+
27
+ debug " #{color(name, GREEN, true)} [ #{request} ]"
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ Sunspot::Rails::LogSubscriber.attach_to :rsolr
@@ -0,0 +1,36 @@
1
+ module Sunspot
2
+ module Rails
3
+ class Railtie < ::Rails::Railtie
4
+ initializer 'sunspot_rails.init' do
5
+ Sunspot.session = Sunspot::Rails.build_session
6
+ ActiveSupport.on_load(:active_record) do
7
+ Sunspot::Adapters::InstanceAdapter.register(Sunspot::Rails::Adapters::ActiveRecordInstanceAdapter, ActiveRecord::Base)
8
+ Sunspot::Adapters::DataAccessor.register(Sunspot::Rails::Adapters::ActiveRecordDataAccessor, ActiveRecord::Base)
9
+ include(Sunspot::Rails::Searchable)
10
+ end
11
+ ActiveSupport.on_load(:action_controller) do
12
+ include(Sunspot::Rails::RequestLifecycle)
13
+ end
14
+ require 'sunspot/rails/log_subscriber'
15
+ RSolr::Connection.module_eval{ include Sunspot::Rails::SolrInstrumentation }
16
+ end
17
+
18
+ # Expose database runtime to controller for logging.
19
+ initializer "sunspot_rails.log_runtime" do |app|
20
+ require "sunspot/rails/railties/controller_runtime"
21
+ ActiveSupport.on_load(:action_controller) do
22
+ include Sunspot::Rails::Railties::ControllerRuntime
23
+ end
24
+ end
25
+
26
+ rake_tasks do
27
+ load 'sunspot/rails/tasks.rb'
28
+ end
29
+
30
+ generators do
31
+ load "generators/sunspot_rails.rb"
32
+ end
33
+
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ module Sunspot
2
+ module Rails
3
+ module Railties
4
+ module ControllerRuntime
5
+ extend ActiveSupport::Concern
6
+
7
+ protected
8
+
9
+ attr_internal :solr_runtime
10
+
11
+ def cleanup_view_runtime
12
+ # TODO only if solr is connected? if not call to super
13
+
14
+ solr_rt_before_render = Sunspot::Rails::LogSubscriber.reset_runtime
15
+ runtime = super
16
+ solr_rt_after_render = Sunspot::Rails::LogSubscriber.reset_runtime
17
+ self.solr_runtime = solr_rt_before_render + solr_rt_after_render
18
+ runtime - solr_rt_after_render
19
+ end
20
+
21
+ def append_info_to_payload(payload)
22
+ super
23
+ payload[:solr_runtime] = solr_runtime
24
+ end
25
+
26
+ module ClassMethods
27
+ def log_process_action(payload)
28
+ messages, solr_runtime = super, payload[:solr_runtime]
29
+ messages << ("Solr: %.1fms" % solr_runtime.to_f) if solr_runtime
30
+ messages
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ module Sunspot #:nodoc:
2
+ module Rails #:nodoc:
3
+ #
4
+ # This module adds an after_filter to ActionController::Base that commits
5
+ # the Sunspot session if any documents have been added, changed, or removed
6
+ # in the course of the request.
7
+ #
8
+ module RequestLifecycle
9
+ class <<self
10
+ def included(base) #:nodoc:
11
+ subclasses = base.subclasses.map do |subclass|
12
+ begin
13
+ subclass.constantize
14
+ rescue NameError
15
+ end
16
+ end.compact
17
+ loaded_controllers = [base].concat(subclasses)
18
+ # Depending on how Sunspot::Rails is loaded, there may already be
19
+ # controllers loaded into memory that subclass this controller. In
20
+ # this case, since after_filter uses the inheritable_attribute
21
+ # structure, the already-loaded subclasses don't get the filters. So,
22
+ # the below ensures that all loaded controllers have the filter.
23
+ loaded_controllers.each do |controller|
24
+ controller.after_filter do
25
+ if Sunspot::Rails.configuration.auto_commit_after_request?
26
+ Sunspot.commit_if_dirty
27
+ elsif Sunspot::Rails.configuration.auto_commit_after_delete_request?
28
+ Sunspot.commit_if_delete_dirty
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,480 @@
1
+ module Sunspot #:nodoc:
2
+ module Rails #:nodoc:
3
+ #
4
+ # This module adds Sunspot functionality to ActiveRecord models. As well as
5
+ # providing class and instance methods, it optionally adds lifecycle hooks
6
+ # to automatically add and remove models from the Solr index as they are
7
+ # created and destroyed.
8
+ #
9
+ module Searchable
10
+ class <<self
11
+ def included(base) #:nodoc:
12
+ base.module_eval do
13
+ extend(ActsAsMethods)
14
+ end
15
+ end
16
+ end
17
+
18
+ module ActsAsMethods
19
+ #
20
+ # Makes a class searchable if it is not already, or adds search
21
+ # configuration if it is. Note that the options passed in are only used
22
+ # the first time this method is called for a particular class; so,
23
+ # search should be defined before activating any mixins that extend
24
+ # search configuration.
25
+ #
26
+ # The block passed into this method is evaluated by the
27
+ # <code>Sunspot.setup</code> method. See the Sunspot documentation for
28
+ # complete information on the functionality provided by that method.
29
+ #
30
+ # ==== Options (+options+)
31
+ #
32
+ # :auto_index<Boolean>::
33
+ # Automatically index models in Solr when they are saved.
34
+ # Default: true
35
+ # :auto_remove<Boolean>::
36
+ # Automatically remove models from the Solr index when they are
37
+ # destroyed. <b>Setting this option to +false+ is not recommended
38
+ # </b>(see the README).
39
+ # :if<Mixed>::
40
+ # Only index models in Solr if the method, proc or string evaluates
41
+ # to true (e.g. <code>:if => :should_index?</code> or <code>:if =>
42
+ # proc { |model| model.foo > 2 }</code>). Models that do not match
43
+ # the constraint will be removed from the index upon save. Multiple
44
+ # constraints can be specified by passing an array (e.g. <code>:if =>
45
+ # [:method1, :method2]</code>).
46
+ # :ignore_attribute_changes_of<Array>::
47
+ # Define attributes, that should not trigger a reindex of that
48
+ # object. Usual suspects are updated_at or counters.
49
+ # :include<Mixed>::
50
+ # Define default ActiveRecord includes, set this to allow ActiveRecord
51
+ # to load required associations when indexing. See ActiveRecord's
52
+ # documentation on eager-loading for examples on how to set this
53
+ # Default: []
54
+ # :unless<Mixed>::
55
+ # Only index models in Solr if the method, proc or string evaluates
56
+ # to false (e.g. <code>:unless => :should_not_index?</code> or <code>
57
+ # :unless => proc { |model| model.foo <= 2 }</code>). Models that do
58
+ # not match the constraint will be removed from the index upon save.
59
+ # Multiple constraints can be specified by passing an array (e.g.
60
+ # <code>:unless => [:method1, :method2]</code>).
61
+ #
62
+ # ==== Example
63
+ #
64
+ # class Post < ActiveRecord::Base
65
+ # searchable do
66
+ # text :title, :body
67
+ # string :sort_title do
68
+ # title.downcase.sub(/^(an?|the)/, '')
69
+ # end
70
+ # integer :blog_id
71
+ # time :updated_at
72
+ # end
73
+ # end
74
+ #
75
+ def searchable(options = {}, &block)
76
+ Sunspot.setup(self, &block)
77
+
78
+ if searchable?
79
+ sunspot_options[:include].concat(Util::Array(options[:include]))
80
+ else
81
+ extend ClassMethods
82
+ include InstanceMethods
83
+
84
+ class_attribute :sunspot_options
85
+
86
+ unless options[:auto_index] == false
87
+ before_save :mark_for_auto_indexing_or_removal
88
+ after_save :perform_index_tasks
89
+ end
90
+
91
+ unless options[:auto_remove] == false
92
+ after_destroy do |searchable|
93
+ searchable.remove_from_index
94
+ end
95
+ end
96
+ options[:include] = Util::Array(options[:include])
97
+
98
+ self.sunspot_options = options
99
+ end
100
+ end
101
+
102
+ #
103
+ # This method is defined on all ActiveRecord::Base subclasses. It
104
+ # is false for classes on which #searchable has not been called, and
105
+ # true for classes on which #searchable has been called.
106
+ #
107
+ # ==== Returns
108
+ #
109
+ # +false+
110
+ #
111
+ def searchable?
112
+ false
113
+ end
114
+ end
115
+
116
+ module ClassMethods
117
+ def self.extended(base) #:nodoc:
118
+ class <<base
119
+ alias_method :search, :solr_search unless method_defined? :search
120
+ alias_method :search_ids, :solr_search_ids unless method_defined? :search_ids
121
+ alias_method :remove_all_from_index, :solr_remove_all_from_index unless method_defined? :remove_all_from_index
122
+ alias_method :remove_all_from_index!, :solr_remove_all_from_index! unless method_defined? :remove_all_from_index!
123
+ alias_method :reindex, :solr_reindex unless method_defined? :reindex
124
+ alias_method :index, :solr_index unless method_defined? :index
125
+ alias_method :index_orphans, :solr_index_orphans unless method_defined? :index_orphans
126
+ alias_method :clean_index_orphans, :solr_clean_index_orphans unless method_defined? :clean_index_orphans
127
+ end
128
+ end
129
+ #
130
+ # Search for instances of this class in Solr. The block is delegated to
131
+ # the Sunspot.search method - see the Sunspot documentation for the full
132
+ # API.
133
+ #
134
+ # ==== Example
135
+ #
136
+ # Post.search(:include => [:blog]) do
137
+ # keywords 'best pizza'
138
+ # with :blog_id, 1
139
+ # order :updated_at, :desc
140
+ # facet :category_ids
141
+ # end
142
+ #
143
+ # ==== Options
144
+ #
145
+ # :include:: Specify associations to eager load
146
+ # :select:: Specify columns to select from database when loading results
147
+ #
148
+ # ==== Returns
149
+ #
150
+ # Sunspot::Search:: Object containing results, totals, facets, etc.
151
+ #
152
+ def solr_search(options = {}, &block)
153
+ solr_execute_search(options) do
154
+ Sunspot.new_search(self, &block)
155
+ end
156
+ end
157
+
158
+ #
159
+ # Get IDs of matching results without loading the result objects from
160
+ # the database. This method may be useful if search is used as an
161
+ # intermediate step in a larger find operation. The block is the same
162
+ # as the block provided to the #search method.
163
+ #
164
+ # ==== Returns
165
+ #
166
+ # Array:: Array of IDs, in the order returned by the search
167
+ #
168
+ def solr_search_ids(&block)
169
+ solr_execute_search_ids do
170
+ solr_search(&block)
171
+ end
172
+ end
173
+
174
+ #
175
+ # Remove instances of this class from the Solr index.
176
+ #
177
+ def solr_remove_all_from_index
178
+ Sunspot.remove_all(self)
179
+ end
180
+
181
+ #
182
+ # Remove all instances of this class from the Solr index and immediately
183
+ # commit.
184
+ #
185
+ #
186
+ def solr_remove_all_from_index!
187
+ Sunspot.remove_all!(self)
188
+ end
189
+
190
+ #
191
+ # Completely rebuild the index for this class. First removes all
192
+ # instances from the index, then loads records and indexes them.
193
+ #
194
+ # See #index for information on options, etc.
195
+ #
196
+ def solr_reindex(options = {})
197
+ solr_remove_all_from_index
198
+ solr_index(options)
199
+ end
200
+
201
+ #
202
+ # Add/update all existing records in the Solr index. The
203
+ # +batch_size+ argument specifies how many records to load out of the
204
+ # database at a time. The default batch size is 50; if nil is passed,
205
+ # records will not be indexed in batches. By default, a commit is issued
206
+ # after each batch; passing +false+ for +batch_commit+ will disable
207
+ # this, and only issue a commit at the end of the process. If associated
208
+ # objects need to indexed also, you can specify +include+ in format
209
+ # accepted by ActiveRecord to improve your sql select performance
210
+ #
211
+ # ==== Options (passed as a hash)
212
+ #
213
+ # batch_size<Integer>:: Batch size with which to load records. Passing
214
+ # 'nil' will skip batches. Default is 50.
215
+ # batch_commit<Boolean>:: Flag signalling if a commit should be done after
216
+ # after each batch is indexed, default is 'true'
217
+ # include<Mixed>:: include option to be passed to the ActiveRecord find,
218
+ # used for including associated objects that need to be
219
+ # indexed with the parent object, accepts all formats
220
+ # ActiveRecord::Base.find does
221
+ # first_id:: The lowest possible ID for this class. Defaults to 0, which
222
+ # is fine for integer IDs; string primary keys will need to
223
+ # specify something reasonable here.
224
+ #
225
+ # ==== Examples
226
+ #
227
+ # # index in batches of 50, commit after each
228
+ # Post.index
229
+ #
230
+ # # index all rows at once, then commit
231
+ # Post.index(:batch_size => nil)
232
+ #
233
+ # # index in batches of 50, commit when all batches complete
234
+ # Post.index(:batch_commit => false)
235
+ #
236
+ # # include the associated +author+ object when loading to index
237
+ # Post.index(:include => :author)
238
+ #
239
+ def solr_index(opts={})
240
+ options = {
241
+ :batch_size => 50,
242
+ :batch_commit => true,
243
+ :include => self.sunspot_options[:include],
244
+ :start => opts.delete(:first_id) || 0
245
+ }.merge(opts)
246
+ find_in_batch_options = {
247
+ :include => options[:include],
248
+ :batch_size => options[:batch_size],
249
+ :start => options[:first_id]
250
+ }
251
+ progress_bar = options[:progress_bar]
252
+ if options[:batch_size]
253
+ batch_counter = 0
254
+ find_in_batches(find_in_batch_options) do |records|
255
+ solr_benchmark options[:batch_size], batch_counter do
256
+ Sunspot.index(records.select { |model| model.indexable? })
257
+ Sunspot.commit if options[:batch_commit]
258
+ end
259
+ # track progress
260
+ progress_bar.increment!(records.length) if progress_bar
261
+ batch_counter += 1
262
+ end
263
+ else
264
+ records = all(:include => options[:include]).select { |model| model.indexable? }
265
+ Sunspot.index!(records)
266
+ end
267
+ # perform a final commit if not committing in batches
268
+ Sunspot.commit unless options[:batch_commit]
269
+ end
270
+
271
+ #
272
+ # Return the IDs of records of this class that are indexed in Solr but
273
+ # do not exist in the database. Under normal circumstances, this should
274
+ # never happen, but this method is provided in case something goes
275
+ # wrong. Usually you will want to rectify the situation by calling
276
+ # #clean_index_orphans or #reindex
277
+ #
278
+ # ==== Returns
279
+ #
280
+ # Array:: Collection of IDs that exist in Solr but not in the database
281
+ def solr_index_orphans
282
+ count = self.count
283
+ indexed_ids = solr_search_ids { paginate(:page => 1, :per_page => count) }.to_set
284
+ all(:select => 'id').each do |object|
285
+ indexed_ids.delete(object.id)
286
+ end
287
+ indexed_ids.to_a
288
+ end
289
+
290
+ #
291
+ # Find IDs of records of this class that are indexed in Solr but do not
292
+ # exist in the database, and remove them from Solr. Under normal
293
+ # circumstances, this should not be necessary; this method is provided
294
+ # in case something goes wrong.
295
+ #
296
+ def solr_clean_index_orphans
297
+ solr_index_orphans.each do |id|
298
+ new do |fake_instance|
299
+ fake_instance.id = id
300
+ end.solr_remove_from_index
301
+ end
302
+ end
303
+
304
+ #
305
+ # Classes that have been defined as searchable return +true+ for this
306
+ # method.
307
+ #
308
+ # ==== Returns
309
+ #
310
+ # +true+
311
+ #
312
+ def searchable?
313
+ true
314
+ end
315
+
316
+ def solr_execute_search(options = {})
317
+ options.assert_valid_keys(:include, :select)
318
+ search = yield
319
+ unless options.empty?
320
+ search.build do |query|
321
+ if options[:include]
322
+ query.data_accessor_for(self).include = options[:include]
323
+ end
324
+ if options[:select]
325
+ query.data_accessor_for(self).select = options[:select]
326
+ end
327
+ end
328
+ end
329
+ search.execute
330
+ end
331
+
332
+ def solr_execute_search_ids(options = {})
333
+ search = yield
334
+ search.raw_results.map { |raw_result| raw_result.primary_key.to_i }
335
+ end
336
+
337
+ protected
338
+
339
+ #
340
+ # Does some logging for benchmarking indexing performance
341
+ #
342
+ def solr_benchmark(batch_size, counter, &block)
343
+ start = Time.now
344
+ logger.info("[#{Time.now}] Start Indexing")
345
+ yield
346
+ elapsed = Time.now-start
347
+ logger.info("[#{Time.now}] Completed Indexing. Rows indexed #{counter * batch_size}. Rows/sec: #{batch_size/elapsed.to_f} (Elapsed: #{elapsed} sec.)")
348
+ end
349
+
350
+ end
351
+
352
+ module InstanceMethods
353
+ def self.included(base) #:nodoc:
354
+ base.module_eval do
355
+ alias_method :index, :solr_index unless method_defined? :index
356
+ alias_method :index!, :solr_index! unless method_defined? :index!
357
+ alias_method :remove_from_index, :solr_remove_from_index unless method_defined? :remove_from_index
358
+ alias_method :remove_from_index!, :solr_remove_from_index! unless method_defined? :remove_from_index!
359
+ alias_method :more_like_this, :solr_more_like_this unless method_defined? :more_like_this
360
+ alias_method :more_like_this_ids, :solr_more_like_this_ids unless method_defined? :more_like_this_ids
361
+ end
362
+ end
363
+ #
364
+ # Index the model in Solr. If the model is already indexed, it will be
365
+ # updated. Using the defaults, you will usually not need to call this
366
+ # method, as models are indexed automatically when they are created or
367
+ # updated. If you have disabled automatic indexing (see
368
+ # ClassMethods#searchable), this method allows you to manage indexing
369
+ # manually.
370
+ #
371
+ def solr_index
372
+ Sunspot.index(self)
373
+ end
374
+
375
+ #
376
+ # Index the model in Solr and immediately commit. See #index
377
+ #
378
+ def solr_index!
379
+ Sunspot.index!(self)
380
+ end
381
+
382
+ #
383
+ # Remove the model from the Solr index. Using the defaults, this should
384
+ # not be necessary, as models will automatically be removed from the
385
+ # index when they are destroyed. If you disable automatic removal
386
+ # (which is not recommended!), you can use this method to manage removal
387
+ # manually.
388
+ #
389
+ def solr_remove_from_index
390
+ Sunspot.remove(self)
391
+ end
392
+
393
+ #
394
+ # Remove the model from the Solr index and commit immediately. See
395
+ # #remove_from_index
396
+ #
397
+ def solr_remove_from_index!
398
+ Sunspot.remove!(self)
399
+ end
400
+
401
+ def solr_more_like_this(*args, &block)
402
+ options = args.extract_options!
403
+ self.class.solr_execute_search(options) do
404
+ Sunspot.new_more_like_this(self, *args, &block)
405
+ end
406
+ end
407
+
408
+ def solr_more_like_this_ids(&block)
409
+ self.class.solr_execute_search_ids do
410
+ solr_more_like_this(&block)
411
+ end
412
+ end
413
+
414
+ def indexable?
415
+ # options[:if] is not specified or they successfully pass
416
+ if_passes = self.class.sunspot_options[:if].nil? ||
417
+ constraint_passes?(self.class.sunspot_options[:if])
418
+
419
+ # options[:unless] is not specified or they successfully pass
420
+ unless_passes = self.class.sunspot_options[:unless].nil? ||
421
+ !constraint_passes?(self.class.sunspot_options[:unless])
422
+
423
+ if_passes and unless_passes
424
+ end
425
+
426
+ private
427
+
428
+ def constraint_passes?(constraint)
429
+ case constraint
430
+ when Symbol
431
+ self.__send__(constraint)
432
+ when String
433
+ self.__send__(constraint.to_sym)
434
+ when Enumerable
435
+ # All constraints must pass
436
+ constraint.all? { |inner_constraint| constraint_passes?(inner_constraint) }
437
+ else
438
+ if constraint.respond_to?(:call) # could be a Proc or anything else that responds to call
439
+ constraint.call(self)
440
+ else
441
+ raise ArgumentError, "Unknown constraint type: #{constraint} (#{constraint.class})"
442
+ end
443
+ end
444
+ end
445
+
446
+ def mark_for_auto_indexing_or_removal
447
+ if indexable?
448
+ # :if/:unless constraints pass or were not present
449
+
450
+ @marked_for_auto_indexing =
451
+ if !new_record? && ignore_attributes = self.class.sunspot_options[:ignore_attribute_changes_of]
452
+ !(changed.map { |attr| attr.to_sym } - ignore_attributes).blank?
453
+ else
454
+ true
455
+ end
456
+
457
+ @marked_for_auto_removal = false
458
+ else
459
+ # :if/:unless constraints did not pass; do not auto index and
460
+ # actually go one step further by removing it from the index
461
+ @marked_for_auto_indexing = false
462
+ @marked_for_auto_removal = true
463
+ end
464
+
465
+ true
466
+ end
467
+
468
+ def perform_index_tasks
469
+ if @marked_for_auto_indexing
470
+ solr_index
471
+ remove_instance_variable(:@marked_for_auto_indexing)
472
+ elsif @marked_for_auto_removal
473
+ solr_remove_from_index
474
+ remove_instance_variable(:@marked_for_auto_removal)
475
+ end
476
+ end
477
+ end
478
+ end
479
+ end
480
+ end