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.
@@ -0,0 +1,7 @@
1
+ require 'elastic/enterprise-search/client'
2
+
3
+ module Elastic
4
+ module EnterpriseSearch
5
+ extend Elastic::EnterpriseSearch::Configuration
6
+ end
7
+ end
@@ -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
@@ -0,0 +1,15 @@
1
+ module Elastic
2
+ module EnterpriseSearch
3
+ module Utils
4
+ extend self
5
+
6
+ def stringify_keys(hash)
7
+ output = {}
8
+ hash.each do |key, value|
9
+ output[key.to_s] = value
10
+ end
11
+ output
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module Elastic
2
+ module EnterpriseSearch
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -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