chewy 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +10 -8
  3. data/Appraisals +8 -0
  4. data/CHANGELOG.md +33 -6
  5. data/Gemfile +6 -4
  6. data/README.md +240 -111
  7. data/gemfiles/rails.4.2.activerecord.gemfile +1 -0
  8. data/gemfiles/rails.4.2.activerecord.kaminari.gemfile +1 -0
  9. data/gemfiles/rails.4.2.activerecord.will_paginate.gemfile +1 -0
  10. data/gemfiles/sequel.4.23.gemfile +13 -0
  11. data/lib/chewy.rb +24 -11
  12. data/lib/chewy/config.rb +4 -4
  13. data/lib/chewy/index.rb +1 -1
  14. data/lib/chewy/index/settings.rb +1 -1
  15. data/lib/chewy/query.rb +43 -4
  16. data/lib/chewy/railtie.rb +2 -2
  17. data/lib/chewy/rake_helper.rb +62 -0
  18. data/lib/chewy/rspec/update_index.rb +3 -3
  19. data/lib/chewy/strategy.rb +6 -0
  20. data/lib/chewy/strategy/active_job.rb +28 -0
  21. data/lib/chewy/strategy/atomic.rb +1 -1
  22. data/lib/chewy/strategy/sidekiq.rb +3 -1
  23. data/lib/chewy/type.rb +2 -1
  24. data/lib/chewy/type/adapter/active_record.rb +9 -2
  25. data/lib/chewy/type/adapter/base.rb +6 -0
  26. data/lib/chewy/type/adapter/mongoid.rb +7 -1
  27. data/lib/chewy/type/adapter/orm.rb +1 -1
  28. data/lib/chewy/type/adapter/sequel.rb +125 -0
  29. data/lib/chewy/type/mapping.rb +26 -1
  30. data/lib/chewy/type/observe.rb +40 -17
  31. data/lib/chewy/version.rb +1 -1
  32. data/lib/sequel/plugins/chewy_observe.rb +71 -0
  33. data/lib/tasks/chewy.rake +19 -61
  34. data/spec/chewy/config_spec.rb +9 -5
  35. data/spec/chewy/fields/base_spec.rb +21 -7
  36. data/spec/chewy/index/actions_spec.rb +5 -5
  37. data/spec/chewy/query_spec.rb +69 -0
  38. data/spec/chewy/runtime_spec.rb +1 -1
  39. data/spec/chewy/strategy/active_job_spec.rb +49 -0
  40. data/spec/chewy/strategy_spec.rb +2 -2
  41. data/spec/chewy/type/adapter/sequel_spec.rb +46 -0
  42. data/spec/chewy/type/import_spec.rb +4 -2
  43. data/spec/chewy/type/mapping_spec.rb +19 -0
  44. data/spec/chewy/type/observe_spec.rb +43 -14
  45. data/spec/chewy_spec.rb +2 -3
  46. data/spec/spec_helper.rb +6 -3
  47. data/spec/support/active_record.rb +5 -8
  48. data/spec/support/mongoid.rb +5 -8
  49. data/spec/support/sequel.rb +69 -0
  50. metadata +14 -3
@@ -7,6 +7,12 @@ module Chewy
7
7
 
8
8
  attr_reader :target, :options
9
9
 
10
+ # Returns `true` if this adapter is applicable for the given target.
11
+ #
12
+ def self.accepts? target
13
+ true
14
+ end
15
+
10
16
  # Camelcased name, used as type class constant name.
11
17
  # For returned value 'Product' will be generated class name `ProductsIndex::Product`
12
18
  #
@@ -5,6 +5,12 @@ module Chewy
5
5
  module Adapter
6
6
  class Mongoid < Orm
7
7
 
8
+ def self.accepts?(target)
9
+ defined?(::Mongoid::Document) && (
10
+ target.is_a?(Class) && target.ancestors.include?(::Mongoid::Document) ||
11
+ target.is_a?(::Mongoid::Criteria))
12
+ end
13
+
8
14
  def identify collection
9
15
  super(collection).map { |id| id.is_a?(BSON::ObjectId) ? id.to_s : id }
10
16
  end
@@ -13,7 +19,7 @@ module Chewy
13
19
 
14
20
  def cleanup_default_scope!
15
21
  if Chewy.logger && @default_scope.options.values_at(:sort, :limit, :skip).compact.present?
16
- Chewy.logger.warn('Default type scope order, limit and offest are ignored and will be nullified')
22
+ Chewy.logger.warn('Default type scope order, limit and offset are ignored and will be nullified')
17
23
  end
18
24
 
19
25
  @default_scope = @default_scope.reorder(nil)
@@ -53,7 +53,7 @@ module Chewy
53
53
  # deleted from index:
54
54
  #
55
55
  # users = User.all
56
- # users.each { |user| user.destroy if user.incative? }
56
+ # users.each { |user| user.destroy if user.inactive? }
57
57
  # UsersIndex::User.import users # inactive users will be deleted from index
58
58
  # # or
59
59
  # UsersIndex::User.import users.map(&:id) # deleted user ids will be deleted from index
@@ -0,0 +1,125 @@
1
+ require 'chewy/type/adapter/base'
2
+
3
+ module Chewy
4
+ class Type
5
+ module Adapter
6
+ class Sequel < Base
7
+
8
+ attr_reader :default_dataset
9
+
10
+ def self.accepts?(target)
11
+ defined?(::Sequel::Model) && (
12
+ target.is_a?(Class) && target < ::Sequel::Model || target.is_a?(::Sequel::Dataset))
13
+ end
14
+
15
+ def initialize(*args)
16
+ @options = args.extract_options!
17
+
18
+ if dataset? args.first
19
+ dataset = args.first
20
+ @target = dataset.model
21
+ @default_dataset = dataset.unordered.unlimited
22
+ else
23
+ model = args.first
24
+ @target = model
25
+ @default_dataset = model.where(nil)
26
+ end
27
+ end
28
+
29
+ def name
30
+ @name ||= (options[:name].presence || target.name).camelize.demodulize
31
+ end
32
+
33
+ def identify(obj)
34
+ if dataset? obj
35
+ obj.select_map(target_pk)
36
+ else
37
+ Array.wrap(obj).map do |item|
38
+ model?(item) ? item.pk : item
39
+ end
40
+ end
41
+ end
42
+
43
+ def import(*args, &block)
44
+ import_options = args.extract_options!
45
+ batch_size = import_options[:batch_size] || BATCH_SIZE
46
+
47
+ if args.empty?
48
+ import_dataset(default_dataset, batch_size, &block)
49
+ elsif args.one? && dataset?(args.first)
50
+ import_dataset(args.first, batch_size, &block)
51
+ else
52
+ import_models(args.flatten.compact, batch_size, &block)
53
+ end
54
+ end
55
+
56
+ def load(*args)
57
+ load_options = args.extract_options!
58
+ index_ids = args.flatten.map(&:id) # args contains index instances
59
+
60
+ type_name = load_options[:_type].type_name.to_sym
61
+ additional_scope = load_options[type_name].try(:[], :scope) || load_options[:scope]
62
+
63
+ dataset = select_by_ids(target, index_ids)
64
+
65
+ if additional_scope.is_a?(Proc)
66
+ index_ids.map!(&:to_s)
67
+ dataset.instance_exec(&additional_scope).to_a.select do |model|
68
+ index_ids.include? model.pk.to_s
69
+ end
70
+ else
71
+ dataset.to_a
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def import_dataset(dataset, batch_size)
78
+ dataset = dataset.limit(batch_size)
79
+
80
+ dataset.db.transaction(isolation: :committed) do
81
+ 0.step(Float::INFINITY, batch_size).lazy
82
+ .map { |offset| dataset.offset(offset).to_a }
83
+ .take_while(&:any?)
84
+ .map { |items| yield grouped_objects(items) }
85
+ .reduce(:&)
86
+ end
87
+ end
88
+
89
+ def import_models(objects, batch_size)
90
+ objects_by_id = objects.index_by do |item|
91
+ model?(item) ? item.pk : item
92
+ end
93
+
94
+ indexed = objects_by_id.keys.each_slice(batch_size).map do |ids|
95
+ models = select_by_ids(default_dataset, ids).to_a
96
+ models.each { |model| objects_by_id.delete(model.pk) }
97
+ models.empty? || yield(grouped_objects(models))
98
+ end
99
+
100
+ deleted = objects_by_id.keys.each_slice(batch_size).map do |ids|
101
+ yield delete: objects_by_id.values_at(*ids)
102
+ end
103
+
104
+ indexed.all? && deleted.all?
105
+ end
106
+
107
+ def select_by_ids(dataset, ids)
108
+ dataset.where(target_pk => Array.wrap(ids))
109
+ end
110
+
111
+ def target_pk
112
+ target.primary_key
113
+ end
114
+
115
+ def dataset?(obj)
116
+ obj.is_a? ::Sequel::Dataset
117
+ end
118
+
119
+ def model?(obj)
120
+ obj.is_a? ::Sequel::Model
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -6,6 +6,8 @@ module Chewy
6
6
  included do
7
7
  class_attribute :root_object, instance_reader: false, instance_writer: false
8
8
  class_attribute :_templates
9
+ class_attribute :_agg_defs
10
+ self._agg_defs = {}
9
11
  end
10
12
 
11
13
  module ClassMethods
@@ -118,7 +120,30 @@ module Chewy
118
120
  end
119
121
  end
120
122
 
121
- # Defines dynamic template in mapping root objests
123
+ # Defines an aggregation that can be bound to a query or filter
124
+ #
125
+ # Suppose that a user has posts and each post has ratings
126
+ # avg_post_rating is the mean of all ratings
127
+ #
128
+ # class UsersIndex < Chewy::Index
129
+ # define_type User do
130
+ # field :posts do
131
+ # field :rating
132
+ # end
133
+ #
134
+ # agg :avg_rating do
135
+ # { avg: { field: 'posts.rating' } }
136
+ # end
137
+ # end
138
+ # end
139
+ def agg *args, &block
140
+ options = args.extract_options!
141
+ build_root unless root_object
142
+ self._agg_defs = _agg_defs.merge(args.first => block)
143
+ end
144
+ alias_method :aggregation, :agg
145
+
146
+ # Defines dynamic template in mapping root objects
122
147
  #
123
148
  # class CarsIndex < Chewy::Index
124
149
  # define_type Car do
@@ -3,41 +3,64 @@ module Chewy
3
3
  module Observe
4
4
  extend ActiveSupport::Concern
5
5
 
6
- def self.update_proc(type_name, *args, &block)
7
- options = args.extract_options!
8
- method = args.first
9
-
10
- Proc.new do
11
- backreference = if method && method.to_s == 'self'
12
- self
13
- elsif method
14
- send(method)
15
- else
16
- instance_eval(&block)
6
+ module Helpers
7
+ def update_proc(type_name, *args, &block)
8
+ options = args.extract_options!
9
+ method = args.first
10
+
11
+ Proc.new do
12
+ backreference = if method && method.to_s == 'self'
13
+ self
14
+ elsif method
15
+ send(method)
16
+ else
17
+ instance_eval(&block)
18
+ end
19
+
20
+ reference = if type_name.is_a?(Proc)
21
+ type_name.arity == 0 ?
22
+ instance_exec(&type_name) :
23
+ type_name.call(self)
24
+ else
25
+ type_name
26
+ end
27
+
28
+ Chewy.derive_type(reference).update_index(backreference, options)
17
29
  end
30
+ end
18
31
 
19
- Chewy.derive_type(type_name).update_index(backreference, options)
32
+ def extract_callback_options!(args)
33
+ options = args.extract_options!
34
+ options.each_key.with_object({}) { |key, hash|
35
+ hash[key] = options.delete(key) if [:if, :unless].include?(key)
36
+ }.tap {
37
+ args.push(options) unless options.empty?
38
+ }
20
39
  end
21
40
  end
22
41
 
42
+ extend Helpers
43
+
23
44
  module MongoidMethods
24
45
  def update_index(type_name, *args, &block)
46
+ callback_options = Observe.extract_callback_options!(args)
25
47
  update_proc = Observe.update_proc(type_name, *args, &block)
26
48
 
27
- after_save &update_proc
28
- after_destroy &update_proc
49
+ after_save(callback_options, &update_proc)
50
+ after_destroy(callback_options, &update_proc)
29
51
  end
30
52
  end
31
53
 
32
54
  module ActiveRecordMethods
33
55
  def update_index(type_name, *args, &block)
56
+ callback_options = Observe.extract_callback_options!(args)
34
57
  update_proc = Observe.update_proc(type_name, *args, &block)
35
58
 
36
59
  if Chewy.use_after_commit_callbacks
37
- after_commit &update_proc
60
+ after_commit(callback_options, &update_proc)
38
61
  else
39
- after_save &update_proc
40
- after_destroy &update_proc
62
+ after_save(callback_options, &update_proc)
63
+ after_destroy(callback_options, &update_proc)
41
64
  end
42
65
  end
43
66
  end
@@ -1,3 +1,3 @@
1
1
  module Chewy
2
- VERSION = '0.8.1'
2
+ VERSION = '0.8.2'
3
3
  end
@@ -0,0 +1,71 @@
1
+ require 'active_support/callbacks'
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # This Sequel plugin adds support for chewy's model-observing hook for
6
+ # updating indexes after model save or destroy.
7
+ #
8
+ # Usage:
9
+ #
10
+ # # Make all model subclasses support the `update_index` hook (called
11
+ # # before loading subclasses).
12
+ # Sequel::Model.plugin :chewy_observe
13
+ #
14
+ # # Make the Album class support the `update_index` hooks.
15
+ # Album.plugin :chewy_observe
16
+ #
17
+ # # Declare one or more `update_index` observers in model.
18
+ # class Album < Sequel::Model
19
+ # update_index('albums#album') { self }
20
+ # end
21
+ #
22
+ module ChewyObserve
23
+ extend ::Chewy::Type::Observe::Helpers
24
+
25
+ def self.apply(model)
26
+ model.instance_eval do
27
+ include ActiveSupport::Callbacks
28
+ define_callbacks :commit, :save, :destroy
29
+ end
30
+ end
31
+
32
+ # Class level methods for Sequel::Model
33
+ #
34
+ module ClassMethods
35
+ def update_index(type_name, *args, &block)
36
+ callback_options = ChewyObserve.extract_callback_options!(args)
37
+ update_proc = ChewyObserve.update_proc(type_name, *args, &block)
38
+
39
+ if Chewy.use_after_commit_callbacks
40
+ set_callback(:commit, callback_options, &update_proc)
41
+ else
42
+ set_callback(:save, callback_options, &update_proc)
43
+ set_callback(:destroy, callback_options, &update_proc)
44
+ end
45
+ end
46
+ end
47
+
48
+ # Instance level methods for Sequel::Model
49
+ #
50
+ module InstanceMethods
51
+ def after_commit
52
+ run_callbacks(:commit) do
53
+ super
54
+ end
55
+ end
56
+
57
+ def after_save
58
+ run_callbacks(:save) do
59
+ super
60
+ end
61
+ end
62
+
63
+ def after_destroy
64
+ run_callbacks(:destroy) do
65
+ super
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -1,83 +1,41 @@
1
- def subscribe_task_stats!
2
- ActiveSupport::Notifications.subscribe('import_objects.chewy') do |name, start, finish, id, payload|
3
- duration = (finish - start).round(2)
4
- puts " Imported #{payload[:type]} for #{duration}s, documents total: #{payload[:import].try(:[], :index).to_i}"
5
- payload[:errors].each do |action, errors|
6
- puts " #{action.to_s.humanize} errors:"
7
- errors.each do |error, documents|
8
- puts " `#{error}`"
9
- puts " on #{documents.count} documents: #{documents}"
10
- end
11
- end if payload[:errors]
12
- end
13
- end
14
-
15
- def eager_load_chewy!
16
- dirs = Chewy::Railtie.all_engines.map { |engine| engine.paths['app/chewy'].existent }.flatten.uniq
17
-
18
- dirs.each do |dir|
19
- Dir.glob(File.join(dir, '**/*.rb')).each do |file|
20
- require_dependency file
21
- end
22
- end
23
- end
24
-
25
- def normalize_index index
26
- "#{index.to_s.gsub(/index$/i, '').camelize}Index".constantize
27
- end
28
-
29
- def reset_index index
30
- index = normalize_index(index)
31
- puts "Resetting #{index}"
32
- index.reset! (Time.now.to_f * 1000).round
33
- end
34
-
35
- def reset_all
36
- eager_load_chewy!
37
- Chewy::Index.descendants.each { |index| reset_index index }
38
- end
39
-
40
- def update_index index
41
- index = normalize_index(index)
42
- puts "Updating #{index}"
43
- if index.exists?
44
- index.import
45
- else
46
- puts "Index `#{index.index_name}` does not exists. Use rake chewy:reset[#{index.index_name}] to create and update it."
47
- end
48
- end
49
-
50
- def update_all
51
- eager_load_chewy!
52
- Chewy::Index.descendants.each { |index| update_index index }
53
- end
1
+ require 'chewy/rake_helper'
54
2
 
55
3
  namespace :chewy do
56
4
  desc 'Destroy, recreate and import data to specified index'
57
5
  task :reset, [:index] => :environment do |task, args|
58
- subscribe_task_stats!
59
- args[:index].present? ? reset_index(args[:index]) : reset_all
6
+ Chewy::RakeHelper.subscribe_task_stats!
7
+
8
+ if args[:index].present?
9
+ Chewy::RakeHelper.reset_index(args[:index])
10
+ else
11
+ Chewy::RakeHelper.reset_all
12
+ end
60
13
  end
61
14
 
62
15
  namespace :reset do
63
16
  desc 'Destroy, recreate and import data for all found indexes'
64
17
  task all: :environment do
65
- subscribe_task_stats!
66
- reset_all
18
+ Chewy::RakeHelper.subscribe_task_stats!
19
+ Chewy::RakeHelper.reset_all
67
20
  end
68
21
  end
69
22
 
70
23
  desc 'Updates data specified index'
71
24
  task :update, [:index] => :environment do |task, args|
72
- subscribe_task_stats!
73
- args[:index].present? ? update_index(args[:index]) : update_all
25
+ Chewy::RakeHelper.subscribe_task_stats!
26
+
27
+ if args[:index].present?
28
+ Chewy::RakeHelper.update_index(args[:index])
29
+ else
30
+ Chewy::RakeHelper.update_all
31
+ end
74
32
  end
75
33
 
76
34
  namespace :update do
77
35
  desc 'Updates data for all found indexes'
78
36
  task all: :environment do
79
- subscribe_task_stats!
80
- update_all
37
+ Chewy::RakeHelper.subscribe_task_stats!
38
+ Chewy::RakeHelper.update_all
81
39
  end
82
40
  end
83
41
  end