chewy 0.3.0 → 0.4.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.
@@ -1,14 +1,83 @@
1
+ require 'i18n/core_ext/hash'
2
+
3
+ # Rspec matcher `update_index`
4
+ # To use it - add `require 'chewy/rspec'` to the `spec_helper.rb`
5
+ # Simple usage - just pass type as argument.
6
+ #
7
+ # specify { expect { user.save! }.to update_index(UsersIndex::User) }
8
+ # specify { expect { user.save! }.to update_index('users#user') }
9
+ # specify { expect { user.save! }.not_to update_index('users#user') }
10
+ #
11
+ # This example will pass as well because user1 was reindexed
12
+ # and nothing was said about user2:
13
+ #
14
+ # specify { expect { [user1, user2].map(&:save!) }
15
+ # .to update_index(UsersIndex.user).and_reindex(user1) }
16
+ #
17
+ # If you need to specify reindexed records strictly - use `only` chain.
18
+ # Combined matcher chain methods:
19
+ #
20
+ # specify { expect { user1.destroy!; user2.save! } }
21
+ # .to update_index(UsersIndex:User).and_reindex(user2).and_delete(user1)
22
+ #
1
23
  RSpec::Matchers.define :update_index do |type_name, options = {}|
24
+
25
+ # Specify indexed records by passing record itself or id.
26
+ #
27
+ # specify { expect { user.save! }.to update_index(UsersIndex::User).and_reindex(user)
28
+ # specify { expect { user.save! }.to update_index(UsersIndex::User).and_reindex(42)
29
+ # specify { expect { [user1, user2].map(&:save!) }
30
+ # .to update_index(UsersIndex::User).and_reindex(user1, user2) }
31
+ # specify { expect { [user1, user2].map(&:save!) }
32
+ # .to update_index(UsersIndex::User).and_reindex(user1).and_reindex(user2) }
33
+ #
34
+ # Specify indexing count for every particular record. Useful in case
35
+ # urgent index updates.
36
+ #
37
+ # specify { expect { 2.times { user.save! } }
38
+ # .to update_index(UsersIndex::User).and_reindex(user, times: 2) }
39
+ #
40
+ # Specify reindexed attributes. Note that arrays are
41
+ # compared position-independantly.
42
+ #
43
+ # specify { expect { user.update_attributes!(name: 'Duke') }
44
+ # .to update_index(UsersIndex.user).and_reindex(user, with: {name: 'Duke'}) }
45
+ #
46
+ # You can combine all the options and chain `and_reindex` method to
47
+ # specify options for every indexed record:
48
+ #
49
+ # specify { expect { 2.times { [user1, user2].map { |u| u.update_attributes!(name: "Duke#{u.id}") } } }
50
+ # .to update_index(UsersIndex.user)
51
+ # .and_reindex(user1, with: {name: 'Duke42'}) }
52
+ # .and_reindex(user2, times: 1, with: {name: 'Duke43'}) }
53
+ #
2
54
  chain(:and_reindex) do |*args|
3
55
  @reindex ||= {}
4
56
  @reindex.merge!(extract_documents(*args))
5
57
  end
6
58
 
59
+ # Specify deleted records with record itself or id passed.
60
+ #
61
+ # specify { expect { user.destroy! }.to update_index(UsersIndex::User).and_delete(user) }
62
+ # specify { expect { user.destroy! }.to update_index(UsersIndex::User).and_delete(user.id) }
63
+ #
7
64
  chain(:and_delete) do |*args|
8
65
  @delete ||= {}
9
66
  @delete.merge!(extract_documents(*args))
10
67
  end
11
68
 
69
+ # Used for specifying than no other records would be indexed or deleted:
70
+ #
71
+ # specify { expect { [user1, user2].map(&:save!) }
72
+ # .to update_index(UsersIndex.user).and_reindex(user1, user2).only }
73
+ # specify { expect { [user1, user2].map(&:destroy!) }
74
+ # .to update_index(UsersIndex.user).and_delete(user1, user2).only }
75
+ #
76
+ # This example will fail:
77
+ #
78
+ # specify { expect { [user1, user2].map(&:save!) }
79
+ # .to update_index(UsersIndex.user).and_reindex(user1).only }
80
+ #
12
81
  chain(:only) do |*args|
13
82
  @only = true
14
83
  end
@@ -23,9 +92,8 @@ RSpec::Matchers.define :update_index do |type_name, options = {}|
23
92
  updated = []
24
93
  type.stub(:bulk) do |options|
25
94
  updated += options[:body].map do |updated_document|
26
- updated_document = updated_document.symbolize_keys
95
+ updated_document = updated_document.deep_symbolize_keys
27
96
  body = updated_document[:index] || updated_document[:delete]
28
- body[:data] = body[:data].symbolize_keys if body[:data]
29
97
  updated_document
30
98
  end
31
99
  {}
@@ -59,7 +127,7 @@ RSpec::Matchers.define :update_index do |type_name, options = {}|
59
127
  document[:match_count] = (!document[:expected_count] && document[:real_count] > 0) ||
60
128
  (document[:expected_count] && document[:expected_count] == document[:real_count])
61
129
  document[:match_attributes] = document[:expected_attributes].blank? ||
62
- document[:real_attributes].slice(*document[:expected_attributes].keys) == document[:expected_attributes]
130
+ compare_attributes(document[:expected_attributes], document[:real_attributes])
63
131
  end
64
132
  @delete.each do |_, document|
65
133
  document[:match_count] = (!document[:expected_count] && document[:real_count] > 0) ||
@@ -123,7 +191,7 @@ RSpec::Matchers.define :update_index do |type_name, options = {}|
123
191
  options = args.extract_options!
124
192
 
125
193
  expected_count = options[:times] || options[:count]
126
- expected_attributes = (options[:with] || options[:attributes] || {}).symbolize_keys!
194
+ expected_attributes = (options[:with] || options[:attributes] || {}).deep_symbolize_keys
127
195
 
128
196
  Hash[args.flatten.map do |document|
129
197
  id = document.respond_to?(:id) ? document.id.to_s : document.to_s
@@ -136,4 +204,27 @@ RSpec::Matchers.define :update_index do |type_name, options = {}|
136
204
  }]
137
205
  end]
138
206
  end
207
+
208
+ def compare_attributes expected, real
209
+ expected.inject(true) do |result, (key, value)|
210
+ equal = if value.is_a?(Array) && real[key].is_a?(Array)
211
+ array_difference(value, real[key]) && array_difference(real[key], value)
212
+ elsif value.is_a?(Hash) && real[key].is_a?(Hash)
213
+ compare_attributes(value, real[key])
214
+ else
215
+ real[key] == value
216
+ end
217
+ result && equal
218
+ end
219
+ end
220
+
221
+ def array_difference first, second
222
+ difference = first.to_ary.dup
223
+ second.to_ary.each do |element|
224
+ if index = difference.index(element)
225
+ difference.delete_at(index)
226
+ end
227
+ end
228
+ difference.none?
229
+ end
139
230
  end
@@ -36,6 +36,16 @@ module Chewy
36
36
  #
37
37
  # <tt>:batch_size</tt> - import batch size, 1000 objects by default
38
38
  #
39
+ # Method handles destroyed objects as well. In case of objects AcriveRecord::Relation
40
+ # or array passed, objects, responding with true to `destroyed?` method will be deleted
41
+ # from index. In case of ids array passed - documents with missing records ids will be
42
+ # deleted from index:
43
+ #
44
+ # users = User.all
45
+ # users.each { |user| user.destroy if user.incative? }
46
+ # UsersIndex::User.import users # inactive users will be deleted from index
47
+ # UsersIndex::User.import users.map(&:id) # deleted user ids will be deleted from index
48
+ #
39
49
  def import *args, &block
40
50
  import_options = args.extract_options!
41
51
  import_options[:batch_size] ||= BATCH_SIZE
@@ -65,7 +75,7 @@ module Chewy
65
75
 
66
76
  additional_scope = load_options[load_options[:_type].type_name.to_sym].try(:[], :scope) || load_options[:scope]
67
77
 
68
- scope = model.where(id: objects.map(&:id))
78
+ scope = scoped_model(objects.map(&:id))
69
79
  loaded_objects = if additional_scope.is_a?(Proc)
70
80
  scope.instance_exec(&additional_scope)
71
81
  elsif additional_scope.is_a?(::ActiveRecord::Relation)
@@ -85,7 +95,7 @@ module Chewy
85
95
  ids = ids.map(&:to_i).uniq
86
96
 
87
97
  indexed = true
88
- merged_scope(model.where(id: ids)).find_in_batches(import_options.slice(:batch_size)) do |objects|
98
+ merged_scope(scoped_model(ids)).find_in_batches(import_options.slice(:batch_size)) do |objects|
89
99
  ids -= objects.map(&:id)
90
100
  indexed &= block.call index: objects
91
101
  end
@@ -107,6 +117,10 @@ module Chewy
107
117
  scope ? scope.clone.merge(target) : target
108
118
  end
109
119
 
120
+ def scoped_model(ids)
121
+ model.where(Hash[model.primary_key.to_sym || :id, ids])
122
+ end
123
+
110
124
  def model_all
111
125
  ::ActiveRecord::VERSION::MAJOR < 4 ? model.scoped : model.all
112
126
  end
@@ -17,7 +17,9 @@ module Chewy
17
17
  #
18
18
  def import *args
19
19
  import_options = args.extract_options!
20
- bulk_options = import_options.extract!(:refresh, :suffix).reverse_merge!(refresh: true)
20
+ bulk_options = import_options.reject { |k, v| ![:refresh, :suffix].include?(k) }.reverse_merge!(refresh: true)
21
+
22
+ index.create!(bulk_options.slice(:suffix)) unless index.exists?
21
23
 
22
24
  ActiveSupport::Notifications.instrument 'import_objects.chewy', type: self do |payload|
23
25
  adapter.import(*args, import_options) do |action_objects|
@@ -57,6 +59,9 @@ module Chewy
57
59
  # Adds `:suffix` option to bulk import to index with specified suffix.
58
60
  def bulk options = {}
59
61
  suffix = options.delete(:suffix)
62
+
63
+ Chewy.wait_for_status
64
+
60
65
  result = client.bulk options.merge(index: index.build_index_name(suffix: suffix), type: type_name)
61
66
 
62
67
  extract_errors result
@@ -5,17 +5,111 @@ module Chewy
5
5
 
6
6
  included do
7
7
  class_attribute :root_object, instance_reader: false, instance_writer: false
8
+ class_attribute :_templates
8
9
  end
9
10
 
10
11
  module ClassMethods
11
- def root(options = {}, &block)
12
- raise "Root is already defined" if self.root_object
12
+ # Defines root object for mapping and is optional for type
13
+ # definition. Use it only if you need to pass options for root
14
+ # object mapping, such as `date_detection` or `dynamic_date_formats`
15
+ #
16
+ # class UsersIndex < Chewy::Index
17
+ # define_type User do
18
+ # # root object defined implicitly and optionless for current type
19
+ # field :full_name, type: 'string'
20
+ # end
21
+ # end
22
+ #
23
+ # class CarsIndex < Chewy::Index
24
+ # define_type Car do
25
+ # # explicit root definition with additional options
26
+ # root dynamic_date_formats: ['yyyy-MM-dd'] do
27
+ # field :model_name, type: 'string'
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ def root options = {}, &block
33
+ raise "Root is already defined" if root_object
13
34
  build_root(options, &block)
14
35
  end
15
36
 
16
- def field(*args, &block)
37
+ # Defines mapping field for current type
38
+ #
39
+ # class UsersIndex < Chewy::Index
40
+ # define_type User do
41
+ # # passing all the options to field definition:
42
+ # field :full_name, type: 'string', analyzer: 'special'
43
+ # end
44
+ # end
45
+ #
46
+ # The `type` is optional and defaults to `string` if not defined:
47
+ #
48
+ # field :full_name
49
+ #
50
+ # Also, multiple fields might be defined with one call and
51
+ # with the same options:
52
+ #
53
+ # field :first_name, :last_name, analyzer: 'special'
54
+ #
55
+ # The only special option in the field definition
56
+ # is `:value`. If no `:value` specified then just corresponding
57
+ # method will be called for the indexed object. Also
58
+ # `:value` might be a proc or indexed object method name:
59
+ #
60
+ # class User < ActiveRecord::Base
61
+ # def user_full_name
62
+ # [first_name, last_name].join(' ')
63
+ # end
64
+ # end
65
+ #
66
+ # field :full_name, type: 'string', value: :user_full_name
67
+ #
68
+ # The proc evaluates inside the indexed object context if
69
+ # its arity is 0 and in present contexts if there is an argument:
70
+ #
71
+ # field :full_name, type: 'string', value: -> { [first_name, last_name].join(' ') }
72
+ #
73
+ # separator = ' '
74
+ # field :full_name, type: 'string', value: ->(user) { [user.first_name, user.last_name].join(separator) }
75
+ #
76
+ # If array was returned as value - it will be put in index as well.
77
+ #
78
+ # field :tags, type: 'string', value: -> { tags.map(&:name) }
79
+ #
80
+ # Fields supports nesting in case of `object` field type. If
81
+ # `user.quiz` will return an array of objects, then result index content
82
+ # will be an array of hashes, if `user.quiz` is not a collection association
83
+ # then just values hash will be put in the index.
84
+ #
85
+ # field :quiz, type: 'object' do
86
+ # field :question, :answer
87
+ # field :score, type: 'integer'
88
+ # end
89
+ #
90
+ # Nested fields are composed from nested objects:
91
+ #
92
+ # field :name, type: 'object', value: -> { name_translations } do
93
+ # field :ru, value: ->(name) { name['ru'] }
94
+ # field :en, value: ->(name) { name['en'] }
95
+ # end
96
+ #
97
+ # Off course it is possible to define object fields contents dynamically
98
+ # but make sure evaluation proc returns hash:
99
+ #
100
+ # field :name, type: 'object', value: -> { name_translations }
101
+ #
102
+ # The special case is `multi_field`. In that case field composition
103
+ # changes satisfy elasticsearch rules:
104
+ #
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'
108
+ # end
109
+ #
110
+ def field *args, &block
17
111
  options = args.extract_options!
18
- build_root unless self.root_object
112
+ build_root unless root_object
19
113
 
20
114
  if args.size > 1
21
115
  args.map { |name| field(name, options) }
@@ -24,13 +118,42 @@ module Chewy
24
118
  end
25
119
  end
26
120
 
121
+ # Defines dynamic template in mapping root objests
122
+ #
123
+ # class CarsIndex < Chewy::Index
124
+ # define_type Car do
125
+ # template 'model.*', type: 'string', analyzer: 'special'
126
+ # field 'model', type: 'object' # here we can put { ru: 'Мерседес', en: 'Mercedes' }
127
+ # # and template will be applyed to this field
128
+ # end
129
+ # end
130
+ #
131
+ # Name for each template is generated with the following
132
+ # rule: "template_#{dynamic_templates.size + 1}".
133
+ #
134
+ # template 'tit*', mapping_hash
135
+ # template 'title.*', mapping_hash # dot in template causes "path_match" using
136
+ # template /tit.+/, mapping_hash # using "match_pattern": "regexp"
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
139
+ # template template42: {match: 'hello*', mapping: {type: 'object'}} # or even pass a template as is
140
+ #
141
+ def template *args
142
+ build_root unless root_object
143
+
144
+ root_object.dynamic_template *args
145
+ end
146
+ alias_method :dynamic_template, :template
147
+
148
+ # Returns compiled mappings hash for current type
149
+ #
27
150
  def mappings_hash
28
151
  root_object ? root_object.mappings_hash : {}
29
152
  end
30
153
 
31
154
  private
32
155
 
33
- def expand_nested(field, &block)
156
+ def expand_nested field, &block
34
157
  @_current_field.nested(field) if @_current_field
35
158
  if block
36
159
  previous_field, @_current_field = @_current_field, field
@@ -39,7 +162,7 @@ module Chewy
39
162
  end
40
163
  end
41
164
 
42
- def build_root(options = {}, &block)
165
+ def build_root options = {}, &block
43
166
  self.root_object = Chewy::Fields::Root.new(type_name, options)
44
167
  expand_nested(self.root_object, &block)
45
168
  @_current_field = self.root_object
@@ -9,6 +9,9 @@ module Chewy
9
9
  method = args.first
10
10
 
11
11
  update = Proc.new do
12
+ update_options = options.reverse_merge(urgent: Chewy.urgent_update)
13
+ clear_association_cache if update_options[:urgent]
14
+
12
15
  backreference = if method && method.to_s == 'self'
13
16
  self
14
17
  elsif method
@@ -17,8 +20,7 @@ module Chewy
17
20
  instance_eval(&block)
18
21
  end
19
22
 
20
- Chewy.derive_type(type_name).update_index(backreference,
21
- options.reverse_merge(urgent: Chewy.urgent_update))
23
+ Chewy.derive_type(type_name).update_index(backreference, update_options)
22
24
  end
23
25
 
24
26
  after_save &update
data/lib/chewy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Chewy
2
- VERSION = '0.3.0'
2
+ VERSION = '0.4.0'
3
3
  end
@@ -3,4 +3,42 @@ require 'spec_helper'
3
3
  describe Chewy::Fields::Root do
4
4
  specify { described_class.new('name').value.should be_a(Proc) }
5
5
  # TODO: add 'should_behave_like base_field'
6
+
7
+ subject(:field) { described_class.new('product') }
8
+
9
+ describe '#dynamic_template' do
10
+ specify do
11
+ field.dynamic_template 'hello', type: 'string'
12
+ field.dynamic_template 'hello*', :integer
13
+ field.dynamic_template 'hello.*'
14
+ field.dynamic_template /hello/
15
+ field.dynamic_template /hello.*/
16
+ field.dynamic_template template_42: {mapping: {}, match: ''}
17
+ field.dynamic_template /hello\..*/
18
+
19
+ field.mappings_hash.should == {product: {dynamic_templates: [
20
+ {template_1: {mapping: {type: 'string'}, match: 'hello'}},
21
+ {template_2: {mapping: {}, match_mapping_type: 'integer', match: 'hello*'}},
22
+ {template_3: {mapping: {}, path_match: 'hello.*'}},
23
+ {template_4: {mapping: {}, match: 'hello', match_pattern: 'regexp'}},
24
+ {template_5: {mapping: {}, match: 'hello.*', match_pattern: 'regexp'}},
25
+ {template_42: {mapping: {}, match: ''}},
26
+ {template_7: {mapping: {}, path_match: 'hello\..*', match_pattern: 'regexp'}}
27
+ ]}}
28
+ end
29
+
30
+ context do
31
+ subject(:field) { described_class.new('product', dynamic_templates: [
32
+ {template_42: {mapping: {}, match: ''}}
33
+ ]) }
34
+
35
+ specify do
36
+ field.dynamic_template 'hello', type: 'string'
37
+ field.mappings_hash.should == {product: {dynamic_templates: [
38
+ {template_42: {mapping: {}, match: ''}},
39
+ {template_1: {mapping: {type: 'string'}, match: 'hello'}}
40
+ ]}}
41
+ end
42
+ end
43
+ end
6
44
  end
@@ -284,7 +284,7 @@ describe Chewy::Index::Actions do
284
284
  define_type City do
285
285
  field :name, type: 'object'
286
286
  end
287
- end.tap(&:create!)
287
+ end
288
288
  end
289
289
 
290
290
  specify { CitiesIndex.import(city: dummy_cities).should == false }
@@ -308,7 +308,7 @@ describe Chewy::Index::Actions do
308
308
  define_type City do
309
309
  field :name, type: 'object'
310
310
  end
311
- end.tap(&:create!)
311
+ end
312
312
  end
313
313
 
314
314
  specify { expect { CitiesIndex.import!(city: dummy_cities) }.to raise_error Chewy::FailedImport }