nxa-sunspot_rails 0.11.3

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 (53) hide show
  1. data/History.txt +15 -0
  2. data/LICENSE +18 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +260 -0
  5. data/Rakefile +27 -0
  6. data/TODO +8 -0
  7. data/VERSION.yml +4 -0
  8. data/dev_tasks/gemspec.rake +55 -0
  9. data/dev_tasks/rdoc.rake +7 -0
  10. data/dev_tasks/release.rake +4 -0
  11. data/dev_tasks/todo.rake +4 -0
  12. data/generators/sunspot/sunspot_generator.rb +9 -0
  13. data/generators/sunspot/templates/sunspot.yml +18 -0
  14. data/install.rb +1 -0
  15. data/lib/sunspot/rails/adapters.rb +79 -0
  16. data/lib/sunspot/rails/configuration.rb +248 -0
  17. data/lib/sunspot/rails/request_lifecycle.rb +22 -0
  18. data/lib/sunspot/rails/searchable.rb +309 -0
  19. data/lib/sunspot/rails/server.rb +229 -0
  20. data/lib/sunspot/rails/session_proxy.rb +62 -0
  21. data/lib/sunspot/rails/tasks.rb +35 -0
  22. data/lib/sunspot/rails/util.rb +20 -0
  23. data/lib/sunspot/rails.rb +22 -0
  24. data/lib/sunspot/spec/extension.rb +45 -0
  25. data/rails/init.rb +7 -0
  26. data/spec/configuration_spec.rb +102 -0
  27. data/spec/mock_app/app/controllers/application.rb +10 -0
  28. data/spec/mock_app/app/controllers/application_controller.rb +10 -0
  29. data/spec/mock_app/app/controllers/posts_controller.rb +6 -0
  30. data/spec/mock_app/app/models/author.rb +8 -0
  31. data/spec/mock_app/app/models/blog.rb +2 -0
  32. data/spec/mock_app/app/models/post.rb +5 -0
  33. data/spec/mock_app/app/models/post_with_auto.rb +9 -0
  34. data/spec/mock_app/config/boot.rb +110 -0
  35. data/spec/mock_app/config/database.yml +4 -0
  36. data/spec/mock_app/config/environment.rb +42 -0
  37. data/spec/mock_app/config/environments/development.rb +27 -0
  38. data/spec/mock_app/config/environments/test.rb +27 -0
  39. data/spec/mock_app/config/initializers/new_rails_defaults.rb +19 -0
  40. data/spec/mock_app/config/initializers/session_store.rb +15 -0
  41. data/spec/mock_app/config/routes.rb +43 -0
  42. data/spec/mock_app/config/sunspot.yml +19 -0
  43. data/spec/mock_app/db/schema.rb +20 -0
  44. data/spec/model_lifecycle_spec.rb +53 -0
  45. data/spec/model_spec.rb +314 -0
  46. data/spec/request_lifecycle_spec.rb +52 -0
  47. data/spec/schema.rb +20 -0
  48. data/spec/server_spec.rb +124 -0
  49. data/spec/session_spec.rb +24 -0
  50. data/spec/spec_helper.rb +51 -0
  51. data/spec/sunspot_mocking_spec.rb +22 -0
  52. data/spec/util_spec.rb +15 -0
  53. metadata +203 -0
@@ -0,0 +1,248 @@
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
+ # log_level: OFF
17
+ # production:
18
+ # solr:
19
+ # hostname: localhost
20
+ # port: 8983
21
+ # path: /solr/myindex
22
+ # log_level: WARNING
23
+ # solr_home: /some/path
24
+ # master_solr:
25
+ # hostname: localhost
26
+ # port: 8982
27
+ # path: /solr
28
+ # auto_commit_after_request: true
29
+ #
30
+ # Sunspot::Rails uses the configuration to set up the Solr connection, as
31
+ # well as for starting Solr with the appropriate port using the
32
+ # <code>rake sunspot:solr:start</code> task.
33
+ #
34
+ # If the <code>master_solr</code> configuration is present, Sunspot will use
35
+ # the Solr instance specified here for all write operations, and the Solr
36
+ # configured under <code>solr</code> for all read operations.
37
+ #
38
+ class Configuration
39
+ attr_writer :user_configuration
40
+ #
41
+ # The host name at which to connect to Solr. Default 'localhost'.
42
+ #
43
+ # ==== Returns
44
+ #
45
+ # String:: host name
46
+ #
47
+ def hostname
48
+ @hostname ||= (user_configuration_from_key('solr', 'hostname') || 'localhost')
49
+ end
50
+
51
+ #
52
+ # The port at which to connect to Solr. Default 8983.
53
+ #
54
+ # ==== Returns
55
+ #
56
+ # Integer:: port
57
+ #
58
+ def port
59
+ @port ||= (user_configuration_from_key('solr', 'port') || 8983).to_i
60
+ end
61
+
62
+ #
63
+ # The url path to the Solr servlet (useful if you are running multicore).
64
+ # Default '/solr'.
65
+ #
66
+ # ==== Returns
67
+ #
68
+ # String:: path
69
+ #
70
+ def path
71
+ @path ||= (user_configuration_from_key('solr', 'path') || '/solr')
72
+ end
73
+
74
+ #
75
+ # The host name at which to connect to the master Solr instance. Defaults
76
+ # to the 'hostname' configuration option.
77
+ #
78
+ # ==== Returns
79
+ #
80
+ # String:: host name
81
+ #
82
+ def master_hostname
83
+ @master_hostname ||= (user_configuration_from_key('solr', 'master_hostname') || hostname)
84
+ end
85
+
86
+ #
87
+ # The port at which to connect to the master Solr instance. Defaults to
88
+ # the 'port' configuration option.
89
+ #
90
+ # ==== Returns
91
+ #
92
+ # Integer:: port
93
+ #
94
+ def master_port
95
+ @master_port ||= (user_configuration_from_key('solr', 'master_port') || port).to_i
96
+ end
97
+
98
+ #
99
+ # The path to the master Solr servlet (useful if you are running multicore).
100
+ # Defaults to the value of the 'path' configuration option.
101
+ #
102
+ # ==== Returns
103
+ #
104
+ # String:: path
105
+ #
106
+ def master_path
107
+ @master_path ||= (user_configuration_from_key('solr', 'master_path') || path)
108
+ end
109
+
110
+ #
111
+ # True if there is a master Solr instance configured, otherwise false.
112
+ #
113
+ # ==== Returns
114
+ #
115
+ # Boolean:: bool
116
+ #
117
+ def has_master?
118
+ @has_master = !!user_configuration_from_key('master_solr')
119
+ end
120
+
121
+ #
122
+ # The default log_level that should be passed to solr. You can
123
+ # change the individual log_levels in the solr admin interface.
124
+ # Default 'INFO'.
125
+ #
126
+ # ==== Returns
127
+ #
128
+ # String:: log_level
129
+ #
130
+ def log_level
131
+ @log_level ||= (user_configuration_from_key('solr', 'log_level') || 'INFO')
132
+ end
133
+
134
+ #
135
+ # Should the solr index receive a commit after each http-request.
136
+ # Default true
137
+ #
138
+ # ==== Returns
139
+ #
140
+ # Boolean: auto_commit_after_request?
141
+ #
142
+ def auto_commit_after_request?
143
+ @auto_commit_after_request ||=
144
+ user_configuration_from_key('auto_commit_after_request') != false
145
+ end
146
+
147
+ #
148
+ # As for #auto_commit_after_request? but only for deletes
149
+ # Default false
150
+ #
151
+ # ==== Returns
152
+ #
153
+ # Boolean: auto_commit_after_delete_request?
154
+ #
155
+ def auto_commit_after_delete_request?
156
+ @auto_commit_after_delete_request ||=
157
+ (user_configuration_from_key('auto_commit_after_delete_request') || false)
158
+ end
159
+
160
+
161
+ #
162
+ # The log directory for solr logfiles
163
+ #
164
+ # ==== Returns
165
+ #
166
+ # String:: log_dir
167
+ #
168
+ def log_file
169
+ @log_file ||= (user_configuration_from_key('solr', 'log_file') || default_log_file_location )
170
+ end
171
+
172
+ def data_path
173
+ @data_path ||= user_configuration_from_key('solr', 'data_path') || File.join(::Rails.root, 'solr', 'data', ::Rails.env)
174
+ end
175
+
176
+ def pid_path
177
+ @pids_path ||= user_configuration_from_key('solr', 'pid_path') || File.join(::Rails.root, 'solr', 'pids', ::Rails.env)
178
+ end
179
+
180
+ #
181
+ # The solr home directory. Sunspot::Rails expects this directory
182
+ # to contain a config, data and pids directory. See
183
+ # Sunspot::Rails::Server.bootstrap for more information.
184
+ #
185
+ # ==== Returns
186
+ #
187
+ # String:: solr_home
188
+ #
189
+ def solr_home
190
+ @solr_home ||=
191
+ if user_configuration_from_key('solr', 'solr_home')
192
+ user_configuration_from_key('solr', 'solr_home')
193
+ else
194
+ File.join(::Rails.root, 'solr')
195
+ end
196
+ end
197
+
198
+ private
199
+
200
+ #
201
+ # Logging in rails_root/log as solr_<environment>.log as a
202
+ # default.
203
+ #
204
+ # ===== Returns
205
+ #
206
+ # String:: default_log_file_location
207
+ #
208
+ def default_log_file_location
209
+ File.join(::Rails.root, 'log', "solr_" + ::Rails.env + ".log")
210
+ end
211
+
212
+ #
213
+ # return a specific key from the user configuration in config/sunspot.yml
214
+ #
215
+ # ==== Returns
216
+ #
217
+ # Mixed:: requested_key or nil
218
+ #
219
+ def user_configuration_from_key( *keys )
220
+ keys.inject(user_configuration) do |hash, key|
221
+ hash[key] if hash
222
+ end
223
+ end
224
+
225
+ #
226
+ # Memoized hash of configuration options for the current Rails environment
227
+ # as specified in config/sunspot.yml
228
+ #
229
+ # ==== Returns
230
+ #
231
+ # Hash:: configuration options for current environment
232
+ #
233
+ def user_configuration
234
+ @user_configuration ||=
235
+ begin
236
+ path = File.join(::Rails.root, 'config', 'sunspot.yml')
237
+ if File.exist?(path)
238
+ File.open(path) do |file|
239
+ YAML.load(file)[::Rails.env]
240
+ end
241
+ else
242
+ {}
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,22 @@
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
+ if Sunspot::Rails.configuration.auto_commit_after_request?
13
+ Sunspot.commit_if_dirty
14
+ elsif Sunspot::Rails.configuration.auto_commit_after_delete_request?
15
+ Sunspot.commit_if_delete_dirty
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,309 @@
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
+ # :ignore_attribute_changes_of<Array>::
38
+ # Define attributes, that should not trigger a reindex of that
39
+ # object. Usual suspects are update_at or counters.
40
+ #
41
+ # ==== Example
42
+ #
43
+ # class Post < ActiveRecord::Base
44
+ # searchable do
45
+ # text :title, :body
46
+ # string :sort_title do
47
+ # title.downcase.sub(/^(an?|the)/, '')
48
+ # end
49
+ # integer :blog_id
50
+ # time :updated_at
51
+ # end
52
+ # end
53
+ #
54
+ def searchable(options = {}, &block)
55
+ Sunspot.setup(self, &block)
56
+
57
+ unless searchable?
58
+ extend ClassMethods
59
+ include InstanceMethods
60
+
61
+ Sunspot::Rails::Util.sunspot_options[self.to_s.underscore.to_sym] = options
62
+
63
+ unless options[:auto_index] == false
64
+ after_save do |searchable|
65
+ searchable.index if Sunspot::Rails::Util.index_relevant_attribute_changed?( searchable )
66
+ end
67
+ end
68
+
69
+ unless options[:auto_remove] == false
70
+ after_destroy do |searchable|
71
+ searchable.remove_from_index
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ #
78
+ # This method is defined on all ActiveRecord::Base subclasses. It
79
+ # is false for classes on which #searchable has not been called, and
80
+ # true for classes on which #searchable has been called.
81
+ #
82
+ # ==== Returns
83
+ #
84
+ # +false+
85
+ #
86
+ def searchable?
87
+ false
88
+ end
89
+ end
90
+
91
+ module ClassMethods
92
+ #
93
+ # Search for instances of this class in Solr. The block is delegated to
94
+ # the Sunspot.search method - see the Sunspot documentation for the full
95
+ # API.
96
+ #
97
+ # ==== Example
98
+ #
99
+ # Post.search do
100
+ # keywords 'best pizza'
101
+ # with :blog_id, 1
102
+ # order :updated_at, :desc
103
+ # facet :category_ids
104
+ # end
105
+ #
106
+ #
107
+ # ==== Returns
108
+ #
109
+ # Sunspot::Search:: Object containing results, totals, facets, etc.
110
+ #
111
+ def search(&block)
112
+ Sunspot.search(self, &block)
113
+ end
114
+
115
+ #
116
+ # Get IDs of matching results without loading the result objects from
117
+ # the database. This method may be useful if search is used as an
118
+ # intermediate step in a larger find operation. The block is the same
119
+ # as the block provided to the #search method.
120
+ #
121
+ # ==== Returns
122
+ #
123
+ # Array:: Array of IDs, in the order returned by the search
124
+ #
125
+ def search_ids(&block)
126
+ search(&block).raw_results.map { |raw_result| raw_result.primary_key.to_i }
127
+ end
128
+
129
+ #
130
+ # Remove instances of this class from the Solr index.
131
+ #
132
+ def remove_all_from_index
133
+ Sunspot.remove_all(self)
134
+ end
135
+
136
+ #
137
+ # Remove all instances of this class from the Solr index and immediately
138
+ # commit.
139
+ #
140
+ #---
141
+ # XXX Sunspot should implement remove_all!()
142
+ #
143
+ def remove_all_from_index!
144
+ remove_all_from_index
145
+ Sunspot.commit
146
+ end
147
+
148
+ #
149
+ # Completely rebuild the index for this class. First removes all
150
+ # instances from the index, then loads records and save them. The
151
+ # +batch_size+ argument specifies how many records to load out of the
152
+ # database at a time. The default batch size is 500; if nil is passed,
153
+ # records will not be indexed in batches. By default, a commit is issued
154
+ # after each batch; passing +false+ for +batch_commit+ will disable
155
+ # this, and only issue a commit at the end of the process. If associated
156
+ # objects need to indexed also, you can specify +include+ in format
157
+ # accepted by ActiveRecord to improve your sql select performance
158
+ #
159
+ # ==== Options (passed as a hash)
160
+ #
161
+ # batch_size<Integer>:: Batch size with which to load records. Passing
162
+ # 'nil' will skip batches. Default is 500.
163
+ # batch_commit<Boolean>:: Flag signalling if a commit should be done after
164
+ # after each batch is indexed, default is 'true'
165
+ # include<Mixed>:: include option to be passed to the ActiveRecord find,
166
+ # used for including associated objects that need to be
167
+ # indexed with the parent object, accepts all formats
168
+ # ActiveRecord::Base.find does
169
+ #
170
+ # ==== Examples
171
+ #
172
+ # # reindex in batches of 500, commit after each
173
+ # Post.reindex
174
+ #
175
+ # # index all rows at once, then commit
176
+ # Post.reindex(:batch_size => nil)
177
+ #
178
+ # # reindex in batches of 500, commit when all batches complete
179
+ # Post.reindex(:batch_commit => false)
180
+ #
181
+ # # include the associated +author+ object when loading to index
182
+ # Post.reindex(:include => :author)
183
+ #
184
+ def reindex(opts={})
185
+ options = { :batch_size => 500, :batch_commit => true, :include => []}.merge(opts)
186
+ remove_all_from_index
187
+ unless options[:batch_size]
188
+ Sunspot.index!(all(:include => options[:include]))
189
+ else
190
+ offset = 0
191
+ counter = 1
192
+ record_count = count
193
+ last_id = 0
194
+ while(offset < record_count)
195
+ benchmark options[:batch_size], counter do
196
+ records = all(:include => options[:include], :conditions => ["#{table_name}.#{primary_key} > ?", last_id], :limit => options[:batch_size], :order => primary_key)
197
+ Sunspot.index(records)
198
+ last_id = records.last.id
199
+ end
200
+ Sunspot.commit if options[:batch_commit]
201
+ offset += options[:batch_size]
202
+ counter += 1
203
+ end
204
+ Sunspot.commit unless options[:batch_commit]
205
+ end
206
+ end
207
+
208
+ #
209
+ # Return the IDs of records of this class that are indexed in Solr but
210
+ # do not exist in the database. Under normal circumstances, this should
211
+ # never happen, but this method is provided in case something goes
212
+ # wrong. Usually you will want to rectify the situation by calling
213
+ # #clean_index_orphans or #reindex
214
+ #
215
+ # ==== Returns
216
+ #
217
+ # Array:: Collection of IDs that exist in Solr but not in the database
218
+ def index_orphans
219
+ count = self.count
220
+ indexed_ids = search_ids { paginate(:page => 1, :per_page => count) }.to_set
221
+ all(:select => 'id').each do |object|
222
+ indexed_ids.delete(object.id)
223
+ end
224
+ indexed_ids.to_a
225
+ end
226
+
227
+ #
228
+ # Find IDs of records of this class that are indexed in Solr but do not
229
+ # exist in the database, and remove them from Solr. Under normal
230
+ # circumstances, this should not be necessary; this method is provided
231
+ # in case something goes wrong.
232
+ #
233
+ def clean_index_orphans
234
+ index_orphans.each do |id|
235
+ new do |fake_instance|
236
+ fake_instance.id = id
237
+ end.remove_from_index
238
+ end
239
+ end
240
+
241
+ #
242
+ # Classes that have been defined as searchable return +true+ for this
243
+ # method.
244
+ #
245
+ # ==== Returns
246
+ #
247
+ # +true+
248
+ #
249
+ def searchable?
250
+ true
251
+ end
252
+
253
+ protected
254
+
255
+ #
256
+ # Does some logging for benchmarking indexing performance
257
+ #
258
+ def benchmark(batch_size, counter, &block)
259
+ start = Time.now
260
+ logger.info("[#{Time.now}] Start Indexing")
261
+ yield
262
+ elapsed = Time.now-start
263
+ logger.info("[#{Time.now}] Completed Indexing. Rows indexed #{counter * batch_size}. Rows/sec: #{batch_size/elapsed.to_f} (Elapsed: #{elapsed} sec.)")
264
+ end
265
+
266
+ end
267
+
268
+ module InstanceMethods
269
+ #
270
+ # Index the model in Solr. If the model is already indexed, it will be
271
+ # updated. Using the defaults, you will usually not need to call this
272
+ # method, as models are indexed automatically when they are created or
273
+ # updated. If you have disabled automatic indexing (see
274
+ # ClassMethods#searchable), this method allows you to manage indexing
275
+ # manually.
276
+ #
277
+ def index
278
+ Sunspot.index(self)
279
+ end
280
+
281
+ #
282
+ # Index the model in Solr and immediately commit. See #index
283
+ #
284
+ def index!
285
+ Sunspot.index!(self)
286
+ end
287
+
288
+ #
289
+ # Remove the model from the Solr index. Using the defaults, this should
290
+ # not be necessary, as models will automatically be removed from the
291
+ # index when they are destroyed. If you disable automatic removal
292
+ # (which is not recommended!), you can use this method to manage removal
293
+ # manually.
294
+ #
295
+ def remove_from_index
296
+ Sunspot.remove(self)
297
+ end
298
+
299
+ #
300
+ # Remove the model from the Solr index and commit immediately. See
301
+ # #remove_from_index
302
+ #
303
+ def remove_from_index!
304
+ Sunspot.remove!(self)
305
+ end
306
+ end
307
+ end
308
+ end
309
+ end