elastomer-client 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +4 -0
  5. data/Gemfile +5 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +108 -0
  8. data/Rakefile +9 -0
  9. data/docs/notifications.md +71 -0
  10. data/elastomer-client.gemspec +30 -0
  11. data/lib/elastomer/client.rb +307 -0
  12. data/lib/elastomer/client/bulk.rb +257 -0
  13. data/lib/elastomer/client/cluster.rb +208 -0
  14. data/lib/elastomer/client/docs.rb +432 -0
  15. data/lib/elastomer/client/errors.rb +51 -0
  16. data/lib/elastomer/client/index.rb +407 -0
  17. data/lib/elastomer/client/multi_search.rb +115 -0
  18. data/lib/elastomer/client/nodes.rb +87 -0
  19. data/lib/elastomer/client/scan.rb +161 -0
  20. data/lib/elastomer/client/template.rb +85 -0
  21. data/lib/elastomer/client/warmer.rb +96 -0
  22. data/lib/elastomer/core_ext/time.rb +7 -0
  23. data/lib/elastomer/middleware/encode_json.rb +51 -0
  24. data/lib/elastomer/middleware/opaque_id.rb +69 -0
  25. data/lib/elastomer/middleware/parse_json.rb +39 -0
  26. data/lib/elastomer/notifications.rb +83 -0
  27. data/lib/elastomer/version.rb +7 -0
  28. data/script/bootstrap +16 -0
  29. data/script/cibuild +28 -0
  30. data/script/console +9 -0
  31. data/script/testsuite +10 -0
  32. data/test/assertions.rb +74 -0
  33. data/test/client/bulk_test.rb +226 -0
  34. data/test/client/cluster_test.rb +113 -0
  35. data/test/client/docs_test.rb +394 -0
  36. data/test/client/index_test.rb +244 -0
  37. data/test/client/multi_search_test.rb +129 -0
  38. data/test/client/nodes_test.rb +35 -0
  39. data/test/client/scan_test.rb +84 -0
  40. data/test/client/stubbed_client_tests.rb +40 -0
  41. data/test/client/template_test.rb +33 -0
  42. data/test/client/warmer_test.rb +56 -0
  43. data/test/client_test.rb +86 -0
  44. data/test/core_ext/time_test.rb +46 -0
  45. data/test/middleware/encode_json_test.rb +53 -0
  46. data/test/middleware/opaque_id_test.rb +39 -0
  47. data/test/middleware/parse_json_test.rb +54 -0
  48. data/test/test_helper.rb +94 -0
  49. metadata +210 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6cd17dd15348e159466cead916479b5bcf68b80b
4
+ data.tar.gz: b2f0b0e2455ad13ed6a3a1670cf20a58e6239501
5
+ SHA512:
6
+ metadata.gz: c9c2a2798145fa90c3291459de33dcdf01c1ad806b64e19b719fb6f5679c3b4b165583cb3ee4101a1f3e17879d15da93c350e88703256c1fab9755df80eda187
7
+ data.tar.gz: ce53c9838c420f6932d7792a088bdcd6ea7d3735e12f3df51d52459cc849b934cc91ff0703317574ad01ab75d0ca91ad0ef839d55cf7988a7417b499e52fcaed
@@ -0,0 +1,8 @@
1
+ /bin
2
+ /vendor/gems
3
+ /.bundle
4
+ /.rbenv-version
5
+ /vendor/cache/*.gem
6
+ /coverage
7
+ Gemfile.lock
8
+ *.gem
@@ -0,0 +1 @@
1
+ 2.1.0-github
@@ -0,0 +1,4 @@
1
+ ## 0.3.1 (2014-06-24)
2
+ - First rubygems release
3
+ - Make `update_aliases` more flexible
4
+ - Add `Client#semantic_version` for ES version comparisons
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ gem 'simplecov', :require => false, :group => :development, :platform => :ruby_19
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 GitHub Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,108 @@
1
+ # Elastomer
2
+
3
+ Making a stupid simple ElasticSearch client so your project can be smarter!
4
+
5
+ ## Getting Started
6
+
7
+ Use [Boxen](https://setup.githubapp.com) to get your machine set up. Then:
8
+
9
+ ```
10
+ $ git clone https://github.com/github/elastomer-client.git
11
+ $ cd elastomer-client
12
+ $ script/bootstrap
13
+ $ script/testsuite
14
+ ```
15
+
16
+ ## Client
17
+
18
+ The client provides a one-to-one mapping to the ElasticSearch [API
19
+ endpoints](http://www.elasticsearch.org/guide/reference/api/). The API is
20
+ decomposed into logical sections and accessed according to what you are trying
21
+ to accomplish. Each logical section is represented as a [client
22
+ class](lib/elastomer/client) and a top-level accessor is provided for each.
23
+
24
+ #### Cluster
25
+
26
+ API endpoints dealing with cluster level information and settings are found in
27
+ the [Cluster](lib/elastomer/client/cluster.rb) class.
28
+
29
+ ```ruby
30
+ require 'elastomer/client'
31
+ client = Elastomer::Client.new
32
+
33
+ # the current health summary
34
+ client.cluster.health
35
+
36
+ # detailed cluster state information
37
+ client.cluster.state
38
+
39
+ # the list of all index templates
40
+ client.cluster.templates
41
+ ```
42
+
43
+ #### Index
44
+
45
+ The methods in the [Index](lib/elastomer/client/index.rb) class deal with the
46
+ management of indexes in the cluster. This includes setting up type mappings
47
+ and adjusting settings. The actual indexing and search of documents are
48
+ handled by the Docs class (discussed next).
49
+
50
+ ```ruby
51
+ require 'elastomer/client'
52
+ client = Elastomer::Client.new
53
+
54
+ index = client.index('twitter')
55
+ index.create(
56
+ :settings => { 'index.number_of_shards' => 3 },
57
+ :mappings => {
58
+ :tweet => {
59
+ :_source => { :enabled => true },
60
+ :_all => { :enabled => false },
61
+ :properties => {
62
+ :author => { :type => 'string', :index => 'not_analyzed' },
63
+ :tweet => { :type => 'string', :analyze => 'standard' }
64
+ }
65
+ }
66
+ }
67
+ )
68
+
69
+ index.exists?
70
+
71
+ index.exists? :type => 'tweet'
72
+
73
+ index.delete
74
+ ```
75
+
76
+ #### Docs
77
+
78
+ This decomposition is the most questionable, but it's a starting point. The
79
+ [Docs](lib/elastomer/client/docs.rb) class handles the indexing and searching
80
+ of documents. Each instance is scoped to an index and optionally a document
81
+ type.
82
+
83
+ ```ruby
84
+ require 'elastomer/client'
85
+ client = Elastomer::Client.new
86
+
87
+ docs = client.docs('twitter')
88
+
89
+ docs.index({
90
+ :_id => 1,
91
+ :_type => 'tweet',
92
+ :author => '@pea53',
93
+ :tweet => 'announcing Elastomer, the stupid simple ElasticSearch client'
94
+ })
95
+
96
+ docs.search({:query => {:match_all => {}}}, :search_type => 'count')
97
+ ```
98
+
99
+ #### Performance
100
+
101
+ By default Elastomer uses Net::HTTP (via Faraday) to communicate with
102
+ ElasticSearch. You may find that Excon performs better for your use. To enable
103
+ Excon, add it to your bundle and then change your Elastomer initialization
104
+ thusly:
105
+
106
+ ```
107
+ Elastomer::Client.new(url: YOUR_ES_URL, adapter: :excon)
108
+ ```
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.pattern = "test/**/*_test.rb"
7
+ end
8
+
9
+ task :default => :test
@@ -0,0 +1,71 @@
1
+ # Notifications Support
2
+
3
+ Requiring `elastomer/notifications` enables support for broadcasting
4
+ elastomer events through ActiveSupport::Notifications.
5
+
6
+ The event namespace is `request.client.elastomer`.
7
+
8
+ ## Sample event payload
9
+
10
+ ```
11
+ :index => "index-test",
12
+ :type => nil,
13
+ :action => "docs.search",
14
+ :context=> nil,
15
+ :body => "{\"query\":{\"match_all\":{}}}",
16
+ :url => #<URI::HTTP:0x007fb6f3e98b60 URL:http://localhost:19200/index-test/_search?search_type=count>,
17
+ :method => :get,
18
+ :status => 200}
19
+ ```
20
+
21
+ ## Valid actions
22
+ - bulk
23
+ - cluster.get_settings
24
+ - cluster.health
25
+ - cluster.reroute
26
+ - cluster.state
27
+ - cluster.update_settings
28
+ - cluster.available
29
+ - cluster.get_aliases
30
+ - cluster.info
31
+ - cluster.shutdown
32
+ - cluster.update_aliases
33
+ - docs.delete
34
+ - docs.delete_by_query
35
+ - docs.explain
36
+ - docs.get
37
+ - docs.index
38
+ - docs.more_like_this
39
+ - docs.multi_get
40
+ - docs.search
41
+ - docs.source
42
+ - docs.update
43
+ - docs.validate
44
+ - index.analyze
45
+ - index.clear_cache
46
+ - index.close
47
+ - index.create
48
+ - index.delete
49
+ - index.delete_mapping
50
+ - index.exists
51
+ - index.flush
52
+ - index.get_aliases
53
+ - index.get_settings
54
+ - index.mapping
55
+ - index.open
56
+ - index.optimize
57
+ - index.refresh
58
+ - index.segments
59
+ - index.snapshot
60
+ - index.stats
61
+ - index.status
62
+ - index.update_mapping
63
+ - index.update_settings
64
+ - nodes.hot_threads
65
+ - nodes.info
66
+ - nodes.shutdown
67
+ - nodes.stats
68
+ - search.scan
69
+ - search.scroll
70
+ - template.create
71
+ - template.get
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'elastomer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "elastomer-client"
8
+ spec.version = Elastomer::VERSION
9
+ spec.authors = ["Tim Pease", "Grant Rodgers"]
10
+ spec.email = ["tim.pease@github.com", "grant.rodgers@github.com"]
11
+ spec.summary = %q{A library for interacting with the GitHub Search infrastructure}
12
+ spec.description = %q{Elastomer is a low level API client for the
13
+ Elasticsearch HTTP interface.}
14
+ spec.homepage = "https://github.com/github/elastomer-client"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_dependency "addressable", "~> 2.3"
23
+ spec.add_dependency "faraday", "~> 0.8"
24
+ spec.add_dependency "multi_json", "~> 1.7"
25
+ spec.add_dependency "semantic", "~> 1.3"
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.5"
28
+ spec.add_development_dependency "minitest","~> 4.7"
29
+ spec.add_development_dependency "rake"
30
+ end
@@ -0,0 +1,307 @@
1
+ require 'addressable/template'
2
+ require 'faraday'
3
+ require 'multi_json'
4
+ require 'semantic'
5
+
6
+ require 'elastomer/version'
7
+
8
+ module Elastomer
9
+
10
+ class Client
11
+
12
+ # Create a new client that can be used to make HTTP requests to the
13
+ # ElasticSearch server.
14
+ #
15
+ # opts - The options Hash
16
+ # :host - the host as a String
17
+ # :port - the port number of the server
18
+ # :url - the URL as a String (overrides :host and :port)
19
+ # :read_timeout - the timeout in seconds when reading from an HTTP connection
20
+ # :open_timeout - the timeout in seconds when opening an HTTP connection
21
+ # :adapter - the Faraday adapter to use (defaults to :excon)
22
+ # :opaque_id - set to `true` to use the 'X-Opaque-Id' request header
23
+ #
24
+ def initialize( opts = {} )
25
+ host = opts.fetch :host, 'localhost'
26
+ port = opts.fetch :port, 9200
27
+ @url = opts.fetch :url, "http://#{host}:#{port}"
28
+
29
+ uri = Addressable::URI.parse @url
30
+ @host = uri.host
31
+ @port = uri.port
32
+
33
+ @read_timeout = opts.fetch :read_timeout, 5
34
+ @open_timeout = opts.fetch :open_timeout, 2
35
+ @adapter = opts.fetch :adapter, Faraday.default_adapter
36
+ @opaque_id = opts.fetch :opaque_id, false
37
+ end
38
+
39
+ attr_reader :host, :port, :url
40
+ attr_reader :read_timeout, :open_timeout
41
+
42
+ # Returns true if the server is available; returns false otherwise.
43
+ def available?
44
+ response = head '/', :action => 'cluster.available'
45
+ response.success?
46
+ rescue StandardError
47
+ false
48
+ end
49
+
50
+ # Returns the version String of the attached ElasticSearch instance.
51
+ def version
52
+ @version ||= info['version']['number']
53
+ end
54
+
55
+ # Returns a Semantic::Version for the attached ElasticSearch instance.
56
+ # See https://rubygems.org/gems/semantic
57
+ def semantic_version
58
+ Semantic::Version.new(version)
59
+ end
60
+
61
+ # Returns the information Hash from the attached ElasticSearch instance.
62
+ def info
63
+ response = get '/', :action => 'cluster.info'
64
+ response.body
65
+ end
66
+
67
+ # Internal: Provides access to the Faraday::Connection used by this client
68
+ # for all requests to the server.
69
+ #
70
+ # Returns a Faraday::Connection
71
+ def connection
72
+ @connection ||= Faraday.new(url) do |conn|
73
+ conn.request :encode_json
74
+ conn.response :parse_json
75
+ conn.request :opaque_id if @opaque_id
76
+
77
+ Array === @adapter ?
78
+ conn.adapter(*@adapter) :
79
+ conn.adapter(@adapter)
80
+
81
+ conn.options[:timeout] = read_timeout
82
+ conn.options[:open_timeout] = open_timeout
83
+ end
84
+ end
85
+
86
+ # Internal: Sends an HTTP HEAD request to the server.
87
+ #
88
+ # path - The path as a String
89
+ # params - Parameters Hash
90
+ #
91
+ # Returns a Faraday::Response
92
+ def head( path, params = {} )
93
+ request :head, path, params
94
+ end
95
+
96
+ # Internal: Sends an HTTP GET request to the server.
97
+ #
98
+ # path - The path as a String
99
+ # params - Parameters Hash
100
+ #
101
+ # Returns a Faraday::Response
102
+ # Raises an Elastomer::Client::Error on 4XX and 5XX responses
103
+ def get( path, params = {} )
104
+ request :get, path, params
105
+ end
106
+
107
+ # Internal: Sends an HTTP PUT request to the server.
108
+ #
109
+ # path - The path as a String
110
+ # params - Parameters Hash
111
+ #
112
+ # Returns a Faraday::Response
113
+ # Raises an Elastomer::Client::Error on 4XX and 5XX responses
114
+ def put( path, params = {} )
115
+ request :put, path, params
116
+ end
117
+
118
+ # Internal: Sends an HTTP POST request to the server.
119
+ #
120
+ # path - The path as a String
121
+ # params - Parameters Hash
122
+ #
123
+ # Returns a Faraday::Response
124
+ # Raises an Elastomer::Client::Error on 4XX and 5XX responses
125
+ def post( path, params = {} )
126
+ request :post, path, params
127
+ end
128
+
129
+ # Internal: Sends an HTTP DELETE request to the server.
130
+ #
131
+ # path - The path as a String
132
+ # params - Parameters Hash
133
+ #
134
+ # Returns a Faraday::Response
135
+ # Raises an Elastomer::Client::Error on 4XX and 5XX responses
136
+ def delete( path, params = {} )
137
+ request :delete, path, params
138
+ end
139
+
140
+ # Internal: Sends an HTTP request to the server. If the `params` Hash
141
+ # contains a :body key, it will be deleted from the Hash and the value
142
+ # will be used as the body of the request.
143
+ #
144
+ # method - The HTTP method to send [:head, :get, :put, :post, :delete]
145
+ # path - The path as a String
146
+ # params - Parameters Hash
147
+ # :body - Will be used as the request body
148
+ # :read_timeout - Optional read timeout (in seconds) for the request
149
+ #
150
+ # Returns a Faraday::Response
151
+ # Raises an Elastomer::Client::Error on 4XX and 5XX responses
152
+ def request( method, path, params )
153
+ body = params.delete :body
154
+ body = MultiJson.dump body if Hash === body
155
+
156
+ read_timeout = params.delete :read_timeout
157
+
158
+ path = expand_path path, params
159
+
160
+ response = instrument(path, body, params) do
161
+ case method
162
+ when :head
163
+ connection.head(path) { |req| req.options[:timeout] = read_timeout if read_timeout }
164
+
165
+ when :get
166
+ connection.get(path) { |req|
167
+ req.body = body if body
168
+ req.options[:timeout] = read_timeout if read_timeout
169
+ }
170
+
171
+ when :put
172
+ connection.put(path, body) { |req| req.options[:timeout] = read_timeout if read_timeout }
173
+
174
+ when :post
175
+ connection.post(path, body) { |req| req.options[:timeout] = read_timeout if read_timeout }
176
+
177
+ when :delete
178
+ connection.delete(path) { |req|
179
+ req.body = body if body
180
+ req.options[:timeout] = read_timeout if read_timeout
181
+ }
182
+
183
+ else
184
+ raise ArgumentError, "unknown HTTP request method: #{method.inspect}"
185
+ end
186
+ end
187
+
188
+ handle_errors response
189
+
190
+ rescue Faraday::Error::TimeoutError => boom
191
+ raise ::Elastomer::Client::TimeoutError.new(boom, path)
192
+
193
+ # ensure
194
+ # # FIXME: this is here until we get a real logger in place
195
+ # STDERR.puts "[#{response.status.inspect}] curl -X#{method.to_s.upcase} '#{url}#{path}'" unless response.nil?
196
+ end
197
+
198
+ # Internal: Apply path expansions to the `path` and append query
199
+ # parameters to the `path`. We are using an Addressable::Template to
200
+ # replace '{expansion}' fields found in the path with the values extracted
201
+ # from the `params` Hash. Any remaining elements in the `params` hash are
202
+ # treated as query parameters and appended to the end of the path.
203
+ #
204
+ # path - The path as a String
205
+ # params - Parameters Hash
206
+ #
207
+ # Examples
208
+ #
209
+ # expand_path('/foo{/bar}', {:bar => 'hello', :q => 'what', :p => 2})
210
+ # #=> '/foo/hello?q=what&p=2'
211
+ #
212
+ # expand_path('/foo{/bar}{/baz}', {:baz => 'no bar'}
213
+ # #=> '/foo/no%20bar'
214
+ #
215
+ # Returns an Addressable::Uri
216
+ def expand_path( path, params )
217
+ template = Addressable::Template.new path
218
+
219
+ expansions = {}
220
+ query_values = params.dup
221
+ query_values.delete :action
222
+ query_values.delete :context
223
+
224
+ template.keys.map(&:to_sym).each do |key|
225
+ value = query_values.delete key
226
+ value = assert_param_presence(value, key) unless path =~ /{\/#{key}}/ && value.nil?
227
+ expansions[key] = value
228
+ end
229
+
230
+ uri = template.expand(expansions)
231
+ uri.query_values = query_values unless query_values.empty?
232
+ uri.to_s
233
+ end
234
+
235
+ # Internal: A noop method that simply yields to the block. This method
236
+ # will be replaced when the 'elastomer/notifications' module is included.
237
+ #
238
+ # path - The full request path as a String
239
+ # body - The request body as a String or `nil`
240
+ # params - The request params Hash
241
+ # block - The block that will be instrumented
242
+ #
243
+ # Returns the response from the block
244
+ def instrument( path, body, params )
245
+ yield
246
+ end
247
+
248
+ # Internal: Inspect the Faraday::Response and raise an error if the status
249
+ # is in the 5XX range or if the response body contains an 'error' field.
250
+ # In the latter case, the value of the 'error' field becomes our exception
251
+ # message. In the absence of an 'error' field the response body is used
252
+ # as the exception message.
253
+ #
254
+ # The raised exception will contain the response object.
255
+ #
256
+ # response - The Faraday::Response object.
257
+ #
258
+ # Returns the response.
259
+ # Raises an Elastomer::Client::Error on 500 responses or responses
260
+ # containing and 'error' field.
261
+ def handle_errors( response )
262
+ raise Error, response if response.status >= 500
263
+ raise Error, response if Hash === response.body && response.body['error']
264
+
265
+ response
266
+ end
267
+
268
+ # Internal: Ensure that the parameter has a valid value. Things like `nil`
269
+ # and empty strings are right out. This method also performs a little
270
+ # formating on the parameter:
271
+ #
272
+ # * leading and trailing whitespace is removed
273
+ # * arrays are flattend
274
+ # * and then joined into a String
275
+ # * numerics are converted to their string equivalents
276
+ #
277
+ # param - The param Object to validate
278
+ # name - Optional param name as a String (used in exception messages)
279
+ #
280
+ # Returns the validated param as a String.
281
+ # Raises an ArgumentError if the param is not valid.
282
+ def assert_param_presence( param, name = 'input value' )
283
+ case param
284
+ when String, Numeric
285
+ param = param.to_s.strip
286
+ raise ArgumentError, "#{name} cannot be blank: #{param.inspect}" if param =~ /\A\s*\z/
287
+ param
288
+
289
+ when Array
290
+ param.flatten.map { |item| assert_param_presence(item, name) }.join(',')
291
+
292
+ when nil
293
+ raise ArgumentError, "#{name} cannot be nil"
294
+
295
+ else
296
+ raise ArgumentError, "#{name} is invalid: #{param.inspect}"
297
+ end
298
+ end
299
+
300
+ end # Client
301
+ end # Elastomer
302
+
303
+ # require all files in the `client` sub-directory
304
+ Dir.glob(File.expand_path('../client/*.rb', __FILE__)).each { |fn| require fn }
305
+
306
+ # require all files in the `middleware` sub-directory
307
+ Dir.glob(File.expand_path('../middleware/*.rb', __FILE__)).each { |fn| require fn }