chewy 0.7.0 → 0.8.0

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