elastic-app-search 0.7.0

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