sunspot_activerecord 0.0.1

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