rubberband 0.0.2
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.
- data/.gitignore +2 -0
- data/LICENSE +202 -0
- data/README.rdoc +36 -0
- data/Rakefile +59 -0
- data/TODO +29 -0
- data/VERSION +1 -0
- data/lib/elasticsearch.rb +14 -0
- data/lib/elasticsearch/client.rb +20 -0
- data/lib/elasticsearch/client/abstract_client.rb +34 -0
- data/lib/elasticsearch/client/admin_cluster.rb +56 -0
- data/lib/elasticsearch/client/admin_index.rb +109 -0
- data/lib/elasticsearch/client/auto_discovering_client.rb +21 -0
- data/lib/elasticsearch/client/default_scope.rb +29 -0
- data/lib/elasticsearch/client/hits.rb +82 -0
- data/lib/elasticsearch/client/index.rb +96 -0
- data/lib/elasticsearch/client/retrying_client.rb +80 -0
- data/lib/elasticsearch/encoding.rb +2 -0
- data/lib/elasticsearch/encoding/base.rb +17 -0
- data/lib/elasticsearch/encoding/json.rb +19 -0
- data/lib/elasticsearch/transport.rb +3 -0
- data/lib/elasticsearch/transport/base.rb +32 -0
- data/lib/elasticsearch/transport/base_protocol.rb +98 -0
- data/lib/elasticsearch/transport/http.rb +263 -0
- data/test/elasticsearch_test.rb +7 -0
- data/test/hits_test.rb +34 -0
- data/test/test_helper.rb +10 -0
- metadata +136 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module ElasticSearch
|
2
|
+
module Transport
|
3
|
+
class RetryableError < StandardError; end
|
4
|
+
class FatalError < StandardError; end
|
5
|
+
|
6
|
+
DEFAULTS = {
|
7
|
+
:encoder => ElasticSearch::Encoding::JSON
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
class Base
|
11
|
+
include BaseProtocol
|
12
|
+
|
13
|
+
attr_accessor :server, :options
|
14
|
+
|
15
|
+
def initialize(server, options={})
|
16
|
+
@server = server
|
17
|
+
@options = DEFAULTS.merge(options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def connect!
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def close
|
25
|
+
end
|
26
|
+
|
27
|
+
def encoder
|
28
|
+
@encoder ||= @options[:encoder].new
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module ElasticSearch
|
2
|
+
module Transport
|
3
|
+
module BaseProtocol
|
4
|
+
|
5
|
+
# index api
|
6
|
+
|
7
|
+
def index(index, type, id, document, options={})
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
def get(index, type, id, options={})
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def delete(options)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def search(index, type, query, options={})
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def count(index, type, query, options={})
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
|
27
|
+
# admin index api
|
28
|
+
def index_status(index_list, options={})
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_index(index, create_options={}, options={})
|
33
|
+
raise NotImplementedError
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete_index(index, options={})
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
def alias_index(operations, options={})
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
|
44
|
+
def update_mapping(index, type, mapping, options)
|
45
|
+
raise NotImplementedError
|
46
|
+
end
|
47
|
+
|
48
|
+
def flush(index_list, options={})
|
49
|
+
raise NotImplementedError
|
50
|
+
end
|
51
|
+
|
52
|
+
def refresh(index_list, options={})
|
53
|
+
raise NotImplementedError
|
54
|
+
end
|
55
|
+
|
56
|
+
def snapshot(index_list, options={})
|
57
|
+
raise NotImplementedError
|
58
|
+
end
|
59
|
+
|
60
|
+
def optimize(index_list, options={})
|
61
|
+
raise NotImplementedError
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# admin cluster api
|
66
|
+
def cluster_health(index_list, options={})
|
67
|
+
raise NotImplementedError
|
68
|
+
end
|
69
|
+
|
70
|
+
def cluster_state(options={})
|
71
|
+
raise NotImplementedError
|
72
|
+
end
|
73
|
+
|
74
|
+
def nodes_info(node_list, options={})
|
75
|
+
raise NotImplementedError
|
76
|
+
end
|
77
|
+
|
78
|
+
def nodes_stats(node_list, options={})
|
79
|
+
raise NotImplementedError
|
80
|
+
end
|
81
|
+
|
82
|
+
def shutdown_nodes(node_list, options={})
|
83
|
+
raise NotImplementedError
|
84
|
+
end
|
85
|
+
|
86
|
+
def restart_nodes(node_list, options={})
|
87
|
+
raise NotImplementedError
|
88
|
+
end
|
89
|
+
|
90
|
+
# misc
|
91
|
+
def all_nodes
|
92
|
+
raise NotImplementedError
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'patron'
|
2
|
+
require 'cgi'
|
3
|
+
|
4
|
+
module ElasticSearch
|
5
|
+
module Transport
|
6
|
+
class HTTP < Base
|
7
|
+
|
8
|
+
class ConnectionFailed < RetryableError; end
|
9
|
+
class HostResolutionError < RetryableError; end
|
10
|
+
class TimeoutError < RetryableError; end
|
11
|
+
class RequestError < FatalError; end
|
12
|
+
|
13
|
+
DEFAULTS = {
|
14
|
+
:timeout => 5
|
15
|
+
}.freeze
|
16
|
+
|
17
|
+
def initialize(server, options={})
|
18
|
+
super
|
19
|
+
@options = DEFAULTS.merge(@options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def connect!
|
23
|
+
@session = Patron::Session.new
|
24
|
+
@session.base_url = @server
|
25
|
+
@session.timeout = @options[:timeout]
|
26
|
+
@session.headers['User-Agent'] = 'ElasticSearch.rb v0.1'
|
27
|
+
request(:get, "/") # try a get to see if the server responds
|
28
|
+
end
|
29
|
+
|
30
|
+
# index api (TODO modulize)
|
31
|
+
|
32
|
+
def index(index, type, id, document, options={})
|
33
|
+
body = encoder.is_encoded?(document) ? document : encoder.encode(document)
|
34
|
+
if id.nil?
|
35
|
+
response = request(:post, generate_path(:index => index, :type => type), body)
|
36
|
+
else
|
37
|
+
response = request(:put, generate_path(:index => index, :type => type, :id => id), body)
|
38
|
+
end
|
39
|
+
handle_error(response) unless response.status == 200
|
40
|
+
encoder.decode(response.body)
|
41
|
+
end
|
42
|
+
|
43
|
+
def get(index, type, id, options={})
|
44
|
+
response = request(:get, generate_path(:index => index, :type => type, :id => id))
|
45
|
+
return nil if response.status == 404
|
46
|
+
|
47
|
+
handle_error(response) unless response.status == 200
|
48
|
+
hit = encoder.decode(response.body)
|
49
|
+
unescape_id!(hit) #TODO extract these two calls from here and search
|
50
|
+
set_encoding!(hit)
|
51
|
+
hit # { "_id", "_index", "_type", "_source" }
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete(index, type, id, options={})
|
55
|
+
response = request(:delete, generate_path(:index => index, :type => type, :id => id))
|
56
|
+
handle_error(response) unless response.status == 200 # ElasticSearch always returns 200 on delete, even if the object doesn't exist
|
57
|
+
encoder.decode(response.body)
|
58
|
+
end
|
59
|
+
|
60
|
+
def search(index, type, query, options={})
|
61
|
+
if query.is_a?(Hash)
|
62
|
+
# patron cannot submit get requests with content, so if query is a hash, post it instead (assume a query hash is using the query dsl)
|
63
|
+
response = request(:post, generate_path(:index => index, :type => type, :op => "_search", :params => options), encoder.encode(query))
|
64
|
+
else
|
65
|
+
response = request(:get, generate_path(:index => index, :type => type, :op => "_search", :params => options.merge(:q => query)))
|
66
|
+
end
|
67
|
+
handle_error(response) unless response.status == 200
|
68
|
+
results = encoder.decode(response.body)
|
69
|
+
# unescape ids
|
70
|
+
results["hits"]["hits"].each do |hit|
|
71
|
+
unescape_id!(hit)
|
72
|
+
set_encoding!(hit)
|
73
|
+
end
|
74
|
+
results # {"hits"=>{"hits"=>[{"_id", "_type", "_source", "_index"}], "total"}, "_shards"=>{"failed", "total", "successful"}}
|
75
|
+
end
|
76
|
+
|
77
|
+
def count(index, type, query, options={})
|
78
|
+
if query.is_a?(Hash)
|
79
|
+
# patron cannot submit get requests with content, so if query is a hash, post it instead (assume a query hash is using the query dsl)
|
80
|
+
response = request(:post, generate_path(:index => index, :type => type, :op => "_count", :params => options), encoder.encode(query))
|
81
|
+
else
|
82
|
+
response = request(:get, generate_path(:index => index, :type => type, :op => "_count", :params => options.merge(:q => query)))
|
83
|
+
end
|
84
|
+
handle_error(response) unless response.status == 200
|
85
|
+
encoder.decode(response.body) # {"count", "_shards"=>{"failed", "total", "successful"}}
|
86
|
+
end
|
87
|
+
|
88
|
+
# admin index api (TODO modulize)
|
89
|
+
#
|
90
|
+
def index_status(index_list, options={})
|
91
|
+
standard_request(:get, generate_path(:index => index_list, :op => "_status"))
|
92
|
+
end
|
93
|
+
|
94
|
+
def create_index(index, create_options={}, options={})
|
95
|
+
standard_request(:put, generate_path(:index => index), encoder.encode(create_options))
|
96
|
+
end
|
97
|
+
|
98
|
+
def delete_index(index, options={})
|
99
|
+
standard_request(:delete, generate_path(:index => index))
|
100
|
+
end
|
101
|
+
|
102
|
+
def alias_index(operations, options={})
|
103
|
+
standard_request(:post, generate_path(:op => "_aliases"), encoder.encode(operations))
|
104
|
+
end
|
105
|
+
|
106
|
+
def update_mapping(index, type, mapping, options)
|
107
|
+
standard_request(:put, generate_path(:index => index, :type => type, :op => "_mapping", :params => options), encoder.encode(mapping))
|
108
|
+
end
|
109
|
+
|
110
|
+
def flush(index_list, options={})
|
111
|
+
standard_request(:post, generate_path(:index => index_list, :op => "_flush", :params => options), "")
|
112
|
+
end
|
113
|
+
|
114
|
+
def refresh(index_list, options={})
|
115
|
+
standard_request(:post, generate_path(:index => index_list, :op => "_refresh"), "")
|
116
|
+
end
|
117
|
+
|
118
|
+
def snapshot(index_list, options={})
|
119
|
+
standard_request(:post, generate_path(:index => index_list, :type => "_gateway", :op => "snapshot"), "")
|
120
|
+
end
|
121
|
+
|
122
|
+
def optimize(index_list, options={})
|
123
|
+
standard_request(:post, generate_path(:index => index_list, :op => "_optimize", :params => options), "")
|
124
|
+
end
|
125
|
+
|
126
|
+
# admin cluster api (TODO modulize)
|
127
|
+
|
128
|
+
def cluster_health(index_list, options={})
|
129
|
+
standard_request(:get, generate_path(:index => "_cluster", :type => "health", :id => index_list, :params => options))
|
130
|
+
end
|
131
|
+
|
132
|
+
def cluster_state(options={})
|
133
|
+
standard_request(:get, generate_path(:index => "_cluster", :op => "state"))
|
134
|
+
end
|
135
|
+
|
136
|
+
def nodes_info(node_list, options={})
|
137
|
+
standard_request(:get, generate_path(:index => "_cluster", :type => "nodes", :id => node_list))
|
138
|
+
end
|
139
|
+
|
140
|
+
def nodes_stats(node_list, options={})
|
141
|
+
standard_request(:get, generate_path(:index => "_cluster", :type => "nodes", :id => node_list, :op => "stats"))
|
142
|
+
end
|
143
|
+
|
144
|
+
def shutdown_nodes(node_list, options={})
|
145
|
+
standard_request(:post, generate_path(:index => "_cluster", :type => "nodes", :id => node_list, :op => "_shutdown", :params => options), "")
|
146
|
+
end
|
147
|
+
|
148
|
+
def restart_nodes(node_list, options={})
|
149
|
+
standard_request(:post, generate_path(:index => "_cluster", :type => "nodes", :id => node_list, :op => "_restart", :params => options), "")
|
150
|
+
end
|
151
|
+
|
152
|
+
# misc helper methods (TODO modulize)
|
153
|
+
def all_nodes
|
154
|
+
http_addresses = nodes_info([])["nodes"].collect { |id, node| node["http_address"] }
|
155
|
+
http_addresses.collect! do |a|
|
156
|
+
if a =~ /inet\[.*\/([\d.:]+)\]/
|
157
|
+
$1
|
158
|
+
end
|
159
|
+
end.compact!
|
160
|
+
http_addresses
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def standard_request(*args)
|
166
|
+
response = request(*args)
|
167
|
+
handle_error(response) unless response.status == 200
|
168
|
+
encoder.decode(response.body)
|
169
|
+
end
|
170
|
+
|
171
|
+
# :index - one or many index names
|
172
|
+
# :type - one or many types
|
173
|
+
# :id - one id
|
174
|
+
# :params - hash of query params
|
175
|
+
def generate_path(options)
|
176
|
+
path = ""
|
177
|
+
path << "/#{Array(options[:index]).collect { |i| escape(i.downcase) }.join(",")}" if options[:index] && !options[:index].empty?
|
178
|
+
path << "/#{Array(options[:type]).collect { |t| escape(t) }.join(",")}" if options[:type] && !options[:type].empty?
|
179
|
+
path << "/#{Array(options[:id]).collect { |id| escape(id) }.join(",")}" if options[:id] && !options[:id].empty?
|
180
|
+
path << "/#{options[:op]}" if options[:op]
|
181
|
+
path << "?" << query_string(options[:params]) if options[:params] && !options[:params].empty?
|
182
|
+
path
|
183
|
+
end
|
184
|
+
|
185
|
+
def unescape_id!(hit)
|
186
|
+
hit["_id"] = unescape(hit["_id"])
|
187
|
+
nil
|
188
|
+
end
|
189
|
+
|
190
|
+
def set_encoding!(hit)
|
191
|
+
encode_utf8(hit["_source"])
|
192
|
+
nil
|
193
|
+
end
|
194
|
+
|
195
|
+
# faster than CGI.escape
|
196
|
+
# stolen from RSolr, which stole it from Rack
|
197
|
+
def escape(string)
|
198
|
+
string.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
199
|
+
#'%'+$1.unpack('H2'*$1.size).join('%').upcase
|
200
|
+
'%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
|
201
|
+
}.tr(' ', '+')
|
202
|
+
end
|
203
|
+
|
204
|
+
def unescape(string)
|
205
|
+
CGI.unescape(string)
|
206
|
+
end
|
207
|
+
|
208
|
+
if ''.respond_to?(:force_encoding) && ''.respond_to?(:encoding)
|
209
|
+
# encodes the string as utf-8 in Ruby 1.9
|
210
|
+
def encode_utf8(string)
|
211
|
+
# ElasticSearch only ever returns json in UTF-8 (per the JSON spec) so we can use force_encoding here (#TODO what about ids? can we assume those are always ascii?)
|
212
|
+
string.force_encoding(Encoding::UTF_8)
|
213
|
+
end
|
214
|
+
else
|
215
|
+
# returns the unaltered string in Ruby 1.8
|
216
|
+
def encode_utf8(string)
|
217
|
+
string
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# Return the bytesize of String; uses String#size under Ruby 1.8 and
|
222
|
+
# String#bytesize under 1.9.
|
223
|
+
if ''.respond_to?(:bytesize)
|
224
|
+
def bytesize(string)
|
225
|
+
string.bytesize
|
226
|
+
end
|
227
|
+
else
|
228
|
+
def bytesize(string)
|
229
|
+
string.size
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
#doesn't handle arrays or hashes or what have you
|
234
|
+
def query_string(params)
|
235
|
+
params.collect { |k,v| "#{escape(k.to_s)}=#{escape(v.to_s)}" }.join("&")
|
236
|
+
end
|
237
|
+
|
238
|
+
def request(method, path, body=nil, headers={})
|
239
|
+
begin
|
240
|
+
#puts "request: #{@session.base_url}#{path} body:#{body}"
|
241
|
+
response = @session.request(method, path, headers, :data => body)
|
242
|
+
handle_error(response) if response.status >= 500
|
243
|
+
response
|
244
|
+
rescue Exception => e
|
245
|
+
case e
|
246
|
+
when Patron::ConnectionFailed
|
247
|
+
raise ConnectionFailed
|
248
|
+
when Patron::HostResolutionError
|
249
|
+
raise HostResolutionError
|
250
|
+
when TimeoutError
|
251
|
+
raise TimeoutError
|
252
|
+
else
|
253
|
+
raise e
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def handle_error(response)
|
259
|
+
raise RequestError, "(#{response.status}) #{response.body}"
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
data/test/hits_test.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class HitsTest < Test::Unit::TestCase
|
4
|
+
context "A Hit instance" do
|
5
|
+
setup do
|
6
|
+
@response = {"_source" => {"foo" => "bar"}, "_id" => "1"}
|
7
|
+
@hit = ElasticSearch::Api::Hit.new(@response)
|
8
|
+
end
|
9
|
+
|
10
|
+
should "set id" do
|
11
|
+
assert_equal @response["_id"], @hit.id
|
12
|
+
end
|
13
|
+
|
14
|
+
should "set hit attributes" do
|
15
|
+
assert_equal @response["_source"]["foo"], @hit.foo
|
16
|
+
end
|
17
|
+
|
18
|
+
should "be frozen" do
|
19
|
+
assert @hit.attributes.frozen?
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
context "A Hits instance" do
|
26
|
+
setup do
|
27
|
+
@response1 = {"_source" => {"foo" => "bar"}, "_id" => "1"}
|
28
|
+
@hit = ElasticSearch::Api::Hit.new(@response)
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
should "exist"
|
33
|
+
end
|
34
|
+
end
|
data/test/test_helper.rb
ADDED