elastomer-client 2.3.0 → 3.0.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 (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