elastic-site-search 2.0.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 +58 -0
- data/.gitignore +10 -0
- data/.travis.yml +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +201 -0
- data/NOTICE.txt +3 -0
- data/README.md +418 -0
- data/Rakefile +1 -0
- data/elastic-site-search.gemspec +26 -0
- data/lib/data/ca-bundle.crt +3554 -0
- data/lib/elastic/site-search.rb +9 -0
- data/lib/elastic/site-search/client.rb +527 -0
- data/lib/elastic/site-search/configuration.rb +67 -0
- data/lib/elastic/site-search/exceptions.rb +11 -0
- data/lib/elastic/site-search/ext/backport-uri.rb +33 -0
- data/lib/elastic/site-search/request.rb +156 -0
- data/lib/elastic/site-search/result_set.rb +84 -0
- data/lib/elastic/site-search/sso.rb +22 -0
- data/lib/elastic/site-search/version.rb +5 -0
- data/logo-site-search.png +0 -0
- data/spec/client_spec.rb +728 -0
- data/spec/configuration_spec.rb +37 -0
- data/spec/fixtures/vcr/analytics_autoselects.yml +57 -0
- data/spec/fixtures/vcr/analytics_autoselects_with_document_type.yml +57 -0
- data/spec/fixtures/vcr/analytics_autoselects_with_document_type_and_time_range.yml +57 -0
- data/spec/fixtures/vcr/analytics_autoselects_with_time_range.yml +57 -0
- data/spec/fixtures/vcr/analytics_clicks.yml +57 -0
- data/spec/fixtures/vcr/analytics_clicks_with_document_type.yml +57 -0
- data/spec/fixtures/vcr/analytics_clicks_with_document_type_and_time_range.yml +57 -0
- data/spec/fixtures/vcr/analytics_clicks_with_time_range.yml +57 -0
- data/spec/fixtures/vcr/analytics_searches.yml +57 -0
- data/spec/fixtures/vcr/analytics_searches_with_document_type_and_time_range.yml +57 -0
- data/spec/fixtures/vcr/analytics_searches_with_time_range.yml +57 -0
- data/spec/fixtures/vcr/analytics_searchs_with_document_type.yml +57 -0
- data/spec/fixtures/vcr/analytics_top_no_result_queries.yml +57 -0
- data/spec/fixtures/vcr/analytics_top_no_result_queries_paginated.yml +57 -0
- data/spec/fixtures/vcr/analytics_top_queries.yml +57 -0
- data/spec/fixtures/vcr/analytics_top_queries_paginated.yml +57 -0
- data/spec/fixtures/vcr/analytics_top_queries_too_large.yml +53 -0
- data/spec/fixtures/vcr/async_create_or_update_document_failure.yml +48 -0
- data/spec/fixtures/vcr/async_create_or_update_document_success.yml +52 -0
- data/spec/fixtures/vcr/bulk_create_documents.yml +52 -0
- data/spec/fixtures/vcr/bulk_create_or_update_documents_failure.yml +47 -0
- data/spec/fixtures/vcr/bulk_create_or_update_documents_success.yml +52 -0
- data/spec/fixtures/vcr/bulk_create_or_update_documents_verbose_failure.yml +47 -0
- data/spec/fixtures/vcr/bulk_create_or_update_documents_verbose_success.yml +52 -0
- data/spec/fixtures/vcr/bulk_destroy_documents.yml +47 -0
- data/spec/fixtures/vcr/crawl_url.yml +47 -0
- data/spec/fixtures/vcr/create_document.yml +52 -0
- data/spec/fixtures/vcr/create_document_type.yml +47 -0
- data/spec/fixtures/vcr/create_domain.yml +47 -0
- data/spec/fixtures/vcr/create_engine.yml +47 -0
- data/spec/fixtures/vcr/create_or_update_document_create.yml +51 -0
- data/spec/fixtures/vcr/create_or_update_document_update.yml +51 -0
- data/spec/fixtures/vcr/create_user.yml +47 -0
- data/spec/fixtures/vcr/destroy_document.yml +43 -0
- data/spec/fixtures/vcr/destroy_document_type.yml +43 -0
- data/spec/fixtures/vcr/destroy_domain.yml +43 -0
- data/spec/fixtures/vcr/destroy_engine.yml +43 -0
- data/spec/fixtures/vcr/destroy_non_existent_document_type.yml +45 -0
- data/spec/fixtures/vcr/document_receipts_multiple.yml +48 -0
- data/spec/fixtures/vcr/document_receipts_multiple_complete.yml +48 -0
- data/spec/fixtures/vcr/document_type_search.yml +63 -0
- data/spec/fixtures/vcr/document_type_search_pagination.yml +47 -0
- data/spec/fixtures/vcr/document_type_suggest.yml +52 -0
- data/spec/fixtures/vcr/document_type_suggest_pagination.yml +47 -0
- data/spec/fixtures/vcr/engine_search.yml +67 -0
- data/spec/fixtures/vcr/engine_search_facets.yml +164 -0
- data/spec/fixtures/vcr/engine_search_pagination.yml +47 -0
- data/spec/fixtures/vcr/engine_suggest.yml +55 -0
- data/spec/fixtures/vcr/engine_suggest_pagination.yml +47 -0
- data/spec/fixtures/vcr/find_document.yml +50 -0
- data/spec/fixtures/vcr/find_document_type.yml +47 -0
- data/spec/fixtures/vcr/find_domain.yml +47 -0
- data/spec/fixtures/vcr/find_domain_failure.yml +45 -0
- data/spec/fixtures/vcr/find_engine.yml +47 -0
- data/spec/fixtures/vcr/list_document_type.yml +47 -0
- data/spec/fixtures/vcr/list_documents.yml +71 -0
- data/spec/fixtures/vcr/list_documents_with_pagination.yml +71 -0
- data/spec/fixtures/vcr/list_domains.yml +47 -0
- data/spec/fixtures/vcr/list_engines.yml +48 -0
- data/spec/fixtures/vcr/list_users.yml +47 -0
- data/spec/fixtures/vcr/list_users_with_pagination.yml +47 -0
- data/spec/fixtures/vcr/log_clickthrough_failure.yml +45 -0
- data/spec/fixtures/vcr/log_clickthrough_success.yml +47 -0
- data/spec/fixtures/vcr/recrawl_domain_failure.yml +46 -0
- data/spec/fixtures/vcr/recrawl_domain_success.yml +47 -0
- data/spec/fixtures/vcr/show_user.yml +47 -0
- data/spec/fixtures/vcr/update_document.yml +50 -0
- data/spec/fixtures/vcr/update_document_unknown_field_failure.yml +48 -0
- data/spec/fixtures/vcr/update_documents_failure_non_existent_document.yml +47 -0
- data/spec/fixtures/vcr/update_documents_success.yml +47 -0
- data/spec/fixtures/vcr/users_client_secret_incorrect.yml +44 -0
- data/spec/fixtures/vcr/users_no_api_key.yml +44 -0
- data/spec/fixtures/vcr/users_no_client_id_or_secret.yml +45 -0
- data/spec/platform_spec.rb +95 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/ssl_spec.rb +34 -0
- data/spec/sso_spec.rb +24 -0
- metadata +279 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'elastic/site-search/version'
|
|
3
|
+
|
|
4
|
+
module Elastic
|
|
5
|
+
module SiteSearch
|
|
6
|
+
module Configuration
|
|
7
|
+
DEFAULT_ENDPOINT = "https://api.swiftype.com/api/v1/"
|
|
8
|
+
|
|
9
|
+
VALID_OPTIONS_KEYS = [
|
|
10
|
+
:api_key,
|
|
11
|
+
:user_agent,
|
|
12
|
+
:platform_client_id,
|
|
13
|
+
:platform_client_secret,
|
|
14
|
+
:endpoint
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
attr_accessor *VALID_OPTIONS_KEYS
|
|
18
|
+
|
|
19
|
+
def self.extended(base)
|
|
20
|
+
base.reset
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Reset configuration to default values.
|
|
24
|
+
def reset
|
|
25
|
+
self.api_key = nil
|
|
26
|
+
self.endpoint = DEFAULT_ENDPOINT
|
|
27
|
+
self.user_agent = nil
|
|
28
|
+
self.platform_client_id = nil
|
|
29
|
+
self.platform_client_secret = nil
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Yields the Elastic::SiteSearch::Configuration module which can be used to set configuration options.
|
|
34
|
+
#
|
|
35
|
+
# @return self
|
|
36
|
+
def configure
|
|
37
|
+
yield self
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Return a hash of the configured options.
|
|
42
|
+
def options
|
|
43
|
+
options = {}
|
|
44
|
+
VALID_OPTIONS_KEYS.each{|k| options[k] = send(k)}
|
|
45
|
+
options
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Set api_key and endpoint based on a URL with HTTP authentication.
|
|
49
|
+
def authenticated_url=(url)
|
|
50
|
+
uri = URI(url)
|
|
51
|
+
self.api_key = uri.user
|
|
52
|
+
uri.user = nil
|
|
53
|
+
uri.password = nil
|
|
54
|
+
self.endpoint = uri.to_s
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# setter for endpoint that ensures it always ends in '/'
|
|
58
|
+
def endpoint=(endpoint)
|
|
59
|
+
if endpoint.end_with?('/')
|
|
60
|
+
@endpoint = endpoint
|
|
61
|
+
else
|
|
62
|
+
@endpoint = "#{endpoint}/"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Elastic
|
|
2
|
+
module SiteSearch
|
|
3
|
+
class ClientException < StandardError; end
|
|
4
|
+
class NonExistentRecord < ClientException; end
|
|
5
|
+
class RecordAlreadyExists < ClientException; end
|
|
6
|
+
class InvalidCredentials < ClientException; end
|
|
7
|
+
class BadRequest < ClientException; end
|
|
8
|
+
class Forbidden < ClientException; end
|
|
9
|
+
class UnexpectedHTTPException < ClientException; end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# :stopdoc:
|
|
2
|
+
|
|
3
|
+
# Stolen from ruby core's uri/common.rb, with modifications to support 1.8.x
|
|
4
|
+
#
|
|
5
|
+
# https://github.com/ruby/ruby/blob/trunk/lib/uri/common.rb
|
|
6
|
+
#
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
module URI
|
|
10
|
+
def self.encode_www_form(enum)
|
|
11
|
+
enum.map do |k,v|
|
|
12
|
+
if v.nil?
|
|
13
|
+
encode_www_form_component(k)
|
|
14
|
+
elsif v.respond_to?(:to_ary)
|
|
15
|
+
v.to_ary.map do |w|
|
|
16
|
+
str = encode_www_form_component(k)
|
|
17
|
+
unless w.nil?
|
|
18
|
+
str << '='
|
|
19
|
+
str << encode_www_form_component(w)
|
|
20
|
+
end
|
|
21
|
+
end.join('&')
|
|
22
|
+
else
|
|
23
|
+
str = encode_www_form_component(k)
|
|
24
|
+
str << '='
|
|
25
|
+
str << encode_www_form_component(v)
|
|
26
|
+
end
|
|
27
|
+
end.join('&')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.encode_www_form_component(str)
|
|
31
|
+
str.to_s.gsub(/[^*\-.0-9A-Z_a-z]/) { |chr| TBLENCWWWCOMP_[chr] }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
require 'net/https'
|
|
2
|
+
if RUBY_VERSION < '1.9'
|
|
3
|
+
require 'elastic/site-search/ext/backport-uri'
|
|
4
|
+
else
|
|
5
|
+
require 'uri'
|
|
6
|
+
end
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'elastic/site-search/exceptions'
|
|
9
|
+
require 'openssl'
|
|
10
|
+
|
|
11
|
+
module Elastic
|
|
12
|
+
module SiteSearch
|
|
13
|
+
CLIENT_NAME = 'elastic-site-search-ruby'
|
|
14
|
+
CLIENT_VERSION = Elastic::SiteSearch::VERSION
|
|
15
|
+
|
|
16
|
+
module Request
|
|
17
|
+
def get(path, params={})
|
|
18
|
+
request(:get, path, params)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def post(path, params={})
|
|
22
|
+
request(:post, path, params)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def put(path, params={})
|
|
26
|
+
request(:put, path, params)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def delete(path, params={})
|
|
30
|
+
request(:delete, path, params)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Poll a block with backoff until a timeout is reached.
|
|
34
|
+
#
|
|
35
|
+
# @param [Hash] options optional arguments
|
|
36
|
+
# @option options [Numeric] :timeout (10) Number of seconds to wait before timing out
|
|
37
|
+
#
|
|
38
|
+
# @yieldreturn a truthy value to return from poll
|
|
39
|
+
# @yieldreturn [false] to continue polling.
|
|
40
|
+
#
|
|
41
|
+
# @return the truthy value returned from the block.
|
|
42
|
+
#
|
|
43
|
+
# @raise [Timeout::Error] when the timeout expires
|
|
44
|
+
def poll(options={})
|
|
45
|
+
timeout = options[:timeout] || 10
|
|
46
|
+
delay = 0.05
|
|
47
|
+
Timeout.timeout(timeout) do
|
|
48
|
+
while true
|
|
49
|
+
res = yield
|
|
50
|
+
return res if res
|
|
51
|
+
sleep delay *= 2
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Construct and send a request to the API.
|
|
57
|
+
#
|
|
58
|
+
# @raise [Timeout::Error] when the timeout expires
|
|
59
|
+
def request(method, path, params={})
|
|
60
|
+
Timeout.timeout(overall_timeout) do
|
|
61
|
+
uri = URI.parse("#{Elastic::SiteSearch.endpoint}#{path}")
|
|
62
|
+
|
|
63
|
+
request = build_request(method, uri, params)
|
|
64
|
+
|
|
65
|
+
if proxy
|
|
66
|
+
proxy_parts = URI.parse(proxy)
|
|
67
|
+
http = Net::HTTP.new(uri.host, uri.port, proxy_parts.host, proxy_parts.port, proxy_parts.user, proxy_parts.password)
|
|
68
|
+
else
|
|
69
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
http.open_timeout = open_timeout
|
|
73
|
+
http.read_timeout = overall_timeout
|
|
74
|
+
|
|
75
|
+
if uri.scheme == 'https'
|
|
76
|
+
http.use_ssl = true
|
|
77
|
+
# st_ssl_verify_none provides a means to disable SSL verification for debugging purposes. An example
|
|
78
|
+
# is Charles, which uses a self-signed certificate in order to inspect https traffic. This will
|
|
79
|
+
# not be part of this client's public API, this is more of a development enablement option
|
|
80
|
+
http.verify_mode = ENV['st_ssl_verify_none'] == 'true' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
|
|
81
|
+
http.ca_file = File.join(File.dirname(__FILE__), '../..', 'data', 'ca-bundle.crt')
|
|
82
|
+
http.ssl_timeout = open_timeout
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
response = http.request(request)
|
|
86
|
+
handle_errors(response)
|
|
87
|
+
JSON.parse(response.body) if response.body && response.body.strip != ''
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
def handle_errors(response)
|
|
93
|
+
case response
|
|
94
|
+
when Net::HTTPSuccess
|
|
95
|
+
response
|
|
96
|
+
when Net::HTTPUnauthorized
|
|
97
|
+
raise Elastic::SiteSearch::InvalidCredentials, error_message_from_response(response)
|
|
98
|
+
when Net::HTTPNotFound
|
|
99
|
+
raise Elastic::SiteSearch::NonExistentRecord, error_message_from_response(response)
|
|
100
|
+
when Net::HTTPConflict
|
|
101
|
+
raise Elastic::SiteSearch::RecordAlreadyExists, error_message_from_response(response)
|
|
102
|
+
when Net::HTTPBadRequest
|
|
103
|
+
raise Elastic::SiteSearch::BadRequest, error_message_from_response(response)
|
|
104
|
+
when Net::HTTPForbidden
|
|
105
|
+
raise Elastic::SiteSearch::Forbidden, error_message_from_response(response)
|
|
106
|
+
else
|
|
107
|
+
raise Elastic::SiteSearch::UnexpectedHTTPException, "#{response.code} #{response.body}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def error_message_from_response(response)
|
|
112
|
+
body = response.body
|
|
113
|
+
json = JSON.parse(body) if body && body.strip != ''
|
|
114
|
+
return json['error'] if json && json.key?('error')
|
|
115
|
+
body
|
|
116
|
+
rescue JSON::ParserError
|
|
117
|
+
body
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def build_request(method, uri, params)
|
|
121
|
+
klass = case method
|
|
122
|
+
when :get
|
|
123
|
+
Net::HTTP::Get
|
|
124
|
+
when :post
|
|
125
|
+
Net::HTTP::Post
|
|
126
|
+
when :put
|
|
127
|
+
Net::HTTP::Put
|
|
128
|
+
when :delete
|
|
129
|
+
Net::HTTP::Delete
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
case method
|
|
133
|
+
when :get, :delete
|
|
134
|
+
uri.query = URI.encode_www_form(params) if params && !params.empty?
|
|
135
|
+
req = klass.new(uri.request_uri)
|
|
136
|
+
when :post, :put
|
|
137
|
+
req = klass.new(uri.request_uri)
|
|
138
|
+
req.body = JSON.generate(params) unless params.length == 0
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
req['User-Agent'] = Elastic::SiteSearch.user_agent if Elastic::SiteSearch.user_agent
|
|
142
|
+
req['Content-Type'] = 'application/json'
|
|
143
|
+
req['X-Swiftype-Client'] = CLIENT_NAME
|
|
144
|
+
req['X-Swiftype-Client-Version'] = CLIENT_VERSION
|
|
145
|
+
|
|
146
|
+
if platform_access_token
|
|
147
|
+
req['Authorization'] = "Bearer #{platform_access_token}"
|
|
148
|
+
elsif api_key
|
|
149
|
+
req.basic_auth api_key, ''
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
req
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
module Elastic
|
|
2
|
+
module SiteSearch
|
|
3
|
+
# The Elastic::SiteSearch::ResultSet class represents a {search}[https://swiftype.com/documentation/site-search/searching]
|
|
4
|
+
# or {suggest result}[https://swiftype.com/documentation/site-search/autocomplete] returned by the Elastic::SiteSearch API.
|
|
5
|
+
class ResultSet
|
|
6
|
+
# @attribute errors [r]
|
|
7
|
+
# a hash of errors for the search (for example filtering on a missing attribute) keyed by DocumentType slug
|
|
8
|
+
attr_reader :errors
|
|
9
|
+
|
|
10
|
+
# @attribute records [r]
|
|
11
|
+
# a hash of results for the search keyed by DocumentType slug. Use `[]` to access results more easily.
|
|
12
|
+
attr_reader :records
|
|
13
|
+
|
|
14
|
+
# @attribute info [r]
|
|
15
|
+
# a hash of extra query info (for example, facets and number of results) keyed by DocumentType slug.
|
|
16
|
+
# Use the convenience methods of this class for easier access.
|
|
17
|
+
attr_reader :info
|
|
18
|
+
|
|
19
|
+
# Create a Elastic::SiteSearch::ResultSet from deserialized JSON.
|
|
20
|
+
def initialize(results)
|
|
21
|
+
@records = results['records']
|
|
22
|
+
@info = results['info']
|
|
23
|
+
@errors = results['errors']
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get results for the provided DocumentType
|
|
27
|
+
#
|
|
28
|
+
# @param [String] document_type the DocumentType slug to get results for
|
|
29
|
+
def [](document_type)
|
|
30
|
+
records[document_type]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Return a list of DocumentType slugs represented in the ResultSet.
|
|
34
|
+
def document_types
|
|
35
|
+
records.keys
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Return the search facets for the provided DocumentType. Will be
|
|
39
|
+
# empty unless a facets parameter was provided when calling the
|
|
40
|
+
# search API.
|
|
41
|
+
#
|
|
42
|
+
# @param [String] document_type the DocumentType slug to get facets for
|
|
43
|
+
def facets(document_type)
|
|
44
|
+
info[document_type]['facets']
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Return the page of results for this ResultSet
|
|
48
|
+
def current_page
|
|
49
|
+
info[info.keys.first]['current_page']
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Return the number of results per page.
|
|
53
|
+
def per_page
|
|
54
|
+
info[info.keys.first]['per_page']
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Return the number of pages. Since a search can cover multiple
|
|
58
|
+
# DocumentTypes with different numbers of results, the number of
|
|
59
|
+
# pages can vary between DocumentTypes. With no argument, it
|
|
60
|
+
# returns the maximum num_pages for all DocumentTypes in this
|
|
61
|
+
# ResultSet. With a DocumentType slug, it returns the number of
|
|
62
|
+
# pages for that DocumentType.
|
|
63
|
+
#
|
|
64
|
+
# @param [String] document_type the DocumentType slug to return the number of pages for
|
|
65
|
+
def num_pages(document_type=nil)
|
|
66
|
+
if document_type
|
|
67
|
+
info[document_type]['num_pages']
|
|
68
|
+
else
|
|
69
|
+
info.values.map { |v| v['num_pages'] }.max
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Return the total number of results for the query
|
|
74
|
+
def total_result_count(document_type)
|
|
75
|
+
info[document_type]['total_result_count']
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Return the query used for this search
|
|
79
|
+
def query
|
|
80
|
+
info[info.keys.first]['query']
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'digest/sha1'
|
|
2
|
+
|
|
3
|
+
module Elastic
|
|
4
|
+
module SiteSearch
|
|
5
|
+
# Single sign-on for the Site Search Dashboard.
|
|
6
|
+
module SSO
|
|
7
|
+
BASE_URL = 'https://swiftype.com/sso'
|
|
8
|
+
|
|
9
|
+
# Generate a URL that a user can click on to be logged into the Site Search Dashboard.
|
|
10
|
+
# This requires the +platform_client_id+ and +platform_client_secret+ configuration options be set.
|
|
11
|
+
def self.url(user_id)
|
|
12
|
+
timestamp = Time.now.to_i
|
|
13
|
+
|
|
14
|
+
"#{BASE_URL}?user_id=#{user_id}&client_id=#{Elastic::SiteSearch.platform_client_id}×tamp=#{timestamp}&token=#{token(user_id, timestamp)}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.token(user_id, timestamp)
|
|
18
|
+
Digest::SHA1.hexdigest("#{user_id}:#{Elastic::SiteSearch.platform_client_secret}:#{timestamp}")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
Binary file
|
data/spec/client_spec.rb
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Elastic::SiteSearch::Client do
|
|
4
|
+
let(:engine_slug) { 'site-search-api-example' }
|
|
5
|
+
let(:client) { Elastic::SiteSearch::Client.new }
|
|
6
|
+
|
|
7
|
+
before :each do
|
|
8
|
+
Elastic::SiteSearch.api_key = 'hello'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
context 'Search' do
|
|
12
|
+
context '#search' do
|
|
13
|
+
it 'searches all DocumentTypes in the engine' do
|
|
14
|
+
VCR.use_cassette(:engine_search) do
|
|
15
|
+
results = client.search(engine_slug, 'cat')
|
|
16
|
+
expect(results.document_types.size).to eq(2)
|
|
17
|
+
expect(results['videos'].size).to eq(2)
|
|
18
|
+
expect(results['channels'].size).to eq(1)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'searches the engine with options' do
|
|
23
|
+
VCR.use_cassette(:engine_search_pagination) do
|
|
24
|
+
results = client.search(engine_slug, 'cat', {:page => 2})
|
|
25
|
+
expect(results.document_types.size).to eq(2)
|
|
26
|
+
expect(results['videos'].size).to eq(0)
|
|
27
|
+
expect(results['channels'].size).to eq(0)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'includes facets when requested' do
|
|
32
|
+
VCR.use_cassette(:engine_search_facets) do
|
|
33
|
+
results = client.search(engine_slug, nil, {:facets => {:videos => ['category_id']}})
|
|
34
|
+
expect(results.document_types.size).to eq(2)
|
|
35
|
+
expect(results.facets('channels')).to be_empty
|
|
36
|
+
expect(results.facets('videos')['category_id']).to eq({
|
|
37
|
+
"23" => 4,
|
|
38
|
+
"28" => 2,
|
|
39
|
+
"15" => 2,
|
|
40
|
+
"25" => 1,
|
|
41
|
+
"22" => 1,
|
|
42
|
+
"2" => 1,
|
|
43
|
+
"10" => 1
|
|
44
|
+
})
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context '#search_document_type' do
|
|
50
|
+
let(:document_type_slug) { 'videos' }
|
|
51
|
+
|
|
52
|
+
it 'searches only the provided DocumentType' do
|
|
53
|
+
VCR.use_cassette(:document_type_search) do
|
|
54
|
+
results = client.search_document_type(engine_slug, document_type_slug, 'cat')
|
|
55
|
+
expect(results.document_types).to eq(['videos'])
|
|
56
|
+
expect(results['videos'].size).to eq(2)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'searches the DocumentType with options' do
|
|
61
|
+
VCR.use_cassette(:document_type_search_pagination) do
|
|
62
|
+
results = client.search_document_type(engine_slug, document_type_slug, 'cat', {:page => 2})
|
|
63
|
+
expect(results.document_types).to eq(['videos'])
|
|
64
|
+
expect(results[document_type_slug].size).to eq(0)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
context 'Options' do
|
|
71
|
+
let(:options_client) { Elastic::SiteSearch::Client.new(options) }
|
|
72
|
+
|
|
73
|
+
context '#request' do
|
|
74
|
+
context 'open_timeout' do
|
|
75
|
+
let(:options) { { :open_timeout => 3 } }
|
|
76
|
+
it 'respects the Net::HTTP open_timeout option' do
|
|
77
|
+
expect(options_client.open_timeout).to eq(3)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context 'overall_timeout' do
|
|
82
|
+
context 'with timeout specified' do
|
|
83
|
+
let(:options) { { :overall_timeout => 0.001 } }
|
|
84
|
+
it 'respects the overall timeout option' do
|
|
85
|
+
expect(options_client.overall_timeout).to eq(0.001)
|
|
86
|
+
allow_any_instance_of(Net::HTTP).to receive(:request) { sleep 3 }
|
|
87
|
+
expect {
|
|
88
|
+
options_client.search(engine_slug, 'cat')
|
|
89
|
+
}.to raise_error(Timeout::Error)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context 'without timeout specified' do
|
|
94
|
+
let(:options) { Hash.new }
|
|
95
|
+
it 'omits the option' do
|
|
96
|
+
expect(options_client.overall_timeout).to eq(Elastic::SiteSearch::Client::DEFAULT_TIMEOUT.to_f)
|
|
97
|
+
expect(Timeout).to receive(:timeout).with(Elastic::SiteSearch::Client::DEFAULT_TIMEOUT.to_f).and_call_original
|
|
98
|
+
VCR.use_cassette(:engine_search) do
|
|
99
|
+
options_client.search(engine_slug, 'cat')
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context 'with proxy specified' do
|
|
106
|
+
let(:options) { { :proxy => 'http://username:password@localhost:8888' } }
|
|
107
|
+
|
|
108
|
+
it 'will set proxy' do
|
|
109
|
+
expect(options_client.proxy).to eq('http://username:password@localhost:8888')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# There doesn't seem to be an elgant way to test that a request actually uses a proxy, so the best
|
|
113
|
+
# we can do here is ensure that the behavior for methods operates normally
|
|
114
|
+
it 'will execute methods with proxy' do
|
|
115
|
+
VCR.use_cassette(:engine_search) do
|
|
116
|
+
results = options_client.search(engine_slug, 'cat')
|
|
117
|
+
expect(results.document_types.size).to eq(2)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
context 'Suggest' do
|
|
125
|
+
context '#suggest' do
|
|
126
|
+
it 'does prefix searches for all DocumentTypes in the engine' do
|
|
127
|
+
VCR.use_cassette(:engine_suggest) do
|
|
128
|
+
results = client.suggest(engine_slug, 'goo')
|
|
129
|
+
expect(results.document_types.size).to eq(2)
|
|
130
|
+
expect(results['videos'].size).to eq(1)
|
|
131
|
+
expect(results['channels'].size).to eq(1)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'suggests for an engine with options' do
|
|
136
|
+
VCR.use_cassette(:engine_suggest_pagination) do
|
|
137
|
+
results = client.suggest(engine_slug, 'goo', {:page => 2})
|
|
138
|
+
expect(results.document_types.size).to eq(2)
|
|
139
|
+
expect(results['videos'].size).to eq(0)
|
|
140
|
+
expect(results['channels'].size).to eq(0)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
context '#suggest_document_type' do
|
|
146
|
+
let(:document_type_slug) { 'videos' }
|
|
147
|
+
|
|
148
|
+
it 'does a prefix search on the provided DocumentType' do
|
|
149
|
+
VCR.use_cassette(:document_type_suggest) do
|
|
150
|
+
results = client.suggest_document_type(engine_slug, document_type_slug, 'goo')
|
|
151
|
+
expect(results.document_types).to eq(['videos'])
|
|
152
|
+
expect(results['videos'].size).to eq(1)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'suggests for a document types with options' do
|
|
157
|
+
VCR.use_cassette(:document_type_suggest_pagination) do
|
|
158
|
+
results = client.suggest_document_type(engine_slug, document_type_slug, 'goo', {:page => 2})
|
|
159
|
+
expect(results.document_types).to eq(['videos'])
|
|
160
|
+
expect(results[document_type_slug].size).to eq(0)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
context 'Engine' do
|
|
168
|
+
it 'gets all engines' do
|
|
169
|
+
VCR.use_cassette(:list_engines) do
|
|
170
|
+
engines = client.engines
|
|
171
|
+
expect(engines.size).to eq(6)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'gets an engine' do
|
|
176
|
+
VCR.use_cassette(:find_engine) do
|
|
177
|
+
engine = client.engine(engine_slug)
|
|
178
|
+
expect(engine['slug']).to eq(engine_slug)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it 'creates engines' do
|
|
183
|
+
VCR.use_cassette(:create_engine) do
|
|
184
|
+
engine = client.create_engine('new engine from spec')
|
|
185
|
+
expect(engine['slug']).to eq('new-engine-from-spec')
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it 'destroys engines' do
|
|
190
|
+
VCR.use_cassette(:destroy_engine) do
|
|
191
|
+
response = client.destroy_engine('new-engine-from-spec')
|
|
192
|
+
expect(response).to be_nil
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
context 'DocumentType' do
|
|
198
|
+
let(:document_type_slug) { 'videos' }
|
|
199
|
+
|
|
200
|
+
it 'gets all document types' do
|
|
201
|
+
VCR.use_cassette(:list_document_type) do
|
|
202
|
+
document_types = client.document_types(engine_slug)
|
|
203
|
+
expect(document_types.size).to eq(2)
|
|
204
|
+
expect(document_types.map { |dt| dt['name'] }.sort).to eq(['channels', 'videos'])
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'gets a document type' do
|
|
209
|
+
VCR.use_cassette(:find_document_type) do
|
|
210
|
+
document_type = client.document_type(engine_slug, document_type_slug)
|
|
211
|
+
expect(document_type['slug']).to eq(document_type_slug)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
it 'creates a document type' do
|
|
216
|
+
VCR.use_cassette(:create_document_type) do
|
|
217
|
+
name = document_type_slug
|
|
218
|
+
document_type = client.create_document_type(engine_slug, 'new_doc_type')
|
|
219
|
+
expect(document_type['name']).to eq('new_doc_type')
|
|
220
|
+
expect(document_type['slug']).to eq('new-doc-type')
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it 'destroys document types' do
|
|
225
|
+
VCR.use_cassette(:destroy_document_type) do
|
|
226
|
+
response = client.destroy_document_type(engine_slug, 'new-doc-type')
|
|
227
|
+
expect(response).to be_nil
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
it 'raises an error if deleting a non-existent DocumentType' do
|
|
232
|
+
VCR.use_cassette(:destroy_non_existent_document_type) do
|
|
233
|
+
expect do
|
|
234
|
+
response = client.destroy_document_type(engine_slug, 'not_there')
|
|
235
|
+
end.to raise_error
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
context 'Document' do
|
|
241
|
+
|
|
242
|
+
def check_async_response_format(response, options = {})
|
|
243
|
+
expect(response.keys).to match_array(["document_receipts", "batch_link"])
|
|
244
|
+
expect(response["document_receipts"]).to be_a_kind_of(Array)
|
|
245
|
+
expect(response["document_receipts"].first.keys).to match_array(["id", "external_id", "link", "status", "errors"])
|
|
246
|
+
expect(response["document_receipts"].first["external_id"]).to eq(options[:external_id]) if options[:external_id]
|
|
247
|
+
expect(response["document_receipts"].first["status"]).to eq(options[:status]) if options[:status]
|
|
248
|
+
expect(response["document_receipts"].first["errors"]).to eq(options[:errors]) if options[:errors]
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
let(:document_type_slug) { 'videos' }
|
|
252
|
+
let(:document_id) { 'FtX8nswnUKU'}
|
|
253
|
+
let(:documents) do
|
|
254
|
+
[{'external_id'=>'INscMGmhmX4',
|
|
255
|
+
'fields' => [{'name'=>'url', 'value'=>'http://www.youtube.com/watch?v=v1uyQZNg2vE', 'type'=>'enum'},
|
|
256
|
+
{'name'=>'thumbnail_url', 'value'=>'https://i.ytimg.com/vi/INscMGmhmX4/mqdefault.jpg', 'type'=>'enum'},
|
|
257
|
+
{'name'=>'channel_id', 'value'=>'UCTzVrd9ExsI3Zgnlh3_btLg', 'type'=>'enum'},
|
|
258
|
+
{'name'=>'title', 'value'=>'The Original Grumpy Cat', 'type'=>'string'},
|
|
259
|
+
{'name'=>'category_name', 'value'=>'Pets & Animals', 'type'=>'string'}]},
|
|
260
|
+
{'external_id'=>'XfY9Dsg_DZk',
|
|
261
|
+
'fields' => [{'name'=>'url', 'value'=>'http://www.youtube.com/watch?v=XfY9Dsg_DZk', 'type'=>'enum'},
|
|
262
|
+
{'name'=>'thumbnail_url', 'value'=>'https://i.ytimg.com/vi/XfY9Dsg_DZk/mqdefault.jpg', 'type'=>'enum'},
|
|
263
|
+
{'name'=>'channel_id', 'value'=>'UC5VA5j05FjETg-iLekcyiBw', 'type'=>'enum'},
|
|
264
|
+
{'name'=>'title', 'value'=>'Corgi talks to cat', 'type'=>'string'},
|
|
265
|
+
{'name'=>'category_name', 'value'=>'Pets & Animals', 'type'=>'string'}]}]
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
it 'lists documents in a document type' do
|
|
269
|
+
VCR.use_cassette(:list_documents) do
|
|
270
|
+
documents = client.documents(engine_slug, document_type_slug)
|
|
271
|
+
expect(documents.size).to eq(2)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it 'lists documents with pagination' do
|
|
276
|
+
VCR.use_cassette(:list_documents_with_pagination) do
|
|
277
|
+
documents = client.documents(engine_slug, document_type_slug, 2, 2)
|
|
278
|
+
expect(documents.size).to eq(2)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
it 'find a document' do
|
|
283
|
+
VCR.use_cassette(:find_document) do
|
|
284
|
+
document = client.document(engine_slug, document_type_slug, document_id)
|
|
285
|
+
expect(document['external_id']).to eq(document_id)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it 'creates a document' do
|
|
290
|
+
VCR.use_cassette(:create_document) do
|
|
291
|
+
document = client.create_document(engine_slug, document_type_slug, documents.first)
|
|
292
|
+
expect(document['external_id']).to eq('INscMGmhmX4')
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
it 'bulk create multiple documents' do
|
|
297
|
+
VCR.use_cassette(:bulk_create_documents) do
|
|
298
|
+
response = client.create_documents(engine_slug, document_type_slug, documents)
|
|
299
|
+
expect(response).to eq([true, true])
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
it 'destroys a document' do
|
|
304
|
+
VCR.use_cassette(:destroy_document) do
|
|
305
|
+
response = client.destroy_document(engine_slug, document_type_slug, 'INscMGmhmX4')
|
|
306
|
+
expect(response).to be_nil
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
it 'destroys multiple documents' do
|
|
311
|
+
VCR.use_cassette(:bulk_destroy_documents) do
|
|
312
|
+
response = client.destroy_documents(engine_slug, document_type_slug, ['INscMGmhmX4', 'XfY9Dsg_DZk'])
|
|
313
|
+
expect(response).to eq([true, true])
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
context '#create_or_update_document' do
|
|
318
|
+
it 'creates a document' do
|
|
319
|
+
VCR.use_cassette(:create_or_update_document_create) do
|
|
320
|
+
response = client.create_or_update_document(engine_slug, document_type_slug, {:external_id => 'foobar', :fields => [{:type => :string, :name => 'title', :value => 'new document'}]})
|
|
321
|
+
expect(response['external_id']).to eq('foobar')
|
|
322
|
+
expect(response['title']).to eq('new document')
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it 'updates an existing document' do
|
|
327
|
+
VCR.use_cassette(:create_or_update_document_update) do
|
|
328
|
+
response = client.create_or_update_document(engine_slug, document_type_slug, {:external_id => document_id, :fields => [{:type => :string, :name => 'title', :value => 'new title'}]})
|
|
329
|
+
expect(response['external_id']).to eq(document_id)
|
|
330
|
+
expect(response['title']).to eq('new title')
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
context '#bulk_create_or_update_documents' do
|
|
336
|
+
it 'returns true for all documents successfully created or updated' do
|
|
337
|
+
VCR.use_cassette(:bulk_create_or_update_documents_success) do
|
|
338
|
+
response = client.create_or_update_documents(engine_slug, document_type_slug, documents)
|
|
339
|
+
expect(response).to eq([true, true])
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it 'returns false if a document cannot be created or updated due to an error' do
|
|
344
|
+
documents = [{:external_id => 'failed_doc', :fields => [{:type => :string, :name => :title}]}] # missing value
|
|
345
|
+
|
|
346
|
+
VCR.use_cassette(:bulk_create_or_update_documents_failure) do
|
|
347
|
+
response = client.create_or_update_documents(engine_slug, document_type_slug, documents)
|
|
348
|
+
expect(response).to eq([false])
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
context '#async_create_or_update_documents' do
|
|
354
|
+
it 'returns true for all documents successfully created or updated' do
|
|
355
|
+
VCR.use_cassette(:async_create_or_update_document_success) do
|
|
356
|
+
response = client.async_create_or_update_documents(engine_slug, document_type_slug, documents)
|
|
357
|
+
check_async_response_format(response, :external_id => documents.first["external_id"], :status => "pending")
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
it 'returns false if a document cannot be created or updated due to an error' do
|
|
362
|
+
documents = [{:external_id => 'failed_doc', :fields => [{:type => :string, :name => :title}]}] # missing value
|
|
363
|
+
|
|
364
|
+
VCR.use_cassette(:async_create_or_update_document_failure) do
|
|
365
|
+
response = client.async_create_or_update_documents(engine_slug, document_type_slug, documents)
|
|
366
|
+
check_async_response_format(response, :external_id => documents.first["external_id"], :status => "failed", :errors => ["Missing required parameter: value"])
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
context '#document_receipts' do
|
|
372
|
+
before :each do
|
|
373
|
+
def get_receipt_ids
|
|
374
|
+
receipt_ids = nil
|
|
375
|
+
VCR.use_cassette(:async_create_or_update_document_success) do
|
|
376
|
+
response = client.async_create_or_update_documents(engine_slug, document_type_slug, documents)
|
|
377
|
+
receipt_ids = response["document_receipts"].map { |r| r["id"] }
|
|
378
|
+
end
|
|
379
|
+
receipt_ids
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
it 'returns array of hashes one for each receipt' do
|
|
384
|
+
VCR.use_cassette(:document_receipts_multiple) do
|
|
385
|
+
receipt_ids = get_receipt_ids
|
|
386
|
+
response = client.document_receipts(receipt_ids)
|
|
387
|
+
expect(response).to eq([{"id" => receipt_ids[0], "status" => "pending"}, {"id" => receipt_ids[1], "status" => "pending"}])
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
context '#index_documents' do
|
|
393
|
+
it 'returns #async_create_or_update_documents format return when async has been passed as true' do
|
|
394
|
+
VCR.use_cassette(:async_create_or_update_document_success) do
|
|
395
|
+
response = client.index_documents(engine_slug, document_type_slug, documents, :async => true)
|
|
396
|
+
check_async_response_format(response, :external_id => documents.first["external_id"], :status => "pending")
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
it 'returns document_receipts when successfull' do
|
|
401
|
+
VCR.use_cassette(:async_create_or_update_document_success) do
|
|
402
|
+
VCR.use_cassette(:document_receipts_multiple_complete) do
|
|
403
|
+
response = client.index_documents(engine_slug, document_type_slug, documents)
|
|
404
|
+
# Sort keys for Ruby 1.8.7 compatibility
|
|
405
|
+
expect(response.map(&:keys).map(&:sort)).to eq([["id", "link", "status"], ["id", "link", "status"]])
|
|
406
|
+
expect(response.map { |a| a["status"] }).to eq(["complete", "complete"])
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
it 'should timeout if the process takes longer than the timeout option passed' do
|
|
412
|
+
allow(client).to receive(:document_receipts) { sleep 0.05 }
|
|
413
|
+
|
|
414
|
+
VCR.use_cassette(:async_create_or_update_document_success) do
|
|
415
|
+
expect do
|
|
416
|
+
client.index_documents(engine_slug, document_type_slug, documents, :timeout => 0.01)
|
|
417
|
+
end.to raise_error(Timeout::Error)
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
context '#bulk_create_or_update_documents_verbose' do
|
|
423
|
+
it 'returns true for all documents successfully created or updated' do
|
|
424
|
+
VCR.use_cassette(:bulk_create_or_update_documents_verbose_success) do
|
|
425
|
+
response = client.create_or_update_documents_verbose(engine_slug, document_type_slug, documents)
|
|
426
|
+
expect(response).to eq([true, true])
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
it 'returns a descriptive error message if a document cannot be created or updated due to an error' do
|
|
431
|
+
documents = [{:external_id => 'failed_doc', :fields => [{:type => :string, :name => :title}]}] # missing value
|
|
432
|
+
|
|
433
|
+
VCR.use_cassette(:bulk_create_or_update_documents_verbose_failure) do
|
|
434
|
+
response = client.create_or_update_documents_verbose(engine_slug, document_type_slug, documents)
|
|
435
|
+
expect(response.size).to eq(1)
|
|
436
|
+
expect(response.first).to match /^Invalid field definition/
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
context '#update_document' do
|
|
442
|
+
it 'updates a document given its id and fields to update' do
|
|
443
|
+
fields = {:title => 'awesome new title', :channel_id => 'UC5VA5j05FjETg-iLekcyiBw'}
|
|
444
|
+
VCR.use_cassette(:update_document) do
|
|
445
|
+
response = client.update_document(engine_slug, document_type_slug, document_id, fields)
|
|
446
|
+
expect(response['external_id']).to eq(document_id)
|
|
447
|
+
expect(response['title']).to eq('awesome new title')
|
|
448
|
+
expect(response['channel_id']).to eq('UC5VA5j05FjETg-iLekcyiBw')
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
it 'raises an error if a unknown field is included' do
|
|
453
|
+
fields = {:not_a_field => 'not a field'}
|
|
454
|
+
|
|
455
|
+
VCR.use_cassette(:update_document_unknown_field_failure) do
|
|
456
|
+
expect do
|
|
457
|
+
response = client.update_document(engine_slug, document_type_slug, document_id, fields)
|
|
458
|
+
end.to raise_error
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
context "#update_documents" do
|
|
464
|
+
it 'returns true for each document successfully updated' do
|
|
465
|
+
documents = [{:external_id => 'INscMGmhmX4', :fields => {:title => 'hi'}}, {:external_id => 'XfY9Dsg_DZk', :fields => {:title => 'bye'}}]
|
|
466
|
+
|
|
467
|
+
VCR.use_cassette(:update_documents_success) do
|
|
468
|
+
response = client.update_documents(engine_slug, document_type_slug, documents)
|
|
469
|
+
expect(response).to eq([true, true])
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
it 'returns false if document is not successfully updated' do
|
|
474
|
+
documents = [{:external_id => 'not_there', :fields => [{:name => :title, :value => 'hi', :type => :string}]}]
|
|
475
|
+
|
|
476
|
+
VCR.use_cassette(:update_documents_failure_non_existent_document) do
|
|
477
|
+
response = client.update_documents(engine_slug, document_type_slug, documents)
|
|
478
|
+
expect(response).to eq([false])
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
context 'Analytics' do
|
|
485
|
+
let(:engine_slug) { 'recursion' }
|
|
486
|
+
|
|
487
|
+
context '#analytics_searches' do
|
|
488
|
+
it 'returns search counts for the default time frame' do
|
|
489
|
+
VCR.use_cassette(:analytics_searches) do
|
|
490
|
+
searches = client.analytics_searches(engine_slug)
|
|
491
|
+
expect(searches.size).to eq(15) # FIXME: is this a bug in the API?
|
|
492
|
+
expect(searches.first).to eq(['2013-09-13', 0])
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
it 'returns search counts for a specified time range' do
|
|
497
|
+
VCR.use_cassette(:analytics_searches_with_time_range) do
|
|
498
|
+
searches = client.analytics_searches(engine_slug, :start_date => '2013-01-01', :end_date => '2013-01-07')
|
|
499
|
+
expect(searches.size).to eq(7)
|
|
500
|
+
expect(searches.first).to eq(['2013-01-07', 0])
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
it 'returns search counts for a specified DocumentType' do
|
|
505
|
+
VCR.use_cassette(:analytics_searchs_with_document_type) do
|
|
506
|
+
searches = client.analytics_searches(engine_slug, :document_type_id => 'page')
|
|
507
|
+
expect(searches.size).to eq(15)
|
|
508
|
+
expect(searches.first).to eq(['2013-09-16', 0])
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
it 'returns search counts for a specified DocumentType and time range' do
|
|
513
|
+
VCR.use_cassette(:analytics_searches_with_document_type_and_time_range) do
|
|
514
|
+
searches = client.analytics_autoselects(engine_slug, :document_type_id => 'page', :start_date => '2013-07-01', :end_date => '2013-07-07')
|
|
515
|
+
expect(searches.size).to eq(7)
|
|
516
|
+
expect(searches.first).to eq(['2013-07-07', 0])
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
context '#analytics_autoselects' do
|
|
522
|
+
it 'returns autoselect counts for the default time frame' do
|
|
523
|
+
VCR.use_cassette(:analytics_autoselects) do
|
|
524
|
+
autoselects = client.analytics_autoselects(engine_slug)
|
|
525
|
+
expect(autoselects.size).to eq(15)
|
|
526
|
+
expect(autoselects.first).to eq(['2013-09-13', 0])
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
it 'returns autoselects counts for a specified time range' do
|
|
531
|
+
VCR.use_cassette(:analytics_autoselects_with_time_range) do
|
|
532
|
+
autoselects = client.analytics_autoselects(engine_slug, :start_date => '2013-07-01', :end_date => '2013-07-07')
|
|
533
|
+
expect(autoselects.size).to eq(7)
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
it 'returns autoselect counts for a specified DocumentType' do
|
|
538
|
+
VCR.use_cassette(:analytics_autoselects_with_document_type) do
|
|
539
|
+
autoselects = client.analytics_autoselects(engine_slug, :document_type_id => 'page')
|
|
540
|
+
expect(autoselects.size).to eq(15)
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
it 'returns autoselect counts for a specified DocumentType and time range' do
|
|
545
|
+
VCR.use_cassette(:analytics_autoselects_with_document_type_and_time_range) do
|
|
546
|
+
autoselects = client.analytics_autoselects(engine_slug, :document_type_id => 'page', :start_date => '2013-07-01', :end_date => '2013-07-07')
|
|
547
|
+
expect(autoselects.size).to eq(7)
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
context '#analytics_clicks' do
|
|
553
|
+
it 'returns click counts for the default time frame' do
|
|
554
|
+
VCR.use_cassette(:analytics_clicks) do
|
|
555
|
+
clicks = client.analytics_clicks(engine_slug)
|
|
556
|
+
expect(clicks.size).to eq(15)
|
|
557
|
+
expect(clicks.first).to eq(['2013-09-17', 0])
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
it 'returns clicks counts for a specified time range' do
|
|
562
|
+
VCR.use_cassette(:analytics_clicks_with_time_range) do
|
|
563
|
+
clicks = client.analytics_clicks(engine_slug, :start_date => '2013-07-01', :end_date => '2013-07-07')
|
|
564
|
+
expect(clicks.size).to eq(7)
|
|
565
|
+
expect(clicks.first).to eq(['2013-07-07', 0])
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
it 'returns click counts for a specified DocumentType' do
|
|
570
|
+
VCR.use_cassette(:analytics_clicks_with_document_type) do
|
|
571
|
+
clicks = client.analytics_clicks(engine_slug, :document_type_id => 'page')
|
|
572
|
+
expect(clicks.size).to eq(15)
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
it 'returns click counts for a specified DocumentType and time range' do
|
|
577
|
+
VCR.use_cassette(:analytics_clicks_with_document_type_and_time_range) do
|
|
578
|
+
clicks = client.analytics_clicks(engine_slug, :document_type_id => 'page', :start_date => '2013-07-01', :end_date => '2013-07-07')
|
|
579
|
+
expect(clicks.size).to eq(7)
|
|
580
|
+
expect(clicks.first).to eq(['2013-07-07', 0])
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
context '#analytics_top_queries' do
|
|
586
|
+
it 'returns top queries' do
|
|
587
|
+
VCR.use_cassette(:analytics_top_queries) do
|
|
588
|
+
top_queries = client.analytics_top_queries(engine_slug)
|
|
589
|
+
expect(top_queries.size).to eq(3)
|
|
590
|
+
expect(top_queries.first).to eq(['"fire watch"', 1])
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
it 'returns top queries with pagination' do
|
|
595
|
+
VCR.use_cassette(:analytics_top_queries_paginated) do
|
|
596
|
+
top_queries = client.analytics_top_queries(engine_slug, :start_date => '2013-08-01', :end_date => '2013-08-30', :per_page => 5, :page => 2)
|
|
597
|
+
expect(top_queries.size).to eq(5)
|
|
598
|
+
expect(top_queries.first).to eq(['no simple victory', 1])
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
it 'raises an error if the timeframe is to large' do
|
|
603
|
+
VCR.use_cassette(:analytics_top_queries_too_large) do
|
|
604
|
+
expect do
|
|
605
|
+
top_queries = client.analytics_top_queries(engine_slug, :start_date => '2013-01-01', :end_date => '2013-05-01')
|
|
606
|
+
end.to raise_error(Elastic::SiteSearch::BadRequest)
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
context 'analytics_top_no_result_queries' do
|
|
612
|
+
it 'returns top queries with no results for the default time range' do
|
|
613
|
+
VCR.use_cassette(:analytics_top_no_result_queries) do
|
|
614
|
+
top_no_result_queries = client.analytics_top_no_result_queries(engine_slug)
|
|
615
|
+
expect(top_no_result_queries.size).to eq(2)
|
|
616
|
+
expect(top_no_result_queries.first).to eq(['no results', 10])
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
it 'has top no result queries in date ranges' do
|
|
621
|
+
VCR.use_cassette(:analytics_top_no_result_queries_paginated) do
|
|
622
|
+
top_no_result_queries = client.analytics_top_no_result_queries(engine_slug, :start_date => '2013-08-01', :end_date => '2013-08-30', :per_page => 5, :page => 2)
|
|
623
|
+
expect(top_no_result_queries.size).to eq(1)
|
|
624
|
+
expect(top_no_result_queries.first).to eq(['no result again', 2])
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
context 'Domain' do
|
|
631
|
+
let(:engine_slug) { 'crawler-demo-site' }
|
|
632
|
+
let(:domain_id) { '51534c6e2ed960cc79000001' }
|
|
633
|
+
|
|
634
|
+
it 'gets all domains' do
|
|
635
|
+
VCR.use_cassette(:list_domains) do
|
|
636
|
+
domains = client.domains(engine_slug)
|
|
637
|
+
expect(domains.size).to eq(1)
|
|
638
|
+
expect(domains.first['id']).to eq(domain_id)
|
|
639
|
+
end
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
context '#domain' do
|
|
643
|
+
it 'shows a domain if it exists' do
|
|
644
|
+
VCR.use_cassette(:find_domain) do
|
|
645
|
+
domain = client.domain(engine_slug, domain_id)
|
|
646
|
+
expect(domain['id']).to eq(domain_id)
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
it 'raises an error if the domain does not exist' do
|
|
651
|
+
VCR.use_cassette(:find_domain_failure) do
|
|
652
|
+
expect do
|
|
653
|
+
domain = client.domain(engine_slug, 'bogus')
|
|
654
|
+
end.to raise_error(Elastic::SiteSearch::NonExistentRecord)
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
context '#create_domain' do
|
|
660
|
+
it 'creates a domain' do
|
|
661
|
+
VCR.use_cassette(:create_domain) do
|
|
662
|
+
url = 'http://www.zombo.com/'
|
|
663
|
+
domain = client.create_domain(engine_slug, url)
|
|
664
|
+
expect(domain['submitted_url']).to eq(url)
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
it 'destroys a domain' do
|
|
670
|
+
VCR.use_cassette(:destroy_domain) do
|
|
671
|
+
response = client.destroy_domain(engine_slug, '52324b132ed960589800004a')
|
|
672
|
+
expect(response).to be_nil
|
|
673
|
+
end
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
context '#recrawl_domain' do
|
|
677
|
+
it 'enqueues a request to recrawl a domain' do
|
|
678
|
+
VCR.use_cassette(:recrawl_domain_success) do
|
|
679
|
+
domain = client.recrawl_domain(engine_slug, domain_id)
|
|
680
|
+
expect(domain['id']).to eq(domain_id)
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
it 'raises an exception if domain recrawl is not allowed' do
|
|
685
|
+
VCR.use_cassette(:recrawl_domain_failure) do
|
|
686
|
+
expect do
|
|
687
|
+
domain = client.recrawl_domain(engine_slug, domain_id)
|
|
688
|
+
end.to raise_error(Elastic::SiteSearch::Forbidden)
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
context '#crawl_url' do
|
|
694
|
+
it 'enqueues a request to crawl a URL on a domain' do
|
|
695
|
+
VCR.use_cassette(:crawl_url) do
|
|
696
|
+
url = 'http://crawler-demo-site.herokuapp.com/2012/01/01/first-post.html'
|
|
697
|
+
crawled_url = client.crawl_url(engine_slug, domain_id, url)
|
|
698
|
+
expect(crawled_url['url']).to eq(url)
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
context 'Clickthrough' do
|
|
705
|
+
let(:query) { 'foo' }
|
|
706
|
+
let(:document_type_slug) { 'videos' }
|
|
707
|
+
let(:external_id) { 'FtX8nswnUKU'}
|
|
708
|
+
|
|
709
|
+
context "#log_clickthough" do
|
|
710
|
+
# Not thrilled with this test, but since nothing is returned all we
|
|
711
|
+
# can reasonably check is that an error isn't raised
|
|
712
|
+
it 'returns nil' do
|
|
713
|
+
VCR.use_cassette(:log_clickthrough_success) do
|
|
714
|
+
response = client.log_clickthrough(engine_slug, document_type_slug, query, external_id)
|
|
715
|
+
expect(response).to eq(nil)
|
|
716
|
+
end
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
it 'raises an error when missing params' do
|
|
720
|
+
VCR.use_cassette(:log_clickthrough_failure) do
|
|
721
|
+
expect do
|
|
722
|
+
client.log_clickthrough(engine_slug, document_type_slug, nil, external_id)
|
|
723
|
+
end.to raise_error(Elastic::SiteSearch::BadRequest)
|
|
724
|
+
end
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
end
|
|
728
|
+
end
|