chewy 0.6.2 → 0.7.0

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