sunspot_activerecord 0.0.1

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.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ nbproject
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in sunspot_activerecord.gemspec
4
+ gemspec
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,81 @@
1
+ module SunspotActiveRecord #:nodoc:
2
+ #
3
+ # This module provides Sunspot Adapter implementations for ActiveRecord
4
+ # models.
5
+ #
6
+ module Adapters
7
+ class ActiveRecordInstanceAdapter < Sunspot::Adapters::InstanceAdapter
8
+ #
9
+ # Return the primary key for the adapted instance
10
+ #
11
+ # ==== Returns
12
+ #
13
+ # Integer:: Database ID of model
14
+ #
15
+ def id
16
+ @instance.id
17
+ end
18
+ end
19
+
20
+ class ActiveRecordDataAccessor < Sunspot::Adapters::DataAccessor
21
+ # options for the find
22
+ attr_accessor :include, :select
23
+
24
+ #
25
+ # Set the fields to select from the database. This will be passed
26
+ # to ActiveRecord.
27
+ #
28
+ # ==== Parameters
29
+ #
30
+ # value<Mixed>:: String of comma-separated columns or array of columns
31
+ #
32
+ def select=(value)
33
+ value = value.join(', ') if value.respond_to?(:join)
34
+ @select = value
35
+ end
36
+
37
+ #
38
+ # Get one ActiveRecord instance out of the database by ID
39
+ #
40
+ # ==== Parameters
41
+ #
42
+ # id<String>:: Database ID of model to retreive
43
+ #
44
+ # ==== Returns
45
+ #
46
+ # ActiveRecord::Base:: ActiveRecord model
47
+ #
48
+ def load(id)
49
+ @clazz.first(options_for_find.merge(
50
+ :conditions => { @clazz.primary_key => id}
51
+ ))
52
+ end
53
+
54
+ #
55
+ # Get a collection of ActiveRecord instances out of the database by ID
56
+ #
57
+ # ==== Parameters
58
+ #
59
+ # ids<Array>:: Database IDs of models to retrieve
60
+ #
61
+ # ==== Returns
62
+ #
63
+ # Array:: Collection of ActiveRecord models
64
+ #
65
+ def load_all(ids)
66
+ @clazz.all(options_for_find.merge(
67
+ :conditions => { @clazz.primary_key => ids.map { |id| id }}
68
+ ))
69
+ end
70
+
71
+ private
72
+
73
+ def options_for_find
74
+ options = {}
75
+ options[:include] = @include unless @include.blank?
76
+ options[:select] = @select unless @select.blank?
77
+ options
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,406 @@
1
+ module SunspotActiveRecord #:nodoc:
2
+ #
3
+ # This module adds Sunspot functionality to ActiveRecord models. As well as
4
+ # providing class and instance methods, it optionally adds lifecycle hooks
5
+ # to automatically add and remove models from the Solr index as they are
6
+ # created and destroyed.
7
+ #
8
+ module Searchable
9
+ class <<self
10
+ def included(base) #:nodoc:
11
+ base.module_eval do
12
+ extend(ActsAsMethods)
13
+ end
14
+ end
15
+ end
16
+
17
+ module ActsAsMethods
18
+ #
19
+ # Makes a class searchable if it is not already, or adds search
20
+ # configuration if it is. Note that the options passed in are only used
21
+ # the first time this method is called for a particular class; so,
22
+ # search should be defined before activating any mixins that extend
23
+ # search configuration.
24
+ #
25
+ # The block passed into this method is evaluated by the
26
+ # <code>Sunspot.setup</code> method. See the Sunspot documentation for
27
+ # complete information on the functionality provided by that method.
28
+ #
29
+ # ==== Options (+options+)
30
+ #
31
+ # :auto_index<Boolean>::
32
+ # Automatically index models in Solr when they are saved.
33
+ # Default: true
34
+ # :auto_remove<Boolean>::
35
+ # Automatically remove models from the Solr index when they are
36
+ # destroyed. <b>Setting this option to +false+ is not recommended
37
+ # </b>(see the README).
38
+ # :ignore_attribute_changes_of<Array>::
39
+ # Define attributes, that should not trigger a reindex of that
40
+ # object. Usual suspects are updated_at or counters.
41
+ # :include<Mixed>::
42
+ # Define default ActiveRecord includes, set this to allow ActiveRecord
43
+ # to load required associations when indexing. See ActiveRecord's
44
+ # documentation on eager-loading for examples on how to set this
45
+ # Default: []
46
+ #
47
+ # ==== Example
48
+ #
49
+ # class Post < ActiveRecord::Base
50
+ # searchable do
51
+ # text :title, :body
52
+ # string :sort_title do
53
+ # title.downcase.sub(/^(an?|the)/, '')
54
+ # end
55
+ # integer :blog_id
56
+ # time :updated_at
57
+ # end
58
+ # end
59
+ #
60
+ def searchable(options = {}, &block)
61
+ Sunspot.setup(self, &block)
62
+
63
+ if searchable?
64
+ sunspot_options[:include].concat(Sunspot::Util::Array(options[:include]))
65
+ else
66
+ extend ClassMethods
67
+ include InstanceMethods
68
+
69
+ class_inheritable_hash :sunspot_options
70
+
71
+ unless options[:auto_index] == false
72
+ after_save :maybe_auto_index
73
+ end
74
+
75
+ unless options[:auto_remove] == false
76
+ after_destroy do |searchable|
77
+ searchable.remove_from_index!
78
+ end
79
+ end
80
+ options[:include] = Sunspot::Util::Array(options[:include])
81
+
82
+ self.sunspot_options = options
83
+ end
84
+ end
85
+
86
+ #
87
+ # This method is defined on all ActiveRecord::Base subclasses. It
88
+ # is false for classes on which #searchable has not been called, and
89
+ # true for classes on which #searchable has been called.
90
+ #
91
+ # ==== Returns
92
+ #
93
+ # +false+
94
+ #
95
+ def searchable?
96
+ false
97
+ end
98
+ end
99
+
100
+ module ClassMethods
101
+ def self.extended(base) #:nodoc:
102
+ class <<base
103
+ alias_method :search, :solr_search unless method_defined? :search
104
+ alias_method :search_ids, :solr_search_ids unless method_defined? :search_ids
105
+ alias_method :remove_all_from_index, :solr_remove_all_from_index unless method_defined? :remove_all_from_index
106
+ alias_method :remove_all_from_index!, :solr_remove_all_from_index! unless method_defined? :remove_all_from_index!
107
+ alias_method :reindex, :solr_reindex unless method_defined? :reindex
108
+ alias_method :index, :solr_index unless method_defined? :index
109
+ alias_method :index_orphans, :solr_index_orphans unless method_defined? :index_orphans
110
+ alias_method :clean_index_orphans, :solr_clean_index_orphans unless method_defined? :clean_index_orphans
111
+ end
112
+ end
113
+ #
114
+ # Search for instances of this class in Solr. The block is delegated to
115
+ # the Sunspot.search method - see the Sunspot documentation for the full
116
+ # API.
117
+ #
118
+ # ==== Example
119
+ #
120
+ # Post.search(:include => [:blog]) do
121
+ # keywords 'best pizza'
122
+ # with :blog_id, 1
123
+ # order :updated_at, :desc
124
+ # facet :category_ids
125
+ # end
126
+ #
127
+ # ==== Options
128
+ #
129
+ # :include:: Specify associations to eager load
130
+ # :select:: Specify columns to select from database when loading results
131
+ #
132
+ # ==== Returns
133
+ #
134
+ # Sunspot::Search:: Object containing results, totals, facets, etc.
135
+ #
136
+ def solr_search(options = {}, &block)
137
+ solr_execute_search(options) do
138
+ Sunspot.new_search(self, &block)
139
+ end
140
+ end
141
+
142
+ #
143
+ # Get IDs of matching results without loading the result objects from
144
+ # the database. This method may be useful if search is used as an
145
+ # intermediate step in a larger find operation. The block is the same
146
+ # as the block provided to the #search method.
147
+ #
148
+ # ==== Returns
149
+ #
150
+ # Array:: Array of IDs, in the order returned by the search
151
+ #
152
+ def solr_search_ids(&block)
153
+ solr_execute_search_ids do
154
+ solr_search(&block)
155
+ end
156
+ end
157
+
158
+ #
159
+ # Remove instances of this class from the Solr index.
160
+ #
161
+ def solr_remove_all_from_index
162
+ Sunspot.remove_all(self)
163
+ end
164
+
165
+ #
166
+ # Remove all instances of this class from the Solr index and immediately
167
+ # commit.
168
+ #
169
+ #
170
+ def solr_remove_all_from_index!
171
+ Sunspot.remove_all!(self)
172
+ end
173
+
174
+ #
175
+ # Completely rebuild the index for this class. First removes all
176
+ # instances from the index, then loads records and indexes them.
177
+ #
178
+ # See #index for information on options, etc.
179
+ #
180
+ def solr_reindex(options = {})
181
+ solr_remove_all_from_index
182
+ solr_index(options)
183
+ end
184
+
185
+ #
186
+ # Add/update all existing records in the Solr index. The
187
+ # +batch_size+ argument specifies how many records to load out of the
188
+ # database at a time. The default batch size is 50; if nil is passed,
189
+ # records will not be indexed in batches. By default, a commit is issued
190
+ # after each batch; passing +false+ for +batch_commit+ will disable
191
+ # this, and only issue a commit at the end of the process. If associated
192
+ # objects need to indexed also, you can specify +include+ in format
193
+ # accepted by ActiveRecord to improve your sql select performance
194
+ #
195
+ # ==== Options (passed as a hash)
196
+ #
197
+ # batch_size<Integer>:: Batch size with which to load records. Passing
198
+ # 'nil' will skip batches. Default is 50.
199
+ # batch_commit<Boolean>:: Flag signalling if a commit should be done after
200
+ # after each batch is indexed, default is 'true'
201
+ # include<Mixed>:: include option to be passed to the ActiveRecord find,
202
+ # used for including associated objects that need to be
203
+ # indexed with the parent object, accepts all formats
204
+ # ActiveRecord::Base.find does
205
+ # first_id:: The lowest possible ID for this class. Defaults to 0, which
206
+ # is fine for integer IDs; string primary keys will need to
207
+ # specify something reasonable here.
208
+ #
209
+ # ==== Examples
210
+ #
211
+ # # index in batches of 50, commit after each
212
+ # Post.index
213
+ #
214
+ # # index all rows at once, then commit
215
+ # Post.index(:batch_size => nil)
216
+ #
217
+ # # index in batches of 50, commit when all batches complete
218
+ # Post.index(:batch_commit => false)
219
+ #
220
+ # # include the associated +author+ object when loading to index
221
+ # Post.index(:include => :author)
222
+ #
223
+ def solr_index(opts={})
224
+ options = {
225
+ :batch_size => 50,
226
+ :batch_commit => true,
227
+ :include => self.sunspot_options[:include],
228
+ :first_id => 0
229
+ }.merge(opts)
230
+
231
+ if options[:batch_size]
232
+ counter = 0
233
+ find_in_batches(:include => options[:include], :batch_size => options[:batch_size]) do |records|
234
+ solr_benchmark options[:batch_size], counter do
235
+ Sunspot.index(records)
236
+ end
237
+ Sunspot.commit if options[:batch_commit]
238
+ counter += 1
239
+ end
240
+ Sunspot.commit unless options[:batch_commit]
241
+ else
242
+ Sunspot.index!(all(:include => options[:include]))
243
+ end
244
+ end
245
+
246
+ #
247
+ # Return the IDs of records of this class that are indexed in Solr but
248
+ # do not exist in the database. Under normal circumstances, this should
249
+ # never happen, but this method is provided in case something goes
250
+ # wrong. Usually you will want to rectify the situation by calling
251
+ # #clean_index_orphans or #reindex
252
+ #
253
+ # ==== Returns
254
+ #
255
+ # Array:: Collection of IDs that exist in Solr but not in the database
256
+ def solr_index_orphans
257
+ count = self.count
258
+ indexed_ids = solr_search_ids { paginate(:page => 1, :per_page => count) }.to_set
259
+ all(:select => 'id').each do |object|
260
+ indexed_ids.delete(object.id)
261
+ end
262
+ indexed_ids.to_a
263
+ end
264
+
265
+ #
266
+ # Find IDs of records of this class that are indexed in Solr but do not
267
+ # exist in the database, and remove them from Solr. Under normal
268
+ # circumstances, this should not be necessary; this method is provided
269
+ # in case something goes wrong.
270
+ #
271
+ def solr_clean_index_orphans
272
+ solr_index_orphans.each do |id|
273
+ new do |fake_instance|
274
+ fake_instance.id = id
275
+ end.solr_remove_from_index
276
+ end
277
+ end
278
+
279
+ #
280
+ # Classes that have been defined as searchable return +true+ for this
281
+ # method.
282
+ #
283
+ # ==== Returns
284
+ #
285
+ # +true+
286
+ #
287
+ def searchable?
288
+ true
289
+ end
290
+
291
+ def solr_execute_search(options = {})
292
+ options.assert_valid_keys(:include, :select)
293
+ search = yield
294
+ unless options.empty?
295
+ search.build do |query|
296
+ if options[:include]
297
+ query.data_accessor_for(self).include = options[:include]
298
+ end
299
+ if options[:select]
300
+ query.data_accessor_for(self).select = options[:select]
301
+ end
302
+ end
303
+ end
304
+ search.execute
305
+ end
306
+
307
+ def solr_execute_search_ids(options = {})
308
+ search = yield
309
+ search.raw_results.map { |raw_result| raw_result.primary_key.to_i }
310
+ end
311
+
312
+ protected
313
+
314
+ #
315
+ # Does some logging for benchmarking indexing performance
316
+ #
317
+ def solr_benchmark(batch_size, counter, &block)
318
+ start = Time.now
319
+ logger.info("[#{Time.now}] Start Indexing")
320
+ yield
321
+ elapsed = Time.now-start
322
+ logger.info("[#{Time.now}] Completed Indexing. Rows indexed #{counter * batch_size}. Rows/sec: #{batch_size/elapsed.to_f} (Elapsed: #{elapsed} sec.)")
323
+ end
324
+
325
+ end
326
+
327
+ module InstanceMethods
328
+ def self.included(base) #:nodoc:
329
+ base.module_eval do
330
+ alias_method :index, :solr_index unless method_defined? :index
331
+ alias_method :index!, :solr_index! unless method_defined? :index!
332
+ alias_method :remove_from_index, :solr_remove_from_index unless method_defined? :remove_from_index
333
+ alias_method :remove_from_index!, :solr_remove_from_index! unless method_defined? :remove_from_index!
334
+ alias_method :more_like_this, :solr_more_like_this unless method_defined? :more_like_this
335
+ alias_method :more_like_this_ids, :solr_more_like_this_ids unless method_defined? :more_like_this_ids
336
+ end
337
+ end
338
+ #
339
+ # Index the model in Solr. If the model is already indexed, it will be
340
+ # updated. Using the defaults, you will usually not need to call this
341
+ # method, as models are indexed automatically when they are created or
342
+ # updated. If you have disabled automatic indexing (see
343
+ # ClassMethods#searchable), this method allows you to manage indexing
344
+ # manually.
345
+ #
346
+ def solr_index
347
+ Sunspot.index(self)
348
+ end
349
+
350
+ #
351
+ # Index the model in Solr and immediately commit. See #index
352
+ #
353
+ def solr_index!
354
+ Sunspot.index!(self)
355
+ end
356
+
357
+ #
358
+ # Remove the model from the Solr index. Using the defaults, this should
359
+ # not be necessary, as models will automatically be removed from the
360
+ # index when they are destroyed. If you disable automatic removal
361
+ # (which is not recommended!), you can use this method to manage removal
362
+ # manually.
363
+ #
364
+ def solr_remove_from_index
365
+ Sunspot.remove(self)
366
+ end
367
+
368
+ #
369
+ # Remove the model from the Solr index and commit immediately. See
370
+ # #remove_from_index
371
+ #
372
+ def solr_remove_from_index!
373
+ Sunspot.remove!(self)
374
+ end
375
+
376
+ def solr_more_like_this(*args, &block)
377
+ options = args.extract_options!
378
+ self.class.solr_execute_search(options) do
379
+ Sunspot.new_more_like_this(self, *args, &block)
380
+ end
381
+ end
382
+
383
+ def solr_more_like_this_ids(&block)
384
+ self.class.solr_execute_search_ids do
385
+ solr_more_like_this(&block)
386
+ end
387
+ end
388
+
389
+ private
390
+
391
+ def should_be_indexed
392
+ if !new_record? && ignore_attributes = self.class.sunspot_options[:ignore_attribute_changes_of]
393
+ !(changed.map { |attr| attr.to_sym } - ignore_attributes).blank?
394
+ else
395
+ true
396
+ end
397
+ end
398
+
399
+ def maybe_auto_index
400
+ if should_be_indexed
401
+ index!
402
+ end
403
+ end
404
+ end
405
+ end
406
+ end
@@ -0,0 +1,3 @@
1
+ module SunspotActiverecord
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,12 @@
1
+ require 'sunspot'
2
+
3
+ sunspot_activerecord_include_dir = File.expand_path(File.dirname(__FILE__) + '/sunspot_activerecord')
4
+ require File.join(sunspot_activerecord_include_dir, 'adapters')
5
+ require File.join(sunspot_activerecord_include_dir, 'searchable')
6
+
7
+ Sunspot::Adapters::InstanceAdapter.register(SunspotActiveRecord::Adapters::ActiveRecordInstanceAdapter, ActiveRecord::Base)
8
+ Sunspot::Adapters::DataAccessor.register(SunspotActiveRecord::Adapters::ActiveRecordDataAccessor, ActiveRecord::Base)
9
+ ActiveRecord::Base.module_eval { include(SunspotActiveRecord::Searchable) }
10
+
11
+ module SunspotActiveRecord
12
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "sunspot_activerecord/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "sunspot_activerecord"
7
+ s.version = SunspotActiverecord::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Olga Gorun"]
10
+ s.email = ["ogorun@gmail.com"]
11
+ s.homepage = ""
12
+ s.summary = %q{active_record support for sunspot}
13
+ s.description = %q{Simplified version of sunspot_rails gem to enable usage of sunspot+active_record -based models in not Rails projects}
14
+
15
+ s.rubyforge_project = "sunspot_activerecord"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ s.add_dependency 'sunspot'
22
+ s.add_dependency 'activerecord'
23
+ end
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sunspot_activerecord
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Olga Gorun
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-04-23 00:00:00 +03:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: sunspot
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: activerecord
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ description: Simplified version of sunspot_rails gem to enable usage of sunspot+active_record -based models in not Rails projects
50
+ email:
51
+ - ogorun@gmail.com
52
+ executables: []
53
+
54
+ extensions: []
55
+
56
+ extra_rdoc_files: []
57
+
58
+ files:
59
+ - .gitignore
60
+ - Gemfile
61
+ - Rakefile
62
+ - lib/sunspot_activerecord.rb
63
+ - lib/sunspot_activerecord/adapters.rb
64
+ - lib/sunspot_activerecord/searchable.rb
65
+ - lib/sunspot_activerecord/version.rb
66
+ - sunspot_activerecord.gemspec
67
+ has_rdoc: true
68
+ homepage: ""
69
+ licenses: []
70
+
71
+ post_install_message:
72
+ rdoc_options: []
73
+
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ hash: 3
82
+ segments:
83
+ - 0
84
+ version: "0"
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ hash: 3
91
+ segments:
92
+ - 0
93
+ version: "0"
94
+ requirements: []
95
+
96
+ rubyforge_project: sunspot_activerecord
97
+ rubygems_version: 1.3.7
98
+ signing_key:
99
+ specification_version: 3
100
+ summary: active_record support for sunspot
101
+ test_files: []
102
+