robsharp-sunspot_rails 1.1.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) 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 +160 -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 +464 -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 +62 -0
  25. data/lib/sunspot/rails/version.rb +5 -0
  26. data/rails/init.rb +10 -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/model_lifecycle_spec.rb +63 -0
  48. data/spec/model_spec.rb +409 -0
  49. data/spec/request_lifecycle_spec.rb +52 -0
  50. data/spec/schema.rb +27 -0
  51. data/spec/server_spec.rb +36 -0
  52. data/spec/session_spec.rb +24 -0
  53. data/spec/spec_helper.rb +51 -0
  54. data/spec/stub_session_proxy_spec.rb +122 -0
  55. metadata +170 -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,464 @@
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
+ # :paginate<Boolean>::
48
+ # Retreive models using page numbers rather than database-style limit
49
+ # and offset. Assumes the total count of models cannot be determined
50
+ # without performing at least one request.
51
+ # Default: false
52
+ # :max_batch_size<Integer>::
53
+ # Override the command line specified batch size that is used for
54
+ # indexing on a per-model basis.
55
+ #
56
+ # ==== Example
57
+ #
58
+ # class Post < ActiveRecord::Base
59
+ # searchable do
60
+ # text :title, :body
61
+ # string :sort_title do
62
+ # title.downcase.sub(/^(an?|the)/, '')
63
+ # end
64
+ # integer :blog_id
65
+ # time :updated_at
66
+ # end
67
+ # end
68
+ #
69
+ def searchable(options = {}, &block)
70
+ Sunspot.setup(self, &block)
71
+
72
+ if searchable?
73
+ sunspot_options[:include].concat(Util::Array(options[:include]))
74
+ else
75
+ extend ClassMethods
76
+ include InstanceMethods
77
+
78
+ class_inheritable_hash :sunspot_options
79
+
80
+ unless options[:auto_index] == false
81
+ before_save :maybe_mark_for_auto_indexing
82
+ after_save :maybe_auto_index
83
+ end
84
+
85
+ unless options[:auto_remove] == false
86
+ after_destroy do |searchable|
87
+ searchable.remove_from_index
88
+ end
89
+ end
90
+ options[:include] = Util::Array(options[:include])
91
+
92
+ self.sunspot_options = options
93
+ end
94
+ end
95
+
96
+ #
97
+ # This method is defined on all ActiveRecord::Base subclasses. It
98
+ # is false for classes on which #searchable has not been called, and
99
+ # true for classes on which #searchable has been called.
100
+ #
101
+ # ==== Returns
102
+ #
103
+ # +false+
104
+ #
105
+ def searchable?
106
+ false
107
+ end
108
+ end
109
+
110
+ module ClassMethods
111
+ def self.extended(base) #:nodoc:
112
+ class <<base
113
+ alias_method :search, :solr_search unless method_defined? :search
114
+ alias_method :search_ids, :solr_search_ids unless method_defined? :search_ids
115
+ alias_method :remove_all_from_index, :solr_remove_all_from_index unless method_defined? :remove_all_from_index
116
+ alias_method :remove_all_from_index!, :solr_remove_all_from_index! unless method_defined? :remove_all_from_index!
117
+ alias_method :reindex, :solr_reindex unless method_defined? :reindex
118
+ alias_method :index, :solr_index unless method_defined? :index
119
+ alias_method :index_orphans, :solr_index_orphans unless method_defined? :index_orphans
120
+ alias_method :clean_index_orphans, :solr_clean_index_orphans unless method_defined? :clean_index_orphans
121
+ end
122
+ end
123
+ #
124
+ # Search for instances of this class in Solr. The block is delegated to
125
+ # the Sunspot.search method - see the Sunspot documentation for the full
126
+ # API.
127
+ #
128
+ # ==== Example
129
+ #
130
+ # Post.search(:include => [:blog]) do
131
+ # keywords 'best pizza'
132
+ # with :blog_id, 1
133
+ # order :updated_at, :desc
134
+ # facet :category_ids
135
+ # end
136
+ #
137
+ # ==== Options
138
+ #
139
+ # :include:: Specify associations to eager load
140
+ # :select:: Specify columns to select from database when loading results
141
+ #
142
+ # ==== Returns
143
+ #
144
+ # Sunspot::Search:: Object containing results, totals, facets, etc.
145
+ #
146
+ def solr_search(options = {}, &block)
147
+ solr_execute_search(options) do
148
+ Sunspot.new_search(self, &block)
149
+ end
150
+ end
151
+
152
+ #
153
+ # Get IDs of matching results without loading the result objects from
154
+ # the database. This method may be useful if search is used as an
155
+ # intermediate step in a larger find operation. The block is the same
156
+ # as the block provided to the #search method.
157
+ #
158
+ # ==== Returns
159
+ #
160
+ # Array:: Array of IDs, in the order returned by the search
161
+ #
162
+ def solr_search_ids(&block)
163
+ solr_execute_search_ids do
164
+ solr_search(&block)
165
+ end
166
+ end
167
+
168
+ #
169
+ # Remove instances of this class from the Solr index.
170
+ #
171
+ def solr_remove_all_from_index
172
+ Sunspot.remove_all(self)
173
+ end
174
+
175
+ #
176
+ # Remove all instances of this class from the Solr index and immediately
177
+ # commit.
178
+ #
179
+ #
180
+ def solr_remove_all_from_index!
181
+ Sunspot.remove_all!(self)
182
+ end
183
+
184
+ #
185
+ # Completely rebuild the index for this class. First removes all
186
+ # instances from the index, then loads records and indexes them.
187
+ #
188
+ # See #index for information on options, etc.
189
+ #
190
+ def solr_reindex(options = {})
191
+ solr_remove_all_from_index
192
+ solr_index(options)
193
+ end
194
+
195
+ #
196
+ # Add/update all existing records in the Solr index. The
197
+ # +batch_size+ argument specifies how many records to load out of the
198
+ # database at a time. The default batch size is 500; if nil is passed,
199
+ # records will not be indexed in batches. By default, a commit is issued
200
+ # after each batch; passing +false+ for +batch_commit+ will disable
201
+ # this, and only issue a commit at the end of the process. If associated
202
+ # objects need to indexed also, you can specify +include+ in format
203
+ # accepted by ActiveRecord to improve your sql select performance
204
+ #
205
+ # ==== Options (passed as a hash)
206
+ #
207
+ # batch_size<Integer>:: Batch size with which to load records. Passing
208
+ # 'nil' will skip batches. Default is 500.
209
+ # batch_commit<Boolean>:: Flag signalling if a commit should be done after
210
+ # after each batch is indexed, default is 'true'
211
+ # include<Mixed>:: include option to be passed to the ActiveRecord find,
212
+ # used for including associated objects that need to be
213
+ # indexed with the parent object, accepts all formats
214
+ # ActiveRecord::Base.find does
215
+ # first_id:: The lowest possible ID for this class. Defaults to 0, which
216
+ # is fine for integer IDs; string primary keys will need to
217
+ # specify something reasonable here.
218
+ #
219
+ # ==== Examples
220
+ #
221
+ # # index in batches of 500, commit after each
222
+ # Post.index
223
+ #
224
+ # # index all rows at once, then commit
225
+ # Post.index(:batch_size => nil)
226
+ #
227
+ # # index in batches of 500, commit when all batches complete
228
+ # Post.index(:batch_commit => false)
229
+ #
230
+ # # include the associated +author+ object when loading to index
231
+ # Post.index(:include => :author)
232
+ #
233
+ def solr_index(opts={})
234
+ if self.sunspot_options[:paginate]
235
+ solr_index_paged(opts)
236
+ else
237
+ solr_index_batched(opts)
238
+ end
239
+ end
240
+
241
+ #
242
+ # The default method of indexing records into the Solr index. Performs
243
+ # batching using the ID of the last retrieved records and a limit. Ideal
244
+ # for use with database/activerecord etc.
245
+ #
246
+ def solr_index_batched(opts={})
247
+ options = { :batch_size => 500, :batch_commit => true, :include => self.sunspot_options[:include], :first_id => 0}.merge(opts)
248
+ unless options[:batch_size]
249
+ Sunspot.index!(all(:include => options[:include]))
250
+ else
251
+ offset = 0
252
+ counter = 1
253
+ record_count = count
254
+ last_id = options[:first_id]
255
+ while(offset < record_count)
256
+ solr_benchmark options[:batch_size], counter do
257
+ records = find(:all, :include => options[:include], :conditions => ["#{table_name}.#{primary_key} > ?", last_id], :limit => options[:batch_size], :order => primary_key)
258
+ Sunspot.index(records)
259
+ last_id = records.last.id
260
+ end
261
+ Sunspot.commit if options[:batch_commit]
262
+ offset += options[:batch_size]
263
+ counter += 1
264
+ end
265
+ Sunspot.commit unless options[:batch_commit]
266
+ end
267
+ end
268
+
269
+ #
270
+ # Index records into Solr using pagintated pattern. Ideal for when you
271
+ # won't know the total number of records until after the first batch is
272
+ # retreived. Use with activeresource/pulling records from a webservice
273
+ # etc.
274
+ #
275
+ def solr_index_paged(opts={})
276
+ options = { :batch_size => 500, :batch_commit => true, :include => self.sunspot_options[:include], :first_id => 0}.merge(opts)
277
+ options[:batch_size] = self.sunspot_options[:max_batch_size] if options[:batch_size] > self.sunspot_options[:max_batch_size]
278
+
279
+ unless options[:batch_size]
280
+ Sunspot.index!(all(:include => options[:include]))
281
+ else
282
+ page = 0
283
+ per_page = options[:batch_size]
284
+ total_entries = options[:batch_size]
285
+ while ((page * per_page) < total_entries)
286
+ records = find(:all,
287
+ :include => options[:include],
288
+ :params => { :page => page + 1,
289
+ :per_page => per_page })
290
+ Sunspot.index(records)
291
+ Sunspot.commit if options[:batch_commit]
292
+
293
+ total_entries = records.total_entries
294
+ page += 1
295
+ end
296
+ Sunspot.commit unless options[:batch_commit]
297
+ end
298
+ end
299
+
300
+ #
301
+ # Return the IDs of records of this class that are indexed in Solr but
302
+ # do not exist in the database. Under normal circumstances, this should
303
+ # never happen, but this method is provided in case something goes
304
+ # wrong. Usually you will want to rectify the situation by calling
305
+ # #clean_index_orphans or #reindex
306
+ #
307
+ # ==== Returns
308
+ #
309
+ # Array:: Collection of IDs that exist in Solr but not in the database
310
+ def solr_index_orphans
311
+ count = self.count
312
+ indexed_ids = solr_search_ids { paginate(:page => 1, :per_page => count) }.to_set
313
+ all(:select => 'id').each do |object|
314
+ indexed_ids.delete(object.id)
315
+ end
316
+ indexed_ids.to_a
317
+ end
318
+
319
+ #
320
+ # Find IDs of records of this class that are indexed in Solr but do not
321
+ # exist in the database, and remove them from Solr. Under normal
322
+ # circumstances, this should not be necessary; this method is provided
323
+ # in case something goes wrong.
324
+ #
325
+ def solr_clean_index_orphans
326
+ solr_index_orphans.each do |id|
327
+ new do |fake_instance|
328
+ fake_instance.id = id
329
+ end.solr_remove_from_index
330
+ end
331
+ end
332
+
333
+ #
334
+ # Classes that have been defined as searchable return +true+ for this
335
+ # method.
336
+ #
337
+ # ==== Returns
338
+ #
339
+ # +true+
340
+ #
341
+ def searchable?
342
+ true
343
+ end
344
+
345
+ def solr_execute_search(options = {})
346
+ options.assert_valid_keys(:include, :select)
347
+ search = yield
348
+ unless options.empty?
349
+ search.build do |query|
350
+ if options[:include]
351
+ query.data_accessor_for(self).include = options[:include]
352
+ end
353
+ if options[:select]
354
+ query.data_accessor_for(self).select = options[:select]
355
+ end
356
+ end
357
+ end
358
+ search.execute
359
+ end
360
+
361
+ def solr_execute_search_ids(options = {})
362
+ search = yield
363
+ search.raw_results.map { |raw_result| raw_result.primary_key.to_i }
364
+ end
365
+
366
+ protected
367
+
368
+ #
369
+ # Does some logging for benchmarking indexing performance
370
+ #
371
+ def solr_benchmark(batch_size, counter, &block)
372
+ start = Time.now
373
+ logger.info("[#{Time.now}] Start Indexing")
374
+ yield
375
+ elapsed = Time.now-start
376
+ logger.info("[#{Time.now}] Completed Indexing. Rows indexed #{counter * batch_size}. Rows/sec: #{batch_size/elapsed.to_f} (Elapsed: #{elapsed} sec.)")
377
+ end
378
+
379
+ end
380
+
381
+ module InstanceMethods
382
+ def self.included(base) #:nodoc:
383
+ base.module_eval do
384
+ alias_method :index, :solr_index unless method_defined? :index
385
+ alias_method :index!, :solr_index! unless method_defined? :index!
386
+ alias_method :remove_from_index, :solr_remove_from_index unless method_defined? :remove_from_index
387
+ alias_method :remove_from_index!, :solr_remove_from_index! unless method_defined? :remove_from_index!
388
+ alias_method :more_like_this, :solr_more_like_this unless method_defined? :more_like_this
389
+ alias_method :more_like_this_ids, :solr_more_like_this_ids unless method_defined? :more_like_this_ids
390
+ end
391
+ end
392
+ #
393
+ # Index the model in Solr. If the model is already indexed, it will be
394
+ # updated. Using the defaults, you will usually not need to call this
395
+ # method, as models are indexed automatically when they are created or
396
+ # updated. If you have disabled automatic indexing (see
397
+ # ClassMethods#searchable), this method allows you to manage indexing
398
+ # manually.
399
+ #
400
+ def solr_index
401
+ Sunspot.index(self)
402
+ end
403
+
404
+ #
405
+ # Index the model in Solr and immediately commit. See #index
406
+ #
407
+ def solr_index!
408
+ Sunspot.index!(self)
409
+ end
410
+
411
+ #
412
+ # Remove the model from the Solr index. Using the defaults, this should
413
+ # not be necessary, as models will automatically be removed from the
414
+ # index when they are destroyed. If you disable automatic removal
415
+ # (which is not recommended!), you can use this method to manage removal
416
+ # manually.
417
+ #
418
+ def solr_remove_from_index
419
+ Sunspot.remove(self)
420
+ end
421
+
422
+ #
423
+ # Remove the model from the Solr index and commit immediately. See
424
+ # #remove_from_index
425
+ #
426
+ def solr_remove_from_index!
427
+ Sunspot.remove!(self)
428
+ end
429
+
430
+ def solr_more_like_this(*args, &block)
431
+ options = args.extract_options!
432
+ self.class.solr_execute_search(options) do
433
+ Sunspot.new_more_like_this(self, *args, &block)
434
+ end
435
+ end
436
+
437
+ def solr_more_like_this_ids(&block)
438
+ self.class.solr_execute_search_ids do
439
+ solr_more_like_this(&block)
440
+ end
441
+ end
442
+
443
+ private
444
+
445
+ def maybe_mark_for_auto_indexing
446
+ @marked_for_auto_indexing =
447
+ if !new_record? && ignore_attributes = self.class.sunspot_options[:ignore_attribute_changes_of]
448
+ @marked_for_auto_indexing = !(changed.map { |attr| attr.to_sym } - ignore_attributes).blank?
449
+ else
450
+ true
451
+ end
452
+ true
453
+ end
454
+
455
+ def maybe_auto_index
456
+ if @marked_for_auto_indexing
457
+ solr_index
458
+ remove_instance_variable(:@marked_for_auto_indexing)
459
+ end
460
+ end
461
+ end
462
+ end
463
+ end
464
+ end