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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +11 -2
  3. data/.dockerignore +38 -0
  4. data/.gitignore +1 -0
  5. data/CHANGELOG.md +80 -1
  6. data/CONTRIBUTING.MD +184 -0
  7. data/DOCKER_README.MD +89 -0
  8. data/Dockerfile +7 -0
  9. data/README.md +4 -4
  10. data/SECURITY.md +1 -1
  11. data/algolia.gemspec +3 -1
  12. data/lib/algolia/analytics_client.rb +1 -1
  13. data/lib/algolia/config/personalization_config.rb +20 -0
  14. data/lib/algolia/config/recommend_config.rb +6 -0
  15. data/lib/algolia/config/recommendation_config.rb +2 -15
  16. data/lib/algolia/helpers.rb +51 -1
  17. data/lib/algolia/http/http_requester.rb +4 -4
  18. data/lib/algolia/insights_client.rb +1 -1
  19. data/lib/algolia/logger_helper.rb +1 -1
  20. data/lib/algolia/personalization_client.rb +60 -0
  21. data/lib/algolia/recommend_client.rb +134 -0
  22. data/lib/algolia/recommendation_client.rb +2 -55
  23. data/lib/algolia/responses/add_api_key_response.rb +1 -1
  24. data/lib/algolia/responses/delete_api_key_response.rb +1 -1
  25. data/lib/algolia/responses/dictionary_response.rb +33 -0
  26. data/lib/algolia/responses/restore_api_key_response.rb +1 -1
  27. data/lib/algolia/responses/update_api_key_response.rb +1 -1
  28. data/lib/algolia/search_client.rb +185 -8
  29. data/lib/algolia/search_index.rb +21 -56
  30. data/lib/algolia/transport/request_options.rb +1 -1
  31. data/lib/algolia/transport/transport.rb +11 -10
  32. data/lib/algolia/version.rb +1 -1
  33. data/lib/algolia.rb +5 -0
  34. data/renovate.json +5 -0
  35. data/test/algolia/integration/account_client_test.rb +2 -2
  36. data/test/algolia/integration/analytics_client_test.rb +6 -2
  37. data/test/algolia/integration/mocks/mock_requester.rb +13 -11
  38. data/test/algolia/integration/personalization_client_test.rb +30 -0
  39. data/test/algolia/integration/recommend_client_test.rb +70 -0
  40. data/test/algolia/integration/search_client_test.rb +108 -13
  41. data/test/algolia/integration/search_index_test.rb +31 -0
  42. data/test/algolia/unit/helpers_test.rb +4 -2
  43. data/test/test_helper.rb +32 -0
  44. metadata +45 -5
@@ -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
- response = @transporter.read(:GET, path_encode('/1/indexes/%s/settings', @name) + handle_params({ getVersion: 2 }), {}, opts)
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
- response = @transporter.write(:PUT, path_encode('/1/indexes/%s/settings', @name), settings, opts)
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
@@ -34,7 +34,7 @@ module Algolia
34
34
  def add_headers(opts = {})
35
35
  unless opts[:headers].nil?
36
36
  opts[:headers].each do |opt, value|
37
- @headers[opt.to_sym] = value
37
+ @headers[opt.to_s] = value
38
38
  end
39
39
  opts.delete(:headers)
40
40
  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] = method.downcase
94
- request[:path] = build_uri_path(path, request_options.params)
95
- request[:body] = build_body(body, request_options, method)
96
- request[:headers] = generate_headers(request_options)
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
- # @option options [String] :headers
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
@@ -1,3 +1,3 @@
1
1
  module Algolia
2
- VERSION = '2.0.0'.freeze
2
+ VERSION = '2.3.2'.freeze
3
3
  end
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'
data/renovate.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": [
3
+ "config:base"
4
+ ]
5
+ }
@@ -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: ['objectID'] })
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 = client.add_ab_test(ab_test)
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 = client.add_ab_test(ab_test)
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, _body, headers, _timeout, _connect_timeout)
7
- connection = get_connection(host)
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
- method: method,
14
- status: 200,
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: response[:status],
21
- body: response[:body],
22
- headers: response[: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 = @@search_client.get_api_key(@api_key[:value])
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
- loop do
237
- begin
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 = @@search_client.get_api_key(@api_key[:value])
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: to_query_string({ query: '', hitsPerPage: 2 }) }
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
- secured_index1.search('')
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)