es-elasticity 0.11.1 → 0.11.5

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.
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