chewy 0.6.2 → 0.7.0

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.travis.yml +35 -29
  4. data/Appraisals +37 -0
  5. data/CHANGELOG.md +115 -4
  6. data/Gemfile +2 -3
  7. data/README.md +135 -40
  8. data/chewy.gemspec +4 -3
  9. data/gemfiles/rails.3.2.activerecord.gemfile +13 -0
  10. data/gemfiles/rails.3.2.activerecord.kaminari.gemfile +14 -0
  11. data/gemfiles/rails.3.2.activerecord.will_paginate.gemfile +14 -0
  12. data/gemfiles/rails.4.0.activerecord.gemfile +13 -0
  13. data/gemfiles/rails.4.0.activerecord.kaminari.gemfile +14 -0
  14. data/gemfiles/rails.4.0.activerecord.will_paginate.gemfile +14 -0
  15. data/gemfiles/rails.4.0.mongoid.gemfile +13 -0
  16. data/gemfiles/rails.4.0.mongoid.kaminari.gemfile +14 -0
  17. data/gemfiles/rails.4.0.mongoid.will_paginate.gemfile +14 -0
  18. data/gemfiles/rails.4.1.activerecord.gemfile +13 -0
  19. data/gemfiles/rails.4.1.activerecord.kaminari.gemfile +14 -0
  20. data/gemfiles/rails.4.1.activerecord.will_paginate.gemfile +14 -0
  21. data/gemfiles/rails.4.1.mongoid.gemfile +13 -0
  22. data/gemfiles/rails.4.1.mongoid.kaminari.gemfile +14 -0
  23. data/gemfiles/rails.4.1.mongoid.will_paginate.gemfile +14 -0
  24. data/gemfiles/rails.4.2.activerecord.gemfile +13 -0
  25. data/gemfiles/rails.4.2.activerecord.kaminari.gemfile +14 -0
  26. data/gemfiles/rails.4.2.activerecord.will_paginate.gemfile +14 -0
  27. data/gemfiles/rails.4.2.mongoid.gemfile +13 -0
  28. data/gemfiles/rails.4.2.mongoid.kaminari.gemfile +14 -0
  29. data/gemfiles/rails.4.2.mongoid.will_paginate.gemfile +14 -0
  30. data/lib/chewy.rb +65 -0
  31. data/lib/chewy/config.rb +44 -93
  32. data/lib/chewy/errors.rb +14 -5
  33. data/lib/chewy/fields/base.rb +8 -7
  34. data/lib/chewy/fields/root.rb +2 -2
  35. data/lib/chewy/index.rb +7 -9
  36. data/lib/chewy/log_subscriber.rb +34 -0
  37. data/lib/chewy/query.rb +41 -27
  38. data/lib/chewy/query/criteria.rb +28 -23
  39. data/lib/chewy/query/scoping.rb +20 -0
  40. data/lib/chewy/railtie.rb +51 -13
  41. data/lib/chewy/repository.rb +61 -0
  42. data/lib/chewy/rspec/update_index.rb +3 -6
  43. data/lib/chewy/search.rb +28 -7
  44. data/lib/chewy/strategy.rb +60 -0
  45. data/lib/chewy/strategy/atomic.rb +31 -0
  46. data/lib/chewy/strategy/base.rb +27 -0
  47. data/lib/chewy/strategy/bypass.rb +15 -0
  48. data/lib/chewy/strategy/urgent.rb +17 -0
  49. data/lib/chewy/type.rb +19 -5
  50. data/lib/chewy/type/adapter/active_record.rb +28 -117
  51. data/lib/chewy/type/adapter/base.rb +35 -0
  52. data/lib/chewy/type/adapter/mongoid.rb +23 -123
  53. data/lib/chewy/type/adapter/object.rb +41 -19
  54. data/lib/chewy/type/adapter/orm.rb +142 -0
  55. data/lib/chewy/type/import.rb +43 -16
  56. data/lib/chewy/type/observe.rb +8 -21
  57. data/lib/chewy/version.rb +1 -1
  58. data/lib/tasks/chewy.rake +8 -4
  59. data/spec/chewy/config_spec.rb +20 -97
  60. data/spec/chewy/fields/base_spec.rb +24 -11
  61. data/spec/chewy/fields/time_fields_spec.rb +27 -0
  62. data/spec/chewy/index/settings_spec.rb +2 -1
  63. data/spec/chewy/index_spec.rb +98 -79
  64. data/spec/chewy/query/criteria_spec.rb +14 -0
  65. data/spec/chewy/query_spec.rb +1 -1
  66. data/spec/chewy/repository_spec.rb +50 -0
  67. data/spec/chewy/search_spec.rb +100 -0
  68. data/spec/chewy/strategy_spec.rb +109 -0
  69. data/spec/chewy/type/adapter/active_record_spec.rb +110 -46
  70. data/spec/chewy/type/adapter/mongoid_spec.rb +123 -74
  71. data/spec/chewy/type/adapter/object_spec.rb +51 -34
  72. data/spec/chewy/type/import_spec.rb +21 -21
  73. data/spec/chewy/type/observe_spec.rb +26 -29
  74. data/spec/chewy/type_spec.rb +19 -0
  75. data/spec/chewy_spec.rb +19 -3
  76. data/spec/spec_helper.rb +1 -1
  77. data/spec/support/active_record.rb +2 -1
  78. data/spec/support/mongoid.rb +29 -38
  79. metadata +85 -55
  80. data/gemfiles/Gemfile.rails-3.2.active_record +0 -6
  81. data/gemfiles/Gemfile.rails-3.2.active_record.kaminari +0 -7
  82. data/gemfiles/Gemfile.rails-3.2.active_record.will_paginate +0 -7
  83. data/gemfiles/Gemfile.rails-4.0.active_record +0 -6
  84. data/gemfiles/Gemfile.rails-4.0.active_record.kaminari +0 -7
  85. data/gemfiles/Gemfile.rails-4.0.active_record.will_paginate +0 -7
  86. data/gemfiles/Gemfile.rails-4.0.mongoid +0 -6
  87. data/gemfiles/Gemfile.rails-4.0.mongoid.kaminari +0 -7
  88. data/gemfiles/Gemfile.rails-4.0.mongoid.will_paginate +0 -7
  89. data/gemfiles/Gemfile.rails-4.1.active_record +0 -6
  90. data/gemfiles/Gemfile.rails-4.1.active_record.kaminari +0 -7
  91. data/gemfiles/Gemfile.rails-4.1.active_record.will_paginate +0 -7
  92. data/gemfiles/Gemfile.rails-4.1.mongoid +0 -6
  93. data/gemfiles/Gemfile.rails-4.1.mongoid.kaminari +0 -7
  94. data/gemfiles/Gemfile.rails-4.1.mongoid.will_paginate +0 -7
  95. data/gemfiles/Gemfile.rails-4.2.active_record +0 -6
  96. data/gemfiles/Gemfile.rails-4.2.active_record.kaminari +0 -7
  97. data/gemfiles/Gemfile.rails-4.2.active_record.will_paginate +0 -7
  98. data/gemfiles/Gemfile.rails-4.2.mongoid +0 -6
  99. data/gemfiles/Gemfile.rails-4.2.mongoid.kaminari +0 -7
  100. data/gemfiles/Gemfile.rails-4.2.mongoid.will_paginate +0 -7
  101. data/spec/chewy/index/search_spec.rb +0 -46
@@ -5,6 +5,8 @@ module Chewy
5
5
  class Base
6
6
  BATCH_SIZE = 1000
7
7
 
8
+ attr_reader :target, :options
9
+
8
10
  # Camelcased name, used as type class constant name.
9
11
  # For returned value 'Product' will be generated class name `ProductsIndex::Product`
10
12
  #
@@ -20,6 +22,14 @@ module Chewy
20
22
  @type_name ||= name.underscore
21
23
  end
22
24
 
25
+ # Returns shortest identifies for further postponed importing.
26
+ # For ORM/ODM it will be an array of ids for simple objects -
27
+ # just objects themselves
28
+ #
29
+ def identify collection
30
+ raise NotImplementedError
31
+ end
32
+
23
33
  # Splits passed objects to groups according to `:batch_size` options.
24
34
  # For every group crates hash with action keys. Example:
25
35
  #
@@ -40,6 +50,31 @@ module Chewy
40
50
  def load *args
41
51
  raise NotImplementedError
42
52
  end
53
+
54
+ private
55
+
56
+ def grouped_objects(objects)
57
+ objects.group_by do |object|
58
+ delete_from_index?(object) ? :delete : :index
59
+ end
60
+ end
61
+
62
+ def delete_from_index?(object)
63
+ if object.respond_to?(:delete_from_index?)
64
+ ActiveSupport::Deprecation.warn('`delete_from_index?` method in models is deprecated and will be removed soon. Use per-type `delete_if` option for `define_type`')
65
+ delete = object.delete_from_index?
66
+ end
67
+
68
+ delete_if = options[:delete_if]
69
+ delete ||= case delete_if
70
+ when Symbol, String
71
+ object.send delete_if
72
+ when Proc
73
+ delete_if.arity == 1 ? delete_if.call(object) : object.instance_exec(&delete_if)
74
+ end
75
+
76
+ !!delete
77
+ end
43
78
  end
44
79
  end
45
80
  end
@@ -1,145 +1,45 @@
1
- require 'chewy/type/adapter/base'
1
+ require 'chewy/type/adapter/orm'
2
2
 
3
3
  module Chewy
4
4
  class Type
5
5
  module Adapter
6
- class Mongoid < Base
7
- def initialize *args
8
- @options = args.extract_options!
9
- subject = args.first
10
- if subject.is_a?(::Mongoid::Criteria)
11
- @model = subject.klass
12
- @scope = subject
13
- else
14
- @model = subject
15
- end
16
- end
17
-
18
- def name
19
- @name ||= (options[:name].present? ? options[:name].to_s.camelize : model.model_name.to_s).demodulize
20
- end
21
-
22
- # Import method for Mongoid takes import data and import options
23
- #
24
- # Import data types:
25
- #
26
- # * Nothing passed - imports all the model data
27
- # * ActiveRecord scope
28
- # * Objects collection
29
- # * Ids collection
30
- #
31
- # Import options:
32
- #
33
- # <tt>:batch_size</tt> - import batch size, 1000 objects by default
34
- #
35
- # Method handles destroyed objects as well. In case of objects AcriveRecord::Relation
36
- # or array passed, objects, responding with true to `destroyed?` method will be deleted
37
- # from index. In case of ids array passed - documents with missing records ids will be
38
- # deleted from index:
39
- #
40
- # users = User.all
41
- # users.each { |user| user.destroy if user.incative? }
42
- # UsersIndex::User.import users # inactive users will be deleted from index
43
- # # or
44
- # UsersIndex::User.import users.map(&:id) # deleted user ids will be deleted from index
45
- #
46
- # Also there is custom API method `delete_from_index?`. It it returns `true`
47
- # object will be deleted from index. Note that if this method is defined and
48
- # return `false` Chewy will still check `destroyed?` method. This is useful
49
- # for paranoid objects sdeleting implementation.
50
- #
51
- # class User
52
- # alias_method :delete_from_index?, :deleted_at?
53
- # end
54
- #
55
- # users = User.all
56
- # users.each { |user| user.deleted_at = Time.now }
57
- # UsersIndex::User.import users # paranoid deleted users will be deleted from index
58
- # # or
59
- # UsersIndex::User.import users.map(&:id) # user ids will be deleted from index
60
- #
61
- def import *args, &block
62
- import_options = args.extract_options!
63
- import_options[:batch_size] ||= BATCH_SIZE
64
- batch_size = import_options[:batch_size]
65
-
66
- collection = args.none? ? model_all :
67
- (args.one? && args.first.is_a?(::Mongoid::Criteria) ? args.first : args.flatten.compact)
6
+ class Mongoid < Orm
7
+ private
68
8
 
69
- if collection.is_a?(::Mongoid::Criteria)
70
- result = true
71
- merged_scope(collection).batch_size(batch_size).no_timeout.each_slice(batch_size) do |batch|
72
- result &= block.call grouped_objects(batch)
73
- end
74
- result
75
- else
76
- if collection.all? { |object| object.respond_to?(:id) }
77
- collection.each_slice(batch_size).map do |group|
78
- block.call grouped_objects(group)
79
- end.all?
80
- else
81
- import_ids(collection, import_options, &block)
82
- end
9
+ def cleanup_default_scope!
10
+ if Chewy.logger && @default_scope.options.values_at(:sort, :limit, :skip).compact.present?
11
+ Chewy.logger.warn('Default type scope order, limit and offest are ignored and will be nullified')
83
12
  end
84
- end
85
-
86
- def load *args
87
- load_options = args.extract_options!
88
- objects = args.flatten
89
-
90
- additional_scope = load_options[load_options[:_type].type_name.to_sym].try(:[], :scope) || load_options[:scope]
91
13
 
92
- scope = scoped_model(objects.map(&:id))
93
- loaded_objects = if additional_scope.is_a?(Proc)
94
- scope.instance_exec(&additional_scope)
95
- elsif additional_scope.is_a?(::Mongoid::Criteria)
96
- scope.merge(additional_scope)
97
- else
98
- scope
99
- end.index_by { |object| object.id.to_s }
100
-
101
- objects.map { |object| loaded_objects[object.id.to_s] }
14
+ @default_scope = @default_scope.reorder(nil)
15
+ @default_scope.options.delete(:limit)
16
+ @default_scope.options.delete(:skip)
102
17
  end
103
18
 
104
- private
105
-
106
- attr_reader :model, :scope, :options
107
-
108
- def import_ids(ids, import_options = {}, &block)
109
- ids.uniq!
110
- batch_size = import_options[:batch_size] || BATCH_SIZE
111
-
112
- indexed = true
113
- merged_scope(scoped_model(ids)).batch_size(batch_size).no_timeout.each_slice(batch_size) do |batch|
114
- ids -= batch.map(&:id)
115
- indexed &= block.call(grouped_objects(batch))
116
- end
117
-
118
- deleted = ids.each_slice(batch_size).map do |group|
119
- block.call(delete: group)
19
+ def import_scope(scope, batch_size)
20
+ scope.batch_size(batch_size).no_timeout.pluck(:_id).each_slice(batch_size).map do |ids|
21
+ yield grouped_objects(default_scope_where_ids_in(ids))
120
22
  end.all?
23
+ end
121
24
 
122
- indexed && deleted
25
+ def pluck_ids(scope)
26
+ scope.pluck(:_id)
123
27
  end
124
28
 
125
- def grouped_objects(objects)
126
- objects.group_by do |object|
127
- delete = object.delete_from_index? if object.respond_to?(:delete_from_index?)
128
- delete ||= object.destroyed?
129
- delete ? :delete : :index
130
- end
29
+ def scope_where_ids_in(scope, ids)
30
+ scope.where(:_id.in => ids)
131
31
  end
132
32
 
133
- def merged_scope(target)
134
- scope ? scope.clone.merge(target) : target
33
+ def all_scope
34
+ target.all
135
35
  end
136
36
 
137
- def scoped_model(ids)
138
- model.where(:_id.in => ids)
37
+ def relation_class
38
+ ::Mongoid::Criteria
139
39
  end
140
40
 
141
- def model_all
142
- model.all
41
+ def object_class
42
+ ::Mongoid::Document
143
43
  end
144
44
  end
145
45
  end
@@ -10,7 +10,11 @@ module Chewy
10
10
  end
11
11
 
12
12
  def name
13
- @name ||= (options[:name] || target).to_s.camelize.demodulize
13
+ @name ||= (options[:name] || @target).to_s.camelize.demodulize
14
+ end
15
+
16
+ def identify collection
17
+ Array.wrap(collection)
14
18
  end
15
19
 
16
20
  # Imports passed data with options
@@ -23,31 +27,30 @@ module Chewy
23
27
  #
24
28
  # <tt>:batch_size</tt> - import batch size, 1000 objects by default
25
29
  #
26
- # If methods `delete_from_index?` or `destroyed?` are defined for object
27
- # and any return true then object will be deleted from index. But to be
28
- # destroyed objects need to respond to `id` method as well, so ElasticSearch
29
- # could know which one to delete.
30
+ # If method `destroyed?` is defined for object and returns true or object
31
+ # satisfy `delete_if` type option then object will be deleted from index.
32
+ # But to be destroyed objects need to respond to `id` method as well, so
33
+ # ElasticSearch could know which one to delete.
30
34
  #
31
35
  def import *args, &block
32
36
  import_options = args.extract_options!
33
37
  batch_size = import_options.delete(:batch_size) || BATCH_SIZE
34
- objects = args.flatten.compact
35
38
 
36
- objects.each_slice(batch_size).map do |group|
37
- action_groups = group.group_by do |object|
38
- raise "Object is not a `#{target}`" if class_target? && !object.is_a?(target)
39
- delete = object.delete_from_index? if object.respond_to?(:delete_from_index?)
40
- delete ||= object.destroyed? if object.respond_to?(:destroyed?)
41
- delete ? :delete : :index
42
- end
43
- block.call action_groups
44
- end.all?
39
+ objects = args.empty? && @target.respond_to?(import_all_method) ?
40
+ @target.send(import_all_method) : args.flatten.compact
41
+
42
+ import_objects(objects, batch_size, &block)
45
43
  end
46
44
 
47
45
  def load *args
48
46
  load_options = args.extract_options!
49
47
  objects = args.flatten
50
- if class_target?
48
+ if target.respond_to?(load_all_method)
49
+ target.send(load_all_method, objects)
50
+ elsif target.respond_to?(load_one_method)
51
+ objects.map { |object| target.send(load_one_method, object) }
52
+ elsif target.respond_to?(:wrap)
53
+ ActiveSupport::Deprecation.warn('Loading with `wrap` method is deprecated. Rename it to `load_one` or pass `load_one_method: :my_load_method` option to `define_type`')
51
54
  objects.map { |object| target.wrap(object) }
52
55
  else
53
56
  objects
@@ -56,10 +59,29 @@ module Chewy
56
59
 
57
60
  private
58
61
 
59
- attr_reader :target, :options
62
+ def import_objects(objects, batch_size)
63
+ objects.each_slice(batch_size).map do |group|
64
+ yield grouped_objects(group)
65
+ end.all?
66
+ end
67
+
68
+ def delete_from_index?(object)
69
+ delete = super
70
+ delete ||= object.destroyed? if object.respond_to?(:destroyed?)
71
+ delete ||= object[:_destroyed] || object['_destroyed'] if object.is_a?(Hash)
72
+ !!delete
73
+ end
74
+
75
+ def import_all_method
76
+ @import_all_method ||= options[:import_all_method] || :call
77
+ end
78
+
79
+ def load_all_method
80
+ @load_all_method ||= options[:load_all_method] || :load_all
81
+ end
60
82
 
61
- def class_target?
62
- @class_target ||= @target.is_a?(Class)
83
+ def load_one_method
84
+ @load_one_method ||= options[:load_one_method] || :load_one
63
85
  end
64
86
  end
65
87
  end
@@ -0,0 +1,142 @@
1
+ require 'chewy/type/adapter/base'
2
+
3
+ module Chewy
4
+ class Type
5
+ module Adapter
6
+ class Orm < Base
7
+ attr_reader :default_scope
8
+
9
+ def initialize *args
10
+ @options = args.extract_options!
11
+ class_or_relation = args.first
12
+ if class_or_relation.is_a?(relation_class)
13
+ @target = class_or_relation.klass
14
+ @default_scope = class_or_relation
15
+ else
16
+ @target = class_or_relation
17
+ @default_scope = all_scope
18
+ end
19
+ cleanup_default_scope!
20
+ end
21
+
22
+ def name
23
+ @name ||= (options[:name].present? ? options[:name].to_s.camelize : target.model_name.to_s).demodulize
24
+ end
25
+
26
+ def identify collection
27
+ ids = if collection.is_a?(relation_class)
28
+ pluck_ids(collection)
29
+ else
30
+ Array.wrap(collection).map do |entity|
31
+ entity.is_a?(object_class) ? entity.id : entity
32
+ end
33
+ end
34
+ end
35
+
36
+ # Import method for ORM takes import data and import options
37
+ #
38
+ # Import data types:
39
+ #
40
+ # * Nothing passed - imports all the model data according to type
41
+ # default scope
42
+ # * ORM scope
43
+ # * Objects collection
44
+ # * Ids collection
45
+ #
46
+ # Import options:
47
+ #
48
+ # <tt>:batch_size</tt> - import batch size, 1000 objects by default
49
+ #
50
+ # Method handles destroyed objects as well. In case of objects ORM scope
51
+ # or array passed, objects, responding with true to `destroyed?` method will be deleted
52
+ # from index. In case of ids array passed - documents with missing records ids will be
53
+ # deleted from index:
54
+ #
55
+ # users = User.all
56
+ # users.each { |user| user.destroy if user.incative? }
57
+ # UsersIndex::User.import users # inactive users will be deleted from index
58
+ # # or
59
+ # UsersIndex::User.import users.map(&:id) # deleted user ids will be deleted from index
60
+ #
61
+ # Also there is custom type option `delete_if`. It it returns `true`
62
+ # object will be deleted from index. Note that if this option is defined and
63
+ # return `false` Chewy will still check `destroyed?` method. This is useful
64
+ # for paranoid objects deleting implementation.
65
+ #
66
+ # define_type User, delete_if: ->{ deleted_at } do
67
+ # ...
68
+ # end
69
+ #
70
+ # users = User.all
71
+ # users.each { |user| user.deleted_at = Time.now }
72
+ # UsersIndex::User.import users # paranoid deleted users will be deleted from index
73
+ # # or
74
+ # UsersIndex::User.import users.map(&:id) # user ids will be deleted from index
75
+ #
76
+ def import *args, &block
77
+ import_options = args.extract_options!
78
+ batch_size = import_options[:batch_size] || BATCH_SIZE
79
+
80
+ collection = args.empty? ? default_scope :
81
+ (args.one? && args.first.is_a?(relation_class) ? args.first : args.flatten.compact)
82
+
83
+ if collection.is_a?(relation_class)
84
+ import_scope(collection, batch_size, &block)
85
+ else
86
+ import_objects(collection, batch_size, &block)
87
+ end
88
+ end
89
+
90
+ def load *args
91
+ load_options = args.extract_options!
92
+ objects = args.flatten
93
+
94
+ additional_scope = load_options[load_options[:_type].type_name.to_sym].try(:[], :scope) || load_options[:scope]
95
+
96
+ scope = all_scope_where_ids_in(objects.map(&:id))
97
+ loaded_objects = if additional_scope.is_a?(Proc)
98
+ scope.instance_exec(&additional_scope)
99
+ elsif additional_scope.is_a?(relation_class)
100
+ scope.merge(additional_scope)
101
+ else
102
+ scope
103
+ end.index_by { |object| object.id.to_s }
104
+
105
+ objects.map { |object| loaded_objects[object.id.to_s] }
106
+ end
107
+
108
+ private
109
+
110
+ def import_objects(collection, batch_size)
111
+ hash = collection.index_by do |entity|
112
+ entity.is_a?(object_class) ? entity.id : entity
113
+ end
114
+
115
+ indexed = hash.keys.each_slice(batch_size).map do |ids|
116
+ batch = default_scope_where_ids_in(ids)
117
+ if batch.empty?
118
+ true
119
+ else
120
+ batch.each { |object| hash.delete(object.id) }
121
+ yield grouped_objects(batch)
122
+ end
123
+ end.all?
124
+
125
+ deleted = hash.keys.each_slice(batch_size).map do |group|
126
+ yield delete: hash.values_at(*group)
127
+ end.all?
128
+
129
+ indexed && deleted
130
+ end
131
+
132
+ def default_scope_where_ids_in(ids)
133
+ scope_where_ids_in(default_scope, ids)
134
+ end
135
+
136
+ def all_scope_where_ids_in(ids)
137
+ scope_where_ids_in(all_scope, ids)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end