pallan-sunspot_rails 0.9.10

Sign up to get free protection for your applications and to get access to all the features.
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!