sunspot_rails 1.2.1 → 1.3.0.rc6

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 (50) hide show
  1. data/.gitignore +7 -0
  2. data/.rspec +1 -0
  3. data/History.txt +15 -0
  4. data/README.rdoc +14 -7
  5. data/Rakefile +0 -6
  6. data/TESTING.md +32 -18
  7. data/dev_tasks/spec.rake +103 -18
  8. data/gemfiles/rails-2.3.14 +15 -0
  9. data/gemfiles/rails-3.0.10 +15 -0
  10. data/gemfiles/rails-3.1.1 +15 -0
  11. data/lib/sunspot/rails.rb +13 -6
  12. data/lib/sunspot/rails/configuration.rb +25 -4
  13. data/lib/sunspot/rails/log_subscriber.rb +33 -0
  14. data/lib/sunspot/rails/railtie.rb +10 -0
  15. data/lib/sunspot/rails/railties/controller_runtime.rb +36 -0
  16. data/lib/sunspot/rails/searchable.rb +88 -20
  17. data/lib/sunspot/rails/server.rb +10 -77
  18. data/lib/sunspot/rails/solr_instrumentation.rb +20 -0
  19. data/lib/sunspot/rails/solr_logging.rb +16 -17
  20. data/lib/sunspot/rails/stub_session_proxy.rb +56 -2
  21. data/lib/sunspot/rails/tasks.rb +56 -33
  22. data/lib/sunspot_rails.rb +5 -0
  23. data/spec/configuration_spec.rb +27 -5
  24. data/spec/model_lifecycle_spec.rb +1 -1
  25. data/spec/model_spec.rb +240 -1
  26. data/spec/rails_template/app/controllers/application_controller.rb +10 -0
  27. data/spec/rails_template/app/controllers/posts_controller.rb +6 -0
  28. data/spec/rails_template/app/models/author.rb +8 -0
  29. data/spec/rails_template/app/models/blog.rb +12 -0
  30. data/spec/rails_template/app/models/location.rb +2 -0
  31. data/spec/rails_template/app/models/photo_post.rb +2 -0
  32. data/spec/rails_template/app/models/post.rb +11 -0
  33. data/spec/rails_template/app/models/post_with_auto.rb +10 -0
  34. data/spec/rails_template/app/models/post_with_default_scope.rb +11 -0
  35. data/spec/rails_template/config/boot.rb +127 -0
  36. data/spec/rails_template/config/preinitializer.rb +22 -0
  37. data/spec/rails_template/config/routes.rb +9 -0
  38. data/spec/rails_template/config/sunspot.yml +22 -0
  39. data/spec/rails_template/db/schema.rb +27 -0
  40. data/spec/request_lifecycle_spec.rb +1 -1
  41. data/spec/searchable_spec.rb +12 -0
  42. data/spec/server_spec.rb +2 -6
  43. data/spec/session_spec.rb +35 -2
  44. data/spec/shared_examples/indexed_after_save.rb +8 -0
  45. data/spec/shared_examples/not_indexed_after_save.rb +8 -0
  46. data/spec/spec_helper.rb +4 -2
  47. data/spec/stub_session_proxy_spec.rb +2 -2
  48. data/sunspot_rails.gemspec +43 -0
  49. metadata +114 -44
  50. 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
@@ -227,22 +241,31 @@ module Sunspot #:nodoc:
227
241
  :batch_size => 50,
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
  #
@@ -388,22 +411,67 @@ module Sunspot #:nodoc:
388
411
  end
389
412
  end
390
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
+
391
426
  private
392
427
 
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?
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)
397
440
  else
398
- true
441
+ raise ArgumentError, "Unknown constraint type: #{constraint} (#{constraint.class})"
399
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
+
400
465
  true
401
466
  end
402
467
 
403
- def maybe_auto_index
468
+ def perform_index_tasks
404
469
  if @marked_for_auto_indexing
405
470
  solr_index
406
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)
407
475
  end
408
476
  end
409
477
  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
  #
@@ -126,48 +101,6 @@ module Sunspot
126
101
  def configuration
127
102
  Sunspot::Rails.configuration
128
103
  end
129
-
130
- #
131
- # Directory to store solr config files
132
- #
133
- # ==== Returns
134
- #
135
- # String:: config_path
136
- #
137
- def config_path
138
- File.join(solr_home, 'conf')
139
- end
140
-
141
- #
142
- # Copy default solr configuration files from sunspot
143
- # gem to the new solr_home/config directory
144
- #
145
- # ==== Returns
146
- #
147
- # Boolean:: success
148
- #
149
- def install_solr_home
150
- unless File.exists?(solr_home)
151
- Sunspot::Installer.execute(
152
- solr_home,
153
- :force => true,
154
- :verbose => true
155
- )
156
- end
157
- end
158
-
159
- #
160
- # Create new solr_home, config, log and pid directories
161
- #
162
- # ==== Returns
163
- #
164
- # Boolean:: success
165
- #
166
- def create_solr_directories
167
- [solr_data_dir, pid_dir].each do |path|
168
- FileUtils.mkdir_p( path )
169
- end
170
- end
171
104
  end
172
105
  end
173
106
  end
@@ -0,0 +1,20 @@
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
+ module InstanceMethods
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
20
+ 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