elastic-app-search 0.7.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,6 @@
1
+ require 'elastic/app-search/client'
2
+
3
+ module Elastic
4
+ module AppSearch
5
+ end
6
+ end
@@ -0,0 +1,67 @@
1
+ require 'set'
2
+ require 'elastic/app-search/request'
3
+ require 'elastic/app-search/utils'
4
+ require 'jwt'
5
+
6
+ module Elastic
7
+ module AppSearch
8
+ # API client for the {Elastic App Search API}[https://www.elastic.co/cloud/app-search-service].
9
+ class Client
10
+ autoload :Documents, 'elastic/app-search/client/documents'
11
+ autoload :Engines, 'elastic/app-search/client/engines'
12
+ autoload :Search, 'elastic/app-search/client/search'
13
+ autoload :QuerySuggestion, 'elastic/app-search/client/query_suggestion'
14
+ autoload :SearchSettings, 'elastic/app-search/client/search_settings'
15
+
16
+ DEFAULT_TIMEOUT = 15
17
+
18
+ include Elastic::AppSearch::Request
19
+
20
+ attr_reader :api_key, :open_timeout, :overall_timeout, :api_endpoint
21
+
22
+ # Create a new Elastic::AppSearch::Client client
23
+ #
24
+ # @param options [Hash] a hash of configuration options that will override what is set on the Elastic::AppSearch class.
25
+ # @option options [String] :account_host_key or :host_identifier is your Host Identifier to use with this client.
26
+ # @option options [String] :api_key can be any of your API Keys. Each has a different scope, so ensure you are using the correct key.
27
+ # @option options [Numeric] :overall_timeout overall timeout for requests in seconds (default: 15s)
28
+ # @option options [Numeric] :open_timeout the number of seconds Net::HTTP (default: 15s)
29
+ # will wait while opening a connection before raising a Timeout::Error
30
+ def initialize(options = {})
31
+ @api_endpoint = options.fetch(:api_endpoint) { "https://#{options.fetch(:account_host_key) { options.fetch(:host_identifier) }}.api.swiftype.com/api/as/v1/" }
32
+ @api_key = options.fetch(:api_key)
33
+ @open_timeout = options.fetch(:open_timeout, DEFAULT_TIMEOUT).to_f
34
+ @overall_timeout = options.fetch(:overall_timeout, DEFAULT_TIMEOUT).to_f
35
+ end
36
+
37
+ module SignedSearchOptions
38
+ ALGORITHM = 'HS256'.freeze
39
+
40
+ module ClassMethods
41
+ # Build a JWT for authentication
42
+ #
43
+ # @param [String] api_key the API Key to sign the request with
44
+ # @param [String] api_key_name the unique name for the API Key
45
+ # @option options see the {App Search API}[https://swiftype.com/documentation/app-search/] for supported search options.
46
+ #
47
+ # @return [String] the JWT to use for authentication
48
+ def create_signed_search_key(api_key, api_key_name, options = {})
49
+ payload = Utils.symbolize_keys(options).merge(:api_key_name => api_key_name)
50
+ JWT.encode(payload, api_key, ALGORITHM)
51
+ end
52
+ end
53
+
54
+ def self.included(base)
55
+ base.extend(ClassMethods)
56
+ end
57
+ end
58
+
59
+ include Elastic::AppSearch::Client::Documents
60
+ include Elastic::AppSearch::Client::Engines
61
+ include Elastic::AppSearch::Client::Search
62
+ include Elastic::AppSearch::Client::SignedSearchOptions
63
+ include Elastic::AppSearch::Client::QuerySuggestion
64
+ include Elastic::AppSearch::Client::SearchSettings
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,91 @@
1
+ # Documents have fields that can be searched or filtered.
2
+ #
3
+ # For more information on indexing documents, see the {App Search documentation}[https://swiftype.com/documentation/app-search/].
4
+ module Elastic
5
+ module AppSearch
6
+ class Client
7
+ module Documents
8
+
9
+ # Retrieve all Documents from the API for the {App Search API}[https://swiftype.com/documentation/app-search/]
10
+ #
11
+ # @param [String] engine_name the unique Engine name
12
+ # @option options see the {App Search API}[https://swiftype.com/documentation/app-search/] for supported options.
13
+ #
14
+ # @return [Array<Hash>] an Array of Documents
15
+ def list_documents(engine_name, options = {})
16
+ params = Utils.symbolize_keys(options)
17
+ request(:get, "engines/#{engine_name}/documents/list", params)
18
+ end
19
+
20
+ # Retrieve Documents from the API by IDs for the {App Search API}[https://swiftype.com/documentation/app-search/]
21
+ #
22
+ # @param [String] engine_name the unique Engine name
23
+ # @param [Array<String>] ids an Array of Document IDs
24
+ #
25
+ # @return [Hash] list results
26
+ def get_documents(engine_name, ids)
27
+ get("engines/#{engine_name}/documents", ids)
28
+ end
29
+
30
+ # Index a document using the {App Search API}[https://swiftype.com/documentation/app-search/].
31
+ #
32
+ # @param [String] engine_name the unique Engine name
33
+ # @param [Array] document a Document Hash
34
+ #
35
+ # @return [Hash] processed Document Status hash
36
+ #
37
+ # @raise [Elastic::AppSearch::InvalidDocument] when the document has processing errors returned from the api
38
+ # @raise [Timeout::Error] when timeout expires waiting for statuses
39
+ def index_document(engine_name, document)
40
+ response = index_documents(engine_name, [document])
41
+ errors = response.first['errors']
42
+ raise InvalidDocument.new(errors.join('; ')) if errors.any?
43
+ response.first.tap { |h| h.delete('errors') }
44
+ end
45
+
46
+ # Index a batch of documents using the {App Search API}[https://swiftype.com/documentation/app-search/].
47
+ #
48
+ # @param [String] engine_name the unique Engine name
49
+ # @param [Array] documents an Array of Document Hashes
50
+ #
51
+ # @return [Array<Hash>] an Array of processed Document Status hashes
52
+ #
53
+ # @raise [Elastic::AppSearch::InvalidDocument] when any documents have processing errors returned from the api
54
+ # @raise [Timeout::Error] when timeout expires waiting for statuses
55
+ def index_documents(engine_name, documents)
56
+ documents.map! { |document| normalize_document(document) }
57
+ post("engines/#{engine_name}/documents", documents)
58
+ end
59
+
60
+ # Update a batch of documents using the {App Search API}[https://swiftype.com/documentation/app-search/].
61
+ #
62
+ # @param [String] engine_name the unique Engine name
63
+ # @param [Array] documents an Array of Document Hashes including valid ids
64
+ #
65
+ # @return [Array<Hash>] an Array of processed Document Status hashes
66
+ #
67
+ # @raise [Elastic::AppSearch::InvalidDocument] when any documents have processing errors returned from the api
68
+ # @raise [Timeout::Error] when timeout expires waiting for statuses
69
+ def update_documents(engine_name, documents)
70
+ documents.map! { |document| normalize_document(document) }
71
+ patch("engines/#{engine_name}/documents", documents)
72
+ end
73
+
74
+ # Destroy a batch of documents given a list of IDs
75
+ #
76
+ # @param [Array<String>] ids an Array of Document IDs
77
+ #
78
+ # @return [Array<Hash>] an Array of Document destroy result hashes
79
+ def destroy_documents(engine_name, ids)
80
+ delete("engines/#{engine_name}/documents", ids)
81
+ end
82
+
83
+ private
84
+
85
+ def normalize_document(document)
86
+ Utils.stringify_keys(document)
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,27 @@
1
+ # Engines are the core concept representing indexes in App Search.
2
+ #
3
+ module Elastic
4
+ module AppSearch
5
+ class Client
6
+ module Engines
7
+ def list_engines(current: 1, size: 20)
8
+ get("engines", :page => { :current => current, :size => size })
9
+ end
10
+
11
+ def get_engine(engine_name)
12
+ get("engines/#{engine_name}")
13
+ end
14
+
15
+ def create_engine(engine_name, language = nil)
16
+ params = { :name => engine_name }
17
+ params[:language] = language if language
18
+ post("engines", params)
19
+ end
20
+
21
+ def destroy_engine(engine_name)
22
+ delete("engines/#{engine_name}")
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module Elastic
2
+ module AppSearch
3
+ class Client
4
+ module QuerySuggestion
5
+ # Request Query Suggestions
6
+ #
7
+ # @param [String] engine_name the unique Engine name
8
+ # @param [String] query the search query to suggest for
9
+ # @options options see the {App Search API}[https://swiftype.com/documentation/app-search/] for supported search options.
10
+ #
11
+ # @return [Hash] search results
12
+ def query_suggestion(engine_name, query, options = {})
13
+ params = Utils.symbolize_keys(options).merge(:query => query)
14
+ request(:post, "engines/#{engine_name}/query_suggestion", params)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ module Elastic
2
+ module AppSearch
3
+ class Client
4
+ module Search
5
+ # Search for documents
6
+ #
7
+ # @param [String] engine_name the unique Engine name
8
+ # @param [String] query the search query
9
+ # @option options see the {App Search API}[https://swiftype.com/documentation/app-search/] for supported search options.
10
+ #
11
+ # @return [Hash] search results
12
+ def search(engine_name, query, options = {})
13
+ params = Utils.symbolize_keys(options).merge(:query => query)
14
+ request(:post, "engines/#{engine_name}/search", params)
15
+ end
16
+
17
+ # Run multiple searches for documents on a single request
18
+ #
19
+ # @param [String] engine_name the unique Engine name
20
+ # @param [{query: String, options: Hash}] searches to execute
21
+ # see the {App Search API}[https://swiftype.com/documentation/app-search/] for supported search options.
22
+ #
23
+ # @return [Array<Hash>] an Array of searh sesults
24
+ def multi_search(engine_name, searches)
25
+ params = searches.map do |search|
26
+ search = Utils.symbolize_keys(search)
27
+ query = search[:query]
28
+ options = search[:options] || {}
29
+ Utils.symbolize_keys(options).merge(:query => query)
30
+ end
31
+ request(:post, "engines/#{engine_name}/multi_search", {
32
+ queries: params
33
+ })
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ # Search Settings is used to adjust weights and boosts
2
+ module Elastic
3
+ module AppSearch
4
+ class Client
5
+ module SearchSettings
6
+
7
+ # Show all Weights and Boosts applied to the search fields of an Engine.
8
+ #
9
+ # @param [String] engine_name the unique Engine name
10
+ #
11
+ # @return [Hash] current Search Settings
12
+ def show_settings(engine_name)
13
+ get("engines/#{engine_name}/search_settings")
14
+ end
15
+
16
+ # Update Weights or Boosts for search fields of an Engine.
17
+ #
18
+ # @param [String] engine_name the unique Engine name
19
+ # @param [Hash] settings new Search Settings Hash
20
+ #
21
+ # @return [Hash] new Search Settings
22
+ def update_settings(engine_name, settings)
23
+ put("engines/#{engine_name}/search_settings", settings)
24
+ end
25
+
26
+ # Reset Engine's Search Settings to default values.
27
+ #
28
+ # @param [String] engine_name the unique Engine name
29
+ #
30
+ # @return [Hash] default Search Settings
31
+ def reset_settings(engine_name)
32
+ post("engines/#{engine_name}/search_settings/reset")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ module Elastic
2
+ module AppSearch
3
+ class ClientException < StandardError
4
+ attr_reader :errors
5
+
6
+ def initialize(response)
7
+ @errors = if response.is_a?(Array)
8
+ response.flat_map { |r| r['errors'] }
9
+ else
10
+ response['errors'] || [response]
11
+ end
12
+ message = (errors.count == 1) ? "Error: #{errors.first}" : "Errors: #{errors.inspect}"
13
+ super(message)
14
+ end
15
+ end
16
+
17
+ class NonExistentRecord < ClientException; end
18
+ class InvalidCredentials < ClientException; end
19
+ class BadRequest < ClientException; end
20
+ class Forbidden < ClientException; end
21
+ class InvalidDocument < ClientException; end
22
+ class RequestEntityTooLarge < ClientException; end
23
+
24
+ class UnexpectedHTTPException < ClientException
25
+ def initialize(response, response_json)
26
+ errors = (response_json['errors'] || [response.message]).map { |e| "(#{response.code}) #{e}" }
27
+ super({ 'errors' => errors })
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,147 @@
1
+ require 'net/https'
2
+ require 'json'
3
+ require 'time'
4
+ require 'elastic/app-search/exceptions'
5
+ require 'elastic/app-search/version'
6
+ require 'openssl'
7
+
8
+ module Elastic
9
+ module AppSearch
10
+ CLIENT_NAME = 'elastic-app-search-ruby'
11
+ CLIENT_VERSION = Elastic::AppSearch::VERSION
12
+
13
+ module Request
14
+ attr_accessor :last_request
15
+
16
+ def get(path, params={})
17
+ request(:get, path, params)
18
+ end
19
+
20
+ def post(path, params={})
21
+ request(:post, path, params)
22
+ end
23
+
24
+ def put(path, params={})
25
+ request(:put, path, params)
26
+ end
27
+
28
+ def patch(path, params={})
29
+ request(:patch, path, params)
30
+ end
31
+
32
+ def delete(path, params={})
33
+ request(:delete, path, params)
34
+ end
35
+
36
+ # Construct and send a request to the API.
37
+ #
38
+ # @raise [Timeout::Error] when the timeout expires
39
+ def request(method, path, params = {})
40
+ Timeout.timeout(overall_timeout) do
41
+ uri = URI.parse("#{api_endpoint}#{path}")
42
+
43
+ request = build_request(method, uri, params)
44
+ http = Net::HTTP.new(uri.host, uri.port)
45
+ http.open_timeout = open_timeout
46
+ http.read_timeout = overall_timeout
47
+
48
+ http.set_debug_output(STDERR) if debug?
49
+
50
+ if uri.scheme == 'https'
51
+ http.use_ssl = true
52
+ # st_ssl_verify_none provides a means to disable SSL verification for debugging purposes. An example
53
+ # is Charles, which uses a self-signed certificate in order to inspect https traffic. This will
54
+ # not be part of this client's public API, this is more of a development enablement option
55
+ http.verify_mode = ENV['st_ssl_verify_none'] == 'true' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
56
+ http.ca_file = File.join(File.dirname(__FILE__), '../..', 'data', 'ca-bundle.crt')
57
+ http.ssl_timeout = open_timeout
58
+ end
59
+
60
+ @last_request = request
61
+
62
+ response = http.request(request)
63
+ response_json = parse_response(response)
64
+
65
+ case response
66
+ when Net::HTTPSuccess
67
+ return response_json
68
+ when Net::HTTPBadRequest
69
+ raise Elastic::AppSearch::BadRequest, response_json
70
+ when Net::HTTPUnauthorized
71
+ raise Elastic::AppSearch::InvalidCredentials, response_json
72
+ when Net::HTTPNotFound
73
+ raise Elastic::AppSearch::NonExistentRecord, response_json
74
+ when Net::HTTPForbidden
75
+ raise Elastic::AppSearch::Forbidden, response_json
76
+ when Net::HTTPRequestEntityTooLarge
77
+ raise Elastic::AppSearch::RequestEntityTooLarge, response_json
78
+ else
79
+ raise Elastic::AppSearch::UnexpectedHTTPException.new(response, response_json)
80
+ end
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ def parse_response(response)
87
+ body = response.body.to_s.strip
88
+ body == '' ? {} : JSON.parse(body)
89
+ end
90
+
91
+ def debug?
92
+ @debug ||= (ENV['AS_DEBUG'] == 'true')
93
+ end
94
+
95
+ def serialize_json(object)
96
+ JSON.generate(clean_json(object))
97
+ end
98
+
99
+ def clean_json(object)
100
+ case object
101
+ when Hash
102
+ object.inject({}) do |builder, (key, value)|
103
+ builder[key] = clean_json(value)
104
+ builder
105
+ end
106
+ when Enumerable
107
+ object.map { |value| clean_json(value) }
108
+ else
109
+ clean_atom(object)
110
+ end
111
+ end
112
+
113
+ def clean_atom(atom)
114
+ if atom.is_a?(Time)
115
+ atom.to_datetime
116
+ else
117
+ atom
118
+ end
119
+ end
120
+
121
+ def build_request(method, uri, params)
122
+ klass = case method
123
+ when :get
124
+ Net::HTTP::Get
125
+ when :post
126
+ Net::HTTP::Post
127
+ when :put
128
+ Net::HTTP::Put
129
+ when :patch
130
+ Net::HTTP::Patch
131
+ when :delete
132
+ Net::HTTP::Delete
133
+ end
134
+
135
+ req = klass.new(uri.request_uri)
136
+ req.body = serialize_json(params) unless params.length == 0
137
+
138
+ req['X-Swiftype-Client'] = CLIENT_NAME
139
+ req['X-Swiftype-Client-Version'] = CLIENT_VERSION
140
+ req['Content-Type'] = 'application/json'
141
+ req['Authorization'] = "Bearer #{api_key}"
142
+
143
+ req
144
+ end
145
+ end
146
+ end
147
+ end