elastomer-client 2.3.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +0 -4
  4. data/CHANGELOG.md +8 -0
  5. data/docker/docker-compose.cibuild.yml +8 -0
  6. data/docker/docker-compose.es24.yml +34 -0
  7. data/docker/docker-compose.es56.yml +37 -0
  8. data/docker/elasticsearch.yml +15 -0
  9. data/docs/index.md +2 -2
  10. data/docs/notifications.md +1 -1
  11. data/elastomer-client.gemspec +2 -0
  12. data/lib/elastomer/client.rb +86 -33
  13. data/lib/elastomer/client/app_delete_by_query.rb +158 -0
  14. data/lib/elastomer/client/delete_by_query.rb +8 -115
  15. data/lib/elastomer/client/docs.rb +63 -13
  16. data/lib/elastomer/client/errors.rb +10 -2
  17. data/lib/elastomer/client/index.rb +40 -12
  18. data/lib/elastomer/client/multi_percolate.rb +2 -2
  19. data/lib/elastomer/client/native_delete_by_query.rb +60 -0
  20. data/lib/elastomer/client/percolator.rb +6 -3
  21. data/lib/elastomer/client/scroller.rb +22 -7
  22. data/lib/elastomer/client/tasks.rb +188 -0
  23. data/lib/elastomer/client/warmer.rb +6 -0
  24. data/lib/elastomer/notifications.rb +1 -0
  25. data/lib/elastomer/version.rb +1 -1
  26. data/lib/elastomer/version_support.rb +177 -0
  27. data/script/cibuild +77 -6
  28. data/script/cibuild-elastomer-client +1 -0
  29. data/script/cibuild-elastomer-client-es24 +8 -0
  30. data/script/cibuild-elastomer-client-es56 +8 -0
  31. data/script/poll-for-es +20 -0
  32. data/test/client/{delete_by_query_test.rb → app_delete_by_query_test.rb} +7 -7
  33. data/test/client/bulk_test.rb +9 -13
  34. data/test/client/cluster_test.rb +2 -2
  35. data/test/client/docs_test.rb +133 -49
  36. data/test/client/errors_test.rb +21 -1
  37. data/test/client/es_5_x_warmer_test.rb +13 -0
  38. data/test/client/index_test.rb +104 -39
  39. data/test/client/multi_percolate_test.rb +13 -6
  40. data/test/client/multi_search_test.rb +5 -5
  41. data/test/client/native_delete_by_query_test.rb +123 -0
  42. data/test/client/nodes_test.rb +1 -1
  43. data/test/client/percolator_test.rb +10 -2
  44. data/test/client/repository_test.rb +1 -1
  45. data/test/client/scroller_test.rb +16 -6
  46. data/test/client/snapshot_test.rb +1 -1
  47. data/test/client/stubbed_client_test.rb +1 -1
  48. data/test/client/tasks_test.rb +139 -0
  49. data/test/client/template_test.rb +1 -1
  50. data/test/client/warmer_test.rb +8 -4
  51. data/test/client_test.rb +99 -0
  52. data/test/core_ext/time_test.rb +1 -1
  53. data/test/notifications_test.rb +4 -0
  54. data/test/test_helper.rb +129 -21
  55. data/test/version_support_test.rb +119 -0
  56. metadata +59 -5
@@ -23,10 +23,8 @@ module Elastomer
23
23
  Scroller.new(self, query, opts)
24
24
  end
25
25
 
26
- # DEPRECATED in ES 2.1.0 - use a Scroll query sorted by _doc: https://www.elastic.co/guide/en/elasticsearch/reference/2.3/search-request-search-type.html#scan
27
- #
28
26
  # Create a new Scroller instance for scrolling all results from a `query`
29
- # via "scan" semantics.
27
+ # via "scan" semantics by sorting by _doc.
30
28
  #
31
29
  # query - The query to scan as a Hash or a JSON encoded String
32
30
  # opts - Options Hash
@@ -45,8 +43,7 @@ module Elastomer
45
43
  #
46
44
  # Returns a new Scroller instance
47
45
  def scan( query, opts = {} )
48
- opts = opts.merge(:search_type => "scan")
49
- Scroller.new(self, query, opts)
46
+ Scroller.new(self, add_sort_by_doc(query), opts)
50
47
  end
51
48
 
52
49
  # Begin scrolling a query.
@@ -99,7 +96,7 @@ module Elastomer
99
96
  #
100
97
  # Returns the response body as a Hash.
101
98
  def continue_scroll( scroll_id, scroll = "5m" )
102
- response = get "/_search/scroll", :body => scroll_id, :scroll => scroll, :action => "search.scroll"
99
+ response = get "/_search/scroll", :body => {:scroll_id => scroll_id}, :scroll => scroll, :action => "search.scroll"
103
100
  response.body
104
101
  rescue RequestError => err
105
102
  if err.error && err.error["caused_by"]["type"] == "search_context_missing_exception"
@@ -120,7 +117,25 @@ module Elastomer
120
117
  response.body
121
118
  end
122
119
 
123
- DEFAULT_OPTS = {
120
+ # Internal: Add sort by doc to query.
121
+ #
122
+ # Raises an exception if the query contains a sort already.
123
+ # Returns the query as a hash
124
+ def add_sort_by_doc(query)
125
+ if query.nil?
126
+ query = {}
127
+ elsif query.is_a? String
128
+ query = MultiJson.load(query)
129
+ end
130
+
131
+ if query.has_key? :sort
132
+ raise ArgumentError, "Query cannot contain a sort (found sort '#{query[:sort]}' in query: #{query})"
133
+ end
134
+
135
+ query.merge(:sort => [:_doc])
136
+ end
137
+
138
+ DEFAULT_OPTS = {
124
139
  :index => nil,
125
140
  :type => nil,
126
141
  :scroll => "5m",
@@ -0,0 +1,188 @@
1
+ module Elastomer
2
+ class Client
3
+
4
+ # Returns a Tasks instance for querying the cluster bound to this client for
5
+ # metadata about internal tasks in flight, and to submit administrative
6
+ # requests (like cancellation) concerning those tasks.
7
+ #
8
+ # Returns a new Tasks object associated with this client
9
+ def tasks
10
+ Tasks.new(self)
11
+ end
12
+
13
+ class Tasks
14
+
15
+ # TODO - validate params from this whitelist
16
+ PARAMETERS = %i[
17
+ nodes
18
+ actions
19
+ parent_task_id
20
+ wait_for_completion
21
+ pretty
22
+ detailed
23
+ timeout
24
+ group_by
25
+ ].to_set.freeze
26
+
27
+ # Create a new Tasks for introspecting on internal cluster activity.
28
+ # More context: https://www.elastic.co/guide/en/elasticsearch/reference/5.6/tasks.html
29
+ #
30
+ # client - Elastomer::Client used for HTTP requests to the server
31
+ #
32
+ # Raises IncompatibleVersionException if caller attempts to access Tasks API on ES version < 5.0.0
33
+ def initialize(client)
34
+ @client = client
35
+ end
36
+
37
+ attr_reader :client
38
+
39
+ # Fetch results from the generic _tasks endpoint.
40
+ #
41
+ # params - Hash of request parameters, including:
42
+ #
43
+ # Examples
44
+ #
45
+ # tasks.get
46
+ # tasks.get :nodes => "DmteLdw1QmSgW3GZmjmoKA,DmteLdw1QmSgW3GZmjmoKB", :actions => "cluster:*", :detailed => true
47
+ #
48
+ # Examples (ES 5+ only)
49
+ #
50
+ # tasks.get :group_by => "parents"
51
+ # tasks.get :group_by => "parents", :actions => "*reindex", ...
52
+ #
53
+ # Returns the response body as a Hash
54
+ def get(params = {})
55
+ response = client.get "/_tasks", params
56
+ response.body
57
+ end
58
+
59
+ # Fetch results from the _tasks endpoint for a particular cluster node and task ID.
60
+ # NOTE: the API docs note the behavior wrong for this call; "task_id:<task_id>" is really "<node_id>:<task_id>"
61
+ # where "node_id" is a value from the "nodes" hash returned from the /_tasks endpoint, and "task_id" is
62
+ # from the "tasks" child hash of any of the "nodes" entries of the /_tasks endpoint
63
+ #
64
+ # node_id - the name of the ES cluster node hosting the target task
65
+ # task_id - the numerical ID of the task to return data about in the response
66
+ # params - Hash of request parameters to include
67
+ #
68
+ # Examples
69
+ #
70
+ # tasks.get_by_id "DmteLdw1QmSgW3GZmjmoKA", 123
71
+ # tasks.get_by_id "DmteLdw1QmSgW3GZmjmoKA", 456, :pretty => true
72
+ #
73
+ # Returns the response body as a Hash
74
+ def get_by_id(node_id, task_id, params = {})
75
+ raise ArgumentError, "invalid node ID provided: #{node_id.inspect}" if node_id.to_s.empty?
76
+ raise ArgumentError, "invalid task ID provided: #{task_id.inspect}" unless task_id.is_a?(Integer)
77
+
78
+ # in this API, the task ID is included in the path, not as a request parameter.
79
+ response = client.get "/_tasks/#{node_id}:#{task_id}", params
80
+ response.body
81
+ end
82
+
83
+ # Fetch task details for all child tasks of the specified parent task.
84
+ # NOTE: the API docs note the behavior wrong for this call: "parentTaskId:<task_id>"
85
+ # is not the correct syntax for the parent_task_id param value. The correct
86
+ # value syntax is "<parent_node_id>:<parent_task_id>"
87
+ #
88
+ # parent_node_id - ID of the node the parent task is hosted by
89
+ # parent_task_id - ID of a parent task who's child tasks' data will be returned in the response
90
+ # params - Hash of request parameters to include
91
+ #
92
+ # Examples
93
+ #
94
+ # tasks.get_by_parent_id "DmteLdw1QmSgW3GZmjmoKA", 123
95
+ # tasks.get_by_parent_id "DmteLdw1QmSgW3GZmjmoKB", 456, :detailed => true
96
+ #
97
+ # Returns the response body as a Hash
98
+ def get_by_parent_id(parent_node_id, parent_task_id, params = {})
99
+ raise ArgumentError, "invalid parent node ID provided: #{parent_node_id.inspect}" if node_id.to_s.empty?
100
+ raise ArgumentError, "invalid parent task ID provided: #{parent_task_id.inspect}" unless parent_task_id.is_a?(Integer)
101
+
102
+ # in this API, we pass the parent task ID as a formatted parameter in a request to the _tasks endpoint
103
+ formatted_parent = { :parent_task_id => "#{parent_node_id}:#{parent_task_id}" }
104
+ response = client.get "/_tasks", params.merge(formatted_parent)
105
+ response.body
106
+ end
107
+
108
+ # Wait for the specified amount of time (10 seconds by default) for some task(s) to complete.
109
+ # Filters for task(s) to wait upon using same filter params as Tasks#get(params)
110
+ #
111
+ # timeout - maximum time to wait for target task to complete before returning, example: "5s"
112
+ # params - Hash of request params to include (mostly task filters in this context)
113
+ #
114
+ # Examples
115
+ #
116
+ # tasks.wait_for "5s", :actions => "*health"
117
+ # tasks.wait_for("30s", :actions => "*reindex", :nodes => "DmteLdw1QmSgW3GZmjmoKA,DmteLdw1QmSgW3GZmjmoKB")
118
+ #
119
+ # Returns the response body as a Hash when timeout expires or target tasks complete
120
+ # COMPATIBILITY WARNING: the response body differs between ES versions for this API
121
+ def wait_for(timeout = "10s", params = {})
122
+ params_with_wait = params.merge({ :wait_for_completion => true, :timeout => timeout })
123
+ self.get(params_with_wait)
124
+ end
125
+
126
+ # Wait for the specified amount of time (10 seconds by default) for some task(s) to complete.
127
+ # Filters for task(s) to wait upon using same IDs and filter params as Tasks#get_by_id(params)
128
+ #
129
+ # node_id - the ID of the node on which the target task is hosted
130
+ # task_id - the ID of the task to wait on
131
+ # timeout - time for call to await target tasks completion before returning
132
+ # params - Hash of request params to include (mostly task filters in this context)
133
+ #
134
+ # Examples
135
+ #
136
+ # tasks.wait_by_id "DmteLdw1QmSgW3GZmjmoKA", 123, "15s"
137
+ # tasks.wait_by_id "DmteLdw1QmSgW3GZmjmoKA", 456, "30s", :actions => "*search"
138
+ #
139
+ # Returns the response body as a Hash when timeout expires or target tasks complete
140
+ def wait_by_id(node_id, task_id, timeout = "10s", params = {})
141
+ raise ArgumentError, "invalid node ID provided: #{node_id.inspect}" if node_id.to_s.empty?
142
+ raise ArgumentError, "invalid task ID provided: #{task_id.inspect}" unless task_id.is_a?(Integer)
143
+
144
+ params_with_wait = params.merge({ :wait_for_completion => true, :timeout => timeout })
145
+ self.get_by_id(node_id, task_id, params_with_wait)
146
+ end
147
+
148
+ # Cancels a task running on a particular node.
149
+ # NOTE: the API docs note the behavior wrong for this call; "task_id:<task_id>" is really "<node_id>:<task_id>"
150
+ # where "node_id" is a value from the "nodes" hash returned from the /_tasks endpoint, and "task_id" is
151
+ # from the "tasks" child hash of any of the "nodes" entries of the /_tasks endpoint
152
+ #
153
+ # node_id - the ES node hosting the task to be cancelled
154
+ # task_id - ID of the task to be cancelled
155
+ # params - Hash of request parameters to include
156
+ #
157
+ # Examples
158
+ #
159
+ # tasks.cancel_by_id "DmteLdw1QmSgW3GZmjmoKA", 123
160
+ # tasks.cancel_by_id "DmteLdw1QmSgW3GZmjmoKA", 456, :pretty => true
161
+ #
162
+ # Returns the response body as a Hash
163
+ def cancel_by_id(node_id, task_id, params = {})
164
+ raise ArgumentError, "invalid node ID provided: #{node_id.inspect}" if node_id.to_s.empty?
165
+ raise ArgumentError, "invalid task ID provided: #{task_id.inspect}" unless task_id.is_a?(Integer)
166
+
167
+ response = client.post "/_tasks/#{node_id}:#{task_id}/_cancel", params
168
+ response.body
169
+ end
170
+
171
+ # Cancels a task or group of tasks using various filtering parameters.
172
+ #
173
+ # params - Hash of request parameters to include
174
+ #
175
+ # Examples
176
+ #
177
+ # tasks.cancel :actions => "*reindex"
178
+ # tasks.cancel :actions => "*search", :nodes => "DmteLdw1QmSgW3GZmjmoKA,DmteLdw1QmSgW3GZmjmoKB,DmteLdw1QmSgW3GZmjmoKC"
179
+ #
180
+ # Returns the response body as a Hash
181
+ def cancel(params = {})
182
+ response = client.post "/_tasks/_cancel", params
183
+ response.body
184
+ end
185
+
186
+ end # end class Tasks
187
+ end # end class Client
188
+ end # end module Elastomer
@@ -1,6 +1,8 @@
1
1
  module Elastomer
2
2
  class Client
3
3
 
4
+ # DEPRECATED: Warmers have been removed from Elasticsearch as of 5.0.
5
+ # See https://www.elastic.co/guide/en/elasticsearch/reference/5.0/indices-warmers.html
4
6
  class Warmer
5
7
 
6
8
  # Create a new Warmer helper for making warmer API requests.
@@ -9,6 +11,10 @@ module Elastomer
9
11
  # index_name - The name of the index as a String
10
12
  # name - The name of the warmer as a String
11
13
  def initialize(client, index_name, name)
14
+ unless client.version_support.supports_warmers?
15
+ raise IncompatibleVersionException, "ES #{client.version} does not support warmers"
16
+ end
17
+
12
18
  @client = client
13
19
  @index_name = @client.assert_param_presence(index_name, "index name")
14
20
  @name = @client.assert_param_presence(name, "warmer name")
@@ -69,6 +69,7 @@ module Elastomer
69
69
  payload[:method] = response.env[:method]
70
70
  payload[:status] = response.status
71
71
  payload[:response_body] = response.body
72
+ payload[:retries] = params[:retries]
72
73
  response
73
74
  end
74
75
  end
@@ -1,5 +1,5 @@
1
1
  module Elastomer
2
- VERSION = "2.3.0"
2
+ VERSION = "3.0.0"
3
3
 
4
4
  def self.version
5
5
  VERSION
@@ -0,0 +1,177 @@
1
+ module Elastomer
2
+ # VersionSupport holds methods that (a) encapsulate version differences; or
3
+ # (b) give an intention-revealing name to a conditional check.
4
+ class VersionSupport
5
+ COMMON_INDEXING_PARAMETER_NAMES = %i[
6
+ index
7
+ type
8
+ id
9
+ version
10
+ version_type
11
+ op_type
12
+ routing
13
+ parent
14
+ refresh
15
+ ].freeze
16
+ ES_2_X_INDEXING_PARAMETER_NAMES = %i[consistency ttl timestamp].freeze
17
+ ES_5_X_INDEXING_PARAMETER_NAMES = %i[wait_for_active_shards].freeze
18
+ KNOWN_INDEXING_PARAMETER_NAMES =
19
+ (COMMON_INDEXING_PARAMETER_NAMES + ES_2_X_INDEXING_PARAMETER_NAMES + ES_5_X_INDEXING_PARAMETER_NAMES).freeze
20
+
21
+ attr_reader :version
22
+
23
+ # version - an Elasticsearch version string e.g., 2.3.5 or 5.3.0
24
+ #
25
+ # Raises ArgumentError if version is unsupported.
26
+ def initialize(version)
27
+ if version < "2.3" || version >= "5.7"
28
+ raise ArgumentError, "Elasticsearch version #{version} is not supported by elastomer-client"
29
+ end
30
+
31
+ @version = version
32
+ end
33
+
34
+ # COMPATIBILITY: Return a boolean indicating if this version supports warmers.
35
+ # Warmers were removed in ES 5.0.
36
+ def supports_warmers?
37
+ es_version_2_x?
38
+ end
39
+
40
+ # COMPATIBILITY: The Tasks API is evolving quickly; features, and request/response
41
+ # structure can differ across ES versions
42
+ def tasks_new_response_format?
43
+ es_version_5_x?
44
+ end
45
+
46
+ # COMPATIBILITY: Return a "text"-type mapping for a field.
47
+ #
48
+ # On ES 2.x, this will be a string field. On ES 5+, it will be a text field.
49
+ def text(**args)
50
+ reject_args!(args, :type, :index)
51
+
52
+ if es_version_2_x?
53
+ {type: "string"}.merge(args)
54
+ else
55
+ {type: "text"}.merge(args)
56
+ end
57
+ end
58
+
59
+ # COMPATIBILITY: Return a "keyword"-type mapping for a field.
60
+ #
61
+ # On ES 2.x, this will be a string field with not_analyzed=true. On ES 5+,
62
+ # it will be a keyword field.
63
+ def keyword(**args)
64
+ reject_args!(args, :type, :index)
65
+
66
+ if es_version_2_x?
67
+ {type: "string", index: "not_analyzed"}.merge(args)
68
+ else
69
+ {type: "keyword"}.merge(args)
70
+ end
71
+ end
72
+
73
+ # Elasticsearch 2.0 changed some request formats in a non-backward-compatible
74
+ # way. Some tests need to know what version is running to structure requests
75
+ # as expected.
76
+ #
77
+ # Returns true if Elasticsearch version is 2.x.
78
+ def es_version_2_x?
79
+ version >= "2.0.0" && version < "3.0.0"
80
+ end
81
+
82
+ # Elasticsearch 5.0 changed some request formats in a non-backward-compatible
83
+ # way. Some tests need to know what version is running to structure requests
84
+ # as expected.
85
+ #
86
+ # Returns true if Elasticsearch version is 5.x.
87
+ def es_version_5_x?
88
+ version >= "5.0.0" && version < "6.0.0"
89
+ end
90
+
91
+ # Wraps version check and param gen where ES version >= 5.x requires
92
+ # percolator type + field defined in mappings
93
+ def percolator_type
94
+ if es_version_5_x?
95
+ "percolator"
96
+ else
97
+ ".percolator"
98
+ end
99
+ end
100
+
101
+ # COMPATIBILITY
102
+ # ES 2.x reports query parsing exceptions as query_parse_exception whereas
103
+ # ES 5.x reports them as query_shard_exception or parsing_exception
104
+ # depending on when the error occurs.
105
+ #
106
+ # Returns an Array of Strings to match.
107
+ def query_parse_exception
108
+ if es_version_2_x?
109
+ ["query_parsing_exception"]
110
+ else
111
+ ["query_shard_exception", "parsing_exception"]
112
+ end
113
+ end
114
+
115
+ # COMPATIBILITY
116
+ # ES 5.X supports `delete_by_query` natively again.
117
+ alias :native_delete_by_query? :es_version_5_x?
118
+
119
+ # COMPATIBILITY
120
+ # Return a Hash of indexing request parameters that are valid for this
121
+ # version of Elasticsearch.
122
+ def indexing_directives
123
+ return @indexing_directives if defined?(@indexing_directives)
124
+
125
+ @indexing_directives = indexing_parameter_names.each_with_object({}) do |key, h|
126
+ h[key] = "_#{key}"
127
+ end
128
+ @indexing_directives.freeze
129
+ end
130
+
131
+ # COMPATIBILITY
132
+ # Return a Hash of indexing request parameters that are known to
133
+ # elastomer-client, but not supported by the current version of
134
+ # Elasticsearch.
135
+ def unsupported_indexing_directives
136
+ return @unsupported_indexing_directives if defined?(@unsupported_indexing_directives)
137
+
138
+ unsupported_keys = KNOWN_INDEXING_PARAMETER_NAMES - indexing_parameter_names
139
+
140
+ @unsupported_indexing_directives = unsupported_keys.each_with_object({}) do |key, h|
141
+ h[key] = "_#{key}"
142
+ end
143
+ @unsupported_indexing_directives.freeze
144
+ end
145
+
146
+ # COMPATIBILITY
147
+ # Return a symbol representing the best supported delete_by_query
148
+ # implementation for this version of Elasticsearch.
149
+ def delete_by_query_method
150
+ if es_version_2_x?
151
+ :app_delete_by_query
152
+ else
153
+ :native_delete_by_query
154
+ end
155
+ end
156
+
157
+ private
158
+
159
+ # Internal: Helper to reject arguments that shouldn't be passed because
160
+ # merging them in would defeat the purpose of a compatibility layer.
161
+ def reject_args!(args, *names)
162
+ names.each do |name|
163
+ if args.include?(name.to_s) || args.include?(name.to_sym)
164
+ raise ArgumentError, "Argument '#{name}' is not allowed"
165
+ end
166
+ end
167
+ end
168
+
169
+ def indexing_parameter_names
170
+ if es_version_2_x?
171
+ COMMON_INDEXING_PARAMETER_NAMES + ES_2_X_INDEXING_PARAMETER_NAMES
172
+ else
173
+ COMMON_INDEXING_PARAMETER_NAMES + ES_5_X_INDEXING_PARAMETER_NAMES
174
+ end
175
+ end
176
+ end
177
+ end