chewy 0.4.1 → 0.5.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 (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