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