stretcher 1.13.0 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -16,6 +16,7 @@ A concise, fast ElasticSearch client designed to reflect the actual elastic sear
16
16
  * Configurable logging
17
17
  * Pure, threadsafe, ruby
18
18
  * Easily swap HTTP clients via Faraday
19
+ * Tested against Ruby 2.0,1.9,1.8.7, Jruby, and Rubinius
19
20
 
20
21
  ## Installation
21
22
 
@@ -17,7 +17,6 @@ module Stretcher
17
17
  body = generic_opts
18
18
  end
19
19
 
20
- logger.info { "Stretcher Search: curl -XGET '#{Util.qurl(path_uri('_search'), query_opts)}' -d '#{body.to_json}'" }
21
20
  response = request(:get, "_search", query_opts) do |req|
22
21
  req.body = body
23
22
  end
@@ -28,10 +27,10 @@ module Stretcher
28
27
  request(:post, "_refresh")
29
28
  end
30
29
 
31
- def request(method, path=nil, query_opts=nil, *args, &block)
30
+ def request(method, path=nil, params={}, body=nil, headers={}, &block)
32
31
  prefixed_path = path_uri(path)
33
32
  raise "Cannot issue request, no server specified!" unless @server
34
- @server.request(method, prefixed_path, query_opts, *args, &block)
33
+ @server.request(method, prefixed_path, params, body, headers, &block)
35
34
  end
36
35
 
37
36
  def do_delete_query(query)
@@ -26,21 +26,39 @@ module Stretcher
26
26
  #
27
27
  # docs = [{"_type" => "tweet", "_id" => 91011, "text" => "Bulked"}]
28
28
  # server.index(:foo).bulk_index(docs)
29
- def bulk_index(documents)
30
- @server.bulk documents.reduce("") {|post_data, d_raw|
29
+ def bulk_index(documents, options={})
30
+ bulk_action(:index, documents, options)
31
+ end
32
+
33
+ # Given a hash of documents, will bulk delete
34
+ #
35
+ # docs = [{"_type" => "tweet", "_id" => 91011}]
36
+ # server.index(:foo).bulk_delete(docs)
37
+ def bulk_delete(documents, options={})
38
+ bulk_action(:delete, documents, options)
39
+ end
40
+
41
+ def bulk_action(action, documents, options={})
42
+ action=action.to_sym
43
+
44
+ body = documents.reduce("") {|post_data, d_raw|
31
45
  d = Hashie::Mash.new(d_raw)
32
- action_meta = {"index" => {"_index" => name, "_type" => d["_type"], "_id" => d["_id"] || d["id"]}}
33
- action_meta["index"]["_parent"] = d["_parent"] if d["_parent"]
34
- post_data << (action_meta.to_json + "\n")
35
- post_data << (d.to_json + "\n")
46
+ index_meta = { :_index => name, :_id => (d.delete(:id) || d.delete(:_id)) }
47
+
48
+ d.keys.reduce(index_meta) do |memo, key|
49
+ index_meta[key] = d.delete(key) if key.to_s.start_with?('_')
50
+ end
51
+
52
+ post_data << ({action => index_meta}.to_json + "\n")
53
+ post_data << (d.to_json + "\n") unless action == :delete
54
+ post_data
36
55
  }
56
+ @server.bulk body, options
37
57
  end
38
58
 
39
59
  # Creates the index, with the supplied hash as the optinos body (usually mappings: and settings:))
40
60
  def create(options={})
41
- request(:put) do |req|
42
- req.body = options
43
- end
61
+ request(:put, "/", {}, options)
44
62
  end
45
63
 
46
64
  # Deletes the index
@@ -26,40 +26,29 @@ module Stretcher
26
26
  options = {}
27
27
  end
28
28
 
29
- # If fields is passed in as an array, properly encode it
30
- arg_fields = options[:fields]
31
- if arg_fields.is_a?(Array)
32
- # no #merge! we don't want to mutate args
33
- options = options.merge(:fields => arg_fields.join(","))
34
- end
35
-
36
29
  res = request(:get, id, options)
37
30
  raw ? res : (res["_source"] || res["fields"])
38
31
  end
39
32
 
40
33
  # Retrieves multiple documents of the index type by ID
41
34
  # http://www.elasticsearch.org/guide/reference/api/multi-get/
42
- def mget(ids)
43
- request(:get, '_mget') do |req|
44
- req.body = { :ids => ids }
45
- end
35
+ def mget(ids, options={})
36
+ request(:get, '_mget', options, :ids => ids)
46
37
  end
47
38
 
48
39
  # Explains a query for a specific document
49
- def explain(id, query)
50
- request(:get, "#{id}/_explain") do |req|
51
- req.body = query
52
- end
40
+ def explain(id, query, options={})
41
+ request(:get, "#{id}/_explain", options, query)
53
42
  end
54
43
 
55
44
  # Index an item with a specific ID
56
- def put(id, source)
57
- request(:put, id, source)
45
+ def put(id, source, options={})
46
+ request(:put, id, options, source)
58
47
  end
59
48
 
60
49
  # Index an item with automatic ID generation
61
- def post(source)
62
- request(:post, nil, source)
50
+ def post(source, options={})
51
+ request(:post, nil, options, source)
63
52
  end
64
53
 
65
54
  # Uses the update api to modify a document with a script
@@ -69,12 +58,12 @@ module Stretcher
69
58
  # Takes an optional, third options hash, allowing you to specify
70
59
  # Additional query parameters such as +fields+ and +routing+
71
60
  def update(id, body, options={})
72
- request(:post, Util.qurl("#{id}/_update", options), body)
61
+ request(:post, "#{id}/_update", options, body)
73
62
  end
74
63
 
75
64
  # Deletes the document with the given ID
76
- def delete(id)
77
- request :delete, id
65
+ def delete(id, options={})
66
+ request :delete, id, options
78
67
  rescue Stretcher::RequestError => e
79
68
  raise e if e.http_response.status != 404
80
69
  false
@@ -94,9 +83,7 @@ module Stretcher
94
83
 
95
84
  # Alter the mapping for this type
96
85
  def put_mapping(body)
97
- request(:put, "_mapping") {|req|
98
- req.body = body
99
- }
86
+ request(:put, "_mapping", {}, body)
100
87
  end
101
88
 
102
89
  # Check if this index type is defined, if passed an id
@@ -11,8 +11,8 @@ module Stretcher
11
11
 
12
12
  builder.request :json
13
13
 
14
- builder.options[:read_timeout] = 4 || options[:read_timeout]
15
- builder.options[:open_timeout] = 2 || options[:open_timeout]
14
+ builder.options[:read_timeout] = options[:read_timeout] || 4
15
+ builder.options[:open_timeout] = options[:open_timeout] || 2
16
16
 
17
17
  if faraday_configurator = options[:faraday_configurator]
18
18
  faraday_configurator.call(builder)
@@ -20,6 +20,11 @@ module Stretcher
20
20
  builder.adapter :excon
21
21
  end
22
22
  end
23
+ http.headers = {
24
+ :accept => 'application/json',
25
+ :user_agent => "Stretcher Ruby Gem #{Stretcher::VERSION}",
26
+ "Content-Type" => "application/json"
27
+ }
23
28
 
24
29
  uri_components = URI.parse(uri)
25
30
  if uri_components.user || uri_components.password
@@ -90,10 +95,8 @@ module Stretcher
90
95
 
91
96
  # Perform a raw bulk operation. You probably want to use Stretcher::Index#bulk_index
92
97
  # which properly formats a bulk index request.
93
- def bulk(data)
94
- request(:post, path_uri("/_bulk")) do |req|
95
- req.body = data
96
- end
98
+ def bulk(data, options={})
99
+ request(:post, path_uri("/_bulk"), options, data)
97
100
  end
98
101
 
99
102
  # Retrieves stats for this server
@@ -125,12 +128,10 @@ module Stretcher
125
128
  def msearch(body=[])
126
129
  raise ArgumentError, "msearch takes an array!" unless body.is_a?(Array)
127
130
  fmt_body = body.map(&:to_json).join("\n") + "\n"
128
- logger.info { "Stretcher msearch: curl -XGET #{uri} -d '#{fmt_body}'" }
129
- res = request(:get, path_uri("/_msearch")) {|req|
130
- req.body = fmt_body
131
- }
131
+
132
+ res = request(:get, path_uri("/_msearch"), {}, fmt_body)
132
133
 
133
- errors = res.responses.select {|r| r[:error]}.map(&:error)
134
+ errors = res.responses.map(&:error).compact
134
135
  if !errors.empty?
135
136
  raise RequestError.new(res), "Could not msearch #{errors.inspect}"
136
137
  end
@@ -163,9 +164,7 @@ module Stretcher
163
164
  # as per: http://www.elasticsearch.org/guide/reference/api/admin-indices-aliases.html
164
165
  def aliases(body=nil)
165
166
  if body
166
- request(:post, path_uri("/_aliases")) do |req|
167
- req.body = body
168
- end
167
+ request(:post, path_uri("/_aliases"), {}, body)
169
168
  else
170
169
  request(:get, path_uri("/_aliases"))
171
170
  end
@@ -182,24 +181,25 @@ module Stretcher
182
181
  end
183
182
 
184
183
  # Handy way to query the server, returning *only* the body
185
- # Will raise an exception when the status is not in the 2xx range
186
- def request(method, url=nil, query_opts=nil, *args, &block)
187
- logger.info { "Stretcher: Issuing Request #{method.to_s.upcase}, #{Util.qurl(url,query_opts)}" }
188
-
189
- # Our default client is threadsafe, but some others might not be
190
- check_response(@request_mtx.synchronize {
191
- if block
192
- http.send(method, url, query_opts, *args) do |req|
193
- # Default content type to json, the block can change this of course
194
- req.headers["Content-Type"] = 'application/json' unless req.headers
195
- block.call(req)
196
- end
197
- else
198
- http.send(method, url, query_opts, *args)
199
- end
200
- })
184
+ # Will raise an exception when the status is not in the 2xx range
185
+ def request(method, path, params={}, body=nil, headers={}, &block)
186
+ req = http.build_request(method)
187
+ req.path = path
188
+ req.params.update(Util.clean_params(params)) if params
189
+ req.body = body if body
190
+ req.headers.update(headers) if headers
191
+ block.call(req) if block
192
+ logger.debug { Util.curl_format(req) }
193
+
194
+ @request_mtx.synchronize {
195
+ env = req.to_env(http)
196
+ check_response(http.app.call(env))
197
+ }
201
198
  end
202
-
199
+
200
+ private
201
+
202
+
203
203
  # Internal use only
204
204
  # Check response codes from request
205
205
  def check_response(res)
@@ -1,5 +1,17 @@
1
1
  module Stretcher
2
2
  module Util
3
+
4
+ # cURL formats a Faraday req. Useful for logging
5
+ def self.curl_format(req)
6
+ body = "-d '#{req.body.is_a?(Hash) ? req.body.to_json : req.body}'" if req.body
7
+ headers = req.headers.map {|name, value| "'-H #{name}: #{value}'" }.sort.join(' ')
8
+ method = req.method.to_s.upcase
9
+ url = Util.qurl(req.path,req.params)
10
+
11
+ ["curl -X#{method}", url, body, headers].compact.join(' ')
12
+ end
13
+
14
+ # Formats a url + query opts
3
15
  def self.qurl(url, query_opts=nil)
4
16
  query_opts && !query_opts.empty? ? "#{url}?#{querify(query_opts)}" : url
5
17
  end
@@ -7,5 +19,15 @@ module Stretcher
7
19
  def self.querify(hash)
8
20
  hash.map {|k,v| "#{k}=#{v}"}.join('&')
9
21
  end
22
+
23
+ def self.clean_params params={}
24
+ return unless params
25
+ clean_params = {}
26
+ params.each do |key, value|
27
+ clean_params[key] = value.is_a?(Array) ? value.join(',') : value
28
+ end
29
+ clean_params
30
+ end
31
+
10
32
  end
11
33
  end
@@ -1,3 +1,3 @@
1
1
  module Stretcher
2
- VERSION = "1.13.0"
2
+ VERSION = "1.14.0"
3
3
  end
@@ -11,7 +11,12 @@ describe Stretcher::Index do
11
11
  rescue Stretcher::RequestError::NotFound
12
12
  end
13
13
  server.refresh
14
- i.create
14
+ i.create({
15
+ :settings => {
16
+ :number_of_shards => 1,
17
+ :number_of_replicas => 0
18
+ }
19
+ })
15
20
  # Why do both? Doesn't hurt, and it fixes some races
16
21
  server.refresh
17
22
  i.refresh
@@ -66,14 +71,57 @@ describe Stretcher::Index do
66
71
  end
67
72
 
68
73
  it "should retrieve settings properly" do
69
- index.get_settings['foo']['settings']['index.number_of_replicas'].should_not be_nil
74
+ index.get_settings['foo']['settings']['index.number_of_shards'].should eq("1")
75
+ index.get_settings['foo']['settings']['index.number_of_replicas'].should eq("0")
70
76
  end
71
77
 
72
- it "should bulk index documents properly" do
73
- seed_corpus
74
- corpus.each {|doc|
75
- index.type(doc["_type"]).get(doc["_id"] || doc["id"]).text.should == doc[:text]
76
- }
78
+ describe "bulk operations" do
79
+ it "should bulk index documents properly" do
80
+ seed_corpus
81
+ corpus.each {|doc|
82
+ fetched_doc = index.type(doc["_type"]).get(doc["_id"] || doc["id"], {}, true)
83
+ fetched_doc._source.text.should == doc[:text]
84
+ fetched_doc._source._id.should be_nil
85
+ fetched_doc._source._type.should be_nil
86
+ }
87
+ end
88
+
89
+ it "should bulk delete documents" do
90
+ seed_corpus
91
+ index.bulk_delete([
92
+ {"_type" => 'tweet', "_id" => 'fooid'},
93
+ {"_type" => 'tweet', "_id" => 'barid'},
94
+ ])
95
+ index.refresh
96
+ res = index.search({}, {:query => {:match_all => {}}})
97
+ expect(res.results.map(&:_id)).to match_array ['bazid']
98
+ end
99
+ end
100
+
101
+ it 'allows _routing to be set on bulk index documents' do
102
+ server.index(:with_routing).delete if server.index(:with_routing).exists?
103
+ server.index(:with_routing).create({
104
+ :settings => {
105
+ :number_of_shards => 1,
106
+ :number_of_replicas => 0
107
+ },
108
+ :mappings => {
109
+ :_default_ => {
110
+ :_routing => { :required => true }
111
+ }
112
+ }
113
+ })
114
+
115
+ lambda {server.index(:with_routing).bulk_index(corpus)}.should raise_exception
116
+ routed_corpus = corpus.map do |doc|
117
+ routed_doc = doc.clone
118
+ routed_doc['_routing'] = 'abc'
119
+ routed_doc
120
+ end
121
+
122
+ server.index(:with_routing).bulk_index(routed_corpus)
123
+
124
+ server.index(:with_routing).delete
77
125
  end
78
126
 
79
127
  it "should delete by query" do
@@ -93,11 +141,37 @@ describe Stretcher::Index do
93
141
  end
94
142
 
95
143
  # TODO: Actually use two indexes
96
- it "should msearch across the index returning all docs" do
97
- seed_corpus
98
- res = index.msearch([{:query => {:match_all => {}}}])
99
- res.length.should == 1
100
- res[0].class.should == Stretcher::SearchResults
144
+ describe "msearch" do
145
+ let(:res) {
146
+ seed_corpus
147
+ q2_text = corpus.first[:text]
148
+ queries = [
149
+ {:query => {:match_all => {}}},
150
+ {:query => {:match => {:text => q2_text}}}
151
+ ]
152
+ index.msearch(queries)
153
+ }
154
+
155
+ it "should return an array of Stretcher::SearchResults" do
156
+ res.length.should == 2
157
+ res[0].class.should == Stretcher::SearchResults
158
+ end
159
+
160
+ it "should return the query results in order" do
161
+ # First query returns all docs, second only one
162
+ res[0].results.length.should == corpus.length
163
+ res[1].results.length.should == 1
164
+ end
165
+
166
+ it "should raise an error if any query is bad" do
167
+ queries = [
168
+ {:query => {:match_all => {}}},
169
+ {:query => {:invalid_query => {}}}
170
+ ]
171
+ expect {
172
+ index.msearch(queries)
173
+ }.to raise_error(Stretcher::RequestError)
174
+ end
101
175
  end
102
176
 
103
177
  it "execute the analysis API and return an expected result" do
@@ -1,19 +1,10 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Stretcher::IndexType do
4
- let(:server) { Stretcher::Server.new(ES_URL) }
5
- let(:index) {
6
- i = server.index(:foo)
7
- i
8
- }
9
- let(:type) {
10
- t = index.type(:bar)
11
- t.delete_query(:match_all => {})
12
- index.refresh
13
- mapping = {"bar" => {"properties" => {"message" => {"type" => "string"}}}}
14
- t.put_mapping mapping
15
- t
16
- }
4
+ let(:server) { Stretcher::Server.new(ES_URL, :logger => DEBUG_LOGGER) }
5
+ let(:index) { server.index(:foo) }
6
+
7
+ let(:type) { index.type(:bar) }
17
8
 
18
9
  it "should be existentially aware" do
19
10
  t = index.type(:existential)
@@ -24,9 +15,16 @@ describe Stretcher::IndexType do
24
15
  t.get_mapping.should == mapping
25
16
  end
26
17
 
18
+ before do
19
+ type.delete_query(:match_all => {})
20
+ index.refresh
21
+ type.put_mapping({"bar" => {"properties" => {"message" => {"type" => "string"}}}})
22
+ end
23
+
27
24
  describe "searching" do
28
25
  before do
29
26
  @doc = {:message => "hello"}
27
+
30
28
  type.put(123123, @doc)
31
29
  index.refresh
32
30
  end
@@ -67,6 +65,12 @@ describe Stretcher::IndexType do
67
65
  type.mget([988, 989]).docs.first._source.message.should == 'message one!'
68
66
  type.mget([988, 989]).docs.last._source.message.should == 'message two!'
69
67
  end
68
+
69
+ it 'allows options to be passed through' do
70
+ response = type.mget([988, 989], :fields => 'message')
71
+ response.docs.first.fields.message.should == 'message one!'
72
+ response.docs.last.fields.message.should == 'message two!'
73
+ end
70
74
  end
71
75
 
72
76
  describe "ops on individual docs" do
@@ -76,12 +80,12 @@ describe Stretcher::IndexType do
76
80
  end
77
81
 
78
82
  describe "put" do
79
- it "should put correctly" do
80
- @put_res.should_not be_nil
83
+ it "should put correctly, with options" do
84
+ type.put(987, @doc, :version_type => :external, :version => 42)._version.should == 42
81
85
  end
82
86
 
83
- it "should post correctly" do
84
- type.post(@doc).should_not be_nil
87
+ it "should post correctly, with options" do
88
+ type.post(@doc, :version_type => :external, :version => 42)._version.should == 42
85
89
  end
86
90
  end
87
91
 
@@ -127,18 +131,35 @@ describe Stretcher::IndexType do
127
131
  end
128
132
  end
129
133
 
130
- it "should explain a query" do
131
- type.exists?(987).should be_true
132
- index.refresh
133
- res = type.explain(987, {:query => {:match_all => {}}})
134
- res.should have_key('explanation')
134
+ describe 'explain' do
135
+ it "should explain a query" do
136
+ type.exists?(987).should be_true
137
+ index.refresh
138
+ res = type.explain(987, {:query => {:match_all => {}}})
139
+ res.should have_key('explanation')
140
+ end
141
+
142
+ it 'should allow options to be passed through' do
143
+ index.refresh
144
+ type.explain(987, {:query => {:match_all => {}}}, {:fields => 'message'}).get.fields.message.should == 'hello!'
145
+ end
135
146
  end
136
147
 
137
- it "should update individual docs correctly" do
148
+ it "should update individual docs correctly using ctx.source" do
138
149
  type.update(987, :script => "ctx._source.message = 'Updated!'")
139
150
  type.get(987).message.should == 'Updated!'
140
151
  end
141
152
 
153
+ it "should update individual docs correctly using doc" do
154
+ type.update(987, :doc => {:message => 'Updated!'})
155
+ type.get(987).message.should == 'Updated!'
156
+ end
157
+
158
+ it "should update individual docs correctly using doc and fields" do
159
+ response = type.update(987, {:doc => {:message => 'Updated!'}}, :fields => 'message')
160
+ response.get.fields.message.should == 'Updated!'
161
+ end
162
+
142
163
  it "should delete by query correctly" do
143
164
  type.delete_query("match_all" => {})
144
165
  index.refresh
@@ -150,5 +171,12 @@ describe Stretcher::IndexType do
150
171
  type.delete(987)
151
172
  type.exists?(987).should be_false
152
173
  end
174
+
175
+ it "should allow params to be passed to delete" do
176
+ version = type.get(987, {}, true)._version
177
+ lambda { type.delete(987, :version => version + 1) }.should raise_exception
178
+ type.delete(987, :version => version)
179
+ type.exists?(987).should be_false
180
+ end
153
181
  end
154
182
  end
@@ -1,7 +1,7 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Stretcher::Server do
4
- let(:server) { Stretcher::Server.new(ES_URL) }
4
+ let(:server) { Stretcher::Server.new(ES_URL, :logger => DEBUG_LOGGER) }
5
5
 
6
6
  it "should initialize cleanly" do
7
7
  server.class.should == Stretcher::Server
@@ -38,8 +38,6 @@ describe Stretcher::Server do
38
38
  end
39
39
 
40
40
  it "should perform alias operations properly" do
41
- # Tear down any leftovers from previous runs
42
- server.aliases(:actions => [{:remove => {:alias => :foo_alias}}]) if server.index(:foo_alias).exists?
43
41
  server.index(:foo).delete if server.index(:foo).exists?
44
42
  server.index(:foo).create
45
43
  server.aliases(:actions => [{:add => {:index => :foo, :alias => :foo_alias}}])
@@ -75,4 +73,11 @@ describe Stretcher::Server do
75
73
  analyzed = server.analyze("Candles", :analyzer => :snowball)
76
74
  analyzed.tokens.first.token.should == 'candl'
77
75
  end
76
+
77
+ it 'logs requests correctly' do
78
+ server.logger.should_receive(:debug) do |&block|
79
+ block.call.should == %{curl -XGET http://localhost:9200/_analyze?analyzer=snowball -d 'Candles' '-H Accept: application/json' '-H Content-Type: application/json' '-H User-Agent: Stretcher Ruby Gem #{Stretcher::VERSION}'}
80
+ end
81
+ server.analyze("Candles", :analyzer => :snowball)
82
+ end
78
83
  end
data/spec/spec_helper.rb CHANGED
@@ -4,6 +4,7 @@ Coveralls.wear!
4
4
  require 'rspec'
5
5
  require 'stretcher'
6
6
 
7
+ File.open("test_logs", 'wb') {|f| f.write("")}
7
8
  DEBUG_LOGGER = Logger.new('test_logs')
8
9
  DEBUG_LOGGER.level = Logger::DEBUG
9
10
  ES_URL = 'http://localhost:9200'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stretcher
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.14.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-05 00:00:00.000000000 Z
12
+ date: 2013-06-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday