elastic-enterprise-search 0.1.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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +68 -0
- data/.gitignore +10 -0
- data/.travis.yml +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +201 -0
- data/NOTICE.txt +3 -0
- data/README.md +117 -0
- data/Rakefile +1 -0
- data/elastic-enterprise-search.gemspec +23 -0
- data/lib/data/ca-bundle.crt +3554 -0
- data/lib/elastic/enterprise-search.rb +7 -0
- data/lib/elastic/enterprise-search/client.rb +92 -0
- data/lib/elastic/enterprise-search/configuration.rb +54 -0
- data/lib/elastic/enterprise-search/exceptions.rb +11 -0
- data/lib/elastic/enterprise-search/request.rb +113 -0
- data/lib/elastic/enterprise-search/utils.rb +15 -0
- data/lib/elastic/enterprise-search/version.rb +5 -0
- data/logo-enterprise-search.png +0 -0
- data/spec/client_spec.rb +57 -0
- data/spec/configuration_spec.rb +19 -0
- data/spec/fixtures/vcr/async_create_or_update_document_success.yml +51 -0
- data/spec/fixtures/vcr/destroy_documents_success.yml +51 -0
- data/spec/spec_helper.rb +28 -0
- metadata +128 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'elastic/enterprise-search/configuration'
|
3
|
+
require 'elastic/enterprise-search/request'
|
4
|
+
require 'elastic/enterprise-search/utils'
|
5
|
+
|
6
|
+
module Elastic
|
7
|
+
module EnterpriseSearch
|
8
|
+
# API client for the {Elastic Enterprise Search API}[https://swiftype.com/enterprise-search].
|
9
|
+
class Client
|
10
|
+
DEFAULT_TIMEOUT = 15
|
11
|
+
|
12
|
+
include Elastic::EnterpriseSearch::Request
|
13
|
+
|
14
|
+
def self.configure(&block)
|
15
|
+
Elastic::EnterpriseSearch.configure &block
|
16
|
+
end
|
17
|
+
|
18
|
+
# Create a new Elastic::EnterpriseSearch::Client client
|
19
|
+
#
|
20
|
+
# @param options [Hash] a hash of configuration options that will override what is set on the Elastic::EnterpriseSearch class.
|
21
|
+
# @option options [String] :access_token an Access Token to use for this client
|
22
|
+
# @option options [Numeric] :overall_timeout overall timeout for requests in seconds (default: 15s)
|
23
|
+
# @option options [Numeric] :open_timeout the number of seconds Net::HTTP (default: 15s)
|
24
|
+
# will wait while opening a connection before raising a Timeout::Error
|
25
|
+
# @option options [String] :proxy url of proxy to use, ex: "http://localhost:8888"
|
26
|
+
def initialize(options = {})
|
27
|
+
@options = options
|
28
|
+
end
|
29
|
+
|
30
|
+
def access_token
|
31
|
+
@options[:access_token] || Elastic::EnterpriseSearch.access_token
|
32
|
+
end
|
33
|
+
|
34
|
+
def open_timeout
|
35
|
+
@options[:open_timeout] || DEFAULT_TIMEOUT
|
36
|
+
end
|
37
|
+
|
38
|
+
def proxy
|
39
|
+
@options[:proxy]
|
40
|
+
end
|
41
|
+
|
42
|
+
def overall_timeout
|
43
|
+
(@options[:overall_timeout] || DEFAULT_TIMEOUT).to_f
|
44
|
+
end
|
45
|
+
|
46
|
+
# Documents have fields that can be searched or filtered.
|
47
|
+
#
|
48
|
+
# For more information on indexing documents, see the {Content Source documentation}[https://swiftype.com/documentation/enterprise-search/guides/content-sources].
|
49
|
+
module ContentSourceDocuments
|
50
|
+
|
51
|
+
# Index a batch of documents using the {Content Source API}[https://swiftype.com/documentation/enterprise-search/api/custom-sources].
|
52
|
+
#
|
53
|
+
# @param [String] content_source_key the unique Content Source key as found in your Content Sources dashboard
|
54
|
+
# @param [Array] documents an Array of Document Hashes
|
55
|
+
#
|
56
|
+
# @return [Array<Hash>] an Array of Document indexing Results
|
57
|
+
#
|
58
|
+
# @raise [Elastic::EnterpriseSearch::InvalidDocument] when a single document is missing required fields or contains unsupported fields
|
59
|
+
# @raise [Timeout::Error] when timeout expires waiting for results
|
60
|
+
def index_documents(content_source_key, documents)
|
61
|
+
documents = Array(documents).map! { |document| normalize_document(document) }
|
62
|
+
|
63
|
+
async_create_or_update_documents(content_source_key, documents)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Destroy a batch of documents given a list of external IDs
|
67
|
+
#
|
68
|
+
# @param [Array<String>] document_ids an Array of Document External IDs
|
69
|
+
#
|
70
|
+
# @return [Array<Hash>] an Array of Document destroy result hashes
|
71
|
+
#
|
72
|
+
# @raise [Timeout::Error] when timeout expires waiting for results
|
73
|
+
def destroy_documents(content_source_key, document_ids)
|
74
|
+
document_ids = Array(document_ids)
|
75
|
+
post("ent/sources/#{content_source_key}/documents/bulk_destroy.json", document_ids)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def async_create_or_update_documents(content_source_key, documents)
|
81
|
+
post("ent/sources/#{content_source_key}/documents/bulk_create.json", documents)
|
82
|
+
end
|
83
|
+
|
84
|
+
def normalize_document(document)
|
85
|
+
Utils.stringify_keys(document)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
include Elastic::EnterpriseSearch::Client::ContentSourceDocuments
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'elastic/enterprise-search/version'
|
3
|
+
|
4
|
+
module Elastic
|
5
|
+
module EnterpriseSearch
|
6
|
+
module Configuration
|
7
|
+
DEFAULT_ENDPOINT = "http://localhost:3002/api/v1/"
|
8
|
+
|
9
|
+
VALID_OPTIONS_KEYS = [
|
10
|
+
:access_token,
|
11
|
+
:user_agent,
|
12
|
+
:endpoint
|
13
|
+
].freeze
|
14
|
+
|
15
|
+
attr_accessor *VALID_OPTIONS_KEYS
|
16
|
+
|
17
|
+
def self.extended(base)
|
18
|
+
base.reset
|
19
|
+
end
|
20
|
+
|
21
|
+
# Reset configuration to default values.
|
22
|
+
def reset
|
23
|
+
self.access_token = nil
|
24
|
+
self.endpoint = DEFAULT_ENDPOINT
|
25
|
+
self.user_agent = nil
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# Yields the Elastic::EnterpriseSearch::Configuration module which can be used to set configuration options.
|
30
|
+
#
|
31
|
+
# @return self
|
32
|
+
def configure
|
33
|
+
yield self
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
# Return a hash of the configured options.
|
38
|
+
def options
|
39
|
+
options = {}
|
40
|
+
VALID_OPTIONS_KEYS.each{ |k| options[k] = send(k) }
|
41
|
+
options
|
42
|
+
end
|
43
|
+
|
44
|
+
# setter for endpoint that ensures it always ends in '/'
|
45
|
+
def endpoint=(endpoint)
|
46
|
+
if endpoint.end_with?('/')
|
47
|
+
@endpoint = endpoint
|
48
|
+
else
|
49
|
+
@endpoint = "#{endpoint}/"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Elastic
|
2
|
+
module EnterpriseSearch
|
3
|
+
class ClientException < StandardError; end
|
4
|
+
class NonExistentRecord < ClientException; end
|
5
|
+
class InvalidCredentials < ClientException; end
|
6
|
+
class BadRequest < ClientException; end
|
7
|
+
class Forbidden < ClientException; end
|
8
|
+
class UnexpectedHTTPException < ClientException; end
|
9
|
+
class InvalidDocument < ClientException; end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'json'
|
3
|
+
require 'elastic/enterprise-search/exceptions'
|
4
|
+
require 'openssl'
|
5
|
+
|
6
|
+
module Elastic
|
7
|
+
module EnterpriseSearch
|
8
|
+
CLIENT_NAME = 'elastic-enterprise-search-ruby'
|
9
|
+
CLIENT_VERSION = Elastic::EnterpriseSearch::VERSION
|
10
|
+
|
11
|
+
module Request
|
12
|
+
def get(path, params={})
|
13
|
+
request(:get, path, params)
|
14
|
+
end
|
15
|
+
|
16
|
+
def post(path, params={})
|
17
|
+
request(:post, path, params)
|
18
|
+
end
|
19
|
+
|
20
|
+
def put(path, params={})
|
21
|
+
request(:put, path, params)
|
22
|
+
end
|
23
|
+
|
24
|
+
def delete(path, params={})
|
25
|
+
request(:delete, path, params)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Construct and send a request to the API.
|
29
|
+
#
|
30
|
+
# @raise [Timeout::Error] when the timeout expires
|
31
|
+
def request(method, path, params = {})
|
32
|
+
Timeout.timeout(overall_timeout) do
|
33
|
+
uri = URI.parse("#{Elastic::EnterpriseSearch.endpoint}#{path}")
|
34
|
+
|
35
|
+
request = build_request(method, uri, params)
|
36
|
+
|
37
|
+
if proxy
|
38
|
+
proxy_parts = URI.parse(proxy)
|
39
|
+
http = Net::HTTP.new(uri.host, uri.port, proxy_parts.host, proxy_parts.port, proxy_parts.user, proxy_parts.password)
|
40
|
+
else
|
41
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
42
|
+
end
|
43
|
+
|
44
|
+
http.open_timeout = open_timeout
|
45
|
+
http.read_timeout = overall_timeout
|
46
|
+
|
47
|
+
if uri.scheme == 'https'
|
48
|
+
http.use_ssl = true
|
49
|
+
# st_ssl_verify_none provides a means to disable SSL verification for debugging purposes. An example
|
50
|
+
# is Charles, which uses a self-signed certificate in order to inspect https traffic. This will
|
51
|
+
# not be part of this client's public API, this is more of a development enablement option
|
52
|
+
http.verify_mode = ENV['st_ssl_verify_none'] == 'true' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
53
|
+
http.ca_file = File.join(File.dirname(__FILE__), '..', 'data', 'ca-bundle.crt')
|
54
|
+
http.ssl_timeout = open_timeout
|
55
|
+
end
|
56
|
+
|
57
|
+
response = http.request(request)
|
58
|
+
handle_errors(response)
|
59
|
+
JSON.parse(response.body) if response.body && response.body.strip != ''
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def handle_errors(response)
|
65
|
+
case response
|
66
|
+
when Net::HTTPSuccess
|
67
|
+
response
|
68
|
+
when Net::HTTPUnauthorized
|
69
|
+
raise Elastic::EnterpriseSearch::InvalidCredentials
|
70
|
+
when Net::HTTPNotFound
|
71
|
+
raise Elastic::EnterpriseSearch::NonExistentRecord
|
72
|
+
when Net::HTTPBadRequest
|
73
|
+
raise Elastic::EnterpriseSearch::BadRequest
|
74
|
+
when Net::HTTPForbidden
|
75
|
+
raise Elastic::EnterpriseSearch::Forbidden
|
76
|
+
else
|
77
|
+
raise Elastic::EnterpriseSearch::UnexpectedHTTPException, "#{response.code} #{response.body}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_request(method, uri, params)
|
82
|
+
klass = case method
|
83
|
+
when :get
|
84
|
+
Net::HTTP::Get
|
85
|
+
when :post
|
86
|
+
Net::HTTP::Post
|
87
|
+
when :put
|
88
|
+
Net::HTTP::Put
|
89
|
+
when :delete
|
90
|
+
Net::HTTP::Delete
|
91
|
+
end
|
92
|
+
|
93
|
+
case method
|
94
|
+
when :get, :delete
|
95
|
+
uri.query = URI.encode_www_form(params) if params && !params.empty?
|
96
|
+
req = klass.new(uri.request_uri)
|
97
|
+
when :post, :put
|
98
|
+
req = klass.new(uri.request_uri)
|
99
|
+
req.body = JSON.generate(params) unless params.length == 0
|
100
|
+
end
|
101
|
+
|
102
|
+
req['User-Agent'] = Elastic::EnterpriseSearch.user_agent if Elastic::EnterpriseSearch.user_agent
|
103
|
+
req['Content-Type'] = 'application/json'
|
104
|
+
req['X-Swiftype-Client'] = CLIENT_NAME
|
105
|
+
req['X-Swiftype-Client-Version'] = CLIENT_VERSION
|
106
|
+
req['Authorization'] = "Bearer #{access_token}"
|
107
|
+
puts req
|
108
|
+
|
109
|
+
req
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
Binary file
|
data/spec/client_spec.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Elastic::EnterpriseSearch::Client do
|
4
|
+
let(:engine_slug) { 'enterprise-search-api-example' }
|
5
|
+
let(:client) { Elastic::EnterpriseSearch::Client.new }
|
6
|
+
|
7
|
+
before :each do
|
8
|
+
Elastic::EnterpriseSearch.access_token = 'cGUN-vBokevBhhzyA669'
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'ContentSourceDocuments' do
|
12
|
+
def check_receipt_response_format(response, options = {})
|
13
|
+
expect(response.keys).to match_array(["document_receipts", "batch_link"])
|
14
|
+
expect(response["document_receipts"]).to be_a_kind_of(Array)
|
15
|
+
expect(response["document_receipts"].first.keys).to match_array(["id", "id", "links", "status", "errors"])
|
16
|
+
expect(response["document_receipts"].first["id"]).to eq(options[:id]) if options[:id]
|
17
|
+
expect(response["document_receipts"].first["status"]).to eq(options[:status]) if options[:status]
|
18
|
+
expect(response["document_receipts"].first["errors"]).to eq(options[:errors]) if options[:errors]
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:content_source_key) { '59542d332139de0acacc7dd4' }
|
22
|
+
let(:documents) do
|
23
|
+
[{'id'=>'INscMGmhmX4',
|
24
|
+
'url' => 'http://www.youtube.com/watch?v=v1uyQZNg2vE',
|
25
|
+
'title' => 'The Original Grumpy Cat',
|
26
|
+
'body' => 'this is a test'},
|
27
|
+
{'id'=>'JNDFojsd02',
|
28
|
+
'url' => 'http://www.youtube.com/watch?v=tsdfhk2j',
|
29
|
+
'title' => 'Another Grumpy Cat',
|
30
|
+
'body' => 'this is also a test'}]
|
31
|
+
end
|
32
|
+
|
33
|
+
context '#index_documents' do
|
34
|
+
it 'returns results when successful' do
|
35
|
+
VCR.use_cassette(:async_create_or_update_document_success) do
|
36
|
+
response = client.index_documents(content_source_key, documents)
|
37
|
+
expect(response.size).to eq(2)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context '#destroy_documents' do
|
43
|
+
it 'returns #async_create_or_update_documents format return when async has been passed as true' do
|
44
|
+
VCR.use_cassette(:async_create_or_update_document_success) do
|
45
|
+
VCR.use_cassette(:document_receipts_multiple_complete) do
|
46
|
+
client.index_documents(content_source_key, documents)
|
47
|
+
VCR.use_cassette(:destroy_documents_success) do
|
48
|
+
response = client.destroy_documents(content_source_key, [documents.first['id']])
|
49
|
+
expect(response.size).to eq(1)
|
50
|
+
expect(response.first['success']).to eq(true)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Configuration' do
|
4
|
+
context '.endpoint' do
|
5
|
+
context 'with a trailing /' do
|
6
|
+
it 'adds / to the end of of the URL' do
|
7
|
+
Elastic::EnterpriseSearch.endpoint = 'https://api.swiftype.com/api/v1'
|
8
|
+
expect(Elastic::EnterpriseSearch.endpoint).to eq('https://api.swiftype.com/api/v1/')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'with a trailing /' do
|
13
|
+
it 'leaves the URL alone' do
|
14
|
+
Elastic::EnterpriseSearch.endpoint = 'https://api.swiftype.com/api/v1/'
|
15
|
+
expect(Elastic::EnterpriseSearch.endpoint).to eq('https://api.swiftype.com/api/v1/')
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: post
|
5
|
+
uri: http://localhost:3002/api/v1/ent/sources/59542d332139de0acacc7dd4/documents/bulk_create.json
|
6
|
+
body:
|
7
|
+
encoding: UTF-8
|
8
|
+
string: '[{"id":"INscMGmhmX4","url":"http://www.youtube.com/watch?v=v1uyQZNg2vE","title":"The
|
9
|
+
Original Grumpy Cat","body":"this is a test"},{"id":"JNDFojsd02","url":"http://www.youtube.com/watch?v=tsdfhk2j","title":"Another
|
10
|
+
Grumpy Cat","body":"this is also a test"}]'
|
11
|
+
headers:
|
12
|
+
Accept-Encoding:
|
13
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
14
|
+
Accept:
|
15
|
+
- "*/*"
|
16
|
+
Content-Type:
|
17
|
+
- application/json
|
18
|
+
Authorization:
|
19
|
+
- Bearer cGUN-vBokevBhhzyA669
|
20
|
+
response:
|
21
|
+
status:
|
22
|
+
code: 202
|
23
|
+
message: Accepted
|
24
|
+
headers:
|
25
|
+
X-Frame-Options:
|
26
|
+
- SAMEORIGIN
|
27
|
+
X-Xss-Protection:
|
28
|
+
- 1; mode=block
|
29
|
+
X-Content-Type-Options:
|
30
|
+
- nosniff
|
31
|
+
Content-Type:
|
32
|
+
- application/json; charset=utf-8
|
33
|
+
Cache-Control:
|
34
|
+
- no-cache
|
35
|
+
Set-Cookie:
|
36
|
+
- _st_main_session=BAh7BkkiD3Nlc3Npb25faWQGOgZFVEkiJWE4NGIxMjQ0YjgzNzYwOWU2NzljZGYyMjkwYzM2ODA4BjsAVA%3D%3D--d0ca869176aa0b9f3d57baf66152897d305fabae;
|
37
|
+
path=/; expires=Mon, 05 Jul 2027 17:52:09 -0000; HttpOnly
|
38
|
+
X-Request-Id:
|
39
|
+
- '09b779a9-1704-46e7-991d-9e92e67b0001'
|
40
|
+
X-Runtime:
|
41
|
+
- '0.115497'
|
42
|
+
Connection:
|
43
|
+
- close
|
44
|
+
Server:
|
45
|
+
- thin 1.5.0 codename Knife
|
46
|
+
body:
|
47
|
+
encoding: UTF-8
|
48
|
+
string: '[{"id":null,"id":"1234","errors":[]},{"id":null,"id":"1235","errors":[]}]'
|
49
|
+
http_version:
|
50
|
+
recorded_at: Wed, 05 Jul 2017 17:52:09 GMT
|
51
|
+
recorded_with: VCR 3.0.3
|
@@ -0,0 +1,51 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: post
|
5
|
+
uri: http://localhost:3002/api/v1/ent/sources/59542d332139de0acacc7dd4/documents/bulk_destroy.json
|
6
|
+
body:
|
7
|
+
encoding: UTF-8
|
8
|
+
string: '["INscMGmhmX4"]'
|
9
|
+
headers:
|
10
|
+
Accept-Encoding:
|
11
|
+
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
12
|
+
Accept:
|
13
|
+
- "*/*"
|
14
|
+
Content-Type:
|
15
|
+
- application/json
|
16
|
+
Authorization:
|
17
|
+
- Bearer cGUN-vBokevBhhzyA669
|
18
|
+
response:
|
19
|
+
status:
|
20
|
+
code: 200
|
21
|
+
message: OK
|
22
|
+
headers:
|
23
|
+
X-Frame-Options:
|
24
|
+
- SAMEORIGIN
|
25
|
+
X-Xss-Protection:
|
26
|
+
- 1; mode=block
|
27
|
+
X-Content-Type-Options:
|
28
|
+
- nosniff
|
29
|
+
Content-Type:
|
30
|
+
- application/json; charset=utf-8
|
31
|
+
Etag:
|
32
|
+
- W/"ab494a471abdde82a270b9d9562b7bb9"
|
33
|
+
Cache-Control:
|
34
|
+
- max-age=0, private, must-revalidate
|
35
|
+
Set-Cookie:
|
36
|
+
- _st_main_session=BAh7BkkiD3Nlc3Npb25faWQGOgZFVEkiJTQ5ZGE4NjY4ZjZmY2Q2OTM2MGM0OTIyMDFkNmRmMzFlBjsAVA%3D%3D--75e0f9002e5f32100e4814a9dc015ffd43a5b6eb;
|
37
|
+
path=/; expires=Mon, 05 Jul 2027 17:52:12 -0000; HttpOnly
|
38
|
+
X-Request-Id:
|
39
|
+
- ec1754ea-5fa3-474f-87ba-e754e4c62553
|
40
|
+
X-Runtime:
|
41
|
+
- '1.367282'
|
42
|
+
Connection:
|
43
|
+
- close
|
44
|
+
Server:
|
45
|
+
- thin 1.5.0 codename Knife
|
46
|
+
body:
|
47
|
+
encoding: UTF-8
|
48
|
+
string: '[{"id":"INscMGmhmX4","success":true}]'
|
49
|
+
http_version:
|
50
|
+
recorded_at: Wed, 05 Jul 2017 17:52:12 GMT
|
51
|
+
recorded_with: VCR 3.0.3
|