pallan-sunspot_rails 0.9.10

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 (37) hide show
  1. data/LICENSE +18 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +244 -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 +283 -0
  15. data/lib/sunspot/rails/tasks.rb +31 -0
  16. data/rails/init.rb +10 -0
  17. data/spec/mock_app/app/controllers/application.rb +10 -0
  18. data/spec/mock_app/app/controllers/application_controller.rb +10 -0
  19. data/spec/mock_app/app/controllers/posts_controller.rb +6 -0
  20. data/spec/mock_app/app/models/blog.rb +2 -0
  21. data/spec/mock_app/app/models/post.rb +5 -0
  22. data/spec/mock_app/app/models/post_with_auto.rb +9 -0
  23. data/spec/mock_app/config/boot.rb +110 -0
  24. data/spec/mock_app/config/database.yml +4 -0
  25. data/spec/mock_app/config/environment.rb +41 -0
  26. data/spec/mock_app/config/environments/development.rb +27 -0
  27. data/spec/mock_app/config/environments/test.rb +27 -0
  28. data/spec/mock_app/config/initializers/new_rails_defaults.rb +19 -0
  29. data/spec/mock_app/config/initializers/session_store.rb +15 -0
  30. data/spec/mock_app/config/routes.rb +43 -0
  31. data/spec/mock_app/config/sunspot.yml +13 -0
  32. data/spec/model_lifecycle_spec.rb +38 -0
  33. data/spec/model_spec.rb +205 -0
  34. data/spec/request_lifecycle_spec.rb +10 -0
  35. data/spec/schema.rb +14 -0
  36. data/spec/spec_helper.rb +30 -0
  37. metadata +175 -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,283 @@
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. If the
146
+ # +batch_size+ argument is passed, records will be retrieved from the
147
+ # database in batches of that size (recommended for larger data sets).
148
+ #
149
+ # ==== Parameters
150
+ #
151
+ # batch_size<Integer>:: Batch size with which to load records. Default is none.
152
+ #
153
+ def reindex(batch_size = nil)
154
+ remove_all_from_index
155
+ unless batch_size
156
+ Sunspot.index(all)
157
+ else
158
+ counter = 1
159
+ if self.respond_to?(:find_in_batches)
160
+ self.find_in_batches(:batch_size => batch_size) do |batch|
161
+ benchmark batch_size, counter do
162
+ Sunspot.index(batch)
163
+ end
164
+ counter += 1
165
+ end
166
+ else
167
+ offset = 0
168
+ while(offset < count)
169
+ benchmark batch_size, counter do
170
+ Sunspot.index(all(:offset => offset, :limit => batch_size))
171
+ end
172
+ offset += batch_size
173
+ counter += 1
174
+ end
175
+ end
176
+ end
177
+ end
178
+
179
+ #
180
+ # Return the IDs of records of this class that are indexed in Solr but
181
+ # do not exist in the database. Under normal circumstances, this should
182
+ # never happen, but this method is provided in case something goes
183
+ # wrong. Usually you will want to rectify the situation by calling
184
+ # #clean_index_orphans or #reindex
185
+ #
186
+ # ==== Returns
187
+ #
188
+ # Array:: Collection of IDs that exist in Solr but not in the database
189
+ def index_orphans
190
+ indexed_ids = search_ids.to_set
191
+ all(:select => 'id').each do |object|
192
+ indexed_ids.delete(object.id)
193
+ end
194
+ indexed_ids.to_a
195
+ end
196
+
197
+ #
198
+ # Find IDs of records of this class that are indexed in Solr but do not
199
+ # exist in the database, and remove them from Solr. Under normal
200
+ # circumstances, this should not be necessary; this method is provided
201
+ # in case something goes wrong.
202
+ #
203
+ def clean_index_orphans
204
+ index_orphans.each do |id|
205
+ new do |fake_instance|
206
+ fake_instance.id = id
207
+ end.remove_from_index
208
+ end
209
+ end
210
+
211
+ #
212
+ # Classes that have been defined as searchable return +true+ for this
213
+ # method.
214
+ #
215
+ # ==== Returns
216
+ #
217
+ # +true+
218
+ #
219
+ def searchable?
220
+ true
221
+ end
222
+
223
+ protected
224
+
225
+ #
226
+ # Does some logging for benchmarking indexing performance
227
+ #
228
+ def benchmark(batch_size, counter, &block)
229
+ start = Time.now
230
+ logger.info("[#{Time.now}] Start Indexing")
231
+ yield
232
+ elapsed = Time.now-start
233
+ logger.info("[#{Time.now}] Completed Indexing. Rows indexed #{counter * batch_size}. Rows/sec: #{batch_size/elapsed.to_f} (Elapsed: #{elapsed} sec.)")
234
+ end
235
+
236
+ end
237
+
238
+ module InstanceMethods
239
+ #
240
+ # Index the model in Solr. If the model is already indexed, it will be
241
+ # updated. Using the defaults, you will usually not need to call this
242
+ # method, as models are indexed automatically when they are created or
243
+ # updated. If you have disabled automatic indexing (see
244
+ # ClassMethods#searchable), this method allows you to manage indexing
245
+ # manually.
246
+ #
247
+ def index
248
+ Sunspot.index(self)
249
+ end
250
+
251
+ #
252
+ # Index the model in Solr and immediately commit. See #index
253
+ #
254
+ def index!
255
+ Sunspot.index!(self)
256
+ end
257
+
258
+ #
259
+ # Remove the model from the Solr index. Using the defaults, this should
260
+ # not be necessary, as models will automatically be removed from the
261
+ # index when they are destroyed. If you disable automatic removal
262
+ # (which is not recommended!), you can use this method to manage removal
263
+ # manually.
264
+ #
265
+ def remove_from_index
266
+ Sunspot.remove(self)
267
+ end
268
+
269
+ #
270
+ # Remove the model from the Solr index and commit immediately. See
271
+ # #remove_from_index
272
+ #
273
+ #---
274
+ #FIXME Sunspot should implement remove!()
275
+ #
276
+ def remove_from_index!
277
+ Sunspot.remove(self)
278
+ Sunspot.commit
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,31 @@
1
+ require 'escape'
2
+
3
+ namespace :sunspot do
4
+ namespace :solr do
5
+ desc 'Start the Solr instance'
6
+ task :start => :environment do
7
+ data_path = File.join(::Rails.root, 'solr', 'data', ::Rails.env)
8
+ pid_path = File.join(::Rails.root, 'solr', 'pids', ::Rails.env)
9
+ solr_home =
10
+ if %w(solrconfig schema).all? { |file| File.exist?(File.join(::Rails.root, 'solr', 'conf', "#{file}.xml")) }
11
+ File.join(::Rails.root, 'solr')
12
+ end
13
+ [data_path, pid_path].each { |path| FileUtils.mkdir_p(path) }
14
+ port = Sunspot::Rails.configuration.port
15
+ FileUtils.cd(File.join(pid_path)) do
16
+ command = ['sunspot-solr', 'start', '--', '-p', port.to_s, '-d', data_path]
17
+ if solr_home
18
+ command << '-s' << solr_home
19
+ end
20
+ system(Escape.shell_command(command))
21
+ end
22
+ end
23
+
24
+ desc 'Stop the Solr instance'
25
+ task :stop => :environment do
26
+ FileUtils.cd(File.join(::Rails.root, 'solr', 'pids', ::Rails.env)) do
27
+ system(Escape.shell_command(['sunspot-solr', 'stop']))
28
+ end
29
+ end
30
+ end
31
+ 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,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
@@ -0,0 +1,9 @@
1
+ class PostWithAuto < ActiveRecord::Base
2
+ def self.table_name
3
+ 'posts'
4
+ end
5
+
6
+ searchable do
7
+ string :title
8
+ end
9
+ end
@@ -0,0 +1,110 @@
1
+ # Don't change this file!
2
+ # Configure your app in config/environment.rb and config/environments/*.rb
3
+
4
+ RAILS_ROOT = "#{File.dirname(__FILE__)}/.." unless defined?(RAILS_ROOT)
5
+
6
+ module Rails
7
+ class << self
8
+ def boot!
9
+ unless booted?
10
+ preinitialize
11
+ pick_boot.run
12
+ end
13
+ end
14
+
15
+ def booted?
16
+ defined? Rails::Initializer
17
+ end
18
+
19
+ def pick_boot
20
+ (vendor_rails? ? VendorBoot : GemBoot).new
21
+ end
22
+
23
+ def vendor_rails?
24
+ File.exist?("#{RAILS_ROOT}/vendor/rails")
25
+ end
26
+
27
+ def preinitialize
28
+ load(preinitializer_path) if File.exist?(preinitializer_path)
29
+ end
30
+
31
+ def preinitializer_path
32
+ "#{RAILS_ROOT}/config/preinitializer.rb"
33
+ end
34
+ end
35
+
36
+ class Boot
37
+ def run
38
+ load_initializer
39
+ Rails::Initializer.run(:set_load_path)
40
+ end
41
+ end
42
+
43
+ class VendorBoot < Boot
44
+ def load_initializer
45
+ require "#{RAILS_ROOT}/vendor/rails/railties/lib/initializer"
46
+ Rails::Initializer.run(:install_gem_spec_stubs)
47
+ Rails::GemDependency.add_frozen_gem_path
48
+ end
49
+ end
50
+
51
+ class GemBoot < Boot
52
+ def load_initializer
53
+ self.class.load_rubygems
54
+ load_rails_gem
55
+ require 'initializer'
56
+ end
57
+
58
+ def load_rails_gem
59
+ if version = self.class.gem_version
60
+ gem 'rails', version
61
+ else
62
+ gem 'rails'
63
+ end
64
+ rescue Gem::LoadError => load_error
65
+ $stderr.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.)
66
+ exit 1
67
+ end
68
+
69
+ class << self
70
+ def rubygems_version
71
+ Gem::RubyGemsVersion rescue nil
72
+ end
73
+
74
+ def gem_version
75
+ if defined? RAILS_GEM_VERSION
76
+ RAILS_GEM_VERSION
77
+ elsif ENV.include?('RAILS_GEM_VERSION')
78
+ ENV['RAILS_GEM_VERSION']
79
+ else
80
+ parse_gem_version(read_environment_rb)
81
+ end
82
+ end
83
+
84
+ def load_rubygems
85
+ require 'rubygems'
86
+ min_version = '1.3.1'
87
+ unless rubygems_version >= min_version
88
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version} (you have #{rubygems_version}). Please `gem update --system` and try again.)
89
+ exit 1
90
+ end
91
+
92
+ rescue LoadError
93
+ $stderr.puts %Q(Rails requires RubyGems >= #{min_version}. Please install RubyGems and try again: http://rubygems.rubyforge.org)
94
+ exit 1
95
+ end
96
+
97
+ def parse_gem_version(text)
98
+ $1 if text =~ /^[^#]*RAILS_GEM_VERSION\s*=\s*["']([!~<>=]*\s*[\d.]+)["']/
99
+ end
100
+
101
+ private
102
+ def read_environment_rb
103
+ File.read("#{RAILS_ROOT}/config/environment.rb")
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ # All that for this:
110
+ Rails.boot!