bsy-sunspot_rails 0.10.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/LICENSE +18 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +246 -0
  4. data/Rakefile +19 -0
  5. data/VERSION.yml +4 -0
  6. data/dev_tasks/gemspec.rake +27 -0
  7. data/dev_tasks/rdoc.rake +7 -0
  8. data/dev_tasks/todo.rake +4 -0
  9. data/install.rb +1 -0
  10. data/lib/sunspot/rails.rb +11 -0
  11. data/lib/sunspot/rails/adapters.rb +54 -0
  12. data/lib/sunspot/rails/configuration.rb +94 -0
  13. data/lib/sunspot/rails/request_lifecycle.rb +18 -0
  14. data/lib/sunspot/rails/searchable.rb +304 -0
  15. data/lib/sunspot/rails/tasks.rb +53 -0
  16. data/rails/init.rb +10 -0
  17. data/spec/configuration_spec.rb +16 -0
  18. data/spec/mock_app/app/controllers/application.rb +10 -0
  19. data/spec/mock_app/app/controllers/application_controller.rb +10 -0
  20. data/spec/mock_app/app/controllers/posts_controller.rb +6 -0
  21. data/spec/mock_app/app/models/blog.rb +2 -0
  22. data/spec/mock_app/app/models/post.rb +5 -0
  23. data/spec/mock_app/app/models/post_with_auto.rb +9 -0
  24. data/spec/mock_app/config/boot.rb +110 -0
  25. data/spec/mock_app/config/database.yml +4 -0
  26. data/spec/mock_app/config/environment.rb +41 -0
  27. data/spec/mock_app/config/environments/development.rb +27 -0
  28. data/spec/mock_app/config/environments/test.rb +27 -0
  29. data/spec/mock_app/config/initializers/new_rails_defaults.rb +19 -0
  30. data/spec/mock_app/config/initializers/session_store.rb +15 -0
  31. data/spec/mock_app/config/routes.rb +43 -0
  32. data/spec/mock_app/config/sunspot.yml +13 -0
  33. data/spec/model_lifecycle_spec.rb +38 -0
  34. data/spec/model_spec.rb +278 -0
  35. data/spec/request_lifecycle_spec.rb +10 -0
  36. data/spec/schema.rb +14 -0
  37. data/spec/spec_helper.rb +30 -0
  38. metadata +181 -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
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,304 @@
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
+ indexed_ids = search_ids.to_set
212
+ all(:select => 'id').each do |object|
213
+ indexed_ids.delete(object.id)
214
+ end
215
+ indexed_ids.to_a
216
+ end
217
+
218
+ #
219
+ # Find IDs of records of this class that are indexed in Solr but do not
220
+ # exist in the database, and remove them from Solr. Under normal
221
+ # circumstances, this should not be necessary; this method is provided
222
+ # in case something goes wrong.
223
+ #
224
+ def clean_index_orphans
225
+ index_orphans.each do |id|
226
+ new do |fake_instance|
227
+ fake_instance.id = id
228
+ end.remove_from_index
229
+ end
230
+ end
231
+
232
+ #
233
+ # Classes that have been defined as searchable return +true+ for this
234
+ # method.
235
+ #
236
+ # ==== Returns
237
+ #
238
+ # +true+
239
+ #
240
+ def searchable?
241
+ true
242
+ end
243
+
244
+ protected
245
+
246
+ #
247
+ # Does some logging for benchmarking indexing performance
248
+ #
249
+ def benchmark(batch_size, counter, &block)
250
+ start = Time.now
251
+ logger.info("[#{Time.now}] Start Indexing")
252
+ yield
253
+ elapsed = Time.now-start
254
+ logger.info("[#{Time.now}] Completed Indexing. Rows indexed #{counter * batch_size}. Rows/sec: #{batch_size/elapsed.to_f} (Elapsed: #{elapsed} sec.)")
255
+ end
256
+
257
+ end
258
+
259
+ module InstanceMethods
260
+ #
261
+ # Index the model in Solr. If the model is already indexed, it will be
262
+ # updated. Using the defaults, you will usually not need to call this
263
+ # method, as models are indexed automatically when they are created or
264
+ # updated. If you have disabled automatic indexing (see
265
+ # ClassMethods#searchable), this method allows you to manage indexing
266
+ # manually.
267
+ #
268
+ def index
269
+ Sunspot.index(self)
270
+ end
271
+
272
+ #
273
+ # Index the model in Solr and immediately commit. See #index
274
+ #
275
+ def index!
276
+ Sunspot.index!(self)
277
+ end
278
+
279
+ #
280
+ # Remove the model from the Solr index. Using the defaults, this should
281
+ # not be necessary, as models will automatically be removed from the
282
+ # index when they are destroyed. If you disable automatic removal
283
+ # (which is not recommended!), you can use this method to manage removal
284
+ # manually.
285
+ #
286
+ def remove_from_index
287
+ Sunspot.remove(self)
288
+ end
289
+
290
+ #
291
+ # Remove the model from the Solr index and commit immediately. See
292
+ # #remove_from_index
293
+ #
294
+ #---
295
+ #FIXME Sunspot should implement remove!()
296
+ #
297
+ def remove_from_index!
298
+ Sunspot.remove(self)
299
+ Sunspot.commit
300
+ end
301
+ end
302
+ end
303
+ end
304
+ 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,16 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe "Configuration" do
4
+ it "should handle the 'path' property when not set" do
5
+ config = Sunspot::Rails::Configuration.new
6
+ config.path.should == '/solr'
7
+ end
8
+
9
+ it "should handle the 'path' property when set" do
10
+ silence_stderr do
11
+ Rails.stub!(:env => 'path_test')
12
+ config = Sunspot::Rails::Configuration.new
13
+ config.path.should == '/solr/path_test'
14
+ end
15
+ end
16
+ 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
@@ -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
@@ -0,0 +1,6 @@
1
+ class PostsController < ApplicationController
2
+ def create
3
+ PostWithAuto.create(params[:post])
4
+ render :nothing => true
5
+ end
6
+ end
@@ -0,0 +1,2 @@
1
+ class Blog < ActiveRecord::Base
2
+ end
@@ -0,0 +1,5 @@
1
+ class Post < ActiveRecord::Base
2
+ searchable :auto_index => false, :auto_remove => false do
3
+ string :title
4
+ end
5
+ end