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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +56 -0
- data/.gitignore +10 -0
- data/.rspec +4 -0
- data/.rubocop.yml +1416 -0
- data/.travis.yml +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +201 -0
- data/NOTICE.txt +3 -0
- data/README.md +320 -0
- data/Rakefile +1 -0
- data/elastic-app-search.gemspec +25 -0
- data/lib/data/ca-bundle.crt +3338 -0
- data/lib/elastic/app-search.rb +6 -0
- data/lib/elastic/app-search/client.rb +67 -0
- data/lib/elastic/app-search/client/documents.rb +91 -0
- data/lib/elastic/app-search/client/engines.rb +27 -0
- data/lib/elastic/app-search/client/query_suggestion.rb +19 -0
- data/lib/elastic/app-search/client/search.rb +38 -0
- data/lib/elastic/app-search/client/search_settings.rb +37 -0
- data/lib/elastic/app-search/exceptions.rb +31 -0
- data/lib/elastic/app-search/request.rb +147 -0
- data/lib/elastic/app-search/utils.rb +20 -0
- data/lib/elastic/app-search/version.rb +5 -0
- data/logo-app-search.png +0 -0
- data/script/console +9 -0
- data/spec/client_spec.rb +500 -0
- data/spec/config_helper.rb +22 -0
- data/spec/spec_helper.rb +26 -0
- metadata +151 -0
@@ -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
|