collectiveidea-sunspot_rails 0.10.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/LICENSE +18 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +231 -0
  4. data/Rakefile +19 -0
  5. data/VERSION.yml +4 -0
  6. data/dev_tasks/gemspec.rake +28 -0
  7. data/dev_tasks/rdoc.rake +7 -0
  8. data/dev_tasks/release.rake +4 -0
  9. data/dev_tasks/todo.rake +4 -0
  10. data/install.rb +1 -0
  11. data/lib/sunspot/rails.rb +13 -0
  12. data/lib/sunspot/rails/adapters.rb +54 -0
  13. data/lib/sunspot/rails/configuration.rb +114 -0
  14. data/lib/sunspot/rails/request_lifecycle.rb +18 -0
  15. data/lib/sunspot/rails/searchable.rb +305 -0
  16. data/lib/sunspot/rails/tasks.rb +53 -0
  17. data/rails/init.rb +10 -0
  18. data/spec/configuration_spec.rb +53 -0
  19. data/spec/mock_app/app/controllers/application.rb +10 -0
  20. data/spec/mock_app/app/controllers/application_controller.rb +10 -0
  21. data/spec/mock_app/app/controllers/posts_controller.rb +6 -0
  22. data/spec/mock_app/app/models/blog.rb +2 -0
  23. data/spec/mock_app/app/models/post.rb +5 -0
  24. data/spec/mock_app/app/models/post_with_auto.rb +9 -0
  25. data/spec/mock_app/config/boot.rb +110 -0
  26. data/spec/mock_app/config/database.yml +4 -0
  27. data/spec/mock_app/config/environment.rb +41 -0
  28. data/spec/mock_app/config/environments/development.rb +27 -0
  29. data/spec/mock_app/config/environments/test.rb +27 -0
  30. data/spec/mock_app/config/initializers/new_rails_defaults.rb +19 -0
  31. data/spec/mock_app/config/initializers/session_store.rb +15 -0
  32. data/spec/mock_app/config/routes.rb +43 -0
  33. data/spec/mock_app/config/sunspot.yml +14 -0
  34. data/spec/model_lifecycle_spec.rb +38 -0
  35. data/spec/model_spec.rb +278 -0
  36. data/spec/request_lifecycle_spec.rb +30 -0
  37. data/spec/schema.rb +14 -0
  38. data/spec/spec_helper.rb +32 -0
  39. metadata +182 -0
@@ -0,0 +1,18 @@
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
+ base.after_filter do
12
+ Sunspot.commit_if_dirty if Sunspot::Rails.configuration.auto_commit_after_request?
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,305 @@
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 { extend(ActsAsMethods) }
13
+ end
14
+ end
15
+
16
+ module ActsAsMethods
17
+ #
18
+ # Makes a class searchable if it is not already, or adds search
19
+ # configuration if it is. Note that the options passed in are only used
20
+ # the first time this method is called for a particular class; so,
21
+ # search should be defined before activating any mixins that extend
22
+ # search configuration.
23
+ #
24
+ # The block passed into this method is evaluated by the
25
+ # <code>Sunspot.setup</code> method. See the Sunspot documentation for
26
+ # complete information on the functionality provided by that method.
27
+ #
28
+ # ==== Options (+options+)
29
+ #
30
+ # :auto_index<Boolean>::
31
+ # Automatically index models in Solr when they are saved.
32
+ # Default: true
33
+ # :auto_remove<Boolean>::
34
+ # Automatically remove models from the Solr index when they are
35
+ # destroyed. <b>Setting this option to +false+ is not recommended
36
+ # </b>(see the README).
37
+ #
38
+ # ==== Example
39
+ #
40
+ # class Post < ActiveRecord::Base
41
+ # searchable do
42
+ # text :title, :body
43
+ # string :sort_title do
44
+ # title.downcase.sub(/^(an?|the)/, '')
45
+ # end
46
+ # integer :blog_id
47
+ # time :updated_at
48
+ # end
49
+ # end
50
+ #
51
+ def searchable(options = {}, &block)
52
+ Sunspot.setup(self, &block)
53
+
54
+ unless searchable?
55
+ extend ClassMethods
56
+ include InstanceMethods
57
+
58
+ unless options[:auto_index] == false
59
+ after_save do |searchable|
60
+ searchable.index
61
+ end
62
+ end
63
+
64
+ unless options[:auto_remove] == false
65
+ after_destroy do |searchable|
66
+ searchable.remove_from_index
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ #
73
+ # This method is defined on all ActiveRecord::Base subclasses. It
74
+ # is false for classes on which #searchable has not been called, and
75
+ # true for classes on which #searchable has been called.
76
+ #
77
+ # ==== Returns
78
+ #
79
+ # +false+
80
+ #
81
+ def searchable?
82
+ false
83
+ end
84
+ end
85
+
86
+ module ClassMethods
87
+ #
88
+ # Search for instances of this class in Solr. The block is delegated to
89
+ # the Sunspot.search method - see the Sunspot documentation for the full
90
+ # API.
91
+ #
92
+ # ==== Example
93
+ #
94
+ # Post.search do
95
+ # keywords 'best pizza'
96
+ # with :blog_id, 1
97
+ # order :updated_at, :desc
98
+ # facet :category_ids
99
+ # end
100
+ #
101
+ #
102
+ # ==== Returns
103
+ #
104
+ # Sunspot::Search:: Object containing results, totals, facets, etc.
105
+ #
106
+ def search(&block)
107
+ Sunspot.search(self, &block)
108
+ end
109
+
110
+ #
111
+ # Get IDs of matching results without loading the result objects from
112
+ # the database. This method may be useful if search is used as an
113
+ # intermediate step in a larger find operation. The block is the same
114
+ # as the block provided to the #search method.
115
+ #
116
+ # ==== Returns
117
+ #
118
+ # Array:: Array of IDs, in the order returned by the search
119
+ #
120
+ def search_ids(&block)
121
+ search(&block).raw_results.map { |raw_result| raw_result.primary_key.to_i }
122
+ end
123
+
124
+ #
125
+ # Remove instances of this class from the Solr index.
126
+ #
127
+ def remove_all_from_index
128
+ Sunspot.remove_all(self)
129
+ end
130
+
131
+ #
132
+ # Remove all instances of this class from the Solr index and immediately
133
+ # commit.
134
+ #
135
+ #---
136
+ # XXX Sunspot should implement remove_all!()
137
+ #
138
+ def remove_all_from_index!
139
+ Sunspot.remove_all(self)
140
+ Sunspot.commit
141
+ end
142
+
143
+ #
144
+ # Completely rebuild the index for this class. First removes all
145
+ # instances from the index, then loads records and save them. The
146
+ # +batch_size+ argument specifies how many records to load out of the
147
+ # database at a time. The default batch size is 500; if nil is passed,
148
+ # records will not be indexed in batches. By default, a commit is issued
149
+ # after each batch; passing +false+ for +batch_commit+ will disable
150
+ # this, and only issue a commit at the end of the process. If associated
151
+ # objects need to indexed also, you can specify +include+ in format
152
+ # accepted by ActiveRecord to improve your sql select performance
153
+ #
154
+ # ==== Options (passed as a hash)
155
+ #
156
+ # batch_size<Integer>:: Batch size with which to load records. Passing
157
+ # 'nil' will skip batches. Default is 500.
158
+ # batch_commit<Boolean>:: Flag signalling if a commit should be done after
159
+ # after each batch is indexed, default is 'true'
160
+ # include<Mixed>:: include option to be passed to the ActiveRecord find,
161
+ # used for including associated objects that need to be
162
+ # indexed with the parent object, accepts all formats
163
+ # ActiveRecord::Base.find does
164
+ #
165
+ # ==== Examples
166
+ #
167
+ # # reindex in batches of 500, commit after each
168
+ # Post.reindex
169
+ #
170
+ # # index all rows at once, then commit
171
+ # Post.reindex(:batch_size => nil)
172
+ #
173
+ # # reindex in batches of 500, commit when all batches complete
174
+ # Post.reindex(:batch_commit => false)
175
+ #
176
+ # # include the associated +author+ object when loading to index
177
+ # Post.reindex(:include => :author)
178
+ #
179
+ def reindex(opts={})
180
+ options = { :batch_size => 500, :batch_commit => true, :include => []}.merge(opts)
181
+ remove_all_from_index
182
+ unless options[:batch_size]
183
+ Sunspot.index!(all(:include => options[:include]))
184
+ else
185
+ record_count = count
186
+ counter = 1
187
+ offset = 0
188
+ while(offset < record_count)
189
+ benchmark options[:batch_size], counter do
190
+ Sunspot.index(all(:include => options[:include], :offset => offset, :limit => options[:batch_size], :order => primary_key))
191
+ end
192
+ Sunspot.commit if options[:batch_commit]
193
+ offset += options[:batch_size]
194
+ counter += 1
195
+ end
196
+ Sunspot.commit unless options[:batch_commit]
197
+ end
198
+ end
199
+
200
+ #
201
+ # Return the IDs of records of this class that are indexed in Solr but
202
+ # do not exist in the database. Under normal circumstances, this should
203
+ # never happen, but this method is provided in case something goes
204
+ # wrong. Usually you will want to rectify the situation by calling
205
+ # #clean_index_orphans or #reindex
206
+ #
207
+ # ==== Returns
208
+ #
209
+ # Array:: Collection of IDs that exist in Solr but not in the database
210
+ def index_orphans
211
+ count = self.count
212
+ indexed_ids = search_ids { paginate(:page => 1, :per_page => count) }.to_set
213
+ all(:select => 'id').each do |object|
214
+ indexed_ids.delete(object.id)
215
+ end
216
+ indexed_ids.to_a
217
+ end
218
+
219
+ #
220
+ # Find IDs of records of this class that are indexed in Solr but do not
221
+ # exist in the database, and remove them from Solr. Under normal
222
+ # circumstances, this should not be necessary; this method is provided
223
+ # in case something goes wrong.
224
+ #
225
+ def clean_index_orphans
226
+ index_orphans.each do |id|
227
+ new do |fake_instance|
228
+ fake_instance.id = id
229
+ end.remove_from_index
230
+ end
231
+ end
232
+
233
+ #
234
+ # Classes that have been defined as searchable return +true+ for this
235
+ # method.
236
+ #
237
+ # ==== Returns
238
+ #
239
+ # +true+
240
+ #
241
+ def searchable?
242
+ true
243
+ end
244
+
245
+ protected
246
+
247
+ #
248
+ # Does some logging for benchmarking indexing performance
249
+ #
250
+ def benchmark(batch_size, counter, &block)
251
+ start = Time.now
252
+ logger.info("[#{Time.now}] Start Indexing")
253
+ yield
254
+ elapsed = Time.now-start
255
+ logger.info("[#{Time.now}] Completed Indexing. Rows indexed #{counter * batch_size}. Rows/sec: #{batch_size/elapsed.to_f} (Elapsed: #{elapsed} sec.)")
256
+ end
257
+
258
+ end
259
+
260
+ module InstanceMethods
261
+ #
262
+ # Index the model in Solr. If the model is already indexed, it will be
263
+ # updated. Using the defaults, you will usually not need to call this
264
+ # method, as models are indexed automatically when they are created or
265
+ # updated. If you have disabled automatic indexing (see
266
+ # ClassMethods#searchable), this method allows you to manage indexing
267
+ # manually.
268
+ #
269
+ def index
270
+ Sunspot.index(self)
271
+ end
272
+
273
+ #
274
+ # Index the model in Solr and immediately commit. See #index
275
+ #
276
+ def index!
277
+ Sunspot.index!(self)
278
+ end
279
+
280
+ #
281
+ # Remove the model from the Solr index. Using the defaults, this should
282
+ # not be necessary, as models will automatically be removed from the
283
+ # index when they are destroyed. If you disable automatic removal
284
+ # (which is not recommended!), you can use this method to manage removal
285
+ # manually.
286
+ #
287
+ def remove_from_index
288
+ Sunspot.remove(self)
289
+ end
290
+
291
+ #
292
+ # Remove the model from the Solr index and commit immediately. See
293
+ # #remove_from_index
294
+ #
295
+ #---
296
+ #FIXME Sunspot should implement remove!()
297
+ #
298
+ def remove_from_index!
299
+ Sunspot.remove(self)
300
+ Sunspot.commit
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,53 @@
1
+ require 'escape'
2
+
3
+ namespace :sunspot do
4
+ namespace :solr do
5
+ desc 'Start the Solr instance'
6
+ task :start => :environment do
7
+ if RUBY_PLATFORM =~ /w(in)?32$/
8
+ abort('This command does not work on Windows. Please use rake sunspot:solr:run to run Solr in the foreground.')
9
+ end
10
+ data_path = File.join(::Rails.root, 'solr', 'data', ::Rails.env)
11
+ pid_path = File.join(::Rails.root, 'solr', 'pids', ::Rails.env)
12
+ solr_home =
13
+ if %w(solrconfig schema).all? { |file| File.exist?(File.join(::Rails.root, 'solr', 'conf', "#{file}.xml")) }
14
+ File.join(::Rails.root, 'solr')
15
+ end
16
+ [data_path, pid_path].each { |path| FileUtils.mkdir_p(path) }
17
+ port = Sunspot::Rails.configuration.port
18
+ FileUtils.cd(File.join(pid_path)) do
19
+ command = ['sunspot-solr', 'start', '--', '-p', port.to_s, '-d', data_path]
20
+ if solr_home
21
+ command << '-s' << solr_home
22
+ end
23
+ system(Escape.shell_command(command))
24
+ end
25
+ end
26
+
27
+ desc 'Run the Solr instance in the foreground'
28
+ task :run => :environment do
29
+ data_path = File.join(::Rails.root, 'solr', 'data', ::Rails.env)
30
+ solr_home =
31
+ if %w(solrconfig schema).all? { |file| File.exist?(File.join(::Rails.root, 'solr', 'conf', "#{file}.xml")) }
32
+ File.join(::Rails.root, 'solr')
33
+ end
34
+ FileUtils.mkdir_p(data_path)
35
+ port = Sunspot::Rails.configuration.port
36
+ command = ['sunspot-solr', 'run', '--', '-p', port.to_s, '-d', data_path]
37
+ if RUBY_PLATFORM =~ /w(in)?32$/
38
+ command.first << '.bat'
39
+ end
40
+ if solr_home
41
+ command << '-s' << solr_home
42
+ end
43
+ exec(Escape.shell_command(command))
44
+ end
45
+
46
+ desc 'Stop the Solr instance'
47
+ task :stop => :environment do
48
+ FileUtils.cd(File.join(::Rails.root, 'solr', 'pids', ::Rails.env)) do
49
+ system(Escape.shell_command(['sunspot-solr', 'stop']))
50
+ end
51
+ end
52
+ end
53
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'sunspot'
2
+
3
+ Sunspot.config.solr.url = URI::HTTP.build(:host => Sunspot::Rails.configuration.hostname,
4
+ :port => Sunspot::Rails.configuration.port,
5
+ :path => Sunspot::Rails.configuration.path).to_s
6
+
7
+ Sunspot::Adapters::InstanceAdapter.register(Sunspot::Rails::Adapters::ActiveRecordInstanceAdapter, ActiveRecord::Base)
8
+ Sunspot::Adapters::DataAccessor.register(Sunspot::Rails::Adapters::ActiveRecordDataAccessor, ActiveRecord::Base)
9
+ ActiveRecord::Base.module_eval { include(Sunspot::Rails::Searchable) }
10
+ ActionController::Base.module_eval { include(Sunspot::Rails::RequestLifecycle) }
@@ -0,0 +1,53 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Sunspot::Rails::Configuration, "default values" do
4
+ before(:each) do
5
+ File.should_receive(:exist?).and_return(false)
6
+ end
7
+
8
+ it "should handle the 'hostname' property when not set" do
9
+ config = Sunspot::Rails::Configuration.new
10
+ config.hostname.should == 'localhost'
11
+ end
12
+
13
+ it "should handle the 'path' property when not set" do
14
+ config = Sunspot::Rails::Configuration.new
15
+ config.path.should == '/solr'
16
+ end
17
+
18
+ it "should handle the 'port' property when not set" do
19
+ config = Sunspot::Rails::Configuration.new
20
+ config.port.should == 8983
21
+ end
22
+
23
+ it "should handle the 'auto_commit_after_request' propery when not set" do
24
+ config = Sunspot::Rails::Configuration.new
25
+ config.auto_commit_after_request?.should == true
26
+ end
27
+ end
28
+
29
+ describe Sunspot::Rails::Configuration, "user settings" do
30
+ before(:each) do
31
+ Rails.stub!(:env => 'config_test')
32
+ end
33
+
34
+ it "should handle the 'hostname' property when not set" do
35
+ config = Sunspot::Rails::Configuration.new
36
+ config.hostname.should == 'some.host'
37
+ end
38
+
39
+ it "should handle the 'port' property when not set" do
40
+ config = Sunspot::Rails::Configuration.new
41
+ config.port.should == 1234
42
+ end
43
+
44
+ it "should handle the 'path' property when set" do
45
+ config = Sunspot::Rails::Configuration.new
46
+ config.path.should == '/solr/idx'
47
+ end
48
+
49
+ it "should handle the 'auto_commit_after_request' propery when set" do
50
+ config = Sunspot::Rails::Configuration.new
51
+ config.auto_commit_after_request?.should == false
52
+ end
53
+ end
@@ -0,0 +1,10 @@
1
+ # Filters added to this controller apply to all controllers in the application.
2
+ # Likewise, all the methods added will be available for all controllers.
3
+
4
+ class ApplicationController < ActionController::Base
5
+ helper :all # include all helpers, all the time
6
+ protect_from_forgery # See ActionController::RequestForgeryProtection for details
7
+
8
+ # Scrub sensitive parameters from your log
9
+ # filter_parameter_logging :password
10
+ end