stretcher 1.3.2 → 1.3.3

Sign up to get free protection for your applications and to get access to all the features.
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