chewy 0.7.0 → 0.8.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.rspec +0 -1
  4. data/.travis.yml +2 -2
  5. data/Appraisals +6 -2
  6. data/CHANGELOG.md +29 -1
  7. data/Gemfile +4 -0
  8. data/README.md +137 -19
  9. data/chewy.gemspec +1 -0
  10. data/gemfiles/rails.3.2.activerecord.gemfile +2 -0
  11. data/gemfiles/rails.3.2.activerecord.kaminari.gemfile +1 -1
  12. data/gemfiles/rails.4.0.activerecord.gemfile +2 -0
  13. data/gemfiles/rails.4.0.activerecord.kaminari.gemfile +1 -1
  14. data/gemfiles/rails.4.0.mongoid.gemfile +2 -0
  15. data/gemfiles/rails.4.0.mongoid.kaminari.gemfile +1 -1
  16. data/gemfiles/rails.4.1.activerecord.gemfile +2 -0
  17. data/gemfiles/rails.4.1.activerecord.kaminari.gemfile +1 -1
  18. data/gemfiles/rails.4.1.mongoid.gemfile +2 -0
  19. data/gemfiles/rails.4.1.mongoid.kaminari.gemfile +1 -1
  20. data/gemfiles/rails.4.2.activerecord.gemfile +2 -0
  21. data/gemfiles/rails.4.2.activerecord.kaminari.gemfile +1 -1
  22. data/gemfiles/rails.4.2.mongoid.gemfile +2 -0
  23. data/gemfiles/rails.4.2.mongoid.kaminari.gemfile +1 -1
  24. data/lib/chewy.rb +1 -2
  25. data/lib/chewy/config.rb +3 -3
  26. data/lib/chewy/fields/base.rb +27 -30
  27. data/lib/chewy/fields/root.rb +9 -19
  28. data/lib/chewy/query.rb +34 -1
  29. data/lib/chewy/railtie.rb +1 -0
  30. data/lib/chewy/rspec/update_index.rb +16 -6
  31. data/lib/chewy/strategy.rb +12 -0
  32. data/lib/chewy/strategy/atomic.rb +1 -1
  33. data/lib/chewy/strategy/resque.rb +26 -0
  34. data/lib/chewy/strategy/sidekiq.rb +26 -0
  35. data/lib/chewy/strategy/urgent.rb +1 -1
  36. data/lib/chewy/type.rb +2 -0
  37. data/lib/chewy/type/adapter/active_record.rb +7 -3
  38. data/lib/chewy/type/adapter/mongoid.rb +5 -0
  39. data/lib/chewy/type/adapter/orm.rb +1 -1
  40. data/lib/chewy/type/crutch.rb +31 -0
  41. data/lib/chewy/type/import.rb +7 -6
  42. data/lib/chewy/type/mapping.rb +7 -3
  43. data/lib/chewy/type/observe.rb +24 -35
  44. data/lib/chewy/version.rb +1 -1
  45. data/spec/chewy/fields/base_spec.rb +26 -19
  46. data/spec/chewy/query_spec.rb +13 -0
  47. data/spec/chewy/runtime_spec.rb +1 -1
  48. data/spec/chewy/strategy/resque_spec.rb +35 -0
  49. data/spec/chewy/strategy/sidekiq_spec.rb +35 -0
  50. data/spec/chewy/type/adapter/mongoid_spec.rb +18 -9
  51. data/spec/chewy/type/mapping_spec.rb +14 -9
  52. data/spec/chewy/type/observe_spec.rb +22 -7
  53. data/spec/spec_helper.rb +1 -0
  54. metadata +23 -2
@@ -4,7 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "activerecord", "~> 4.2.0"
6
6
  gem "activesupport", "~> 4.2.0"
7
- gem "kaminari", :require => false
7
+ gem "kaminari", "0.16.3", :require => false
8
8
 
9
9
  group :test do
10
10
  gem "guard"
@@ -4,6 +4,8 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "mongoid", "~> 4.0.0"
6
6
  gem "activesupport", "~> 4.2.0"
7
+ gem "resque", :require => false
8
+ gem "sidekiq", :require => false
7
9
 
8
10
  group :test do
9
11
  gem "guard"
@@ -4,7 +4,7 @@ source "https://rubygems.org"
4
4
 
5
5
  gem "mongoid", "~> 4.0.0"
6
6
  gem "activesupport", "~> 4.2.0"
7
- gem "kaminari", :require => false
7
+ gem "kaminari", "0.16.3", :require => false
8
8
 
9
9
  group :test do
10
10
  gem "guard"
@@ -21,6 +21,7 @@ require 'chewy/index'
21
21
  require 'chewy/type'
22
22
  require 'chewy/fields/base'
23
23
  require 'chewy/fields/root'
24
+ require 'chewy/railtie' if defined?(::Rails)
24
25
 
25
26
  begin
26
27
  require 'kaminari'
@@ -35,8 +36,6 @@ begin
35
36
  rescue LoadError
36
37
  end
37
38
 
38
- require 'chewy/railtie' if defined?(::Rails)
39
-
40
39
  ActiveSupport.on_load(:active_record) do
41
40
  extend Chewy::Type::Observe::ActiveRecordMethods
42
41
 
@@ -25,9 +25,9 @@ module Chewy
25
25
  #
26
26
  :root_strategy,
27
27
 
28
- # The first trategy in stack. `:base` by default.
29
- # If you neet to return to the previous chewy behavior -
30
- # just set it to `:bypass`
28
+ # Default request strategy middleware, used in e.g
29
+ # Rails controllers. See Chewy::Railtie::RequestStrategy
30
+ # for more info.
31
31
  #
32
32
  :request_strategy,
33
33
 
@@ -1,29 +1,43 @@
1
1
  module Chewy
2
2
  module Fields
3
3
  class Base
4
- attr_reader :name, :options, :value
4
+ attr_reader :name, :options, :value, :children
5
+ attr_accessor :parent
5
6
 
6
7
  def initialize(name, options = {})
7
- @name, @options, @nested = name.to_sym, options.deep_symbolize_keys, {}
8
+ @name, @options = name.to_sym, options.deep_symbolize_keys
8
9
  @value = @options.delete(:value)
10
+ @children = []
9
11
  end
10
12
 
11
13
  def multi_field?
12
- nested.any? && !object_field?
14
+ children.any? && !object_field?
13
15
  end
14
16
 
15
17
  def object_field?
16
- (nested.any? && options[:type].blank?) || ['object', 'nested'].include?(options[:type].to_s)
18
+ (children.any? && options[:type].blank?) || ['object', 'nested'].include?(options[:type].to_s)
17
19
  end
18
20
 
19
- def root_field?
20
- false
21
+ def mappings_hash
22
+ mapping = children.any? ? {
23
+ (multi_field? ? :fields : :properties) => children.map(&:mappings_hash).inject(:merge)
24
+ } : {}
25
+ mapping.reverse_merge!(options)
26
+ mapping.reverse_merge!(type: (children.any? ? 'object' : 'string'))
27
+ {name => mapping}
21
28
  end
22
29
 
23
30
  def compose(object, *parent_objects)
31
+ objects = ([object] + parent_objects.flatten).uniq
32
+
24
33
  result = if value && value.is_a?(Proc)
25
- value.arity.zero? ? object.instance_exec(&value) :
26
- value.call(object, *parent_objects.first(value.arity - 1))
34
+ if value.arity == 0
35
+ object.instance_exec(&value)
36
+ elsif value.arity < 0
37
+ value.call(*object)
38
+ else
39
+ value.call(*objects.first(value.arity))
40
+ end
27
41
  elsif object.is_a?(Hash)
28
42
  object[name] || object[name.to_s]
29
43
  else
@@ -31,35 +45,18 @@ module Chewy
31
45
  end
32
46
 
33
47
  result = if result.respond_to?(:to_ary)
34
- result.to_ary.map { |result| nested_compose(result, object, *parent_objects) }
48
+ result.to_ary.map { |result| compose_children(result, *objects) }
35
49
  else
36
- nested_compose(result, object, *parent_objects)
37
- end if nested.any? && !multi_field?
50
+ compose_children(result, *objects)
51
+ end if children.any? && !multi_field?
38
52
 
39
53
  {name => result.as_json(root: false)}
40
54
  end
41
55
 
42
- def nested(field = nil)
43
- if field
44
- @nested[field.name] = field
45
- else
46
- @nested
47
- end
48
- end
49
-
50
- def mappings_hash
51
- mapping = nested.any? ? {
52
- (multi_field? ? :fields : :properties) => nested.values.map(&:mappings_hash).inject(:merge)
53
- } : {}
54
- mapping.reverse_merge!(options)
55
- mapping.reverse_merge!(type: (nested.any? ? 'object' : 'string')) unless root_field?
56
- {name => mapping}
57
- end
58
-
59
56
  private
60
57
 
61
- def nested_compose(value, *parent_objects)
62
- nested.values.map { |field| field.compose(value, *parent_objects) if value }.compact.inject(:merge)
58
+ def compose_children(value, *parent_objects)
59
+ children.map { |field| field.compose(value, *parent_objects) if value }.compact.inject(:merge)
63
60
  end
64
61
  end
65
62
  end
@@ -6,30 +6,20 @@ module Chewy
6
6
  attr_reader :parent
7
7
  attr_reader :parent_id
8
8
 
9
- def initialize(name, options = {})
10
- @id = options.delete(:id) || options.delete(:_id)
11
- @parent = options.delete(:parent) || options.delete(:_parent)
12
- @parent_id = options.delete(:parent_id)
13
- options.reverse_merge!(value: ->(_) { _ })
14
- super(name, options)
15
- options.delete(:type)
16
- @dynamic_templates = []
17
- end
18
-
19
- def multi_field?
20
- false
21
- end
9
+ def initialize(*args)
10
+ super(*args)
22
11
 
23
- def object_field?
24
- true
25
- end
26
-
27
- def root_field?
28
- true
12
+ @id = @options.delete(:id) || options.delete(:_id)
13
+ @parent = @options.delete(:parent) || options.delete(:_parent)
14
+ @parent_id = @options.delete(:parent_id)
15
+ @value ||= ->(_) { _ }
16
+ @dynamic_templates = []
17
+ @options.delete(:type)
29
18
  end
30
19
 
31
20
  def mappings_hash
32
21
  mappings = super
22
+ mappings[name].delete(:type)
33
23
 
34
24
  if dynamic_templates.any?
35
25
  mappings[name][:dynamic_templates] ||= []
@@ -73,6 +73,20 @@ module Chewy
73
73
  chain { criteria.update_request_options explain: (value.nil? ? true : value) }
74
74
  end
75
75
 
76
+ # Adds <tt>script_fields</tt> parameter to search request.
77
+ # UsersIndex.script_fields(
78
+ # distance: {
79
+ # params: {
80
+ # lat: 37.569976,
81
+ # lon: -122.351591
82
+ # },
83
+ # script: "doc['coordinates'].distanceInMiles(lat, lon)"
84
+ # }
85
+ # )
86
+ def script_fields value
87
+ chain { criteria.update_script_fields(value) }
88
+ end
89
+
76
90
  # Sets query compilation mode for search request.
77
91
  # Not used if only one filter for search is specified.
78
92
  # Possible values:
@@ -818,6 +832,20 @@ module Chewy
818
832
  chain { criteria.update_types params, purge: true }
819
833
  end
820
834
 
835
+ # Sets <tt>search_type</tt> for request.
836
+ # For instance, one can use <tt>search_type=count</tt> to fetch only total count of records or to fetch only aggregations without fetching records.
837
+ #
838
+ # scope = UsersIndex.search_type(:count)
839
+ # scope.count == 0 # no records actually fetched
840
+ # scope.total == 10 # but we know a total count of them
841
+ #
842
+ # scope = UsersIndex.aggs(max_age: { max: { field: 'age' } }).search_type(:count)
843
+ # max_age = scope.aggs['max_age']['value']
844
+ #
845
+ def search_type val
846
+ chain { options.merge!(search_type: val) }
847
+ end
848
+
821
849
  # Merges two queries.
822
850
  # Merges all the values in criteria with the same rules as values added manually.
823
851
  #
@@ -909,7 +937,12 @@ module Chewy
909
937
  end
910
938
 
911
939
  def _request
912
- @_request ||= criteria.request_body.merge(index: _indexes.map(&:index_name), type: _types.map(&:type_name))
940
+ @_request ||= begin
941
+ request = criteria.request_body
942
+ request.merge!(index: _indexes.map(&:index_name), type: _types.map(&:type_name))
943
+ request.merge!(search_type: options[:search_type]) if options[:search_type]
944
+ request
945
+ end
913
946
  end
914
947
 
915
948
  def _response
@@ -50,6 +50,7 @@ module Chewy
50
50
  initializer 'chewy.migration_strategy' do
51
51
  ActiveSupport.on_load(:active_record) do
52
52
  ActiveRecord::Migration.send(:include, MigrationStrategy)
53
+ ActiveRecord::Migrator.send(:include, MigrationStrategy) if defined? ActiveRecord::Migrator
53
54
  end
54
55
  end
55
56
 
@@ -100,12 +100,14 @@ RSpec::Matchers.define :update_index do |type_name, options = {}|
100
100
 
101
101
  type = Chewy.derive_type(type_name)
102
102
 
103
- allow(type).to receive(:bulk) do |bulk_options|
104
- @updated += bulk_options[:body].map do |updated_document|
105
- updated_document.deep_symbolize_keys
103
+ instance_eval <<-RUBY
104
+ #{agnostic_stub} do |bulk_options|
105
+ @updated += bulk_options[:body].map do |updated_document|
106
+ updated_document.deep_symbolize_keys
107
+ end
108
+ {}
106
109
  end
107
- {}
108
- end
110
+ RUBY
109
111
 
110
112
  ActiveSupport::Deprecation.warn('`atomic: false` option is removed and not effective anymore, use `strategy: :atomic` option instead') if options.key?(:atomic)
111
113
  Chewy.strategy(options[:strategy] || :atomic) { block.call }
@@ -191,6 +193,14 @@ RSpec::Matchers.define :update_index do |type_name, options = {}|
191
193
  end
192
194
  end
193
195
 
196
+ def agnostic_stub
197
+ if defined? Mocha
198
+ "type.stubs(:bulk).with"
199
+ else
200
+ "allow(type).to receive(:bulk)"
201
+ end
202
+ end
203
+
194
204
  def extract_documents *args
195
205
  options = args.extract_options!
196
206
 
@@ -231,4 +241,4 @@ RSpec::Matchers.define :update_index do |type_name, options = {}|
231
241
  end
232
242
  difference.none?
233
243
  end
234
- end
244
+ end
@@ -3,6 +3,18 @@ require 'chewy/strategy/bypass'
3
3
  require 'chewy/strategy/urgent'
4
4
  require 'chewy/strategy/atomic'
5
5
 
6
+ begin
7
+ require 'resque'
8
+ require 'chewy/strategy/resque'
9
+ rescue LoadError
10
+ end
11
+
12
+ begin
13
+ require 'sidekiq'
14
+ require 'chewy/strategy/sidekiq'
15
+ rescue LoadError
16
+ end
17
+
6
18
  module Chewy
7
19
  # This class represents strategies stack with `:base`
8
20
  # Strategy on top of it. This causes raising exceptions
@@ -24,7 +24,7 @@ module Chewy
24
24
  end
25
25
 
26
26
  def leave
27
- @stash.all? { |type, ids| type.import(ids) }
27
+ @stash.all? { |type, ids| type.import!(ids) }
28
28
  end
29
29
  end
30
30
  end
@@ -0,0 +1,26 @@
1
+ module Chewy
2
+ class Strategy
3
+ # The strategy works the same way as atomic, but performs
4
+ # async index update driven by resque
5
+ #
6
+ # Chewy.strategy(:resque) do
7
+ # User.all.map(&:save) # Does nothing here
8
+ # Post.all.map(&:save) # And here
9
+ # # It imports all the changed users and posts right here
10
+ # end
11
+ #
12
+ class Resque < Atomic
13
+ class Worker
14
+ @queue = :chewy
15
+
16
+ def self.perform(type, ids)
17
+ type.constantize.import!(ids)
18
+ end
19
+ end
20
+
21
+ def leave
22
+ @stash.all? { |type, ids| ::Resque.enqueue(Chewy::Strategy::Resque::Worker, type.name, ids) }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module Chewy
2
+ class Strategy
3
+ # The strategy works the same way as atomic, but performs
4
+ # async index update driven by sidekiq
5
+ #
6
+ # Chewy.strategy(:sidekiq) do
7
+ # User.all.map(&:save) # Does nothing here
8
+ # Post.all.map(&:save) # And here
9
+ # # It imports all the changed users and posts right here
10
+ # end
11
+ #
12
+ class Sidekiq < Atomic
13
+ class Worker
14
+ include ::Sidekiq::Worker
15
+
16
+ def perform(type, ids)
17
+ type.constantize.import!(ids)
18
+ end
19
+ end
20
+
21
+ def leave
22
+ @stash.all? { |type, ids| Chewy::Strategy::Sidekiq::Worker.perform_async(type.name, ids) }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -10,7 +10,7 @@ module Chewy
10
10
  #
11
11
  class Urgent < Base
12
12
  def update type, objects, options = {}
13
- type.import(Array.wrap(objects))
13
+ type.import!(Array.wrap(objects))
14
14
  end
15
15
  end
16
16
  end
@@ -3,6 +3,7 @@ require 'chewy/type/mapping'
3
3
  require 'chewy/type/wrapper'
4
4
  require 'chewy/type/observe'
5
5
  require 'chewy/type/actions'
6
+ require 'chewy/type/crutch'
6
7
  require 'chewy/type/import'
7
8
  require 'chewy/type/adapter/object'
8
9
  require 'chewy/type/adapter/active_record'
@@ -15,6 +16,7 @@ module Chewy
15
16
  include Wrapper
16
17
  include Observe
17
18
  include Actions
19
+ include Crutch
18
20
  include Import
19
21
 
20
22
  singleton_class.delegate :index_name, :client, to: :index
@@ -16,7 +16,7 @@ module Chewy
16
16
  end
17
17
 
18
18
  def import_scope(scope, batch_size)
19
- scope = scope.reorder(target.primary_key.to_sym).limit(batch_size)
19
+ scope = scope.reorder(target_id.asc).limit(batch_size)
20
20
 
21
21
  ids = pluck_ids(scope)
22
22
  result = true
@@ -24,7 +24,7 @@ module Chewy
24
24
  while ids.any?
25
25
  result &= yield grouped_objects(default_scope_where_ids_in(ids))
26
26
  break if ids.size < batch_size
27
- ids = pluck_ids(scope.where(scope.table[target.primary_key].gt(ids.last)))
27
+ ids = pluck_ids(scope.where(target_id.gt(ids.last)))
28
28
  end
29
29
 
30
30
  result
@@ -35,13 +35,17 @@ module Chewy
35
35
  end
36
36
 
37
37
  def scope_where_ids_in(scope, ids)
38
- scope.where(target.primary_key => ids)
38
+ scope.where(target_id.in(Array.wrap(ids)))
39
39
  end
40
40
 
41
41
  def all_scope
42
42
  target.where(nil)
43
43
  end
44
44
 
45
+ def target_id
46
+ target.arel_table[target.primary_key]
47
+ end
48
+
45
49
  def relation_class
46
50
  ::ActiveRecord::Relation
47
51
  end
@@ -4,6 +4,11 @@ module Chewy
4
4
  class Type
5
5
  module Adapter
6
6
  class Mongoid < Orm
7
+
8
+ def identify collection
9
+ super(collection).map { |id| id.is_a?(BSON::ObjectId) ? id.to_s : id }
10
+ end
11
+
7
12
  private
8
13
 
9
14
  def cleanup_default_scope!