kristopher-sunspot_rails 0.10.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/LICENSE +18 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +233 -0
  4. data/Rakefile +19 -0
  5. data/VERSION.yml +4 -0
  6. data/dev_tasks/gemspec.rake +29 -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/generators/sunspot/sunspot_generator.rb +9 -0
  11. data/generators/sunspot/templates/sunspot.yml +19 -0
  12. data/install.rb +1 -0
  13. data/lib/sunspot/rails.rb +13 -0
  14. data/lib/sunspot/rails/adapters.rb +54 -0
  15. data/lib/sunspot/rails/configuration.rb +169 -0
  16. data/lib/sunspot/rails/request_lifecycle.rb +18 -0
  17. data/lib/sunspot/rails/searchable.rb +305 -0
  18. data/lib/sunspot/rails/tasks.rb +47 -0
  19. data/rails/init.rb +10 -0
  20. data/spec/configuration_spec.rb +53 -0
  21. data/spec/mock_app/app/controllers/application.rb +10 -0
  22. data/spec/mock_app/app/controllers/application_controller.rb +10 -0
  23. data/spec/mock_app/app/controllers/posts_controller.rb +6 -0
  24. data/spec/mock_app/app/models/blog.rb +2 -0
  25. data/spec/mock_app/app/models/post.rb +5 -0
  26. data/spec/mock_app/app/models/post_with_auto.rb +9 -0
  27. data/spec/mock_app/config/boot.rb +110 -0
  28. data/spec/mock_app/config/database.yml +4 -0
  29. data/spec/mock_app/config/environment.rb +41 -0
  30. data/spec/mock_app/config/environments/development.rb +27 -0
  31. data/spec/mock_app/config/environments/test.rb +27 -0
  32. data/spec/mock_app/config/initializers/new_rails_defaults.rb +19 -0
  33. data/spec/mock_app/config/initializers/session_store.rb +15 -0
  34. data/spec/mock_app/config/routes.rb +43 -0
  35. data/spec/mock_app/config/sunspot.yml +14 -0
  36. data/spec/model_lifecycle_spec.rb +38 -0
  37. data/spec/model_spec.rb +278 -0
  38. data/spec/request_lifecycle_spec.rb +30 -0
  39. data/spec/schema.rb +14 -0
  40. data/spec/spec_helper.rb +32 -0
  41. metadata +184 -0
@@ -0,0 +1,169 @@
1
+ module Sunspot #:nodoc:
2
+ module Rails #:nodoc:
3
+ #
4
+ # Sunspot::Rails is configured via the config/sunspot.yml file, which
5
+ # contains properties keyed by environment name. A sample sunspot.yml file
6
+ # would look like:
7
+ #
8
+ # development:
9
+ # solr:
10
+ # hostname: localhost
11
+ # port: 8982
12
+ # test:
13
+ # solr:
14
+ # hostname: localhost
15
+ # port: 8983
16
+ #
17
+ # production:
18
+ # solr:
19
+ # hostname: localhost
20
+ # port: 8983
21
+ # path: /solr/myindex
22
+ #
23
+ # Sunspot::Rails uses the configuration to set up the Solr connection, as
24
+ # well as for starting Solr with the appropriate port using the
25
+ # <code>rake sunspot:solr:start</code> task.
26
+ #
27
+ class Configuration
28
+ attr_writer :user_configuration
29
+ #
30
+ # The host name at which to connect to Solr. Default 'localhost'.
31
+ #
32
+ # ==== Returns
33
+ #
34
+ # String:: host name
35
+ #
36
+ def hostname
37
+ @hostname ||= (user_configuration_from_key('solr', 'hostname') || 'localhost')
38
+ end
39
+
40
+ #
41
+ # The port at which to connect to Solr. Default 8983.
42
+ #
43
+ # ==== Returns
44
+ #
45
+ # Integer:: port
46
+ #
47
+ def port
48
+ @port ||= (user_configuration_from_key('solr', 'port') || 8983).to_i
49
+ end
50
+
51
+ #
52
+ # The url path to the Solr servlet (useful if you are running multicore).
53
+ # Default '/solr'.
54
+ #
55
+ # ==== Returns
56
+ #
57
+ # String:: path
58
+ #
59
+ def path
60
+ @path ||= (user_configuration_from_key('solr', 'path') || '/solr')
61
+ end
62
+
63
+ #
64
+ # Should the solr index receive a commit after each http-request.
65
+ # Default true
66
+ #
67
+ # ==== Returns
68
+ #
69
+ # Boolean:: bool
70
+ #
71
+
72
+ def auto_commit_after_request?
73
+ @auto_commit_after_request ||=
74
+ user_configuration_from_key('auto_commit_after_request') != false
75
+ end
76
+
77
+ #
78
+ # The path to the Solr indexes. (Used by the rake tasks).
79
+ # Default RAILS_ROOT + '/solr/data/' + ENVIRONMENT
80
+ #
81
+ # ==== Returns
82
+ #
83
+ # String:: path
84
+ #
85
+ def data_path
86
+ @data_path ||=
87
+ if user_configuration.has_key?('solr')
88
+ "#{user_configuration['solr']['data_path'] || File.join(::Rails.root, 'solr', 'data', ::Rails.env)}"
89
+ end
90
+ end
91
+
92
+ #
93
+ # The path to the Solr pids
94
+ # Default RAILS_ROOT + '/solr/pids/' + ENVIRONMENT
95
+ #
96
+ # ==== Returns
97
+ #
98
+ # String:: path
99
+ #
100
+ def pid_path
101
+ @pids_path ||=
102
+ if user_configuration.has_key?('solr')
103
+ "#{user_configuration['solr']['pid_path'] || File.join(::Rails.root, 'solr', 'pids', ::Rails.env)}"
104
+ end
105
+ end
106
+
107
+ #
108
+ # The path to the Solr home directory
109
+ # Default nil (runs the solr with sunspot default settings).
110
+ #
111
+ # If you have a custom solr conf directory,
112
+ # change this to the directory above your solr conf files
113
+ #
114
+ # e.g. conf files in RAILS_ROOT/solr/conf
115
+ # solr_home: RAILS_ROOT/solr
116
+ #
117
+ # ==== Returns
118
+ #
119
+ # String:: path
120
+ #
121
+ def solr_home
122
+ @solr_home ||=
123
+ if user_configuration.has_key?('solr')
124
+ if user_configuration['solr']['solr_home'].present?
125
+ user_configuration['solr']['solr_home']
126
+ elsif %w(solrconfig schema).all? { |file| File.exist?(File.join(::Rails.root, 'solr', 'conf', "#{file}.xml")) }
127
+ File.join(::Rails.root, 'solr')
128
+ end
129
+ end
130
+ end
131
+
132
+ private
133
+
134
+ #
135
+ # return a specific key from the user configuration in config/sunspot.yml
136
+ #
137
+ # ==== Returns
138
+ #
139
+ #
140
+ def user_configuration_from_key( *keys )
141
+ keys.inject(user_configuration) do |hash, key|
142
+ hash[key] if hash
143
+ end
144
+ end
145
+
146
+ #
147
+ # Memoized hash of configuration options for the current Rails environment
148
+ # as specified in config/sunspot.yml
149
+ #
150
+ # ==== Returns
151
+ #
152
+ # Hash:: configuration options for current environment
153
+ #
154
+ def user_configuration
155
+ @user_configuration ||=
156
+ begin
157
+ path = File.join(::Rails.root, 'config', 'sunspot.yml')
158
+ if File.exist?(path)
159
+ File.open(path) do |file|
160
+ YAML.load(file)[::Rails.env]
161
+ end
162
+ else
163
+ {}
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -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