stretcher 1.3.2 → 1.3.3

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/lib/stretcher.rb CHANGED
@@ -5,6 +5,8 @@ require 'faraday'
5
5
  require 'faraday_middleware'
6
6
  require "stretcher/version"
7
7
  require 'stretcher/request_error'
8
+ require 'stretcher/search_results'
9
+ require 'stretcher/es_component'
8
10
  require 'stretcher/server'
9
11
  require 'stretcher/index'
10
12
  require 'stretcher/index_type'
@@ -0,0 +1,24 @@
1
+ module Stretcher
2
+ # Elasticsearch has symmetry across API endpoints for Server, Index, and Type, lets try and provide some common ground
3
+ class EsComponent
4
+
5
+ private
6
+
7
+ def do_search(generic_opts={}, explicit_body=nil)
8
+ uri_str = '/_search'
9
+ body = nil
10
+ if explicit_body
11
+ uri_str << '?' + Util.querify(generic_opts)
12
+ body = explicit_body
13
+ else
14
+ body = generic_opts
15
+ end
16
+
17
+ logger.info { "Stretcher Search: curl -XGET '#{uri_str}' -d '#{body.to_json}'" }
18
+ response = @server.request(:get, path_uri(uri_str)) do |req|
19
+ req.body = body
20
+ end
21
+ SearchResults.new(:raw => response)
22
+ end
23
+ end
24
+ end
@@ -2,9 +2,9 @@ require 'stretcher/search_results'
2
2
  module Stretcher
3
3
  # Represents an Index context in elastic search.
4
4
  # Generally should be instantiated via Server#index(name).
5
- class Index
5
+ class Index < EsComponent
6
6
  attr_reader :server, :name, :logger
7
-
7
+
8
8
  def initialize(server, name, options={})
9
9
  @server = server
10
10
  @name = name
@@ -14,14 +14,14 @@ module Stretcher
14
14
  # Returns a Stretcher::IndexType object for the type +name+.
15
15
  # Optionally takes a block, which will be passed a single arg with the Index obj
16
16
  # The block syntax returns the evaluated value of the block
17
- #
17
+ #
18
18
  # my_index.index(:foo) # => #<Stretcher::Index ...>
19
19
  # my_index.index(:foo) {|idx| 1+1} # => 2
20
20
  def type(name, &block)
21
21
  t = IndexType.new(self, name)
22
22
  block ? block.call(t) : t
23
23
  end
24
-
24
+
25
25
  # Given a hash of documents, will bulk index
26
26
  #
27
27
  # docs = [{"_type" => "tweet", "_id" => 91011, "text" => "Bulked"}]
@@ -42,7 +42,7 @@ module Stretcher
42
42
  req.body = options
43
43
  end
44
44
  end
45
-
45
+
46
46
  # Deletes the index
47
47
  def delete
48
48
  @server.request :delete, path_uri
@@ -52,27 +52,27 @@ module Stretcher
52
52
  def stats
53
53
  @server.request :get, path_uri("/_stats")
54
54
  end
55
-
55
+
56
56
  # Retrieves status for this index
57
57
  def status
58
- @server.request :get, path_uri("/_status")
58
+ @server.request :get, path_uri("/_status")
59
59
  end
60
-
60
+
61
61
  # Retrieve the mapping for this index
62
62
  def get_mapping
63
63
  @server.request :get, path_uri("/_mapping")
64
64
  end
65
-
65
+
66
66
  # Retrieve settings for this index
67
67
  def get_settings
68
68
  @server.request :get, path_uri("/_settings")
69
69
  end
70
-
70
+
71
71
  # Check if the index has been created on the remote server
72
72
  def exists?
73
73
  @server.http.head(path_uri).status != 404
74
74
  end
75
-
75
+
76
76
  # Issues a search with the given query opts and body, both should be hashes
77
77
  #
78
78
  # res = server.index('foo').search(size: 12, {query: {match_all: {}}})
@@ -82,22 +82,10 @@ module Stretcher
82
82
  # res.results # => [#<Hashie::Mash _id="123" text="Hello">]
83
83
  # res.raw # => #<Hashie::Mash ...> Raw JSON from the search
84
84
  def search(generic_opts={}, explicit_body=nil)
85
- uri_str = '/_search'
86
- body = nil
87
- if explicit_body
88
- uri_str << '?' + Util.querify(generic_opts)
89
- body = explicit_body
90
- else
91
- body = generic_opts
92
- end
93
-
94
- logger.info { "Stretcher Search: curl -XGET '#{uri_str}' -d '#{body.to_json}'" }
95
- response = @server.request(:get, path_uri(uri_str)) do |req|
96
- req.body = body
97
- end
98
- SearchResults.new(raw: response)
85
+ # Written this way to be more RDoc friendly
86
+ do_search(generic_opts, explicit_body)
99
87
  end
100
-
88
+
101
89
  # Searches a list of queries against only this index
102
90
  # This deviates slightly from the official API in that *ONLY*
103
91
  # queries are requried, the empty {} preceding them are not
@@ -108,7 +96,7 @@ module Stretcher
108
96
  def msearch(queries=[])
109
97
  raise ArgumentError, "msearch takes an array!" unless queries.is_a?(Array)
110
98
  req_body = queries.reduce([]) {|acc,q|
111
- acc << {index: name}
99
+ acc << {:index => name}
112
100
  acc << q
113
101
  acc
114
102
  }
@@ -116,7 +104,7 @@ module Stretcher
116
104
  end
117
105
 
118
106
  # Implements the Analyze API
119
- # EX:
107
+ # EX:
120
108
  # index.analyze("Candles", analyzer: :snowball)
121
109
  # # => #<Hashie::Mash tokens=[#<Hashie::Mash end_offset=7 position=1 start_offset=0 token="candl" type="<ALPHANUM>">]>
122
110
  def analyze(text, analysis_params)
@@ -1,9 +1,9 @@
1
1
  module Stretcher
2
2
  # Represents an index scoped to a specific type.
3
3
  # Generally should be instantiated via Index#type(name).
4
- class IndexType
4
+ class IndexType < EsComponent
5
5
  attr_reader :server, :index, :name, :logger
6
-
6
+
7
7
  def initialize(index, name, options={})
8
8
  @index = index
9
9
  @server = index.server
@@ -17,12 +17,12 @@ module Stretcher
17
17
  res = server.request(:get, path_uri("/#{id}"))
18
18
  raw ? res : res["_source"]
19
19
  end
20
-
20
+
21
21
  # Index an item with a specific ID
22
22
  def put(id, source)
23
23
  server.request(:put, path_uri("/#{id}"), source)
24
24
  end
25
-
25
+
26
26
  # Uses the update api to modify a document with a script
27
27
  # To update a doc with ID 987 for example:
28
28
  # type.update(987, script: "ctx._source.message = 'Updated!'")
@@ -30,11 +30,11 @@ module Stretcher
30
30
  def update(id, body)
31
31
  server.request(:post, path_uri("/#{id}/_update"), body)
32
32
  end
33
-
33
+
34
34
  # Deletes the document with the given ID
35
35
  def delete(id)
36
36
  res = server.http.delete path_uri("/#{id}")
37
-
37
+
38
38
  # Since 404s are just not a problem here, let's simply return false
39
39
  if res.status == 404
40
40
  false
@@ -49,7 +49,7 @@ module Stretcher
49
49
  def get_mapping
50
50
  @server.request :get, path_uri("/_mapping")
51
51
  end
52
-
52
+
53
53
  # Delete the mapping for this type. Note this will delete
54
54
  # All documents of this type as well
55
55
  # http://www.elasticsearch.org/guide/reference/api/admin-indices-delete-mapping.html
@@ -63,7 +63,7 @@ module Stretcher
63
63
  req.body = body
64
64
  }
65
65
  end
66
-
66
+
67
67
  # Check if this index type is defined, if passed an id
68
68
  # this will check if the given document exists
69
69
  def exists?(id=nil)
@@ -72,8 +72,9 @@ module Stretcher
72
72
 
73
73
  # Issues an Index#search scoped to this type
74
74
  # See Index#search for more details
75
- def search(generic_opts={}, body=nil)
76
- @index.search(generic_opts.merge(type: name), body)
75
+ def search(generic_opts={}, explicit_body=nil)
76
+ # Written this way to be more RDoc friendly
77
+ do_search(generic_opts, explicit_body)
77
78
  end
78
79
 
79
80
  # Full path to this index type
@@ -1,25 +1,25 @@
1
1
  require 'hashie/dash'
2
2
  module Stretcher
3
3
  # Conveniently represents elastic search results in a more compact fashion
4
- #
4
+ #
5
5
  # Available properties:
6
- #
6
+ #
7
7
  # * raw : The raw response from elastic search
8
8
  # * total : The total number of matched docs
9
9
  # * facets : the facets hash
10
10
  # * results : The hit results with _id merged in to _source
11
11
  class SearchResults < Hashie::Dash
12
- property :raw, required: true
12
+ property :raw, :required => true
13
13
  property :total
14
14
  property :facets
15
15
  property :results
16
-
16
+
17
17
  def initialize(*args)
18
18
  super
19
19
  self.total = raw.hits.total
20
20
  self.facets = raw.facets
21
21
  self.results = raw.hits.hits.collect {|r|
22
- r['_source'].merge({"_id" => r['_id']})
22
+ (r.has_key?('_source') ? r['_source'] : r['fields']).merge({"_id" => r['_id']})
23
23
  }
24
24
  end
25
25
  end
@@ -1,5 +1,5 @@
1
1
  module Stretcher
2
- class Server
2
+ class Server < EsComponent
3
3
  attr_reader :uri, :http, :logger
4
4
 
5
5
  # Instantiate a new instance in a manner convenient for using the block syntax.
@@ -9,7 +9,7 @@ module Stretcher
9
9
  s = self.new(*args)
10
10
  yield s
11
11
  end
12
-
12
+
13
13
  # Represents a Server context in elastic search.
14
14
  # Use +with_server+ when you want to use the block syntax.
15
15
  # The options hash takes an optional instance of Logger under :logger.
@@ -18,25 +18,25 @@ module Stretcher
18
18
  def initialize(uri='http://localhost:9200', options={})
19
19
  @uri = uri
20
20
 
21
- @http = Faraday.new(:url => @uri) do |builder|
21
+ @http = Faraday.new(:url => @uri) do |builder|
22
22
  builder.response :mashify
23
23
  builder.response :json, :content_type => /\bjson$/
24
-
24
+
25
25
  builder.request :json
26
-
26
+
27
27
  builder.adapter :net_http_persistent
28
-
28
+
29
29
  builder.options[:read_timeout] = 4
30
30
  builder.options[:open_timeout] = 2
31
31
  end
32
-
32
+
33
33
  if options[:logger]
34
34
  @logger = options[:logger]
35
35
  else
36
36
  @logger = Logger.new(STDOUT)
37
37
  @logger.level = Logger::WARN
38
38
  end
39
-
39
+
40
40
  @logger.formatter = proc do |severity, datetime, progname, msg|
41
41
  "[Stretcher][#{severity}]: #{msg}\n"
42
42
  end
@@ -45,11 +45,11 @@ module Stretcher
45
45
  # Returns a Stretcher::Index object for the index +name+.
46
46
  # Optionally takes a block, which will be passed a single arg with the Index obj
47
47
  # The block syntax returns the evaluated value of the block
48
- #
48
+ #
49
49
  # my_server.index(:foo) # => #<Stretcher::Index ...>
50
50
  # my_server.index(:foo) {|idx| 1+1} # => 2
51
51
  def index(name, &block)
52
- idx = Index.new(self, name, logger: logger)
52
+ idx = Index.new(self, name, :logger => logger)
53
53
  block ? block.call(idx) : idx
54
54
  end
55
55
 
@@ -68,13 +68,13 @@ module Stretcher
68
68
  def status
69
69
  request :get, path_uri("/_status")
70
70
  end
71
-
71
+
72
72
  # Returns true if the server is currently reachable, raises an error otherwise
73
73
  def up?
74
74
  request(:get, path_uri)
75
75
  true
76
76
  end
77
-
77
+
78
78
  # Takes an array of msearch data as per
79
79
  # http://www.elasticsearch.org/guide/reference/api/multi-search.html
80
80
  # Should look something like:
@@ -92,13 +92,13 @@ module Stretcher
92
92
  res = request(:get, path_uri("/_msearch")) {|req|
93
93
  req.body = fmt_body
94
94
  }
95
-
95
+
96
96
  errors = res.responses.select {|r| r[:error]}.map(&:error)
97
97
  if !errors.empty?
98
- raise RequestError.new(res), "Could not msearch #{errors.inspect}"
98
+ raise RequestError.new(res), "Could not msearch #{errors.inspect}"
99
99
  end
100
-
101
- res['responses'].map {|r| SearchResults.new(raw: r)}
100
+
101
+ res['responses'].map {|r| SearchResults.new(:raw => r)}
102
102
  end
103
103
 
104
104
  # Retrieves multiple documents, possibly from multiple indexes
@@ -108,9 +108,9 @@ module Stretcher
108
108
  req.body = body
109
109
  }
110
110
  end
111
-
111
+
112
112
  # Implements the Analyze API
113
- # EX:
113
+ # EX:
114
114
  # server.analyze("Candles", analyzer: :snowball)
115
115
  # # => #<Hashie::Mash tokens=[#<Hashie::Mash end_offset=7 position=1 start_offset=0 token="candl" type="<ALPHANUM>">]>
116
116
  # as per: http://www.elasticsearch.org/guide/reference/api/admin-indices-analyze.html
@@ -128,7 +128,7 @@ module Stretcher
128
128
  # Handy way to query the server, returning *only* the body
129
129
  # Will raise an exception when the status is not in the 2xx range
130
130
  def request(method, *args, &block)
131
- logger.info { "Stretcher: Issuing Request #{method.upcase}, #{args}" }
131
+ logger.info { "Stretcher: Issuing Request #{method.to_s.upcase}, #{args}" }
132
132
  res = if block
133
133
  http.send(method, *args) do |req|
134
134
  # Elastic search does mostly deal with JSON
@@ -1,3 +1,3 @@
1
1
  module Stretcher
2
- VERSION = "1.3.2"
2
+ VERSION = "1.3.3"
3
3
  end
@@ -2,19 +2,19 @@ require 'spec_helper'
2
2
 
3
3
  describe Stretcher::Index do
4
4
  let(:server) {
5
- Stretcher::Server.new(ES_URL, logger: DEBUG_LOGGER)
5
+ Stretcher::Server.new(ES_URL, :logger => DEBUG_LOGGER)
6
6
  }
7
7
  let(:index) { server.index('foo') }
8
8
  let(:corpus) {
9
9
  [
10
- {text: "Foo", "_type" => 'tweet', "_id" => 'fooid'},
11
- {text: "Bar", "_type" => 'tweet', "_id" => 'barid'},
12
- {text: "Baz", "_type" => 'tweet', "id" => 'bazid'} # Note we support both _id and id
10
+ {:text => "Foo", "_type" => 'tweet', "_id" => 'fooid'},
11
+ {:text => "Bar", "_type" => 'tweet', "_id" => 'barid'},
12
+ {:text => "Baz", "_type" => 'tweet', "id" => 'bazid'} # Note we support both _id and id
13
13
  ]
14
14
  }
15
15
 
16
16
  def create_tweet_mapping
17
- mdata = {tweet:{properties: {text: {type: :string}}}}
17
+ mdata = {:tweet => {:properties => {:text => {:type => :string}}}}
18
18
  index.type('tweet').put_mapping(mdata)
19
19
  end
20
20
 
@@ -22,7 +22,7 @@ describe Stretcher::Index do
22
22
  create_tweet_mapping
23
23
  index.bulk_index(corpus)
24
24
  end
25
-
25
+
26
26
  it "should work on an existential level" do
27
27
  index.delete rescue nil
28
28
  index.exists?.should be_false
@@ -67,26 +67,26 @@ describe Stretcher::Index do
67
67
  seed_corpus
68
68
  match_text = corpus.first[:text]
69
69
  sleep 1 # ES needs time to update!
70
- res = index.search({}, {query: {match: {text: match_text}}})
70
+ res = index.search({}, {:query => {:match => {:text => match_text}}})
71
71
  res.results.first.text.should == match_text
72
72
  end
73
73
 
74
74
  # TODO: Actually use two indexes
75
75
  it "should msearch across the index returning all docs" do
76
76
  seed_corpus
77
- res = index.msearch([{query: {match_all: {}}}])
77
+ res = index.msearch([{:query => {:match_all => {}}}])
78
78
  res.length.should == 1
79
79
  res[0].class.should == Stretcher::SearchResults
80
80
  end
81
81
 
82
82
  it "execute the analysis API and return an expected result" do
83
- analyzed = index.analyze("Candles", analyzer: :snowball)
83
+ analyzed = index.analyze("Candles", :analyzer => :snowball)
84
84
  analyzed.tokens.first.token.should == 'candl'
85
85
  end
86
86
 
87
87
  it "should raise an exception when msearching a non-existant index" do
88
88
  lambda {
89
- res = server.index(:does_not_exist).msearch([{query: {match_all: {}}}])
89
+ res = server.index(:does_not_exist).msearch([{:query => {:match_all => {}}}])
90
90
  }.should raise_exception(Stretcher::RequestError)
91
91
  end
92
92
  end
@@ -16,27 +16,32 @@ describe Stretcher::Index do
16
16
 
17
17
  describe "searching" do
18
18
  before do
19
- @doc = {message: "hello"}
19
+ @doc = {:message => "hello"}
20
20
  end
21
21
 
22
22
  it "should search and find the right doc" do
23
23
  match_text = 'hello'
24
24
  type.put(123123, @doc)
25
25
  sleep 1
26
- res = type.search({}, {query: {match: {message: match_text}}})
26
+ res = type.search({}, {:query => {:match => {:message => match_text}}})
27
+ res.results.first.message.should == @doc[:message]
28
+ end
29
+
30
+ it "should build results when _source is not included in loaded fields" do
31
+ res = type.search({}, {query: {match_all: {}}, fields: ['message']})
27
32
  res.results.first.message.should == @doc[:message]
28
33
  end
29
34
  end
30
35
 
31
36
  describe "put/get" do
32
37
  before do
33
- @doc = {message: "hello!"}
38
+ @doc = {:message => "hello!"}
34
39
  end
35
-
40
+
36
41
  it "should put correctly" do
37
42
  type.put(987, @doc).should_not be_nil
38
43
  end
39
-
44
+
40
45
  it "should get individual documents correctly" do
41
46
  type.get(987).message.should == @doc[:message]
42
47
  end
@@ -48,14 +53,14 @@ describe Stretcher::Index do
48
53
  end
49
54
 
50
55
  it "should update individual docs correctly" do
51
- type.update(987, script: "ctx._source.message = 'Updated!'")
56
+ type.update(987, :script => "ctx._source.message = 'Updated!'")
52
57
  type.get(987).message.should == 'Updated!'
53
58
  end
54
-
59
+
55
60
  it "should delete individual docs correctly" do
56
61
  type.exists?(987).should be_true
57
62
  type.delete(987)
58
- type.exists?(987).should be_false
63
+ type.exists?(987).should be_false
59
64
  end
60
65
  end
61
66
  end
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe Stretcher::Server do
4
4
  let(:server) { Stretcher::Server.new(ES_URL) }
5
-
5
+
6
6
  it "should initialize cleanly" do
7
7
  server.class.should == Stretcher::Server
8
8
  end
@@ -28,7 +28,7 @@ describe Stretcher::Server do
28
28
  it "should check the status w/o error" do
29
29
  server.status.ok.should be_true
30
30
  end
31
-
31
+
32
32
  it "should beget an index object cleanly" do
33
33
  server.index('foo').class.should == Stretcher::Index
34
34
  end
@@ -44,7 +44,7 @@ describe Stretcher::Server do
44
44
  end
45
45
 
46
46
  it "execute the analysis API and return an expected result" do
47
- analyzed = server.analyze("Candles", analyzer: :snowball)
47
+ analyzed = server.analyze("Candles", :analyzer => :snowball)
48
48
  analyzed.tokens.first.token.should == 'candl'
49
49
  end
50
50
  end
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.3.2
4
+ version: 1.3.3
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-02-25 00:00:00.000000000 Z
12
+ date: 2013-03-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday
@@ -152,6 +152,7 @@ files:
152
152
  - README.md
153
153
  - Rakefile
154
154
  - lib/stretcher.rb
155
+ - lib/stretcher/es_component.rb
155
156
  - lib/stretcher/index.rb
156
157
  - lib/stretcher/index_type.rb
157
158
  - lib/stretcher/request_error.rb