sunspot_rails 0.10.4

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