kristopher-sunspot_rails 0.10.6

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