algolia 2.0.0 → 2.3.2
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 +4 -4
- data/.circleci/config.yml +11 -2
- data/.dockerignore +38 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +80 -1
- data/CONTRIBUTING.MD +184 -0
- data/DOCKER_README.MD +89 -0
- data/Dockerfile +7 -0
- data/README.md +4 -4
- data/SECURITY.md +1 -1
- data/algolia.gemspec +3 -1
- data/lib/algolia/analytics_client.rb +1 -1
- data/lib/algolia/config/personalization_config.rb +20 -0
- data/lib/algolia/config/recommend_config.rb +6 -0
- data/lib/algolia/config/recommendation_config.rb +2 -15
- data/lib/algolia/helpers.rb +51 -1
- data/lib/algolia/http/http_requester.rb +4 -4
- data/lib/algolia/insights_client.rb +1 -1
- data/lib/algolia/logger_helper.rb +1 -1
- data/lib/algolia/personalization_client.rb +60 -0
- data/lib/algolia/recommend_client.rb +134 -0
- data/lib/algolia/recommendation_client.rb +2 -55
- data/lib/algolia/responses/add_api_key_response.rb +1 -1
- data/lib/algolia/responses/delete_api_key_response.rb +1 -1
- data/lib/algolia/responses/dictionary_response.rb +33 -0
- data/lib/algolia/responses/restore_api_key_response.rb +1 -1
- data/lib/algolia/responses/update_api_key_response.rb +1 -1
- data/lib/algolia/search_client.rb +185 -8
- data/lib/algolia/search_index.rb +21 -56
- data/lib/algolia/transport/request_options.rb +1 -1
- data/lib/algolia/transport/transport.rb +11 -10
- data/lib/algolia/version.rb +1 -1
- data/lib/algolia.rb +5 -0
- data/renovate.json +5 -0
- data/test/algolia/integration/account_client_test.rb +2 -2
- data/test/algolia/integration/analytics_client_test.rb +6 -2
- data/test/algolia/integration/mocks/mock_requester.rb +13 -11
- data/test/algolia/integration/personalization_client_test.rb +30 -0
- data/test/algolia/integration/recommend_client_test.rb +70 -0
- data/test/algolia/integration/search_client_test.rb +108 -13
- data/test/algolia/integration/search_index_test.rb +31 -0
- data/test/algolia/unit/helpers_test.rb +4 -2
- data/test/test_helper.rb +32 -0
- metadata +45 -5
data/lib/algolia/search_index.rb
CHANGED
|
@@ -5,18 +5,20 @@ module Algolia
|
|
|
5
5
|
include CallType
|
|
6
6
|
include Helpers
|
|
7
7
|
|
|
8
|
-
attr_reader :name, :transporter, :config
|
|
8
|
+
attr_reader :name, :transporter, :config, :logger
|
|
9
9
|
|
|
10
10
|
# Initialize an index
|
|
11
11
|
#
|
|
12
12
|
# @param name [String] name of the index
|
|
13
13
|
# @param transporter [Object] transport object used for the connection
|
|
14
14
|
# @param config [Config] a Config object which contains your APP_ID and API_KEY
|
|
15
|
+
# @param logger [LoggerHelper] an optional LoggerHelper object to use
|
|
15
16
|
#
|
|
16
|
-
def initialize(name, transporter, config)
|
|
17
|
+
def initialize(name, transporter, config, logger = nil)
|
|
17
18
|
@name = name
|
|
18
19
|
@transporter = transporter
|
|
19
20
|
@config = config
|
|
21
|
+
@logger = logger || LoggerHelper.create
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
# # # # # # # # # # # # # # # # # # # # #
|
|
@@ -36,7 +38,7 @@ module Algolia
|
|
|
36
38
|
if status == 'published'
|
|
37
39
|
return
|
|
38
40
|
end
|
|
39
|
-
sleep(time_before_retry / 1000)
|
|
41
|
+
sleep(time_before_retry.to_f / 1000)
|
|
40
42
|
end
|
|
41
43
|
end
|
|
42
44
|
|
|
@@ -301,8 +303,8 @@ module Algolia
|
|
|
301
303
|
request_options = symbolize_hash(opts)
|
|
302
304
|
if get_option(request_options, 'createIfNotExists')
|
|
303
305
|
generate_object_id = true
|
|
304
|
-
request_options.delete(:createIfNotExists)
|
|
305
306
|
end
|
|
307
|
+
request_options.delete(:createIfNotExists)
|
|
306
308
|
|
|
307
309
|
if generate_object_id
|
|
308
310
|
IndexingResponse.new(self, raw_batch(chunk('partialUpdateObject', objects), request_options))
|
|
@@ -830,7 +832,7 @@ module Algolia
|
|
|
830
832
|
end
|
|
831
833
|
|
|
832
834
|
# TODO: consider create a new client with state of retry is shared
|
|
833
|
-
tmp_client = Algolia::Search::Client.new(@config)
|
|
835
|
+
tmp_client = Algolia::Search::Client.new(@config, { logger: logger })
|
|
834
836
|
tmp_index = tmp_client.init_index(tmp_index_name)
|
|
835
837
|
|
|
836
838
|
save_objects_response = tmp_index.save_objects(objects, request_options)
|
|
@@ -974,7 +976,11 @@ module Algolia
|
|
|
974
976
|
# @return [Hash]
|
|
975
977
|
#
|
|
976
978
|
def get_settings(opts = {})
|
|
977
|
-
|
|
979
|
+
opts_default = {
|
|
980
|
+
getVersion: 2
|
|
981
|
+
}
|
|
982
|
+
opts = opts_default.merge(opts)
|
|
983
|
+
response = @transporter.read(:GET, path_encode('/1/indexes/%s/settings', @name), {}, opts)
|
|
978
984
|
|
|
979
985
|
deserialize_settings(response, @config.symbolize_keys)
|
|
980
986
|
end
|
|
@@ -987,7 +993,15 @@ module Algolia
|
|
|
987
993
|
# @return [IndexingResponse]
|
|
988
994
|
#
|
|
989
995
|
def set_settings(settings, opts = {})
|
|
990
|
-
|
|
996
|
+
request_options = symbolize_hash(opts)
|
|
997
|
+
forward_to_replicas = request_options.delete(:forwardToReplicas) || false
|
|
998
|
+
|
|
999
|
+
response = @transporter.write(
|
|
1000
|
+
:PUT,
|
|
1001
|
+
path_encode('/1/indexes/%s/settings', @name) + handle_params({ forwardToReplicas: forward_to_replicas }),
|
|
1002
|
+
settings,
|
|
1003
|
+
request_options
|
|
1004
|
+
)
|
|
991
1005
|
|
|
992
1006
|
IndexingResponse.new(self, response)
|
|
993
1007
|
end
|
|
@@ -1037,55 +1051,6 @@ module Algolia
|
|
|
1037
1051
|
|
|
1038
1052
|
private
|
|
1039
1053
|
|
|
1040
|
-
# Check the passed object to determine if it's an array
|
|
1041
|
-
#
|
|
1042
|
-
# @param object [Object]
|
|
1043
|
-
#
|
|
1044
|
-
def check_array(object)
|
|
1045
|
-
raise AlgoliaError, 'argument must be an array of objects' unless object.is_a?(Array)
|
|
1046
|
-
end
|
|
1047
|
-
|
|
1048
|
-
# Check the passed object
|
|
1049
|
-
#
|
|
1050
|
-
# @param object [Object]
|
|
1051
|
-
# @param in_array [Boolean] whether the object is an array or not
|
|
1052
|
-
#
|
|
1053
|
-
def check_object(object, in_array = false)
|
|
1054
|
-
case object
|
|
1055
|
-
when Array
|
|
1056
|
-
raise AlgoliaError, in_array ? 'argument must be an array of objects' : 'argument must not be an array'
|
|
1057
|
-
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
|
1058
|
-
raise AlgoliaError, "argument must be an #{'array of' if in_array} object, got: #{object.inspect}"
|
|
1059
|
-
end
|
|
1060
|
-
end
|
|
1061
|
-
|
|
1062
|
-
# Check if passed object has a objectID
|
|
1063
|
-
#
|
|
1064
|
-
# @param object [Object]
|
|
1065
|
-
# @param object_id [String]
|
|
1066
|
-
#
|
|
1067
|
-
def get_object_id(object, object_id = nil)
|
|
1068
|
-
check_object(object)
|
|
1069
|
-
object_id ||= object[:objectID] || object['objectID']
|
|
1070
|
-
raise AlgoliaError, "Missing 'objectID'" if object_id.nil?
|
|
1071
|
-
object_id
|
|
1072
|
-
end
|
|
1073
|
-
|
|
1074
|
-
# Build a batch request
|
|
1075
|
-
#
|
|
1076
|
-
# @param action [String] action to perform on the engine
|
|
1077
|
-
# @param objects [Array] objects on which build the action
|
|
1078
|
-
# @param with_object_id [Boolean] if set to true, check if each object has an objectID set
|
|
1079
|
-
#
|
|
1080
|
-
def chunk(action, objects, with_object_id = false)
|
|
1081
|
-
objects.map do |object|
|
|
1082
|
-
check_object(object, true)
|
|
1083
|
-
request = { action: action, body: object }
|
|
1084
|
-
request[:objectID] = get_object_id(object).to_s if with_object_id
|
|
1085
|
-
request
|
|
1086
|
-
end
|
|
1087
|
-
end
|
|
1088
|
-
|
|
1089
1054
|
def raw_batch(requests, opts)
|
|
1090
1055
|
@transporter.write(:POST, path_encode('/1/indexes/%s/batch', @name), { requests: requests }, opts)
|
|
1091
1056
|
end
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require 'faraday'
|
|
2
|
+
# this is the default adapter and it needs to be required to be registered.
|
|
3
|
+
require 'faraday/net_http_persistent' unless Faraday::VERSION < '1'
|
|
2
4
|
|
|
3
5
|
module Algolia
|
|
4
6
|
module Transport
|
|
@@ -89,11 +91,13 @@ module Algolia
|
|
|
89
91
|
# @return [Hash]
|
|
90
92
|
#
|
|
91
93
|
def build_request(method, path, body, request_options)
|
|
92
|
-
request
|
|
93
|
-
request[:method]
|
|
94
|
-
request[:path]
|
|
95
|
-
request[:body]
|
|
96
|
-
request[:headers]
|
|
94
|
+
request = {}
|
|
95
|
+
request[:method] = method.downcase
|
|
96
|
+
request[:path] = build_uri_path(path, request_options.params)
|
|
97
|
+
request[:body] = build_body(body, request_options, method)
|
|
98
|
+
request[:headers] = generate_headers(request_options)
|
|
99
|
+
request[:timeout] = request_options.timeout
|
|
100
|
+
request[:connect_timeout] = request_options.connect_timeout
|
|
97
101
|
request
|
|
98
102
|
end
|
|
99
103
|
|
|
@@ -127,15 +131,12 @@ module Algolia
|
|
|
127
131
|
|
|
128
132
|
# Generates headers from config headers and optional parameters
|
|
129
133
|
#
|
|
130
|
-
# @
|
|
134
|
+
# @param request_options [RequestOptions]
|
|
131
135
|
#
|
|
132
136
|
# @return [Hash] merged headers
|
|
133
137
|
#
|
|
134
138
|
def generate_headers(request_options = {})
|
|
135
|
-
headers
|
|
136
|
-
extra_headers = request_options.headers || {}
|
|
137
|
-
@config.headers.each { |key, val| headers[key.to_s] = val }
|
|
138
|
-
extra_headers.each { |key, val| headers[key.to_s] = val }
|
|
139
|
+
headers = @config.headers.merge(request_options.headers)
|
|
139
140
|
if request_options.compression_type == Defaults::GZIP_ENCODING
|
|
140
141
|
headers['Accept-Encoding'] = Defaults::GZIP_ENCODING
|
|
141
142
|
end
|
data/lib/algolia/version.rb
CHANGED
data/lib/algolia.rb
CHANGED
|
@@ -7,6 +7,8 @@ require 'algolia/config/base_config'
|
|
|
7
7
|
require 'algolia/config/search_config'
|
|
8
8
|
require 'algolia/config/analytics_config'
|
|
9
9
|
require 'algolia/config/insights_config'
|
|
10
|
+
require 'algolia/config/recommend_config'
|
|
11
|
+
require 'algolia/config/personalization_config'
|
|
10
12
|
require 'algolia/config/recommendation_config'
|
|
11
13
|
require 'algolia/enums/call_type'
|
|
12
14
|
require 'algolia/enums/retry_outcome_type'
|
|
@@ -20,6 +22,7 @@ require 'algolia/responses/indexing_response'
|
|
|
20
22
|
require 'algolia/responses/add_api_key_response'
|
|
21
23
|
require 'algolia/responses/update_api_key_response'
|
|
22
24
|
require 'algolia/responses/delete_api_key_response'
|
|
25
|
+
require 'algolia/responses/dictionary_response'
|
|
23
26
|
require 'algolia/responses/restore_api_key_response'
|
|
24
27
|
require 'algolia/responses/multiple_batch_indexing_response'
|
|
25
28
|
require 'algolia/responses/multiple_response'
|
|
@@ -32,6 +35,8 @@ require 'algolia/account_client'
|
|
|
32
35
|
require 'algolia/search_client'
|
|
33
36
|
require 'algolia/analytics_client'
|
|
34
37
|
require 'algolia/insights_client'
|
|
38
|
+
require 'algolia/recommend_client'
|
|
39
|
+
require 'algolia/personalization_client'
|
|
35
40
|
require 'algolia/recommendation_client'
|
|
36
41
|
require 'algolia/error'
|
|
37
42
|
require 'algolia/search_index'
|
|
@@ -14,7 +14,7 @@ class AccountClientTest < BaseTest
|
|
|
14
14
|
|
|
15
15
|
search_client2 = Algolia::Search::Client.create(APPLICATION_ID_2, ADMIN_KEY_2)
|
|
16
16
|
index2 = search_client2.init_index(get_test_index_name('copy_index2'))
|
|
17
|
-
index1.save_object!({ objectID: 'one' })
|
|
17
|
+
index1.save_object!({ objectID: 'one', title: 'Test title' })
|
|
18
18
|
index1.save_rule!({
|
|
19
19
|
objectID: 'one',
|
|
20
20
|
condition: { anchoring: 'is', pattern: 'pattern' },
|
|
@@ -29,7 +29,7 @@ class AccountClientTest < BaseTest
|
|
|
29
29
|
}
|
|
30
30
|
})
|
|
31
31
|
index1.save_synonym!({ objectID: 'one', type: 'synonym', synonyms: %w(one two) })
|
|
32
|
-
index1.set_settings!({ searchableAttributes: ['
|
|
32
|
+
index1.set_settings!({ searchableAttributes: ['title'] })
|
|
33
33
|
|
|
34
34
|
Algolia::Account::Client.copy_index!(index1, index2)
|
|
35
35
|
assert_equal 'one', index2.get_object('one')[:objectID]
|
|
@@ -23,7 +23,9 @@ class AnalyticsClientTest < BaseTest
|
|
|
23
23
|
endAt: tomorrow.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
response =
|
|
26
|
+
response = retry_test do
|
|
27
|
+
client.add_ab_test(ab_test)
|
|
28
|
+
end
|
|
27
29
|
ab_test_id = response[:abTestID]
|
|
28
30
|
|
|
29
31
|
index1.wait_task(response[:taskID])
|
|
@@ -86,7 +88,9 @@ class AnalyticsClientTest < BaseTest
|
|
|
86
88
|
endAt: tomorrow.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
response =
|
|
91
|
+
response = retry_test do
|
|
92
|
+
client.add_ab_test(ab_test)
|
|
93
|
+
end
|
|
90
94
|
ab_test_id = response[:abTestID]
|
|
91
95
|
|
|
92
96
|
index.wait_task(response[:taskID])
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
class MockRequester
|
|
2
|
+
attr_accessor :requests
|
|
2
3
|
def initialize
|
|
3
4
|
@connection = nil
|
|
5
|
+
@requests = []
|
|
4
6
|
end
|
|
5
7
|
|
|
6
|
-
def send_request(host, method, path,
|
|
7
|
-
|
|
8
|
-
response = {
|
|
9
|
-
connection: connection,
|
|
8
|
+
def send_request(host, method, path, body, headers, timeout, connect_timeout)
|
|
9
|
+
request = {
|
|
10
10
|
host: host,
|
|
11
|
+
method: method,
|
|
11
12
|
path: path,
|
|
13
|
+
body: body,
|
|
12
14
|
headers: headers,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
body: '{"hits":[],"nbHits":0,"page":0,"nbPages":1,"hitsPerPage":20,"exhaustiveNbHits":true,"query":"test","params":"query=test","processingTimeMS":1}',
|
|
16
|
-
success: true
|
|
15
|
+
timeout: timeout,
|
|
16
|
+
connect_timeout: connect_timeout
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
@requests.push(request)
|
|
20
|
+
|
|
19
21
|
Algolia::Http::Response.new(
|
|
20
|
-
status:
|
|
21
|
-
body:
|
|
22
|
-
headers:
|
|
22
|
+
status: 200,
|
|
23
|
+
body: '{"hits": [], "status": "published"}',
|
|
24
|
+
headers: {}
|
|
23
25
|
)
|
|
24
26
|
end
|
|
25
27
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require_relative 'base_test'
|
|
2
|
+
require 'date'
|
|
3
|
+
|
|
4
|
+
class PersonalizationClientTest < BaseTest
|
|
5
|
+
describe 'Personalization client' do
|
|
6
|
+
def test_personalization_client
|
|
7
|
+
client = Algolia::Personalization::Client.create(APPLICATION_ID_1, ADMIN_KEY_1)
|
|
8
|
+
personalization_strategy = {
|
|
9
|
+
eventsScoring: [
|
|
10
|
+
{ eventName: 'Add to cart', eventType: 'conversion', score: 50 },
|
|
11
|
+
{ eventName: 'Purchase', eventType: 'conversion', score: 100 }
|
|
12
|
+
],
|
|
13
|
+
facetsScoring: [
|
|
14
|
+
{ facetName: 'brand', score: 100 },
|
|
15
|
+
{ facetName: 'categories', score: 10 }
|
|
16
|
+
],
|
|
17
|
+
personalizationImpact: 0
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
client.set_personalization_strategy(personalization_strategy)
|
|
22
|
+
rescue Algolia::AlgoliaHttpError => e
|
|
23
|
+
raise e unless e.code == 429
|
|
24
|
+
end
|
|
25
|
+
response = client.get_personalization_strategy
|
|
26
|
+
|
|
27
|
+
assert_equal response, personalization_strategy
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require 'securerandom'
|
|
2
|
+
require_relative 'base_test'
|
|
3
|
+
|
|
4
|
+
class RecommendClientTest < BaseTest
|
|
5
|
+
describe 'Recommendations' do
|
|
6
|
+
def test_get_recommendations
|
|
7
|
+
requester = MockRequester.new
|
|
8
|
+
client = Algolia::Recommend::Client.new(@@search_config, http_requester: requester)
|
|
9
|
+
|
|
10
|
+
# It correctly formats queries using the 'bought-together' model
|
|
11
|
+
client.get_recommendations([{ indexName: 'products', objectID: 'B018APC4LE', model: Algolia::Recommend::Model::BOUGHT_TOGETHER }])
|
|
12
|
+
|
|
13
|
+
# It correctly formats queries using the 'related-products' model
|
|
14
|
+
client.get_recommendations([{ indexName: 'products', objectID: 'B018APC4LE', model: Algolia::Recommend::Model::RELATED_PRODUCTS }])
|
|
15
|
+
|
|
16
|
+
# It correctly formats multiple queries.
|
|
17
|
+
client.get_recommendations(
|
|
18
|
+
[
|
|
19
|
+
{ indexName: 'products', objectID: 'B018APC4LE-1', model: Algolia::Recommend::Model::RELATED_PRODUCTS, threshold: 0 },
|
|
20
|
+
{ indexName: 'products', objectID: 'B018APC4LE-2', model: Algolia::Recommend::Model::RELATED_PRODUCTS, threshold: 0 }
|
|
21
|
+
]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# It resets the threshold to 0 if it's not numeric.
|
|
25
|
+
client.get_recommendations([{ indexName: 'products', objectID: 'B018APC4LE', model: Algolia::Recommend::Model::BOUGHT_TOGETHER, threshold: nil }])
|
|
26
|
+
|
|
27
|
+
# It passes the threshold correctly if it's numeric.
|
|
28
|
+
client.get_recommendations([{ indexName: 'products', objectID: 'B018APC4LE', model: Algolia::Recommend::Model::BOUGHT_TOGETHER, threshold: 42 }])
|
|
29
|
+
|
|
30
|
+
assert_requests(
|
|
31
|
+
requester,
|
|
32
|
+
[
|
|
33
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}' },
|
|
34
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"related-products","threshold":0}]}' },
|
|
35
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE-1","model":"related-products","threshold":0},{"indexName":"products","objectID":"B018APC4LE-2","model":"related-products","threshold":0}]}' },
|
|
36
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}' },
|
|
37
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":42}]}' }
|
|
38
|
+
]
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def test_get_related_products
|
|
43
|
+
requester = MockRequester.new
|
|
44
|
+
client = Algolia::Recommend::Client.new(@@search_config, http_requester: requester)
|
|
45
|
+
|
|
46
|
+
client.get_related_products([{ indexName: 'products', objectID: 'B018APC4LE' }])
|
|
47
|
+
|
|
48
|
+
assert_requests(
|
|
49
|
+
requester,
|
|
50
|
+
[{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"related-products","threshold":0}]}' }]
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_get_frequently_bought_together
|
|
55
|
+
requester = MockRequester.new
|
|
56
|
+
client = Algolia::Recommend::Client.new(@@search_config, http_requester: requester)
|
|
57
|
+
|
|
58
|
+
client.get_frequently_bought_together([{ indexName: 'products', objectID: 'B018APC4LE' }])
|
|
59
|
+
client.get_frequently_bought_together([{ indexName: 'products', objectID: 'B018APC4LE', fallbackParameters: {} }])
|
|
60
|
+
|
|
61
|
+
assert_requests(
|
|
62
|
+
requester,
|
|
63
|
+
[
|
|
64
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}' },
|
|
65
|
+
{ method: :post, path: '/1/indexes/*/recommendations', body: '{"requests":[{"indexName":"products","objectID":"B018APC4LE","model":"bought-together","threshold":0}]}' }
|
|
66
|
+
]
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require 'securerandom'
|
|
1
2
|
require_relative 'base_test'
|
|
2
3
|
|
|
3
4
|
class SearchClientTest < BaseTest
|
|
@@ -215,6 +216,7 @@ class SearchClientTest < BaseTest
|
|
|
215
216
|
|
|
216
217
|
def test_api_keys
|
|
217
218
|
assert_equal ['search'], @api_key[:acl]
|
|
219
|
+
assert_equal 'A description', @api_key[:description]
|
|
218
220
|
|
|
219
221
|
api_keys = @@search_client.list_api_keys[:keys].map do |key|
|
|
220
222
|
key[:value]
|
|
@@ -222,7 +224,9 @@ class SearchClientTest < BaseTest
|
|
|
222
224
|
assert_includes api_keys, @api_key[:value]
|
|
223
225
|
|
|
224
226
|
@@search_client.update_api_key!(@api_key[:value], { maxHitsPerQuery: 42 })
|
|
225
|
-
updated_api_key =
|
|
227
|
+
updated_api_key = retry_test do
|
|
228
|
+
@@search_client.get_api_key(@api_key[:value], test: 'test')
|
|
229
|
+
end
|
|
226
230
|
assert_equal 42, updated_api_key[:maxHitsPerQuery]
|
|
227
231
|
|
|
228
232
|
@@search_client.delete_api_key!(@api_key[:value])
|
|
@@ -233,18 +237,13 @@ class SearchClientTest < BaseTest
|
|
|
233
237
|
|
|
234
238
|
assert_equal 'Key does not exist', exception.message
|
|
235
239
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
@@search_client.restore_api_key!(@api_key[:value])
|
|
239
|
-
break
|
|
240
|
-
rescue Algolia::AlgoliaHttpError => e
|
|
241
|
-
if e.code != 404
|
|
242
|
-
raise StandardError
|
|
243
|
-
end
|
|
244
|
-
end
|
|
240
|
+
retry_test do
|
|
241
|
+
@@search_client.restore_api_key!(@api_key[:value])
|
|
245
242
|
end
|
|
246
243
|
|
|
247
|
-
restored_key =
|
|
244
|
+
restored_key = retry_test do
|
|
245
|
+
@@search_client.get_api_key(@api_key[:value])
|
|
246
|
+
end
|
|
248
247
|
|
|
249
248
|
refute_nil restored_key
|
|
250
249
|
end
|
|
@@ -295,7 +294,7 @@ class SearchClientTest < BaseTest
|
|
|
295
294
|
|
|
296
295
|
results = @@search_client.multiple_queries([
|
|
297
296
|
{ indexName: index_name1, params: to_query_string({ query: '', hitsPerPage: 2 }) },
|
|
298
|
-
{ indexName: index_name2, params:
|
|
297
|
+
{ indexName: index_name2, params: { query: '', hitsPerPage: 2 } }
|
|
299
298
|
], { strategy: 'none' })[:results]
|
|
300
299
|
|
|
301
300
|
assert_equal 2, results.length
|
|
@@ -333,7 +332,12 @@ class SearchClientTest < BaseTest
|
|
|
333
332
|
secured_index1 = secured_client.init_index(@index1.name)
|
|
334
333
|
secured_index2 = secured_client.init_index(@index2.name)
|
|
335
334
|
|
|
336
|
-
|
|
335
|
+
res = retry_test do
|
|
336
|
+
secured_index1.search('')
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
assert_equal 1, res[:hits].length
|
|
340
|
+
|
|
337
341
|
exception = assert_raises Algolia::AlgoliaHttpError do
|
|
338
342
|
secured_index2.search('')
|
|
339
343
|
end
|
|
@@ -366,5 +370,96 @@ class SearchClientTest < BaseTest
|
|
|
366
370
|
assert_equal 'The SecuredAPIKey doesn\'t have a validUntil parameter.', exception.message
|
|
367
371
|
end
|
|
368
372
|
end
|
|
373
|
+
|
|
374
|
+
describe 'Custom Dictionaries' do
|
|
375
|
+
def before_all
|
|
376
|
+
@client = Algolia::Search::Client.create(APPLICATION_ID_2, ADMIN_KEY_2)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def test_stopwords_dictionaries
|
|
380
|
+
entry_id = SecureRandom.hex
|
|
381
|
+
assert_equal 0, @client.search_dictionary_entries('stopwords', entry_id)[:nbHits]
|
|
382
|
+
|
|
383
|
+
entry = {
|
|
384
|
+
objectID: entry_id,
|
|
385
|
+
language: 'en',
|
|
386
|
+
word: 'down'
|
|
387
|
+
}
|
|
388
|
+
@client.save_dictionary_entries!('stopwords', [entry])
|
|
389
|
+
|
|
390
|
+
stopwords = @client.search_dictionary_entries('stopwords', entry_id)
|
|
391
|
+
assert_equal 1, stopwords[:nbHits]
|
|
392
|
+
assert_equal stopwords[:hits][0][:objectID], entry[:objectID]
|
|
393
|
+
assert_equal stopwords[:hits][0][:word], entry[:word]
|
|
394
|
+
|
|
395
|
+
@client.delete_dictionary_entries!('stopwords', [entry_id])
|
|
396
|
+
assert_equal 0, @client.search_dictionary_entries('stopwords', entry_id)[:nbHits]
|
|
397
|
+
|
|
398
|
+
old_dictionary_state = @client.search_dictionary_entries('stopwords', '')
|
|
399
|
+
old_dictionary_entries = old_dictionary_state[:hits].map do |hit|
|
|
400
|
+
hit.reject { |key| key == :type }
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
@client.save_dictionary_entries!('stopwords', [entry])
|
|
404
|
+
assert_equal 1, @client.search_dictionary_entries('stopwords', entry_id)[:nbHits]
|
|
405
|
+
|
|
406
|
+
@client.replace_dictionary_entries!('stopwords', old_dictionary_entries)
|
|
407
|
+
assert_equal 0, @client.search_dictionary_entries('stopwords', entry_id)[:nbHits]
|
|
408
|
+
|
|
409
|
+
stopwords_settings = {
|
|
410
|
+
disableStandardEntries: {
|
|
411
|
+
stopwords: {
|
|
412
|
+
en: true
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
@client.set_dictionary_settings!(stopwords_settings)
|
|
418
|
+
|
|
419
|
+
assert_equal @client.get_dictionary_settings, stopwords_settings
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def test_plurals_dictionaries
|
|
423
|
+
entry_id = SecureRandom.hex
|
|
424
|
+
assert_equal 0, @client.search_dictionary_entries('plurals', entry_id)[:nbHits]
|
|
425
|
+
|
|
426
|
+
entry = {
|
|
427
|
+
objectID: entry_id,
|
|
428
|
+
language: 'fr',
|
|
429
|
+
words: %w(cheval chevaux)
|
|
430
|
+
}
|
|
431
|
+
@client.save_dictionary_entries!('plurals', [entry])
|
|
432
|
+
|
|
433
|
+
plurals = @client.search_dictionary_entries('plurals', entry_id)
|
|
434
|
+
assert_equal 1, plurals[:nbHits]
|
|
435
|
+
assert_equal plurals[:hits][0][:objectID], entry[:objectID]
|
|
436
|
+
assert_equal plurals[:hits][0][:words], entry[:words]
|
|
437
|
+
|
|
438
|
+
@client.delete_dictionary_entries!('plurals', [entry_id])
|
|
439
|
+
assert_equal 0, @client.search_dictionary_entries('plurals', entry_id)[:nbHits]
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def test_compounds_dictionaries
|
|
443
|
+
entry_id = SecureRandom.hex
|
|
444
|
+
assert_equal 0, @client.search_dictionary_entries('compounds', entry_id)[:nbHits]
|
|
445
|
+
|
|
446
|
+
entry = {
|
|
447
|
+
objectID: entry_id,
|
|
448
|
+
language: 'de',
|
|
449
|
+
word: 'kopfschmerztablette',
|
|
450
|
+
decomposition: %w(kopf schmerz tablette)
|
|
451
|
+
}
|
|
452
|
+
@client.save_dictionary_entries!('compounds', [entry])
|
|
453
|
+
|
|
454
|
+
compounds = @client.search_dictionary_entries('compounds', entry_id)
|
|
455
|
+
assert_equal 1, compounds[:nbHits]
|
|
456
|
+
assert_equal compounds[:hits][0][:objectID], entry[:objectID]
|
|
457
|
+
assert_equal compounds[:hits][0][:word], entry[:word]
|
|
458
|
+
assert_equal compounds[:hits][0][:decomposition], entry[:decomposition]
|
|
459
|
+
|
|
460
|
+
@client.delete_dictionary_entries!('compounds', [entry_id])
|
|
461
|
+
assert_equal 0, @client.search_dictionary_entries('compounds', entry_id)[:nbHits]
|
|
462
|
+
end
|
|
463
|
+
end
|
|
369
464
|
end
|
|
370
465
|
end
|
|
@@ -274,6 +274,37 @@ class SearchIndexTest < BaseTest
|
|
|
274
274
|
@index.set_settings!(settings)
|
|
275
275
|
|
|
276
276
|
assert_equal @index.get_settings, settings
|
|
277
|
+
|
|
278
|
+
# check that the forwardToReplicas parameter is passed correctly
|
|
279
|
+
assert @index.set_settings!(settings, { forwardToReplicas: true })
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Check version 1 API calling (ref. PR #473)
|
|
283
|
+
def test_version_param
|
|
284
|
+
@index.save_object!(generate_object('obj1')) # create index
|
|
285
|
+
|
|
286
|
+
# Check response's version value by actual access
|
|
287
|
+
assert_equal 2, @index.get_settings[:version]
|
|
288
|
+
assert_equal 1, @index.get_settings(getVersion: 1)[:version]
|
|
289
|
+
assert_equal 2, @index.get_settings(getVersion: 2)[:version]
|
|
290
|
+
|
|
291
|
+
# Check API endpoint handling by mock access
|
|
292
|
+
requester = MockRequester.new
|
|
293
|
+
client = Algolia::Search::Client.new(@@search_config, http_requester: requester)
|
|
294
|
+
index = client.init_index(@index_name)
|
|
295
|
+
|
|
296
|
+
index.get_settings # default
|
|
297
|
+
index.get_settings(getVersion: 1)
|
|
298
|
+
index.get_settings(getVersion: 2)
|
|
299
|
+
|
|
300
|
+
assert_requests(
|
|
301
|
+
requester,
|
|
302
|
+
[
|
|
303
|
+
{ method: :get, path: "/1/indexes/#{@index_name}/settings?getVersion=2" },
|
|
304
|
+
{ method: :get, path: "/1/indexes/#{@index_name}/settings?getVersion=1" },
|
|
305
|
+
{ method: :get, path: "/1/indexes/#{@index_name}/settings?getVersion=2" }
|
|
306
|
+
]
|
|
307
|
+
)
|
|
277
308
|
end
|
|
278
309
|
end
|
|
279
310
|
|
|
@@ -26,13 +26,15 @@ class HelpersTest
|
|
|
26
26
|
old_settings = {
|
|
27
27
|
'attributesToIndex' => %w(attr1 attr2),
|
|
28
28
|
'numericAttributesToIndex' => %w(attr1 attr2),
|
|
29
|
-
'slaves' => %w(index1 index2)
|
|
29
|
+
'slaves' => %w(index1 index2),
|
|
30
|
+
'minWordSizefor1Typo' => 1
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
new_settings = {
|
|
33
34
|
'searchableAttributes' => %w(attr1 attr2),
|
|
34
35
|
'numericAttributesForFiltering' => %w(attr1 attr2),
|
|
35
|
-
'replicas' => %w(index1 index2)
|
|
36
|
+
'replicas' => %w(index1 index2),
|
|
37
|
+
'minWordSizefor1Typo' => 1
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
deserialized_settings = deserialize_settings(old_settings, false)
|