erichummel-sunspot_rails 1.2.1g → 2.0.0.pre.111215

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. data/.gitignore +7 -0
  2. data/.rspec +1 -0
  3. data/History.txt +23 -0
  4. data/README.rdoc +14 -7
  5. data/Rakefile +6 -7
  6. data/dev_tasks/rdoc.rake +2 -2
  7. data/dev_tasks/spec.rake +103 -18
  8. data/gemfiles/rails-2.3.14 +12 -0
  9. data/gemfiles/rails-3.0.11 +12 -0
  10. data/gemfiles/rails-3.1.3 +12 -0
  11. data/gemfiles/rails-3.2.1 +12 -0
  12. data/generators/sunspot/templates/sunspot.yml +3 -1
  13. data/lib/generators/sunspot_rails/install/templates/config/sunspot.yml +3 -1
  14. data/lib/sunspot/rails.rb +14 -3
  15. data/lib/sunspot/rails/configuration.rb +36 -3
  16. data/lib/sunspot/rails/log_subscriber.rb +33 -0
  17. data/lib/sunspot/rails/railtie.rb +10 -0
  18. data/lib/sunspot/rails/railties/controller_runtime.rb +36 -0
  19. data/lib/sunspot/rails/searchable.rb +104 -25
  20. data/lib/sunspot/rails/server.rb +10 -76
  21. data/lib/sunspot/rails/solr_instrumentation.rb +19 -0
  22. data/lib/sunspot/rails/solr_logging.rb +16 -17
  23. data/lib/sunspot/rails/stub_session_proxy.rb +56 -2
  24. data/lib/sunspot/rails/tasks.rb +56 -33
  25. data/lib/sunspot_rails.rb +5 -0
  26. data/spec/configuration_spec.rb +41 -5
  27. data/spec/model_lifecycle_spec.rb +1 -1
  28. data/spec/model_spec.rb +246 -1
  29. data/spec/rails_template/app/controllers/application_controller.rb +10 -0
  30. data/spec/rails_template/app/controllers/posts_controller.rb +6 -0
  31. data/spec/rails_template/app/models/author.rb +8 -0
  32. data/spec/rails_template/app/models/blog.rb +12 -0
  33. data/spec/rails_template/app/models/location.rb +2 -0
  34. data/spec/rails_template/app/models/photo_post.rb +2 -0
  35. data/spec/rails_template/app/models/post.rb +11 -0
  36. data/spec/rails_template/app/models/post_with_auto.rb +10 -0
  37. data/spec/rails_template/app/models/post_with_default_scope.rb +11 -0
  38. data/spec/rails_template/config/boot.rb +127 -0
  39. data/spec/rails_template/config/preinitializer.rb +22 -0
  40. data/spec/rails_template/config/routes.rb +9 -0
  41. data/spec/rails_template/config/sunspot.yml +24 -0
  42. data/spec/rails_template/db/schema.rb +27 -0
  43. data/spec/request_lifecycle_spec.rb +1 -1
  44. data/spec/searchable_spec.rb +12 -0
  45. data/spec/server_spec.rb +3 -7
  46. data/spec/session_spec.rb +35 -2
  47. data/spec/shared_examples/indexed_after_save.rb +8 -0
  48. data/spec/shared_examples/not_indexed_after_save.rb +8 -0
  49. data/spec/spec_helper.rb +4 -2
  50. data/spec/stub_session_proxy_spec.rb +2 -2
  51. data/sunspot_rails.gemspec +43 -0
  52. metadata +144 -87
  53. data/TESTING.md +0 -35
  54. data/VERSION.yml +0 -4
  55. data/lib/sunspot/rails/version.rb +0 -5
@@ -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
@@ -11,6 +11,16 @@ module Sunspot
11
11
  ActiveSupport.on_load(:action_controller) do
12
12
  include(Sunspot::Rails::RequestLifecycle)
13
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
14
24
  end
15
25
 
16
26
  rake_tasks do
@@ -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
@@ -36,6 +36,13 @@ module Sunspot #:nodoc:
36
36
  # Automatically remove models from the Solr index when they are
37
37
  # destroyed. <b>Setting this option to +false+ is not recommended
38
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>).
39
46
  # :ignore_attribute_changes_of<Array>::
40
47
  # Define attributes, that should not trigger a reindex of that
41
48
  # object. Usual suspects are updated_at or counters.
@@ -44,6 +51,13 @@ module Sunspot #:nodoc:
44
51
  # to load required associations when indexing. See ActiveRecord's
45
52
  # documentation on eager-loading for examples on how to set this
46
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>).
47
61
  #
48
62
  # ==== Example
49
63
  #
@@ -67,11 +81,11 @@ module Sunspot #:nodoc:
67
81
  extend ClassMethods
68
82
  include InstanceMethods
69
83
 
70
- class_inheritable_hash :sunspot_options
71
-
84
+ class_attribute :sunspot_options
85
+
72
86
  unless options[:auto_index] == false
73
- before_save :maybe_mark_for_auto_indexing
74
- after_save :maybe_auto_index
87
+ before_save :mark_for_auto_indexing_or_removal
88
+ after_save :perform_index_tasks
75
89
  end
76
90
 
77
91
  unless options[:auto_remove] == false
@@ -224,25 +238,34 @@ module Sunspot #:nodoc:
224
238
  #
225
239
  def solr_index(opts={})
226
240
  options = {
227
- :batch_size => 50,
241
+ :batch_size => Sunspot.config.indexing.default_batch_size,
228
242
  :batch_commit => true,
229
243
  :include => self.sunspot_options[:include],
230
- :first_id => 0
244
+ :start => opts.delete(:first_id) || 0
231
245
  }.merge(opts)
232
-
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]
233
252
  if options[:batch_size]
234
- counter = 0
235
- find_in_batches(:include => options[:include], :batch_size => options[:batch_size]) do |records|
236
- solr_benchmark options[:batch_size], counter do
237
- Sunspot.index(records)
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]
238
258
  end
239
- Sunspot.commit if options[:batch_commit]
240
- counter += 1
259
+ # track progress
260
+ progress_bar.increment!(records.length) if progress_bar
261
+ batch_counter += 1
241
262
  end
242
- Sunspot.commit unless options[:batch_commit]
243
263
  else
244
- Sunspot.index!(all(:include => options[:include]))
264
+ records = all(:include => options[:include]).select { |model| model.indexable? }
265
+ Sunspot.index!(records)
245
266
  end
267
+ # perform a final commit if not committing in batches
268
+ Sunspot.commit unless options[:batch_commit]
246
269
  end
247
270
 
248
271
  #
@@ -252,13 +275,19 @@ module Sunspot #:nodoc:
252
275
  # wrong. Usually you will want to rectify the situation by calling
253
276
  # #clean_index_orphans or #reindex
254
277
  #
278
+ # ==== Options (passed as a hash)
279
+ #
280
+ # batch_size<Integer>:: Batch size with which to load records. Passing
281
+ # Default is 1000 (from ActiveRecord).
282
+ #
255
283
  # ==== Returns
256
284
  #
257
285
  # Array:: Collection of IDs that exist in Solr but not in the database
258
- def solr_index_orphans
286
+ def solr_index_orphans(opts={})
287
+ batch_size = opts[:batch_size] || Sunspot.config.indexing.default_batch_size
259
288
  count = self.count
260
289
  indexed_ids = solr_search_ids { paginate(:page => 1, :per_page => count) }.to_set
261
- all(:select => 'id').each do |object|
290
+ find_each(:select => 'id', :batch_size => batch_size) do |object|
262
291
  indexed_ids.delete(object.id)
263
292
  end
264
293
  indexed_ids.to_a
@@ -270,8 +299,13 @@ module Sunspot #:nodoc:
270
299
  # circumstances, this should not be necessary; this method is provided
271
300
  # in case something goes wrong.
272
301
  #
273
- def solr_clean_index_orphans
274
- solr_index_orphans.each do |id|
302
+ # ==== Options (passed as a hash)
303
+ #
304
+ # batch_size<Integer>:: Batch size with which to load records
305
+ # Default is 50
306
+ #
307
+ def solr_clean_index_orphans(opts={})
308
+ solr_index_orphans(opts).each do |id|
275
309
  new do |fake_instance|
276
310
  fake_instance.id = id
277
311
  end.solr_remove_from_index
@@ -388,22 +422,67 @@ module Sunspot #:nodoc:
388
422
  end
389
423
  end
390
424
 
425
+ def indexable?
426
+ # options[:if] is not specified or they successfully pass
427
+ if_passes = self.class.sunspot_options[:if].nil? ||
428
+ constraint_passes?(self.class.sunspot_options[:if])
429
+
430
+ # options[:unless] is not specified or they successfully pass
431
+ unless_passes = self.class.sunspot_options[:unless].nil? ||
432
+ !constraint_passes?(self.class.sunspot_options[:unless])
433
+
434
+ if_passes and unless_passes
435
+ end
436
+
391
437
  private
392
438
 
393
- def maybe_mark_for_auto_indexing
394
- @marked_for_auto_indexing =
395
- if !new_record? && ignore_attributes = self.class.sunspot_options[:ignore_attribute_changes_of]
396
- @marked_for_auto_indexing = !(changed.map { |attr| attr.to_sym } - ignore_attributes).blank?
439
+ def constraint_passes?(constraint)
440
+ case constraint
441
+ when Symbol
442
+ self.__send__(constraint)
443
+ when String
444
+ self.__send__(constraint.to_sym)
445
+ when Enumerable
446
+ # All constraints must pass
447
+ constraint.all? { |inner_constraint| constraint_passes?(inner_constraint) }
448
+ else
449
+ if constraint.respond_to?(:call) # could be a Proc or anything else that responds to call
450
+ constraint.call(self)
397
451
  else
398
- true
452
+ raise ArgumentError, "Unknown constraint type: #{constraint} (#{constraint.class})"
399
453
  end
454
+ end
455
+ end
456
+
457
+ def mark_for_auto_indexing_or_removal
458
+ if indexable?
459
+ # :if/:unless constraints pass or were not present
460
+
461
+ @marked_for_auto_indexing =
462
+ if !new_record? && ignore_attributes = self.class.sunspot_options[:ignore_attribute_changes_of]
463
+ !(changed.map { |attr| attr.to_sym } - ignore_attributes).blank?
464
+ else
465
+ true
466
+ end
467
+
468
+ @marked_for_auto_removal = false
469
+ else
470
+ # :if/:unless constraints did not pass; do not auto index and
471
+ # actually go one step further by removing it from the index
472
+ @marked_for_auto_indexing = false
473
+ @marked_for_auto_removal = true
474
+ end
475
+
400
476
  true
401
477
  end
402
478
 
403
- def maybe_auto_index
479
+ def perform_index_tasks
404
480
  if @marked_for_auto_indexing
405
481
  solr_index
406
482
  remove_instance_variable(:@marked_for_auto_indexing)
483
+ elsif @marked_for_auto_removal
484
+ solr_remove_from_index
485
+ remove_instance_variable(:@marked_for_auto_removal)
407
486
  end
408
487
  end
409
488
  end
@@ -1,47 +1,15 @@
1
1
  module Sunspot
2
2
  module Rails
3
- class Server < Sunspot::Server
3
+ class Server < Sunspot::Solr::Server
4
4
  # ActiveSupport log levels are integers; this array maps them to the
5
5
  # appropriate java.util.logging.Level constant
6
6
  LOG_LEVELS = %w(FINE INFO WARNING SEVERE SEVERE INFO)
7
7
 
8
- def start
9
- bootstrap
10
- super
11
- end
12
-
13
- def run
14
- bootstrap
15
- super
16
- end
17
-
18
- #
19
- # Bootstrap a new solr_home by creating all required
20
- # directories.
21
- #
22
- # ==== Returns
23
- #
24
- # Boolean:: success
25
- #
26
- def bootstrap
27
- unless @bootstrapped
28
- install_solr_home
29
- @bootstrapped = true
30
- end
31
- end
32
-
33
- #
34
- # Directory to store custom libraries for solr
35
- #
36
- def lib_path
37
- File.join( solr_home, 'lib' )
38
- end
39
-
40
8
  #
41
9
  # Directory in which to store PID files
42
10
  #
43
11
  def pid_dir
44
- File.join(::Rails.root, 'tmp', 'pids')
12
+ configuration.pid_dir || File.join(::Rails.root, 'tmp', 'pids')
45
13
  end
46
14
 
47
15
  #
@@ -59,7 +27,7 @@ module Sunspot
59
27
  # String:: data_path
60
28
  #
61
29
  def solr_data_dir
62
- File.join(solr_home, 'data', ::Rails.env)
30
+ configuration.data_path
63
31
  end
64
32
 
65
33
  #
@@ -76,6 +44,13 @@ module Sunspot
76
44
  configuration.solr_jar || super
77
45
  end
78
46
 
47
+ #
48
+ # Address on which to run Solr
49
+ #
50
+ def bind_address
51
+ configuration.bind_address
52
+ end
53
+
79
54
  #
80
55
  # Port on which to run Solr
81
56
  #
@@ -134,47 +109,6 @@ module Sunspot
134
109
  Sunspot::Rails.configuration
135
110
  end
136
111
 
137
- #
138
- # Directory to store solr config files
139
- #
140
- # ==== Returns
141
- #
142
- # String:: config_path
143
- #
144
- def config_path
145
- File.join(solr_home, 'conf')
146
- end
147
-
148
- #
149
- # Copy default solr configuration files from sunspot
150
- # gem to the new solr_home/config directory
151
- #
152
- # ==== Returns
153
- #
154
- # Boolean:: success
155
- #
156
- def install_solr_home
157
- unless File.exists?(File.join(File.dirname(solr_jar), solr_home))
158
- Sunspot::Installer.execute(
159
- solr_home,
160
- :force => true,
161
- :verbose => true
162
- )
163
- end
164
- end
165
-
166
- #
167
- # Create new solr_home, config, log and pid directories
168
- #
169
- # ==== Returns
170
- #
171
- # Boolean:: success
172
- #
173
- def create_solr_directories
174
- [solr_data_dir, pid_dir].each do |path|
175
- FileUtils.mkdir_p( path )
176
- end
177
- end
178
112
  end
179
113
  end
180
114
  end
@@ -0,0 +1,19 @@
1
+ module Sunspot
2
+ module Rails
3
+ module SolrInstrumentation
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ alias_method_chain :execute, :as_instrumentation
8
+ end
9
+
10
+
11
+ def execute_with_as_instrumentation(path, params={}, *extra)
12
+ ActiveSupport::Notifications.instrument("request.rsolr",
13
+ {:path => path, :parameters => params}) do
14
+ execute_without_as_instrumentation(path, params, *extra)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,20 +1,21 @@
1
1
  module Sunspot
2
2
  module Rails
3
3
  module SolrLogging
4
+
4
5
  class <<self
5
6
  def included(base)
6
- base.module_eval { alias_method_chain(:request, :rails_logging) }
7
+ base.alias_method_chain :execute, :rails_logging
7
8
  end
8
9
  end
9
10
 
10
- def request_with_rails_logging(path, params={}, *extra)
11
+ COMMIT = %r{<commit/>}
11
12
 
12
- # Set up logging text.
13
- body = (params.nil? || params.empty?) ? extra.first : params.inspect
14
- action = path[1..-1].capitalize
15
- if body == "<commit/>"
16
- action = 'Commit'
17
- body = ''
13
+ def execute_with_rails_logging(client, request_context)
14
+ body = (request_context[:data]||"").dup
15
+ action = request_context[:path].capitalize
16
+ if body =~ COMMIT
17
+ action = "Commit"
18
+ body = ""
18
19
  end
19
20
  body = body[0, 800] + '...' if body.length > 800
20
21
 
@@ -22,7 +23,7 @@ module Sunspot
22
23
  response = nil
23
24
  begin
24
25
  ms = Benchmark.ms do
25
- response = request_without_rails_logging(path, params, *extra)
26
+ response = execute_without_rails_logging(client, request_context)
26
27
  end
27
28
  log_name = 'Solr %s (%.1fms)' % [action, ms]
28
29
  ::Rails.logger.debug(format_log_entry(log_name, body))
@@ -39,10 +40,10 @@ module Sunspot
39
40
 
40
41
  def format_log_entry(message, dump = nil)
41
42
  @colorize_logging ||= begin
42
- ::Rails.application.config.colorize_logging # Rails 3
43
- rescue NoMethodError
44
- ActiveRecord::Base.colorize_logging # Rails 2
45
- end
43
+ ::Rails.application.config.colorize_logging # Rails 3
44
+ rescue NoMethodError
45
+ ActiveRecord::Base.colorize_logging # Rails 2
46
+ end
46
47
  if @colorize_logging
47
48
  message_color, dump_color = "4;32;1", "0;1"
48
49
  log_entry = " \e[#{message_color}m#{message}\e[0m "
@@ -56,8 +57,6 @@ module Sunspot
56
57
  end
57
58
  end
58
59
 
59
- begin
60
- RSolr::Client.module_eval { include(Sunspot::Rails::SolrLogging) }
61
- rescue NameError # RSolr 0.9.6
62
- RSolr::Connection::Base.module_eval { include(Sunspot::Rails::SolrLogging) }
60
+ RSolr::Connection.module_eval do
61
+ include Sunspot::Rails::SolrLogging
63
62
  end