robsharp-sunspot_rails 1.1.0.2

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 (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