chewy 0.8.1 → 0.8.2

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