stretcher 1.13.0 → 1.14.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.
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