es-elasticity 0.11.1 → 0.11.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 11dddda9376573d12d8d6c0db18342c7a242199a
4
- data.tar.gz: 2613b868ae0ca372149fa84724790ade9bbca2a2
3
+ metadata.gz: b90fa843196b6ec0a4ed99f4605d089f1127696a
4
+ data.tar.gz: 16e810b1114f1829e21c8e655f2fd7ad40fa1022
5
5
  SHA512:
6
- metadata.gz: 13a78c3cbe8eb55a911d7b39d1a31e07329e4e3f809fde432ece6a12511db5e40422af47f3508938ec597579aef697a05622dc449670537b45b4ba64ee4b22e3
7
- data.tar.gz: bcb01eb45bf67cf5dc2d6c0cfd7bb6ad00fc40167e5068da1e067d6a77f8f7d811b812b7de923fca134eea8ea28510114e9e1e235c9ff61b148a391e30a5f035
6
+ metadata.gz: 9d54bd8d914de323f3ce7d430e7477eda5b74d4b1dbd3bb9127a2a5b460ec7facbbef5fcbc5e00fe2f9f3df674834cca6641724191acb5c2b81922874c6d32a6
7
+ data.tar.gz: cf91125f554221f3ea89364fc8aab76395cfa9f0376fd03369bcfb02bf6235cd7b35b668ea952d7669123883c3c93c83e20cdb8024087b887fe9d95f481865e7
data/CHANGELOG CHANGED
@@ -1,3 +1,14 @@
1
+ v0.11.5
2
+ - Give the option of retrying the deletion for certain exceptions during remap
3
+ v0.11.4
4
+ - Fully clean up if error occurs during remap (assign all aliases back to original index)
5
+ v0.11.3
6
+ - Adds support for preserving the order or normalized names of `highlight` through `highlighted_attrs`
7
+
8
+ v0.11.2
9
+ - Adds support for passing arguments to Search definition through `search(query, search_args)` in index searching and msearch
10
+ - adds _explanation to hold value of returned explanations in the base document
11
+
1
12
  v0.11.1
2
13
  - support `action.destructive_requires_name` setting by being explict about which indices to delete
3
14
 
data/README.md CHANGED
@@ -15,7 +15,7 @@ Elasticity maps those documents into objects, providing a rich object representa
15
15
  Add this line to your application's Gemfile:
16
16
 
17
17
  ```ruby
18
- gem 'es-elasticity', require "elasticity"
18
+ gem 'es-elasticity', require: "elasticity"
19
19
  ```
20
20
 
21
21
  And then execute:
@@ -191,6 +191,26 @@ cursor.each { |doc| ... }
191
191
  adults = adults.active_records(User)
192
192
  ```
193
193
 
194
+ #### Search Args
195
+
196
+ ##### explain: true
197
+ For `search` definitions we support passing `{ explain: true }` to the search as a second argument in order to surface the reason a search result was returned.
198
+
199
+ ```ruby
200
+ # example in single search
201
+ search_results_with_explanation = SearchDoc::A.search(query_body, { explain: true }).search_results
202
+
203
+ # In multisearch
204
+ search_a = SearchDoc::A.search(query_body, { explain: true })
205
+ search_b = SearchDoc::B.search(query_body, { explain: true })
206
+ search_c = SearchDoc::C.search(query_body, { explain: true })
207
+
208
+ multi = Elasticity::MultiSearch.new do |m|
209
+ m.add(:a, search_a, documents: ::SearchDoc::A)
210
+ m.add(:b, search_b, documents: ::SearchDoc::B)
211
+ m.add(:c, search_c, documents: ::SearchDoc::C)
212
+ end
213
+ ```
194
214
  For more information about the `active_records` method, read [ActiveRecord integration](#activerecord-integration).
195
215
 
196
216
  ### Segmented Documents
@@ -12,7 +12,7 @@ module Elasticity
12
12
  end
13
13
 
14
14
  # Define common attributes for all documents
15
- attr_accessor :_id, :highlighted, :_score, :sort
15
+ attr_accessor :_id, :highlighted, :_score, :sort, :_explanation, :highlighted_attrs
16
16
 
17
17
  def attributes=(attributes)
18
18
  attributes.each do |attr, value|
@@ -65,8 +65,9 @@ module Elasticity
65
65
  end
66
66
 
67
67
  # Remap
68
- def remap!
69
- @strategy.remap(@index_config.definition)
68
+ # retry_delay & max_delay are in seconds
69
+ def remap!(retry_delete_on_recoverable_errors: true, retry_delay: 30, max_delay: 600)
70
+ @strategy.remap(@index_config.definition, retry_delete_on_recoverable_errors: retry_delete_on_recoverable_errors, retry_delay: retry_delay, max_delay: max_delay)
70
71
  end
71
72
 
72
73
  # Flushes the index, forcing any writes
@@ -82,8 +83,10 @@ module Elasticity
82
83
  # Searches the index using the parameters provided in the body hash, following the same
83
84
  # structure Elasticsearch expects.
84
85
  # Returns a DocumentSearch object.
85
- def search(body)
86
- search_obj = Search.build(@index_config.client, @strategy.search_index, document_types, body)
86
+ # search_args allows for
87
+ # explain: boolean to specify we should request _explanation of the query
88
+ def search(body, search_args = {})
89
+ search_obj = Search.build(@index_config.client, @strategy.search_index, document_types, body, search_args)
87
90
  Search::DocumentProxy.new(search_obj, self.method(:map_hit))
88
91
  end
89
92
 
@@ -141,23 +144,28 @@ module Elasticity
141
144
  attrs.merge!(sort: hit["sort"])
142
145
  attrs.merge!(hit["_source"]) if hit["_source"]
143
146
 
144
- if hit["highlight"]
145
- highlighted_attrs = attrs.dup
146
- attrs_set = Set.new
147
+ highlighted = nil
147
148
 
148
- hit["highlight"].each do |name, v|
149
+ if hit["highlight"]
150
+ highlighted_attrs = hit["highlight"].each_with_object({}) do |(name, v), attrs|
149
151
  name = name.gsub(/\..*\z/, '')
150
- next if attrs_set.include?(name)
151
- highlighted_attrs[name] = v
152
- attrs_set << name
152
+
153
+ attrs[name] ||= v
153
154
  end
154
155
 
155
- highlighted = @document_klass.new(highlighted_attrs)
156
+ highlighted = @document_klass.new(attrs.merge(highlighted_attrs))
156
157
  end
158
+
159
+ injected_attrs = attrs.merge({
160
+ highlighted: highlighted,
161
+ highlighted_attrs: highlighted_attrs.try(:keys),
162
+ _explanation: hit["_explanation"]
163
+ })
164
+
157
165
  if @document_klass.config.subclasses.present?
158
- @document_klass.config.subclasses[hit["_type"].to_sym].constantize.new(attrs.merge(highlighted: highlighted))
166
+ @document_klass.config.subclasses[hit["_type"].to_sym].constantize.new(injected_attrs)
159
167
  else
160
- @document_klass.new(attrs.merge(highlighted: highlighted))
168
+ @document_klass.new(injected_attrs)
161
169
  end
162
170
  end
163
171
  end
@@ -1,7 +1,7 @@
1
1
  module Elasticity
2
2
  module Search
3
- def self.build(client, index_name, document_types, body)
4
- search_def = Search::Definition.new(index_name, document_types, body)
3
+ def self.build(client, index_name, document_types, body, search_args = {})
4
+ search_def = Search::Definition.new(index_name, document_types, body, search_args)
5
5
  Search::Facade.new(client, search_def)
6
6
  end
7
7
 
@@ -10,10 +10,11 @@ module Elasticity
10
10
  class Definition
11
11
  attr_accessor :index_name, :document_types, :body
12
12
 
13
- def initialize(index_name, document_types, body)
13
+ def initialize(index_name, document_types, body, search_args = {})
14
14
  @index_name = index_name
15
15
  @document_types = document_types
16
16
  @body = body.deep_symbolize_keys!
17
+ @search_args = search_args
17
18
  end
18
19
 
19
20
  def update(body_changes)
@@ -28,11 +29,13 @@ module Elasticity
28
29
  end
29
30
 
30
31
  def to_search_args
31
- { index: @index_name, type: @document_types, body: @body }
32
+ @search_args.merge({ index: @index_name, type: @document_types, body: @body })
32
33
  end
33
34
 
34
35
  def to_msearch_args
35
- { index: @index_name, type: @document_types, search: @body }
36
+ search_body = @search_args.merge(@body)
37
+
38
+ { index: @index_name, type: @document_types, search: search_body }
36
39
  end
37
40
  end
38
41
 
@@ -4,6 +4,11 @@ module Elasticity
4
4
  # runtime changes by simply atomically updating the aliases. For example, look at the remap method
5
5
  # implementation.
6
6
  class AliasIndex
7
+ SNAPSHOT_ERROR_SNIPPET = "Cannot delete indices that are being snapshotted".freeze
8
+ RETRYABLE_ERROR_SNIPPETS = [
9
+ SNAPSHOT_ERROR_SNIPPET
10
+ ].freeze
11
+
7
12
  STATUSES = [:missing, :ok]
8
13
 
9
14
  def initialize(client, index_base_name, document_type)
@@ -29,7 +34,7 @@ module Elasticity
29
34
  #
30
35
  # It does a little bit more to ensure consistency and to handle race-conditions. For more details
31
36
  # look at the implementation.
32
- def remap(index_def)
37
+ def remap(index_def, retry_delete_on_recoverable_errors: false, retry_delay: 0, max_delay: 0)
33
38
  main_indexes = self.main_indexes
34
39
  update_indexes = self.update_indexes
35
40
 
@@ -99,11 +104,23 @@ module Elasticity
99
104
  { remove: { index: original_index, alias: @main_alias } },
100
105
  ]
101
106
  })
102
- @client.index_delete(index: original_index)
103
107
 
108
+ waiting_duration = 0
109
+ begin
110
+ @client.index_delete(index: original_index)
111
+ rescue Elasticsearch::Transport::Transport::ServerError => e
112
+ if retryable_error?(e) && retry_delete_on_recoverable_errors && waiting_duration < max_delay
113
+ waiting_duration += retry_delay
114
+ sleep(retry_delay)
115
+ retry
116
+ else
117
+ raise e
118
+ end
119
+ end
104
120
  rescue
105
121
  @client.index_update_aliases(body: {
106
122
  actions: [
123
+ { add: { index: original_index, alias: @main_alias } },
107
124
  { add: { index: original_index, alias: @update_alias } },
108
125
  { remove: { index: new_index, alias: @update_alias } },
109
126
  ]
@@ -268,6 +285,12 @@ module Elasticity
268
285
  @client.index_create(index: index_name, body: index_def)
269
286
  index_name
270
287
  end
288
+
289
+ def retryable_error?(e)
290
+ RETRYABLE_ERROR_SNIPPETS.any? do |s|
291
+ e.message.match(s)
292
+ end
293
+ end
271
294
  end
272
295
  end
273
296
  end
@@ -1,3 +1,3 @@
1
1
  module Elasticity
2
- VERSION = "0.11.1"
2
+ VERSION = "0.11.5"
3
3
  end
@@ -85,8 +85,9 @@ RSpec.describe "Persistence", elasticsearch: true do
85
85
  c.index_base_name = "cats_and_dogs"
86
86
  c.strategy = Elasticity::Strategies::SingleIndex
87
87
  c.document_type = "cat"
88
+
88
89
  c.mapping = { "properties" => {
89
- name: { type: "string", index: "not_analyzed" },
90
+ name: { type: "text", index: true },
90
91
  age: { type: "integer" }
91
92
  } }
92
93
  end
@@ -104,7 +105,7 @@ RSpec.describe "Persistence", elasticsearch: true do
104
105
  c.strategy = Elasticity::Strategies::SingleIndex
105
106
  c.document_type = "dog"
106
107
  c.mapping = { "properties" => {
107
- name: { type: "string", index: "not_analyzed" },
108
+ name: { type: "text", index: true },
108
109
  age: { type: "integer" },
109
110
  hungry: { type: "boolean" }
110
111
  } }
@@ -167,7 +168,7 @@ RSpec.describe "Persistence", elasticsearch: true do
167
168
  c.mapping = {
168
169
  "properties" => {
169
170
  id: { type: "integer" },
170
- name: { type: "string", index: "not_analyzed" },
171
+ name: { type: "text", index: true },
171
172
  birthdate: { type: "date" },
172
173
  },
173
174
  }
@@ -268,7 +269,7 @@ RSpec.describe "Persistence", elasticsearch: true do
268
269
  c.mapping = {
269
270
  "properties" => {
270
271
  id: { type: "integer" },
271
- name: { type: "string", index: "not_analyzed" },
272
+ name: { type: "text", index: true },
272
273
  },
273
274
  }
274
275
  end
@@ -313,6 +314,88 @@ RSpec.describe "Persistence", elasticsearch: true do
313
314
  expect(results.total).to eq(2010)
314
315
  end
315
316
 
317
+ it "fully cleans up if error occurs deleting the old index during remap" do
318
+ expected_aliases = %w[elasticity_test_users elasticity_test_users_update]
319
+ original_aliases = all_aliases(subject)
320
+ expect(original_aliases).to match_array(expected_aliases)
321
+ number_of_docs = 20
322
+ number_of_docs.times.map do |i|
323
+ subject.new(id: i, name: "User #{i}", birthdate: random_birthdate).tap(&:update)
324
+ end
325
+
326
+ allow_any_instance_of(Elasticity::InstrumentedClient).to receive(:index_delete).and_raise("KAPOW")
327
+ expect do
328
+ subject.remap!
329
+ end.to raise_error("KAPOW")
330
+ cleaned_up_aliases = all_aliases(subject)
331
+ expect(cleaned_up_aliases).to match_array(expected_aliases)
332
+ allow_any_instance_of(Elasticity::InstrumentedClient).to receive(:index_delete).and_call_original
333
+ end
334
+
335
+ context "recovering from remap errors" do
336
+ let(:recoverable_message) do
337
+ '[400] {"error":{"root_cause":[{"type":"remote_transport_exception","reason":"[your_cluster][cluster_id][indices:admin/delete]"}],"type":"illegal_argument_exception","reason":"Cannot delete indices that are being snapshotted: [[full_index_name_and_id]]. Try again after snapshot finishes or cancel the currently running snapshot."},"status":400}'
338
+ end
339
+ after do
340
+ allow_any_instance_of(Elasticity::InstrumentedClient).to receive(:index_delete).and_call_original
341
+ end
342
+
343
+ it "waits for recoverable errors before deleting old index during remap" do
344
+ original_index_name = subject.config.client.index_get_alias(index: "#{subject.ref_index_name}-*", name: subject.ref_index_name).keys.first
345
+
346
+ call_count = 0
347
+ allow_any_instance_of(Elasticity::InstrumentedClient).to receive(:index_delete) do
348
+ call_count +=1
349
+ if call_count < 3
350
+ raise Elasticsearch::Transport::Transport::Errors::BadRequest.new(recoverable_message)
351
+ else
352
+ []
353
+ end
354
+ end
355
+ build_some_docs(subject)
356
+
357
+ subject.remap!(retry_delete_on_recoverable_errors: true, retry_delay: 0.5, max_delay: 1)
358
+ subject.flush_index
359
+ results = subject.search({})
360
+ expect(results.total).to eq(20)
361
+ remapped_index_name = subject.config.client.index_get_alias(index: "#{subject.ref_index_name}-*", name: subject.ref_index_name).keys.first
362
+ expect(remapped_index_name).to_not eq(original_index_name)
363
+ end
364
+
365
+ it "will only retry for a set amount of time" do
366
+ allow_any_instance_of(Elasticity::InstrumentedClient).to receive(:index_delete).and_raise(
367
+ Elasticsearch::Transport::Transport::Errors::BadRequest.new(recoverable_message)
368
+ )
369
+ build_some_docs(subject)
370
+ expect {
371
+ subject.remap!(retry_delete_on_recoverable_errors: true, retry_delay: 0.5, max_delay: 1)
372
+ }.to raise_error(Elasticsearch::Transport::Transport::ServerError)
373
+ end
374
+
375
+ it "will not only retry if the arguments say not to" do
376
+ original_index_name = subject.config.client.index_get_alias(index: "#{subject.ref_index_name}-*", name: subject.ref_index_name).keys.first
377
+ allow_any_instance_of(Elasticity::InstrumentedClient).to receive(:index_delete).with(any_args).and_call_original
378
+ allow_any_instance_of(Elasticity::InstrumentedClient).to receive(:index_delete).with(index: original_index_name).and_raise(
379
+ Elasticsearch::Transport::Transport::Errors::BadRequest.new(recoverable_message)
380
+ )
381
+ build_some_docs(subject)
382
+ expect {
383
+ subject.remap!(retry_delete_on_recoverable_errors: false)
384
+ }.to raise_error(Elasticsearch::Transport::Transport::ServerError)
385
+ end
386
+
387
+ it "will not retry for 'non-recoverable' errors" do
388
+ exception_message = '[404] {"error":"alias [some_index_name] missing","status":404}'
389
+ allow_any_instance_of(Elasticity::InstrumentedClient).to receive(:index_delete).and_raise(
390
+ Elasticsearch::Transport::Transport::Errors::BadRequest.new(exception_message)
391
+ )
392
+ build_some_docs(subject)
393
+ expect {
394
+ subject.remap!(retry_delete_on_recoverable_errors: true, retry_delay: 0.5, max_delay: 1)
395
+ }.to raise_error(Elasticsearch::Transport::Transport::ServerError)
396
+ end
397
+ end
398
+
316
399
  it "bulk indexes, updates and delete" do
317
400
  docs = 2000.times.map do |i|
318
401
  subject.new(_id: i, id: i, name: "User #{i}", birthdate: random_birthdate).tap(&:update)
@@ -342,4 +425,15 @@ RSpec.describe "Persistence", elasticsearch: true do
342
425
  expect(results.total).to eq 0
343
426
  end
344
427
  end
428
+
429
+ def all_aliases(subj)
430
+ base_name = subj.ref_index_name
431
+ subj.config.client.index_get_alias(index: "#{base_name}-*", name: "#{base_name}*").values.first["aliases"].keys
432
+ end
433
+
434
+ def build_some_docs(subj, doc_count = 20)
435
+ doc_count.times.map do |i|
436
+ subj.new(id: i, name: "User #{i}", birthdate: random_birthdate).tap(&:update)
437
+ end
438
+ end
345
439
  end
@@ -0,0 +1,117 @@
1
+ RSpec.describe "Search", elasticsearch: true do
2
+ class CatDoc < Elasticity::Document
3
+ configure do |c|
4
+ c.strategy = Elasticity::Strategies::SingleIndex
5
+ c.document_type = "cat"
6
+ c.mapping = { "properties" => {
7
+ name: { type: "text" },
8
+ description: { type: "text" },
9
+ age: { type: "integer" }
10
+ } }
11
+ end
12
+
13
+ attr_accessor :name, :age, :description
14
+
15
+ def to_document
16
+ { name: name, age: age, description: description }
17
+ end
18
+ end
19
+
20
+ class DogDoc < Elasticity::Document
21
+ configure do |c|
22
+ c.strategy = Elasticity::Strategies::SingleIndex
23
+ c.document_type = "dog"
24
+ c.mapping = { "properties" => {
25
+ name: { type: "keyword" },
26
+ description: { type: "text" },
27
+ age: { type: "integer" },
28
+ hungry: { type: "boolean" }
29
+ } }
30
+ end
31
+ attr_accessor :name, :age, :description, :hungry
32
+
33
+ def to_document
34
+ { name: name, age: age, description: description, hungry: hungry }
35
+ end
36
+ end
37
+
38
+ describe "search_args" do
39
+ before do
40
+ CatDoc.recreate_index
41
+ DogDoc.recreate_index
42
+
43
+ @elastic_search_client.cluster.health wait_for_status: 'yellow'
44
+
45
+ cat = CatDoc.new(name: "felix the cat", age: 10, description: "I am an old cat")
46
+ dog = DogDoc.new(name: "fido", age: 4, hungry: true, description: "I am a hungry dog")
47
+
48
+ cat.update
49
+ dog.update
50
+
51
+ CatDoc.flush_index
52
+ end
53
+
54
+ describe "explain: true" do
55
+ def get_explanations(results)
56
+ results.map(&:_explanation)
57
+ end
58
+
59
+ it "supports on single index search results" do
60
+ results = CatDoc.search({}, { explain: true }).search_results
61
+
62
+ expect(get_explanations(results)).to all( be_truthy )
63
+ end
64
+
65
+ it "supports for multisearch" do
66
+ cat = CatDoc.search({}, { explain: true })
67
+ dog = DogDoc.search({})
68
+
69
+ subject = Elasticity::MultiSearch.new do |m|
70
+ m.add(:cats, cat, documents: CatDoc)
71
+ m.add(:dogs, dog, documents: DogDoc)
72
+ end
73
+
74
+ expect(get_explanations(subject[:cats])).to all( be_truthy )
75
+ expect(get_explanations(subject[:dogs])).to all( be_nil )
76
+ end
77
+ end
78
+
79
+ describe "highlight" do
80
+ it "is nil when the highlight does not return" do
81
+ results = CatDoc.search({}).search_results
82
+
83
+ expect(results.first.highlighted_attrs).to be_nil
84
+ expect(results.first.highlighted).to be_nil
85
+ end
86
+
87
+ describe "when specifying highlight" do
88
+ let(:cat_search_result) {
89
+ highlight_search = {
90
+ query: {
91
+ multi_match: {
92
+ query: "cat",
93
+ fields: ["name^1000", "description"]
94
+ }
95
+ },
96
+ highlight: {
97
+ fields: {
98
+ "*": {}
99
+ }
100
+ }
101
+ }
102
+
103
+ CatDoc.search(highlight_search).search_results.first
104
+ }
105
+
106
+ it "highlighted_attrs returns the highlighted" do
107
+ expect(cat_search_result.highlighted_attrs).to eq(["name", "description"])
108
+ end
109
+
110
+ it "highlighted returns a new object with the name transformed" do
111
+ expect(cat_search_result.highlighted.name.first).to include("felix")
112
+ expect(cat_search_result.highlighted.description.first).to include("old")
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: es-elasticity
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.1
4
+ version: 0.11.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rodrigo Kochenburger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-05-30 00:00:00.000000000 Z
11
+ date: 2018-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -231,6 +231,7 @@ files:
231
231
  - lib/elasticity/strategies/single_index.rb
232
232
  - lib/elasticity/version.rb
233
233
  - spec/functional/persistence_spec.rb
234
+ - spec/functional/search_spec.rb
234
235
  - spec/functional/segmented_spec.rb
235
236
  - spec/rspec_config.rb
236
237
  - spec/units/document_spec.rb
@@ -260,12 +261,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
260
261
  version: '0'
261
262
  requirements: []
262
263
  rubyforge_project:
263
- rubygems_version: 2.6.14
264
+ rubygems_version: 2.6.13
264
265
  signing_key:
265
266
  specification_version: 4
266
267
  summary: ActiveModel-based library for working with Elasticsearch
267
268
  test_files:
268
269
  - spec/functional/persistence_spec.rb
270
+ - spec/functional/search_spec.rb
269
271
  - spec/functional/segmented_spec.rb
270
272
  - spec/rspec_config.rb
271
273
  - spec/units/document_spec.rb