elastic-enterprise-search 0.1.0

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