chewy 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -3
  3. data/CHANGELOG.md +43 -1
  4. data/Gemfile +3 -0
  5. data/README.md +49 -11
  6. data/chewy.gemspec +1 -2
  7. data/gemfiles/Gemfile.rails-3.2 +1 -0
  8. data/gemfiles/Gemfile.rails-4.0 +1 -0
  9. data/lib/chewy.rb +8 -2
  10. data/lib/chewy/backports/deep_dup.rb +46 -0
  11. data/lib/chewy/backports/duplicable.rb +90 -0
  12. data/lib/chewy/config.rb +33 -6
  13. data/lib/chewy/errors.rb +1 -1
  14. data/lib/chewy/fields/base.rb +19 -7
  15. data/lib/chewy/fields/root.rb +13 -0
  16. data/lib/chewy/index/actions.rb +14 -6
  17. data/lib/chewy/index/search.rb +3 -2
  18. data/lib/chewy/query.rb +131 -17
  19. data/lib/chewy/query/compose.rb +27 -17
  20. data/lib/chewy/query/criteria.rb +34 -22
  21. data/lib/chewy/query/loading.rb +94 -10
  22. data/lib/chewy/query/nodes/exists.rb +1 -1
  23. data/lib/chewy/query/nodes/has_relation.rb +1 -1
  24. data/lib/chewy/query/nodes/missing.rb +1 -1
  25. data/lib/chewy/query/pagination.rb +8 -38
  26. data/lib/chewy/query/pagination/kaminari.rb +37 -0
  27. data/lib/chewy/runtime.rb +9 -0
  28. data/lib/chewy/runtime/version.rb +25 -0
  29. data/lib/chewy/type/adapter/active_record.rb +21 -7
  30. data/lib/chewy/type/adapter/base.rb +1 -1
  31. data/lib/chewy/type/adapter/object.rb +9 -6
  32. data/lib/chewy/type/import.rb +7 -4
  33. data/lib/chewy/type/mapping.rb +9 -9
  34. data/lib/chewy/type/wrapper.rb +1 -1
  35. data/lib/chewy/version.rb +1 -1
  36. data/lib/tasks/chewy.rake +40 -21
  37. data/spec/chewy/config_spec.rb +1 -1
  38. data/spec/chewy/fields/base_spec.rb +273 -8
  39. data/spec/chewy/index/actions_spec.rb +1 -2
  40. data/spec/chewy/index/aliases_spec.rb +0 -1
  41. data/spec/chewy/index/search_spec.rb +0 -8
  42. data/spec/chewy/index/settings_spec.rb +0 -2
  43. data/spec/chewy/index_spec.rb +0 -2
  44. data/spec/chewy/query/criteria_spec.rb +85 -18
  45. data/spec/chewy/query/loading_spec.rb +26 -9
  46. data/spec/chewy/query/nodes/and_spec.rb +2 -2
  47. data/spec/chewy/query/nodes/exists_spec.rb +6 -6
  48. data/spec/chewy/query/nodes/missing_spec.rb +4 -4
  49. data/spec/chewy/query/nodes/or_spec.rb +2 -2
  50. data/spec/chewy/query/pagination/kaminari_spec.rb +55 -0
  51. data/spec/chewy/query/pagination_spec.rb +15 -22
  52. data/spec/chewy/query_spec.rb +121 -52
  53. data/spec/chewy/rspec/update_index_spec.rb +0 -1
  54. data/spec/chewy/runtime/version_spec.rb +48 -0
  55. data/spec/chewy/runtime_spec.rb +9 -0
  56. data/spec/chewy/type/adapter/active_record_spec.rb +52 -0
  57. data/spec/chewy/type/adapter/object_spec.rb +33 -0
  58. data/spec/chewy/type/import_spec.rb +1 -3
  59. data/spec/chewy/type/mapping_spec.rb +4 -6
  60. data/spec/chewy/type/observe_spec.rb +0 -2
  61. data/spec/chewy/type/wrapper_spec.rb +0 -2
  62. data/spec/chewy/type_spec.rb +26 -5
  63. data/spec/chewy_spec.rb +0 -2
  64. data/spec/spec_helper.rb +2 -2
  65. metadata +15 -21
  66. data/lib/chewy/fields/default.rb +0 -10
  67. data/spec/chewy/fields/default_spec.rb +0 -6
@@ -3,24 +3,108 @@ module Chewy
3
3
  module Loading
4
4
  extend ActiveSupport::Concern
5
5
 
6
+ # Lazily loads actual ORM/ODM objects for search result.
7
+ # Returns scope marked to return loaded objects array instead of
8
+ # chewy wrappers. In case when object can not be loaded because it
9
+ # was deleted or don't satisfy given scope or options - the
10
+ # result collection will contain nil value in the place of this
11
+ # object. Use `compact` method to avoid this if necessary.
12
+ #
13
+ # UsersIndex.query(...).load #=> [#<User id: 42...>, ...]
14
+ # UsersIndex.query(...).load.filter(...) #=> [#<User id: 42...>, ...]
15
+ #
16
+ # Possible options:
17
+ #
18
+ # <tt>:scope</tt> - used to give a scope for _every_ loaded type.
19
+ #
20
+ # PlacesIndex.query(...).load(scope: ->{ includes(:testimonials) })
21
+ #
22
+ # If places here contain cities and countries then preload will be
23
+ # done like this:
24
+ #
25
+ # City.where(id: [...]).includes(:testimonials)
26
+ # Country.where(id: [...]).includes(:testimonials)
27
+ #
28
+ # It is also possible to pass own scope for every loaded type:
29
+ #
30
+ # PlacesIndex.query(...).load(
31
+ # city: { scope: ->{ includes(:testimonials, :country) }}
32
+ # country: { scope: ->{ includes(:testimonials, :cities) }}
33
+ # )
34
+ #
35
+ # And loading will be performed as:
36
+ #
37
+ # City.where(id: [...]).includes(:testimonials, :country)
38
+ # Country.where(id: [...]).includes(:testimonials, :cities)
39
+ #
40
+ # In case of ActiveRecord objects loading the same result
41
+ # will be reached using ActiveRecord scopes instead of
42
+ # lambdas. But it works only with per-type scopes,
43
+ # and doesn't work with the common scope.
44
+ #
45
+ # PlacesIndex.query(...).load(
46
+ # city: { scope: City.includes(:testimonials, :country) }
47
+ # country: { scope: Country.includes(:testimonials, :cities) }
48
+ # )
49
+ #
50
+ # <tt>:only</tt> - loads objects for the specified types
51
+ #
52
+ # PlacesIndex.query(...).load(only: :city)
53
+ # PlacesIndex.query(...).load(only: [:city])
54
+ # PlacesIndex.query(...).load(only: [:city, :country])
55
+ #
56
+ # <tt>:except</tt> - doesn't load listed types
57
+ #
58
+ # PlacesIndex.query(...).load(except: :city)
59
+ # PlacesIndex.query(...).load(except: [:city])
60
+ # PlacesIndex.query(...).load(except: [:city, :country])
61
+ #
6
62
  def load(options = {})
7
- if defined?(::Kaminari)
8
- ::Kaminari.paginate_array(_load_objects(options),
9
- limit: limit_value, offset: offset_value, total_count: total_count)
10
- else
11
- _load_objects(options)
12
- end
63
+ chain { criteria.update_options preload: options, loaded_objects: true }
64
+ end
65
+
66
+ # This methods is just convenient way to preload some ORM/ODM
67
+ # objects and continue to work with Chewy wrappers. Returns
68
+ # Chewy query scope. Note that `load` method performs ES request
69
+ # so preload method should also be the last in scope methods chain.
70
+ # Takes the same options as the `load` method
71
+ #
72
+ # PlacesIndex.query(...).preload(only: :city)
73
+ #
74
+ # Loaded objects are also attached to corresponding Chewy
75
+ # type wrapper objects and available with `_object` accessor.
76
+ #
77
+ # scope = PlacesIndex.query(...)
78
+ # preload_scope = scope.preload
79
+ # preload_scope.first #=> PlacesIndex::City wrapper instance
80
+ # preload_scope.first._object #=> City model instance
81
+ # scope.load == preload_scope.map(&:_object) #=> true
82
+ #
83
+ def preload(options = {})
84
+ chain { criteria.update_options preload: options, loaded_objects: false }
13
85
  end
14
86
 
15
87
  private
16
88
 
17
- def _load_objects(options)
89
+ def _load_objects!
90
+ options = criteria.options[:preload]
91
+ only = Array.wrap(options[:only]).map(&:to_s)
92
+ except = Array.wrap(options[:except]).map(&:to_s)
93
+
18
94
  loaded_objects = Hash[_results.group_by(&:class).map do |type, objects|
95
+ next if except.include?(type.type_name)
96
+ next if only.any? && !only.include?(type.type_name)
97
+
19
98
  loaded = type.adapter.load(objects, options.merge(_type: type))
20
- [type, loaded.index_by.with_index { |loaded, i| objects[i] }]
21
- end]
99
+ [type, loaded.index_by.with_index do |loaded, i|
100
+ objects[i]._object = loaded
101
+ objects[i]
102
+ end]
103
+ end.compact]
22
104
 
23
- _results.map { |result| loaded_objects[result.class][result] }
105
+ _results.map do |result|
106
+ loaded_objects[result.class][result] if loaded_objects[result.class]
107
+ end
24
108
  end
25
109
  end
26
110
  end
@@ -12,7 +12,7 @@ module Chewy
12
12
  end
13
13
 
14
14
  def __render__
15
- {exists: {term: @name}}
15
+ {exists: {field: @name}}
16
16
  end
17
17
  end
18
18
  end
@@ -50,7 +50,7 @@ module Chewy
50
50
  body = if filters && !queries
51
51
  {filter: filters}
52
52
  else
53
- _composed_query(queries, filters)
53
+ _filtered_query(queries, filters)
54
54
  end || {}
55
55
 
56
56
  {_relation => body.merge(type: @type)} if body
@@ -12,7 +12,7 @@ module Chewy
12
12
  end
13
13
 
14
14
  def __render__
15
- {missing: {term: @name}.merge(@options.slice(:existence, :null_value))}
15
+ {missing: {field: @name}.merge(@options.slice(:existence, :null_value))}
16
16
  end
17
17
  end
18
18
  end
@@ -1,45 +1,15 @@
1
1
  module Chewy
2
2
  class Query
3
3
  module Pagination
4
- extend ActiveSupport::Concern
5
-
6
- included do
7
- include Kaminari if defined?(::Kaminari)
8
- end
9
-
10
- module Kaminari
11
- extend ActiveSupport::Concern
12
-
13
- included do
14
- include ::Kaminari::PageScopeMethods
15
-
16
- delegate :default_per_page, :max_per_page, :max_pages, to: :_kaminari_config
17
-
18
- class_eval <<-METHOD, __FILE__, __LINE__ + 1
19
- def #{::Kaminari.config.page_method_name}(num = 1)
20
- limit(limit_value).offset(limit_value * ([num.to_i, 1].max - 1))
21
- end
22
- METHOD
23
- end
24
-
25
- def total_count
26
- _response['hits']['total']
27
- end
28
-
29
- def limit_value
30
- (criteria.options[:size].presence || default_per_page).to_i
31
- end
32
-
33
- def offset_value
34
- criteria.options[:from].to_i
35
- end
36
-
37
- private
38
-
39
- def _kaminari_config
40
- ::Kaminari.config
41
- end
4
+ # Returns request total found documents count
5
+ #
6
+ # PlacesIndex.query(...).filter(...).total_count
7
+ #
8
+ def total_count
9
+ _response['hits'].try(:[], 'total') || 0
42
10
  end
43
11
  end
44
12
  end
45
13
  end
14
+
15
+ require 'chewy/query/pagination/kaminari' if defined?(::Kaminari)
@@ -0,0 +1,37 @@
1
+ module Chewy
2
+ class Query
3
+ module Pagination
4
+ module Kaminari
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include ::Kaminari::PageScopeMethods
9
+
10
+ delegate :default_per_page, :max_per_page, :max_pages, to: :_kaminari_config
11
+
12
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
13
+ def #{::Kaminari.config.page_method_name}(num = 1)
14
+ limit(limit_value).offset(limit_value * ([num.to_i, 1].max - 1))
15
+ end
16
+ METHOD
17
+ end
18
+
19
+ def limit_value
20
+ (criteria.request_options[:size].presence || default_per_page).to_i
21
+ end
22
+
23
+ def offset_value
24
+ criteria.request_options[:from].to_i
25
+ end
26
+
27
+ private
28
+
29
+ def _kaminari_config
30
+ ::Kaminari.config
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ Chewy::Query::Pagination.send :include, Chewy::Query::Pagination::Kaminari
@@ -0,0 +1,9 @@
1
+ require 'chewy/runtime/version'
2
+
3
+ module Chewy
4
+ module Runtime
5
+ def self.version
6
+ Thread.current[:chewy_runtime_version] ||= Version.new(Chewy.client.info['version']['number'])
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ module Chewy
2
+ module Runtime
3
+ class Version
4
+ include Comparable
5
+ attr_reader :major, :minor, :patch
6
+
7
+ def initialize version
8
+ @major, @minor, @patch = *(version.to_s.split('.', 3) + [0]*3).first(3).map(&:to_i)
9
+ end
10
+
11
+ def to_s
12
+ [major, minor, patch].join('.')
13
+ end
14
+
15
+ def <=> other
16
+ other = self.class.new(other) unless other.is_a?(self.class)
17
+ [
18
+ major <=> other.major,
19
+ minor <=> other.minor,
20
+ patch <=> other.patch
21
+ ].detect { |c| c != 0 } || 0
22
+ end
23
+ end
24
+ end
25
+ end
@@ -16,11 +16,7 @@ module Chewy
16
16
  end
17
17
 
18
18
  def name
19
- @name ||= options[:name].present? ? options[:name].to_s.camelize : model.model_name.to_s
20
- end
21
-
22
- def type_name
23
- @type_name ||= (options[:name].presence || model.model_name).to_s.underscore
19
+ @name ||= (options[:name].present? ? options[:name].to_s.camelize : model.model_name.to_s).demodulize
24
20
  end
25
21
 
26
22
  # Import method fo ActiveRecord takes import data and import options
@@ -44,8 +40,24 @@ module Chewy
44
40
  # users = User.all
45
41
  # users.each { |user| user.destroy if user.incative? }
46
42
  # UsersIndex::User.import users # inactive users will be deleted from index
43
+ # # or
47
44
  # UsersIndex::User.import users.map(&:id) # deleted user ids will be deleted from index
48
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
+ #
49
61
  def import *args, &block
50
62
  import_options = args.extract_options!
51
63
  import_options[:batch_size] ||= BATCH_SIZE
@@ -97,7 +109,7 @@ module Chewy
97
109
  indexed = true
98
110
  merged_scope(scoped_model(ids)).find_in_batches(import_options.slice(:batch_size)) do |objects|
99
111
  ids -= objects.map(&:id)
100
- indexed &= block.call index: objects
112
+ indexed &= block.call(grouped_objects(objects))
101
113
  end
102
114
 
103
115
  deleted = ids.in_groups_of(import_options[:batch_size], false).map do |group|
@@ -109,7 +121,9 @@ module Chewy
109
121
 
110
122
  def grouped_objects(objects)
111
123
  objects.group_by do |object|
112
- object.destroyed? ? :delete : :index
124
+ delete = object.delete_from_index? if object.respond_to?(:delete_from_index?)
125
+ delete ||= object.destroyed?
126
+ delete ? :delete : :index
113
127
  end
114
128
  end
115
129
 
@@ -17,7 +17,7 @@ module Chewy
17
17
  # `ProductsIndex.type_hash['product']` or `ProductsIndex.product`
18
18
  #
19
19
  def type_name
20
- raise NotImplementedError
20
+ @type_name ||= name.underscore
21
21
  end
22
22
 
23
23
  # Splits passed objects to groups according to `:batch_size` options.
@@ -10,11 +10,7 @@ module Chewy
10
10
  end
11
11
 
12
12
  def name
13
- @name ||= (options[:name] || target).to_s.camelize
14
- end
15
-
16
- def type_name
17
- @type_name ||= (options[:name] || target).to_s.underscore
13
+ @name ||= (options[:name] || target).to_s.camelize.demodulize
18
14
  end
19
15
 
20
16
  # Imports passed data with options
@@ -27,6 +23,11 @@ module Chewy
27
23
  #
28
24
  # <tt>:batch_size</tt> - import batch size, 1000 objects by default
29
25
  #
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
+ #
30
31
  def import *args, &block
31
32
  import_options = args.extract_options!
32
33
  batch_size = import_options.delete(:batch_size) || BATCH_SIZE
@@ -35,7 +36,9 @@ module Chewy
35
36
  objects.in_groups_of(batch_size, false).map do |group|
36
37
  action_groups = group.group_by do |object|
37
38
  raise "Object is not a `#{target}`" if class_target? && !object.is_a?(target)
38
- object.respond_to?(:destroyed?) && object.destroyed? ? :delete : :index
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
39
42
  end
40
43
  block.call action_groups
41
44
  end.all?
@@ -15,6 +15,8 @@ module Chewy
15
15
  # UsersIndex::User.import suffix: Time.now.to_i # imports data to index with specified suffix if such is exists
16
16
  # UsersIndex::User.import batch_size: 300 # import batch size
17
17
  #
18
+ # See adapters documentation for more details.
19
+ #
18
20
  def import *args
19
21
  import_options = args.extract_options!
20
22
  bulk_options = import_options.reject { |k, v| ![:refresh, :suffix].include?(k) }.reverse_merge!(refresh: true)
@@ -34,7 +36,7 @@ module Chewy
34
36
  end
35
37
 
36
38
  # Perform import operation for specified documents.
37
- # Raises Chewy::FailedImport exception in case of import errors.
39
+ # Raises Chewy::ImportFailed exception in case of import errors.
38
40
  #
39
41
  # UsersIndex::User.import! # imports default data set
40
42
  # UsersIndex::User.import! User.active # imports active users
@@ -44,6 +46,8 @@ module Chewy
44
46
  # UsersIndex::User.import! suffix: Time.now.to_i # imports data to index with specified suffix if such is exists
45
47
  # UsersIndex::User.import! batch_size: 300 # import batch size
46
48
  #
49
+ # See adapters documentation for more details.
50
+ #
47
51
  def import! *args
48
52
  errors = nil
49
53
  subscriber = ActiveSupport::Notifications.subscribe('import_objects.chewy') do |*args|
@@ -51,7 +55,7 @@ module Chewy
51
55
  end
52
56
  import *args
53
57
  ActiveSupport::Notifications.unsubscribe(subscriber)
54
- raise Chewy::FailedImport.new(self, errors) if errors.present?
58
+ raise Chewy::ImportFailed.new(self, errors) if errors.present?
55
59
  true
56
60
  end
57
61
 
@@ -60,9 +64,8 @@ module Chewy
60
64
  def bulk options = {}
61
65
  suffix = options.delete(:suffix)
62
66
 
63
- Chewy.wait_for_status
64
-
65
67
  result = client.bulk options.merge(index: index.build_index_name(suffix: suffix), type: type_name)
68
+ Chewy.wait_for_status
66
69
 
67
70
  extract_errors result
68
71
  end
@@ -82,14 +82,14 @@ module Chewy
82
82
  # will be an array of hashes, if `user.quiz` is not a collection association
83
83
  # then just values hash will be put in the index.
84
84
  #
85
- # field :quiz, type: 'object' do
85
+ # field :quiz do
86
86
  # field :question, :answer
87
87
  # field :score, type: 'integer'
88
88
  # end
89
89
  #
90
90
  # Nested fields are composed from nested objects:
91
91
  #
92
- # field :name, type: 'object', value: -> { name_translations } do
92
+ # field :name, value: -> { name_translations } do
93
93
  # field :ru, value: ->(name) { name['ru'] }
94
94
  # field :en, value: ->(name) { name['en'] }
95
95
  # end
@@ -99,12 +99,12 @@ module Chewy
99
99
  #
100
100
  # field :name, type: 'object', value: -> { name_translations }
101
101
  #
102
- # The special case is `multi_field`. In that case field composition
103
- # changes satisfy elasticsearch rules:
102
+ # The special case is multi_field. If type options and block are
103
+ # both present field is treated as a multi-field. In that case field
104
+ # composition changes satisfy elasticsearch rules:
104
105
  #
105
- # field :full_name, type: 'multi_field', value: ->{ full_name.try(:strip) } do
106
- # field :full_name, index: 'analyzed', analyzer: 'name'
107
- # field :sorted, index: 'analyzed', analyzer: 'sorted'
106
+ # field :full_name, type: 'string', analyzer: 'name', value: ->{ full_name.try(:strip) } do
107
+ # field :sorted, analyzer: 'sorted'
108
108
  # end
109
109
  #
110
110
  def field *args, &block
@@ -114,7 +114,7 @@ module Chewy
114
114
  if args.size > 1
115
115
  args.map { |name| field(name, options) }
116
116
  else
117
- expand_nested(Chewy::Fields::Default.new(args.first, options), &block)
117
+ expand_nested(Chewy::Fields::Base.new(args.first, options), &block)
118
118
  end
119
119
  end
120
120
 
@@ -135,7 +135,7 @@ module Chewy
135
135
  # template 'title.*', mapping_hash # dot in template causes "path_match" using
136
136
  # template /tit.+/, mapping_hash # using "match_pattern": "regexp"
137
137
  # template /title\..+/, mapping_hash # "\." - escaped dot causes "path_match" using
138
- # template /tit.+/, 'string' mapping_hash # "match_mapping_type" as the optionsl second argument
138
+ # template /tit.+/, 'string', mapping_hash # "match_mapping_type" as the optionsl second argument
139
139
  # template template42: {match: 'hello*', mapping: {type: 'object'}} # or even pass a template as is
140
140
  #
141
141
  def template *args