nuatt_sunspot_rails 1.1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/History.txt +40 -0
  2. data/LICENSE +18 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +256 -0
  5. data/Rakefile +27 -0
  6. data/TODO +8 -0
  7. data/VERSION.yml +4 -0
  8. data/dev_tasks/gemspec.rake +33 -0
  9. data/dev_tasks/rdoc.rake +24 -0
  10. data/dev_tasks/release.rake +4 -0
  11. data/dev_tasks/todo.rake +4 -0
  12. data/generators/sunspot/sunspot_generator.rb +9 -0
  13. data/generators/sunspot/templates/sunspot.yml +18 -0
  14. data/install.rb +1 -0
  15. data/lib/sunspot/rails.rb +58 -0
  16. data/lib/sunspot/rails/adapters.rb +83 -0
  17. data/lib/sunspot/rails/configuration.rb +272 -0
  18. data/lib/sunspot/rails/request_lifecycle.rb +31 -0
  19. data/lib/sunspot/rails/searchable.rb +412 -0
  20. data/lib/sunspot/rails/server.rb +173 -0
  21. data/lib/sunspot/rails/solr_logging.rb +58 -0
  22. data/lib/sunspot/rails/spec_helper.rb +19 -0
  23. data/lib/sunspot/rails/stub_session_proxy.rb +88 -0
  24. data/lib/sunspot/rails/tasks.rb +61 -0
  25. data/lib/sunspot/rails/version.rb +5 -0
  26. data/rails/init.rb +7 -0
  27. data/spec/configuration_spec.rb +102 -0
  28. data/spec/mock_app/app/controllers/application.rb +10 -0
  29. data/spec/mock_app/app/controllers/application_controller.rb +10 -0
  30. data/spec/mock_app/app/controllers/posts_controller.rb +6 -0
  31. data/spec/mock_app/app/models/author.rb +8 -0
  32. data/spec/mock_app/app/models/blog.rb +12 -0
  33. data/spec/mock_app/app/models/location.rb +2 -0
  34. data/spec/mock_app/app/models/photo_post.rb +2 -0
  35. data/spec/mock_app/app/models/post.rb +10 -0
  36. data/spec/mock_app/app/models/post_with_auto.rb +10 -0
  37. data/spec/mock_app/config/boot.rb +110 -0
  38. data/spec/mock_app/config/database.yml +4 -0
  39. data/spec/mock_app/config/environment.rb +42 -0
  40. data/spec/mock_app/config/environments/development.rb +27 -0
  41. data/spec/mock_app/config/environments/test.rb +27 -0
  42. data/spec/mock_app/config/initializers/new_rails_defaults.rb +19 -0
  43. data/spec/mock_app/config/initializers/session_store.rb +15 -0
  44. data/spec/mock_app/config/routes.rb +43 -0
  45. data/spec/mock_app/config/sunspot.yml +19 -0
  46. data/spec/mock_app/db/schema.rb +27 -0
  47. data/spec/mock_app/db/test.db +0 -0
  48. data/spec/model_lifecycle_spec.rb +63 -0
  49. data/spec/model_spec.rb +409 -0
  50. data/spec/request_lifecycle_spec.rb +52 -0
  51. data/spec/schema.rb +27 -0
  52. data/spec/server_spec.rb +36 -0
  53. data/spec/session_spec.rb +24 -0
  54. data/spec/spec_helper.rb +51 -0
  55. data/spec/stub_session_proxy_spec.rb +122 -0
  56. metadata +183 -0
@@ -0,0 +1,31 @@
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
+ loaded_controllers =
12
+ [base].concat(base.subclasses.map { |subclass| subclass.constantize })
13
+ # Depending on how Sunspot::Rails is loaded, there may already be
14
+ # controllers loaded into memory that subclass this controller. In
15
+ # this case, since after_filter uses the inheritable_attribute
16
+ # structure, the already-loaded subclasses don't get the filters. So,
17
+ # the below ensures that all loaded controllers have the filter.
18
+ loaded_controllers.each do |controller|
19
+ controller.after_filter do
20
+ if Sunspot::Rails.configuration.auto_commit_after_request?
21
+ Sunspot.commit_if_dirty
22
+ elsif Sunspot::Rails.configuration.auto_commit_after_delete_request?
23
+ Sunspot.commit_if_delete_dirty
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,412 @@
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
+ # :ignore_attribute_changes_of<Array>::
40
+ # Define attributes, that should not trigger a reindex of that
41
+ # object. Usual suspects are updated_at or counters.
42
+ # :include<Mixed>::
43
+ # Define default ActiveRecord includes, set this to allow ActiveRecord
44
+ # to load required associations when indexing. See ActiveRecord's
45
+ # documentation on eager-loading for examples on how to set this
46
+ # Default: []
47
+ #
48
+ # ==== Example
49
+ #
50
+ # class Post < ActiveRecord::Base
51
+ # searchable do
52
+ # text :title, :body
53
+ # string :sort_title do
54
+ # title.downcase.sub(/^(an?|the)/, '')
55
+ # end
56
+ # integer :blog_id
57
+ # time :updated_at
58
+ # end
59
+ # end
60
+ #
61
+ def searchable(options = {}, &block)
62
+ Sunspot.setup(self, &block)
63
+
64
+ if searchable?
65
+ sunspot_options[:include].concat(Util::Array(options[:include]))
66
+ else
67
+ extend ClassMethods
68
+ include InstanceMethods
69
+
70
+ class_inheritable_hash :sunspot_options
71
+
72
+ unless options[:auto_index] == false
73
+ before_save :maybe_mark_for_auto_indexing
74
+ after_save :maybe_auto_index
75
+ end
76
+
77
+ unless options[:auto_remove] == false
78
+ after_destroy do |searchable|
79
+ searchable.remove_from_index
80
+ end
81
+ end
82
+ options[:include] = Util::Array(options[:include])
83
+
84
+ self.sunspot_options = options
85
+ end
86
+ end
87
+
88
+ #
89
+ # This method is defined on all ActiveRecord::Base subclasses. It
90
+ # is false for classes on which #searchable has not been called, and
91
+ # true for classes on which #searchable has been called.
92
+ #
93
+ # ==== Returns
94
+ #
95
+ # +false+
96
+ #
97
+ def searchable?
98
+ false
99
+ end
100
+ end
101
+
102
+ module ClassMethods
103
+ def self.extended(base) #:nodoc:
104
+ class <<base
105
+ alias_method :search, :solr_search unless method_defined? :search
106
+ alias_method :search_ids, :solr_search_ids unless method_defined? :search_ids
107
+ alias_method :remove_all_from_index, :solr_remove_all_from_index unless method_defined? :remove_all_from_index
108
+ alias_method :remove_all_from_index!, :solr_remove_all_from_index! unless method_defined? :remove_all_from_index!
109
+ alias_method :reindex, :solr_reindex unless method_defined? :reindex
110
+ alias_method :index, :solr_index unless method_defined? :index
111
+ alias_method :index_orphans, :solr_index_orphans unless method_defined? :index_orphans
112
+ alias_method :clean_index_orphans, :solr_clean_index_orphans unless method_defined? :clean_index_orphans
113
+ end
114
+ end
115
+ #
116
+ # Search for instances of this class in Solr. The block is delegated to
117
+ # the Sunspot.search method - see the Sunspot documentation for the full
118
+ # API.
119
+ #
120
+ # ==== Example
121
+ #
122
+ # Post.search(:include => [:blog]) do
123
+ # keywords 'best pizza'
124
+ # with :blog_id, 1
125
+ # order :updated_at, :desc
126
+ # facet :category_ids
127
+ # end
128
+ #
129
+ # ==== Options
130
+ #
131
+ # :include:: Specify associations to eager load
132
+ # :select:: Specify columns to select from database when loading results
133
+ #
134
+ # ==== Returns
135
+ #
136
+ # Sunspot::Search:: Object containing results, totals, facets, etc.
137
+ #
138
+ def solr_search(options = {}, &block)
139
+ solr_execute_search(options) do
140
+ Sunspot.new_search(self, &block)
141
+ end
142
+ end
143
+
144
+ #
145
+ # Get IDs of matching results without loading the result objects from
146
+ # the database. This method may be useful if search is used as an
147
+ # intermediate step in a larger find operation. The block is the same
148
+ # as the block provided to the #search method.
149
+ #
150
+ # ==== Returns
151
+ #
152
+ # Array:: Array of IDs, in the order returned by the search
153
+ #
154
+ def solr_search_ids(&block)
155
+ solr_execute_search_ids do
156
+ solr_search(&block)
157
+ end
158
+ end
159
+
160
+ #
161
+ # Remove instances of this class from the Solr index.
162
+ #
163
+ def solr_remove_all_from_index
164
+ Sunspot.remove_all(self)
165
+ end
166
+
167
+ #
168
+ # Remove all instances of this class from the Solr index and immediately
169
+ # commit.
170
+ #
171
+ #
172
+ def solr_remove_all_from_index!
173
+ Sunspot.remove_all!(self)
174
+ end
175
+
176
+ #
177
+ # Completely rebuild the index for this class. First removes all
178
+ # instances from the index, then loads records and indexes them.
179
+ #
180
+ # See #index for information on options, etc.
181
+ #
182
+ def solr_reindex(options = {})
183
+ solr_remove_all_from_index
184
+ solr_index(options)
185
+ end
186
+
187
+ #
188
+ # Add/update all existing records in the Solr index. The
189
+ # +batch_size+ argument specifies how many records to load out of the
190
+ # database at a time. The default batch size is 500; if nil is passed,
191
+ # records will not be indexed in batches. By default, a commit is issued
192
+ # after each batch; passing +false+ for +batch_commit+ will disable
193
+ # this, and only issue a commit at the end of the process. If associated
194
+ # objects need to indexed also, you can specify +include+ in format
195
+ # accepted by ActiveRecord to improve your sql select performance
196
+ #
197
+ # ==== Options (passed as a hash)
198
+ #
199
+ # batch_size<Integer>:: Batch size with which to load records. Passing
200
+ # 'nil' will skip batches. Default is 500.
201
+ # batch_commit<Boolean>:: Flag signalling if a commit should be done after
202
+ # after each batch is indexed, default is 'true'
203
+ # include<Mixed>:: include option to be passed to the ActiveRecord find,
204
+ # used for including associated objects that need to be
205
+ # indexed with the parent object, accepts all formats
206
+ # ActiveRecord::Base.find does
207
+ # first_id:: The lowest possible ID for this class. Defaults to 0, which
208
+ # is fine for integer IDs; string primary keys will need to
209
+ # specify something reasonable here.
210
+ #
211
+ # ==== Examples
212
+ #
213
+ # # index in batches of 500, commit after each
214
+ # Post.index
215
+ #
216
+ # # index all rows at once, then commit
217
+ # Post.index(:batch_size => nil)
218
+ #
219
+ # # index in batches of 500, commit when all batches complete
220
+ # Post.index(:batch_commit => false)
221
+ #
222
+ # # include the associated +author+ object when loading to index
223
+ # Post.index(:include => :author)
224
+ #
225
+ def solr_index(opts={})
226
+ options = { :batch_size => 500, :batch_commit => true, :include => self.sunspot_options[:include], :first_id => 0}.merge(opts)
227
+ unless options[:batch_size]
228
+ Sunspot.index!(all(:include => options[:include]))
229
+ else
230
+ offset = 0
231
+ counter = 1
232
+ record_count = count
233
+ last_id = options[:first_id]
234
+ while(offset < record_count)
235
+ solr_benchmark options[:batch_size], counter do
236
+ records = all(:include => options[:include], :conditions => ["#{table_name}.#{primary_key} > ?", last_id], :limit => options[:batch_size], :order => primary_key)
237
+ Sunspot.index(records)
238
+ last_id = records.last.id
239
+ end
240
+ Sunspot.commit if options[:batch_commit]
241
+ offset += options[:batch_size]
242
+ counter += 1
243
+ end
244
+ Sunspot.commit unless options[:batch_commit]
245
+ end
246
+ end
247
+
248
+ #
249
+ # Return the IDs of records of this class that are indexed in Solr but
250
+ # do not exist in the database. Under normal circumstances, this should
251
+ # never happen, but this method is provided in case something goes
252
+ # wrong. Usually you will want to rectify the situation by calling
253
+ # #clean_index_orphans or #reindex
254
+ #
255
+ # ==== Returns
256
+ #
257
+ # Array:: Collection of IDs that exist in Solr but not in the database
258
+ def solr_index_orphans
259
+ count = self.count
260
+ indexed_ids = solr_search_ids { paginate(:page => 1, :per_page => count) }.to_set
261
+ all(:select => 'id').each do |object|
262
+ indexed_ids.delete(object.id)
263
+ end
264
+ indexed_ids.to_a
265
+ end
266
+
267
+ #
268
+ # Find IDs of records of this class that are indexed in Solr but do not
269
+ # exist in the database, and remove them from Solr. Under normal
270
+ # circumstances, this should not be necessary; this method is provided
271
+ # in case something goes wrong.
272
+ #
273
+ def solr_clean_index_orphans
274
+ solr_index_orphans.each do |id|
275
+ new do |fake_instance|
276
+ fake_instance.id = id
277
+ end.solr_remove_from_index
278
+ end
279
+ end
280
+
281
+ #
282
+ # Classes that have been defined as searchable return +true+ for this
283
+ # method.
284
+ #
285
+ # ==== Returns
286
+ #
287
+ # +true+
288
+ #
289
+ def searchable?
290
+ true
291
+ end
292
+
293
+ def solr_execute_search(options = {})
294
+ options.assert_valid_keys(:include, :select)
295
+ search = yield
296
+ unless options.empty?
297
+ search.build do |query|
298
+ if options[:include]
299
+ query.data_accessor_for(self).include = options[:include]
300
+ end
301
+ if options[:select]
302
+ query.data_accessor_for(self).select = options[:select]
303
+ end
304
+ end
305
+ end
306
+ search.execute
307
+ end
308
+
309
+ def solr_execute_search_ids(options = {})
310
+ search = yield
311
+ search.raw_results.map { |raw_result| raw_result.primary_key.to_i }
312
+ end
313
+
314
+ protected
315
+
316
+ #
317
+ # Does some logging for benchmarking indexing performance
318
+ #
319
+ def solr_benchmark(batch_size, counter, &block)
320
+ start = Time.now
321
+ logger.info("[#{Time.now}] Start Indexing")
322
+ yield
323
+ elapsed = Time.now-start
324
+ logger.info("[#{Time.now}] Completed Indexing. Rows indexed #{counter * batch_size}. Rows/sec: #{batch_size/elapsed.to_f} (Elapsed: #{elapsed} sec.)")
325
+ end
326
+
327
+ end
328
+
329
+ module InstanceMethods
330
+ def self.included(base) #:nodoc:
331
+ base.module_eval do
332
+ alias_method :index, :solr_index unless method_defined? :index
333
+ alias_method :index!, :solr_index! unless method_defined? :index!
334
+ alias_method :remove_from_index, :solr_remove_from_index unless method_defined? :remove_from_index
335
+ alias_method :remove_from_index!, :solr_remove_from_index! unless method_defined? :remove_from_index!
336
+ alias_method :more_like_this, :solr_more_like_this unless method_defined? :more_like_this
337
+ alias_method :more_like_this_ids, :solr_more_like_this_ids unless method_defined? :more_like_this_ids
338
+ end
339
+ end
340
+ #
341
+ # Index the model in Solr. If the model is already indexed, it will be
342
+ # updated. Using the defaults, you will usually not need to call this
343
+ # method, as models are indexed automatically when they are created or
344
+ # updated. If you have disabled automatic indexing (see
345
+ # ClassMethods#searchable), this method allows you to manage indexing
346
+ # manually.
347
+ #
348
+ def solr_index
349
+ Sunspot.index(self)
350
+ end
351
+
352
+ #
353
+ # Index the model in Solr and immediately commit. See #index
354
+ #
355
+ def solr_index!
356
+ Sunspot.index!(self)
357
+ end
358
+
359
+ #
360
+ # Remove the model from the Solr index. Using the defaults, this should
361
+ # not be necessary, as models will automatically be removed from the
362
+ # index when they are destroyed. If you disable automatic removal
363
+ # (which is not recommended!), you can use this method to manage removal
364
+ # manually.
365
+ #
366
+ def solr_remove_from_index
367
+ Sunspot.remove(self)
368
+ end
369
+
370
+ #
371
+ # Remove the model from the Solr index and commit immediately. See
372
+ # #remove_from_index
373
+ #
374
+ def solr_remove_from_index!
375
+ Sunspot.remove!(self)
376
+ end
377
+
378
+ def solr_more_like_this(*args, &block)
379
+ options = args.extract_options!
380
+ self.class.solr_execute_search(options) do
381
+ Sunspot.new_more_like_this(self, *args, &block)
382
+ end
383
+ end
384
+
385
+ def solr_more_like_this_ids(&block)
386
+ self.class.solr_execute_search_ids do
387
+ solr_more_like_this(&block)
388
+ end
389
+ end
390
+
391
+ private
392
+
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?
397
+ else
398
+ true
399
+ end
400
+ true
401
+ end
402
+
403
+ def maybe_auto_index
404
+ if @marked_for_auto_indexing
405
+ solr_index
406
+ remove_instance_variable(:@marked_for_auto_indexing)
407
+ end
408
+ end
409
+ end
410
+ end
411
+ end
412
+ end
@@ -0,0 +1,173 @@
1
+ module Sunspot
2
+ module Rails
3
+ class Server < Sunspot::Server
4
+ # ActiveSupport log levels are integers; this array maps them to the
5
+ # appropriate java.util.logging.Level constant
6
+ LOG_LEVELS = %w(FINE INFO WARNING SEVERE SEVERE INFO)
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
+ #
41
+ # Directory in which to store PID files
42
+ #
43
+ def pid_dir
44
+ File.join(::Rails.root, 'tmp', 'pids')
45
+ end
46
+
47
+ #
48
+ # Name of the PID file
49
+ #
50
+ def pid_file
51
+ "sunspot-solr-#{::Rails.env}.pid"
52
+ end
53
+
54
+ #
55
+ # Directory to store lucene index data files
56
+ #
57
+ # ==== Returns
58
+ #
59
+ # String:: data_path
60
+ #
61
+ def solr_data_dir
62
+ File.join(solr_home, 'data', ::Rails.env)
63
+ end
64
+
65
+ #
66
+ # Directory to use for Solr home.
67
+ #
68
+ def solr_home
69
+ File.join(::Rails.root, 'solr')
70
+ end
71
+
72
+ #
73
+ # Solr start jar
74
+ #
75
+ def solr_jar
76
+ configuration.solr_jar || super
77
+ end
78
+
79
+ #
80
+ # Port on which to run Solr
81
+ #
82
+ def port
83
+ configuration.port
84
+ end
85
+
86
+ #
87
+ # Severity level for logging. This is based on the severity level for the
88
+ # Rails logger.
89
+ #
90
+ def log_level
91
+ LOG_LEVELS[::Rails.logger.level]
92
+ end
93
+
94
+ #
95
+ # Log file for Solr. File is in the rails log/ directory.
96
+ #
97
+ def log_file
98
+ File.join(::Rails.root, 'log', "sunspot-solr-#{::Rails.env}.log")
99
+ end
100
+
101
+ #
102
+ # Minimum Java heap size for Solr
103
+ #
104
+ def min_memory
105
+ configuration.min_memory
106
+ end
107
+
108
+ #
109
+ # Maximum Java heap size for Solr
110
+ #
111
+ def max_memory
112
+ configuration.max_memory
113
+ end
114
+
115
+ private
116
+
117
+ #
118
+ # access to the Sunspot::Rails::Configuration, defined in
119
+ # sunspot.yml. Use Sunspot::Rails.configuration if you want
120
+ # to access the configuration directly.
121
+ #
122
+ # ==== returns
123
+ #
124
+ # Sunspot::Rails::Configuration:: configuration
125
+ #
126
+ def configuration
127
+ Sunspot::Rails.configuration
128
+ 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
+ end
172
+ end
173
+ end