sunspot_rails 1.2.1 → 1.3.0.rc6

Sign up to get free protection for your applications and to get access to all the features.
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