algolia 2.0.0.pre.alpha.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +146 -0
  3. data/.github/ISSUE_TEMPLATE.md +20 -0
  4. data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  5. data/.gitignore +38 -0
  6. data/.rubocop.yml +186 -0
  7. data/.rubocop_todo.yml +14 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Gemfile +18 -0
  10. data/LICENSE +21 -0
  11. data/README.md +56 -0
  12. data/Rakefile +45 -0
  13. data/Steepfile +6 -0
  14. data/algolia.gemspec +41 -0
  15. data/bin/console +21 -0
  16. data/bin/setup +8 -0
  17. data/lib/algolia.rb +42 -0
  18. data/lib/algolia/account_client.rb +65 -0
  19. data/lib/algolia/analytics_client.rb +105 -0
  20. data/lib/algolia/config/algolia_config.rb +40 -0
  21. data/lib/algolia/config/analytics_config.rb +20 -0
  22. data/lib/algolia/config/insights_config.rb +20 -0
  23. data/lib/algolia/config/recommendation_config.rb +20 -0
  24. data/lib/algolia/config/search_config.rb +40 -0
  25. data/lib/algolia/defaults.rb +35 -0
  26. data/lib/algolia/enums/call_type.rb +4 -0
  27. data/lib/algolia/enums/retry_outcome_type.rb +5 -0
  28. data/lib/algolia/error.rb +29 -0
  29. data/lib/algolia/helpers.rb +83 -0
  30. data/lib/algolia/http/http_requester.rb +84 -0
  31. data/lib/algolia/http/response.rb +23 -0
  32. data/lib/algolia/insights_client.rb +238 -0
  33. data/lib/algolia/iterators/base_iterator.rb +19 -0
  34. data/lib/algolia/iterators/object_iterator.rb +27 -0
  35. data/lib/algolia/iterators/paginator_iterator.rb +44 -0
  36. data/lib/algolia/iterators/rule_iterator.rb +9 -0
  37. data/lib/algolia/iterators/synonym_iterator.rb +9 -0
  38. data/lib/algolia/logger_helper.rb +14 -0
  39. data/lib/algolia/recommendation_client.rb +60 -0
  40. data/lib/algolia/responses/add_api_key_response.rb +38 -0
  41. data/lib/algolia/responses/base_response.rb +9 -0
  42. data/lib/algolia/responses/delete_api_key_response.rb +40 -0
  43. data/lib/algolia/responses/indexing_response.rb +28 -0
  44. data/lib/algolia/responses/multiple_batch_indexing_response.rb +29 -0
  45. data/lib/algolia/responses/multiple_response.rb +45 -0
  46. data/lib/algolia/responses/restore_api_key_response.rb +36 -0
  47. data/lib/algolia/responses/update_api_key_response.rb +39 -0
  48. data/lib/algolia/search_client.rb +614 -0
  49. data/lib/algolia/search_index.rb +1094 -0
  50. data/lib/algolia/transport/request_options.rb +94 -0
  51. data/lib/algolia/transport/retry_strategy.rb +117 -0
  52. data/lib/algolia/transport/stateful_host.rb +26 -0
  53. data/lib/algolia/transport/transport.rb +161 -0
  54. data/lib/algolia/user_agent.rb +25 -0
  55. data/lib/algolia/version.rb +3 -0
  56. data/sig/config/algolia_config.rbs +24 -0
  57. data/sig/config/analytics_config.rbs +11 -0
  58. data/sig/config/insights_config.rbs +11 -0
  59. data/sig/config/recommendation_config.rbs +11 -0
  60. data/sig/config/search_config.rbs +11 -0
  61. data/sig/enums/call_type.rbs +5 -0
  62. data/sig/helpers.rbs +12 -0
  63. data/sig/http/http_requester.rbs +17 -0
  64. data/sig/http/response.rbs +14 -0
  65. data/sig/interfaces/_connection.rbs +16 -0
  66. data/sig/iterators/base_iterator.rbs +15 -0
  67. data/sig/iterators/object_iterator.rbs +6 -0
  68. data/sig/iterators/paginator_iterator.rbs +8 -0
  69. data/sig/iterators/rule_iterator.rbs +5 -0
  70. data/sig/iterators/synonym_iterator.rbs +5 -0
  71. data/sig/transport/request_options.rbs +33 -0
  72. data/sig/transport/stateful_host.rbs +21 -0
  73. data/test/algolia/integration/account_client_test.rb +47 -0
  74. data/test/algolia/integration/analytics_client_test.rb +113 -0
  75. data/test/algolia/integration/base_test.rb +9 -0
  76. data/test/algolia/integration/insights_client_test.rb +80 -0
  77. data/test/algolia/integration/mocks/mock_requester.rb +45 -0
  78. data/test/algolia/integration/recommendation_client_test.rb +30 -0
  79. data/test/algolia/integration/search_client_test.rb +361 -0
  80. data/test/algolia/integration/search_index_test.rb +698 -0
  81. data/test/algolia/unit/helpers_test.rb +69 -0
  82. data/test/algolia/unit/retry_strategy_test.rb +139 -0
  83. data/test/algolia/unit/user_agent_test.rb +16 -0
  84. data/test/test_helper.rb +89 -0
  85. data/upgrade_guide.md +595 -0
  86. metadata +307 -0
@@ -0,0 +1,45 @@
1
+ class MockRequester
2
+ def initialize
3
+ @connection = nil
4
+ end
5
+
6
+ def send_request(host, method, path, _body, headers, _timeout, _connect_timeout)
7
+ connection = get_connection(host)
8
+ response = {
9
+ connection: connection,
10
+ host: host,
11
+ path: path,
12
+ 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
17
+ }
18
+
19
+ Algolia::Http::Response.new(
20
+ status: response[:status],
21
+ body: response[:body],
22
+ headers: response[:headers]
23
+ )
24
+ end
25
+
26
+ # Retrieve the connection from the @connections
27
+ #
28
+ # @param host [StatefulHost]
29
+ #
30
+ # @return [Faraday::Connection]
31
+ #
32
+ def get_connection(host)
33
+ @connection = host
34
+ end
35
+
36
+ # Build url from host, path and parameters
37
+ #
38
+ # @param host [StatefulHost]
39
+ #
40
+ # @return [String]
41
+ #
42
+ def build_url(host)
43
+ host.protocol + host.url
44
+ end
45
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'base_test'
2
+ require 'date'
3
+
4
+ class RecommendationClientTest < BaseTest
5
+ describe 'Recommendation client' do
6
+ def test_recommendation_client
7
+ client = Algolia::Recommendation::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,361 @@
1
+ require_relative 'base_test'
2
+
3
+ class SearchClientTest < BaseTest
4
+ describe 'customize search client' do
5
+ def test_with_custom_adapter
6
+ client = Algolia::Search::Client.new(@@search_config, adapter: 'httpclient')
7
+ index = client.init_index(get_test_index_name('test_custom_adapter'))
8
+
9
+ index.save_object!({ name: 'test', data: 10 }, { auto_generate_object_id_if_not_exist: true })
10
+ response = index.search('test')
11
+
12
+ refute_empty response[:hits]
13
+ assert_equal 'test', response[:hits][0][:name]
14
+ assert_equal 10, response[:hits][0][:data]
15
+ end
16
+
17
+ def test_with_custom_requester
18
+ client = Algolia::Search::Client.new(@@search_config, http_requester: MockRequester.new)
19
+ index = client.init_index(get_test_index_name('test_custom_requester'))
20
+
21
+ response = index.search('test')
22
+
23
+ refute_nil response[:hits]
24
+ end
25
+
26
+ def test_without_providing_config
27
+ client = Algolia::Search::Client.create(APPLICATION_ID_1, ADMIN_KEY_1)
28
+ index = client.init_index(get_test_index_name('test_no_config'))
29
+ index.save_object!({ name: 'test', data: 10 }, { auto_generate_object_id_if_not_exist: true })
30
+ response = index.search('test')
31
+
32
+ refute_empty response[:hits]
33
+ assert_equal 'test', response[:hits][0][:name]
34
+ assert_equal 10, response[:hits][0][:data]
35
+ end
36
+ end
37
+
38
+ describe 'copy and move index' do
39
+ def before_all
40
+ super
41
+ @index_name = get_test_index_name('copy_index')
42
+ @index = @@search_client.init_index(@index_name)
43
+ end
44
+
45
+ def test_copy_and_move_index
46
+ responses = Algolia::MultipleResponse.new
47
+
48
+ objects = [
49
+ { objectID: 'one', company: 'apple' },
50
+ { objectID: 'two', company: 'algolia' }
51
+ ]
52
+ responses.push(@index.save_objects(objects))
53
+
54
+ settings = { attributesForFaceting: ['company'] }
55
+ responses.push(@index.set_settings(settings))
56
+
57
+ synonym = {
58
+ objectID: 'google_placeholder',
59
+ type: 'placeholder',
60
+ placeholder: '<GOOG>',
61
+ replacements: %w(Google GOOG)
62
+ }
63
+ responses.push(@index.save_synonym(synonym))
64
+
65
+ rule = {
66
+ objectID: 'company_auto_faceting',
67
+ condition: {
68
+ anchoring: 'contains',
69
+ pattern: '{facet:company}'
70
+ },
71
+ consequence: {
72
+ params: { automaticFacetFilters: ['company'] }
73
+ }
74
+ }
75
+ responses.push(@index.save_rule(rule))
76
+
77
+ responses.wait
78
+
79
+ copy_settings_index = @@search_client.init_index(get_test_index_name('copy_index_settings'))
80
+ copy_rules_index = @@search_client.init_index(get_test_index_name('copy_index_rules'))
81
+ copy_synonyms_index = @@search_client.init_index(get_test_index_name('copy_index_synonyms'))
82
+ copy_full_copy_index = @@search_client.init_index(get_test_index_name('copy_index_full_copy'))
83
+ @@search_client.copy_settings!(@index_name, copy_settings_index.index_name)
84
+ @@search_client.copy_rules!(@index_name, copy_rules_index.index_name)
85
+ @@search_client.copy_synonyms!(@index_name, copy_synonyms_index.index_name)
86
+ @@search_client.copy_index!(@index_name, copy_full_copy_index.index_name)
87
+
88
+ assert_equal @index.get_settings, copy_settings_index.get_settings
89
+ assert_equal @index.get_rule(rule[:objectID]), copy_rules_index.get_rule(rule[:objectID])
90
+ assert_equal @index.get_synonym(synonym[:objectID]), copy_synonyms_index.get_synonym(synonym[:objectID])
91
+ assert_equal @index.get_settings, copy_full_copy_index.get_settings
92
+ assert_equal @index.get_rule(rule[:objectID]), copy_full_copy_index.get_rule(rule[:objectID])
93
+ assert_equal @index.get_synonym(synonym[:objectID]), copy_full_copy_index.get_synonym(synonym[:objectID])
94
+
95
+ moved_index = @@search_client.init_index(get_test_index_name('move_index'))
96
+ @@search_client.move_index!(@index_name, moved_index.index_name)
97
+
98
+ moved_index.get_synonym('google_placeholder')
99
+ moved_index.get_rule('company_auto_faceting')
100
+ assert_equal moved_index.get_settings[:attributesForFaceting], ['company']
101
+
102
+ moved_index.browse_objects.each do |obj|
103
+ assert_includes objects, obj
104
+ end
105
+ end
106
+ end
107
+
108
+ describe 'MCM' do
109
+ def before_all
110
+ super
111
+ @mcm_client = Algolia::Search::Client.create(MCM_APPLICATION_ID, MCM_ADMIN_KEY)
112
+ end
113
+
114
+ def test_mcm
115
+ clusters = @mcm_client.list_clusters
116
+ assert_equal 2, clusters[:clusters].length
117
+
118
+ cluster_name = clusters[:clusters][0][:clusterName]
119
+
120
+ mcm_user_id0 = get_mcm_user_name(0)
121
+ mcm_user_id1 = get_mcm_user_name(1)
122
+ mcm_user_id2 = get_mcm_user_name(2)
123
+
124
+ @mcm_client.assign_user_id(mcm_user_id0, cluster_name)
125
+ @mcm_client.assign_user_ids([mcm_user_id1, mcm_user_id2], cluster_name)
126
+
127
+ 0.upto(2) do |i|
128
+ retrieved_user = retrieve_user_id(i)
129
+ assert_equal(retrieved_user, {
130
+ userID: get_mcm_user_name(i),
131
+ clusterName: cluster_name,
132
+ nbRecords: 0,
133
+ dataSize: 0
134
+ })
135
+ end
136
+
137
+ refute_equal 0, @mcm_client.list_user_ids[:userIDs].length
138
+ refute_equal 0, @mcm_client.get_top_user_ids[:topUsers].length
139
+
140
+ 0.upto(2) do |i|
141
+ remove_user_id(i)
142
+ end
143
+
144
+ 0.upto(2) do |i|
145
+ assert_removed(i)
146
+ end
147
+
148
+ has_pending_mappings = @mcm_client.pending_mappings?({ retrieveMappings: true })
149
+ refute_nil has_pending_mappings
150
+ assert has_pending_mappings[:pending]
151
+ assert has_pending_mappings[:clusters]
152
+ assert_instance_of Hash, has_pending_mappings[:clusters]
153
+
154
+ has_pending_mappings = @mcm_client.pending_mappings?({ retrieveMappings: false })
155
+ refute_nil has_pending_mappings
156
+ assert has_pending_mappings[:pending]
157
+ refute has_pending_mappings[:clusters]
158
+ end
159
+
160
+ def retrieve_user_id(number)
161
+ loop do
162
+ begin
163
+ return @mcm_client.get_user_id(get_mcm_user_name(number))
164
+ rescue Algolia::AlgoliaHttpError => e
165
+ if e.code != 404
166
+ raise StandardError
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ def remove_user_id(number)
173
+ loop do
174
+ begin
175
+ return @mcm_client.remove_user_id(get_mcm_user_name(number))
176
+ rescue Algolia::AlgoliaHttpError => e
177
+ if e.code != 400
178
+ raise StandardError
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ def assert_removed(number)
185
+ loop do
186
+ begin
187
+ return @mcm_client.get_user_id(get_mcm_user_name(number))
188
+ rescue Algolia::AlgoliaHttpError => e
189
+ if e.code == 404
190
+ return true
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ describe 'API keys' do
198
+ def before_all
199
+ super
200
+ response = @@search_client.add_api_key!(['search'], {
201
+ description: 'A description',
202
+ indexes: ['index'],
203
+ maxHitsPerQuery: 1000,
204
+ maxQueriesPerIPPerHour: 1000,
205
+ queryParameters: 'typoTolerance=strict',
206
+ referers: ['referer'],
207
+ validity: 600
208
+ })
209
+ @api_key = @@search_client.get_api_key(response.raw_response[:key])
210
+ end
211
+
212
+ def teardown
213
+ @@search_client.delete_api_key!(@api_key[:value])
214
+ end
215
+
216
+ def test_api_keys
217
+ assert_equal ['search'], @api_key[:acl]
218
+
219
+ api_keys = @@search_client.list_api_keys[:keys].map do |key|
220
+ key[:value]
221
+ end
222
+ assert_includes api_keys, @api_key[:value]
223
+
224
+ @@search_client.update_api_key!(@api_key[:value], { maxHitsPerQuery: 42 })
225
+ updated_api_key = @@search_client.get_api_key(@api_key[:value])
226
+ assert_equal 42, updated_api_key[:maxHitsPerQuery]
227
+
228
+ @@search_client.delete_api_key!(@api_key[:value])
229
+
230
+ exception = assert_raises Algolia::AlgoliaHttpError do
231
+ @@search_client.get_api_key(@api_key[:value])
232
+ end
233
+
234
+ assert_equal 'Key does not exist', exception.message
235
+
236
+ @@search_client.restore_api_key!(@api_key[:value])
237
+
238
+ restored_key = @@search_client.get_api_key(@api_key[:value])
239
+
240
+ refute_nil restored_key
241
+ end
242
+ end
243
+
244
+ describe 'Get logs' do
245
+ def test_logs
246
+ @@search_client.list_indexes
247
+ @@search_client.list_indexes
248
+
249
+ assert_equal 2, @@search_client.get_logs({
250
+ length: 2,
251
+ offset: 0,
252
+ type: 'all'
253
+ })[:logs].length
254
+ end
255
+ end
256
+
257
+ describe 'Multiple Operations' do
258
+ def before_all
259
+ @index1 = @@search_client.init_index(get_test_index_name('multiple_operations'))
260
+ @index2 = @@search_client.init_index(get_test_index_name('multiple_operations_dev'))
261
+ end
262
+
263
+ def test_multiple_operations
264
+ index_name1 = @index1.index_name
265
+ index_name2 = @index2.index_name
266
+
267
+ response = @@search_client.multiple_batch!([
268
+ { indexName: index_name1, action: 'addObject', body: { firstname: 'Jimmie' } },
269
+ { indexName: index_name1, action: 'addObject', body: { firstname: 'Jimmie' } },
270
+ { indexName: index_name2, action: 'addObject', body: { firstname: 'Jimmie' } },
271
+ { indexName: index_name2, action: 'addObject', body: { firstname: 'Jimmie' } }
272
+ ])
273
+
274
+ object_ids = response.raw_response[:objectIDs]
275
+ objects = @@search_client.multiple_get_objects([
276
+ { indexName: index_name1, objectID: object_ids[0] },
277
+ { indexName: index_name1, objectID: object_ids[1] },
278
+ { indexName: index_name2, objectID: object_ids[2] },
279
+ { indexName: index_name2, objectID: object_ids[3] }
280
+ ])[:results]
281
+
282
+ assert_equal object_ids[0], objects[0][:objectID]
283
+ assert_equal object_ids[1], objects[1][:objectID]
284
+ assert_equal object_ids[2], objects[2][:objectID]
285
+ assert_equal object_ids[3], objects[3][:objectID]
286
+
287
+ results = @@search_client.multiple_queries([
288
+ { indexName: index_name1, params: to_query_string({ query: '', hitsPerPage: 2 }) },
289
+ { indexName: index_name2, params: to_query_string({ query: '', hitsPerPage: 2 }) }
290
+ ], { strategy: 'none' })[:results]
291
+
292
+ assert_equal 2, results.length
293
+ assert_equal 2, results[0][:hits].length
294
+ assert_equal 2, results[0][:nbHits]
295
+ assert_equal 2, results[1][:hits].length
296
+ assert_equal 2, results[1][:nbHits]
297
+
298
+ results = @@search_client.multiple_queries([
299
+ { indexName: index_name1, params: to_query_string({ query: '', hitsPerPage: 2 }) },
300
+ { indexName: index_name2, params: to_query_string({ query: '', hitsPerPage: 2 }) }
301
+ ], { strategy: 'stopIfEnoughMatches' })[:results]
302
+
303
+ assert_equal 2, results.length
304
+ assert_equal 2, results[0][:hits].length
305
+ assert_equal 2, results[0][:nbHits]
306
+ assert_equal 0, results[1][:hits].length
307
+ assert_equal 0, results[1][:nbHits]
308
+ end
309
+
310
+ describe 'Secured API keys' do
311
+ def test_secured_api_keys
312
+ @index1 = @@search_client.init_index(get_test_index_name('secured_api_keys'))
313
+ @index2 = @@search_client.init_index(get_test_index_name('secured_api_keys_dev'))
314
+ @index1.save_object!({ objectID: 'one' })
315
+ @index2.save_object!({ objectID: 'one' })
316
+
317
+ now = Time.now.to_i
318
+ secured_api_key = Algolia::Search::Client.generate_secured_api_key(SEARCH_KEY_1, {
319
+ validUntil: now + (10 * 60),
320
+ restrictIndices: @index1.index_name
321
+ })
322
+
323
+ secured_client = Algolia::Search::Client.create(APPLICATION_ID_1, secured_api_key)
324
+ secured_index1 = secured_client.init_index(@index1.index_name)
325
+ secured_index2 = secured_client.init_index(@index2.index_name)
326
+
327
+ secured_index1.search('')
328
+ exception = assert_raises Algolia::AlgoliaHttpError do
329
+ secured_index2.search('')
330
+ end
331
+
332
+ assert_equal 403, exception.code
333
+ assert_equal 'Index not allowed with this API key', exception.message
334
+ end
335
+ end
336
+
337
+ describe 'Expired Secured API keys' do
338
+ def test_expired_secured_api_keys
339
+ now = Time.now.to_i
340
+ secured_api_key = Algolia::Search::Client.generate_secured_api_key('foo', {
341
+ validUntil: now - (10 * 60)
342
+ })
343
+ remaining = Algolia::Search::Client.get_secured_api_key_remaining_validity(secured_api_key)
344
+ assert remaining < 0
345
+
346
+ secured_api_key = Algolia::Search::Client.generate_secured_api_key('foo', {
347
+ validUntil: now + (10 * 60)
348
+ })
349
+ remaining = Algolia::Search::Client.get_secured_api_key_remaining_validity(secured_api_key)
350
+ assert remaining > 0
351
+
352
+ secured_api_key = Algolia::Search::Client.generate_secured_api_key('foo', {})
353
+ exception = assert_raises Algolia::AlgoliaError do
354
+ Algolia::Search::Client.get_secured_api_key_remaining_validity(secured_api_key)
355
+ end
356
+
357
+ assert_equal 'The SecuredAPIKey doesn\'t have a validUntil parameter.', exception.message
358
+ end
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,698 @@
1
+ require 'httpclient'
2
+ require_relative 'base_test'
3
+
4
+ class SearchIndexTest < BaseTest
5
+ describe 'pass request options' do
6
+ def before_all
7
+ super
8
+ @index = @@search_client.init_index(get_test_index_name('options'))
9
+ end
10
+
11
+ def test_with_wrong_credentials
12
+ exception = assert_raises Algolia::AlgoliaHttpError do
13
+ @index.save_object(generate_object('111'), {
14
+ headers: {
15
+ 'X-Algolia-Application-Id' => 'XXXXX',
16
+ 'X-Algolia-API-Key' => 'XXXXX'
17
+ }
18
+ })
19
+ end
20
+
21
+ assert_equal 'Invalid Application-ID or API key', exception.message
22
+ end
23
+ end
24
+
25
+ describe 'save objects' do
26
+ def before_all
27
+ super
28
+ @index = @@search_client.init_index(get_test_index_name('indexing'))
29
+ end
30
+
31
+ def retrieve_last_object_ids(responses)
32
+ responses.last.raw_response[:objectIDs]
33
+ end
34
+
35
+ def test_save_objects
36
+ responses = Algolia::MultipleResponse.new
37
+ object_ids = []
38
+
39
+ obj1 = generate_object('obj1')
40
+ responses.push(@index.save_object(obj1))
41
+ object_ids.push(retrieve_last_object_ids(responses))
42
+ obj2 = generate_object
43
+ response = @index.save_object(obj2, { auto_generate_object_id_if_not_exist: true })
44
+ responses.push(response)
45
+ object_ids.push(retrieve_last_object_ids(responses))
46
+ responses.push(@index.save_objects([]))
47
+ object_ids.push(retrieve_last_object_ids(responses))
48
+ obj3 = generate_object('obj3')
49
+ obj4 = generate_object('obj4')
50
+ responses.push(@index.save_objects([obj3, obj4]))
51
+ object_ids.push(retrieve_last_object_ids(responses))
52
+ obj5 = generate_object
53
+ obj6 = generate_object
54
+ responses.push(@index.save_objects([obj5, obj6], { auto_generate_object_id_if_not_exist: true }))
55
+ object_ids.push(retrieve_last_object_ids(responses))
56
+ object_ids.flatten!
57
+ objects = 1.upto(1000).map do |i|
58
+ generate_object(i.to_s)
59
+ end
60
+
61
+ @index.config.batch_size = 100
62
+ responses.push(@index.save_objects(objects))
63
+ responses.wait
64
+
65
+ assert_equal obj1[:property], @index.get_object(object_ids[0])[:property]
66
+ assert_equal obj2[:property], @index.get_object(object_ids[1])[:property]
67
+ assert_equal obj3[:property], @index.get_object(object_ids[2])[:property]
68
+ assert_equal obj4[:property], @index.get_object(object_ids[3])[:property]
69
+ assert_equal obj5[:property], @index.get_object(object_ids[4])[:property]
70
+ assert_equal obj6[:property], @index.get_object(object_ids[5])[:property]
71
+
72
+ results = @index.get_objects((1..1000).to_a)[:results]
73
+
74
+ results.each do |obj|
75
+ assert_includes(objects, obj)
76
+ end
77
+
78
+ assert_equal objects.length, results.length
79
+ browsed_objects = []
80
+ @index.browse_objects do |hit|
81
+ browsed_objects.push(hit)
82
+ end
83
+
84
+ assert_equal 1006, browsed_objects.length
85
+ objects.each do |obj|
86
+ assert_includes(browsed_objects, obj)
87
+ end
88
+
89
+ [obj1, obj3, obj4].each do |obj|
90
+ assert_includes(browsed_objects, obj)
91
+ end
92
+
93
+ responses = Algolia::MultipleResponse.new
94
+
95
+ obj1[:property] = 'new property'
96
+ responses.push(@index.partial_update_object(obj1))
97
+
98
+ obj3[:property] = 'new property 3'
99
+ obj4[:property] = 'new property 4'
100
+ responses.push(@index.partial_update_objects([obj3, obj4]))
101
+
102
+ responses.wait
103
+
104
+ assert_equal obj1[:property], @index.get_object(object_ids[0])[:property]
105
+ assert_equal obj3[:property], @index.get_object(object_ids[2])[:property]
106
+ assert_equal obj4[:property], @index.get_object(object_ids[3])[:property]
107
+
108
+ delete_by_obj = { objectID: 'obj_del_by', _tags: 'algolia', property: 'property' }
109
+ @index.save_object!(delete_by_obj)
110
+
111
+ responses = Algolia::MultipleResponse.new
112
+
113
+ responses.push(@index.delete_object(object_ids.shift))
114
+ responses.push(@index.delete_by({ tagFilters: ['algolia'] }))
115
+ responses.push(@index.delete_objects(object_ids))
116
+ responses.push(@index.clear_objects)
117
+
118
+ responses.wait
119
+
120
+ browsed_objects = []
121
+ @index.browse_objects do |hit|
122
+ browsed_objects.push(hit)
123
+ end
124
+
125
+ assert_equal 0, browsed_objects.length
126
+ end
127
+
128
+ def test_save_object_without_object_id_and_fail
129
+ exception = assert_raises Algolia::AlgoliaError do
130
+ @index.save_object(generate_object)
131
+ end
132
+
133
+ assert_equal "Missing 'objectID'", exception.message
134
+ end
135
+
136
+ def test_save_objects_with_single_object_and_fail
137
+ exception = assert_raises Algolia::AlgoliaError do
138
+ @index.save_objects(generate_object)
139
+ end
140
+
141
+ assert_equal 'argument must be an array of objects', exception.message
142
+ end
143
+
144
+ def test_save_objects_with_array_of_integers_and_fail
145
+ exception = assert_raises Algolia::AlgoliaError do
146
+ @index.save_objects([2222, 3333])
147
+ end
148
+
149
+ assert_equal 'argument must be an array of object, got: 2222', exception.message
150
+ end
151
+ end
152
+
153
+ describe 'settings' do
154
+ def before_all
155
+ super
156
+ @index_name = get_test_index_name('settings')
157
+ @index = @@search_client.init_index(@index_name)
158
+ end
159
+
160
+ def test_settings
161
+ @index.save_object!(generate_object('obj1'))
162
+
163
+ settings = {
164
+ searchableAttributes: %w(attribute1 attribute2 attribute3 ordered(attribute4) unordered(attribute5)),
165
+ attributesForFaceting: %w(attribute1 filterOnly(attribute2) searchable(attribute3)),
166
+ unretrievableAttributes: %w(
167
+ attribute1
168
+ attribute2
169
+ ),
170
+ attributesToRetrieve: %w(
171
+ attribute3
172
+ attribute4
173
+ ),
174
+ ranking: %w(asc(attribute1) desc(attribute2) attribute custom exact filters geo proximity typo words),
175
+ customRanking: %w(asc(attribute1) desc(attribute1)),
176
+ replicas: [
177
+ @index_name + '_replica1',
178
+ @index_name + '_replica2'
179
+ ],
180
+ maxValuesPerFacet: 100,
181
+ sortFacetValuesBy: 'count',
182
+ attributesToHighlight: %w(
183
+ attribute1
184
+ attribute2
185
+ ),
186
+ attributesToSnippet: %w(attribute1:10 attribute2:8),
187
+ highlightPreTag: '<strong>',
188
+ highlightPostTag: '</strong>',
189
+ snippetEllipsisText: ' and so on.',
190
+ restrictHighlightAndSnippetArrays: true,
191
+ hitsPerPage: 42,
192
+ paginationLimitedTo: 43,
193
+ minWordSizefor1Typo: 2,
194
+ minWordSizefor2Typos: 6,
195
+ typoTolerance: 'false',
196
+ allowTyposOnNumericTokens: false,
197
+ ignorePlurals: true,
198
+ disableTypoToleranceOnAttributes: %w(
199
+ attribute1
200
+ attribute2
201
+ ),
202
+ disableTypoToleranceOnWords: %w(
203
+ word1
204
+ word2
205
+ ),
206
+ separatorsToIndex: '()[]',
207
+ queryType: 'prefixNone',
208
+ removeWordsIfNoResults: 'allOptional',
209
+ advancedSyntax: true,
210
+ optionalWords: %w(
211
+ word1
212
+ word2
213
+ ),
214
+ removeStopWords: true,
215
+ disablePrefixOnAttributes: %w(
216
+ attribute1
217
+ attribute2
218
+ ),
219
+ disableExactOnAttributes: %w(
220
+ attribute1
221
+ attribute2
222
+ ),
223
+ exactOnSingleWordQuery: 'word',
224
+ enableRules: false,
225
+ numericAttributesForFiltering: %w(
226
+ attribute1
227
+ attribute2
228
+ ),
229
+ allowCompressionOfIntegerArray: true,
230
+ attributeForDistinct: 'attribute1',
231
+ distinct: 2,
232
+ replaceSynonymsInHighlight: false,
233
+ minProximity: 7,
234
+ responseFields: %w(
235
+ hits
236
+ hitsPerPage
237
+ ),
238
+ maxFacetHits: 100,
239
+ camelCaseAttributes: %w(
240
+ attribute1
241
+ attribute2
242
+ ),
243
+ decompoundedAttributes: {
244
+ de: %w(attribute1 attribute2),
245
+ fi: ['attribute3']
246
+ },
247
+ keepDiacriticsOnCharacters: 'øé',
248
+ queryLanguages: %w(
249
+ en
250
+ fr
251
+ ),
252
+ alternativesAsExact: ['ignorePlurals'],
253
+ advancedSyntaxFeatures: ['exactPhrase'],
254
+ userData: {
255
+ customUserData: 42.0
256
+ },
257
+ indexLanguages: ['ja']
258
+ }
259
+
260
+ @index.set_settings!(settings)
261
+
262
+ # Because the response settings dict contains the extra version key, we
263
+ # also add it to the expected settings dict to prevent the test to fail
264
+ # for a missing key.
265
+ settings[:version] = 2
266
+
267
+ assert_equal @index.get_settings, settings
268
+
269
+ settings[:typoTolerance] = 'min'
270
+ settings[:ignorePlurals] = %w(en fr)
271
+ settings[:removeStopWords] = %w(en fr)
272
+ settings[:distinct] = true
273
+
274
+ @index.set_settings!(settings)
275
+
276
+ assert_equal @index.get_settings, settings
277
+ end
278
+ end
279
+
280
+ describe 'search' do
281
+ def before_all
282
+ super
283
+ @index = @@search_client.init_index(get_test_index_name('search'))
284
+ @index.save_objects!(create_employee_records, { auto_generate_object_id_if_not_exist: true })
285
+ @index.set_settings!(attributesForFaceting: ['searchable(company)'])
286
+ end
287
+
288
+ def test_search_objects
289
+ response = @index.search('algolia')
290
+
291
+ assert_equal 2, response[:nbHits]
292
+ assert_equal 0, Algolia::Search::Index.get_object_position(response, 'nicolas-dessaigne')
293
+ assert_equal 1, Algolia::Search::Index.get_object_position(response, 'julien-lemoine')
294
+ assert_equal(-1, Algolia::Search::Index.get_object_position(response, ''))
295
+ end
296
+
297
+ def test_find_objects
298
+ exception = assert_raises Algolia::AlgoliaHttpError do
299
+ @index.find_object(-> (_hit) { false }, { query: '', paginate: false })
300
+ end
301
+
302
+ assert_equal 'Object not found', exception.message
303
+
304
+ response = @index.find_object(-> (_hit) { true }, { query: '', paginate: false })
305
+ assert_equal 0, response[:position]
306
+ assert_equal 0, response[:page]
307
+
308
+ condition = -> (obj) do
309
+ obj.has_key?(:company) && obj[:company] == 'Apple'
310
+ end
311
+
312
+ exception = assert_raises Algolia::AlgoliaHttpError do
313
+ @index.find_object(condition, { query: 'algolia', paginate: false })
314
+ end
315
+
316
+ assert_equal 'Object not found', exception.message
317
+
318
+ exception = assert_raises Algolia::AlgoliaHttpError do
319
+ @index.find_object(condition, { query: '', paginate: false, hitsPerPage: 5 })
320
+ end
321
+
322
+ assert_equal 'Object not found', exception.message
323
+
324
+ response = @index.find_object(condition, { query: '', paginate: true, hitsPerPage: 5 })
325
+ assert_equal 0, response[:position]
326
+ assert_equal 2, response[:page]
327
+
328
+ response = @index.search('elon', { clickAnalytics: true })
329
+
330
+ refute_nil response[:queryID]
331
+
332
+ response = @index.search('elon', { facets: '*', facetFilters: ['company:tesla'] })
333
+
334
+ assert_equal 1, response[:nbHits]
335
+
336
+ response = @index.search('elon', { facets: '*', filters: '(company:tesla OR company:spacex)' })
337
+
338
+ assert_equal 2, response[:nbHits]
339
+
340
+ response = @index.search_for_facet_values('company', 'a')
341
+
342
+ assert(response[:facetHits].any? { |hit| hit[:value] == 'Algolia' })
343
+ assert(response[:facetHits].any? { |hit| hit[:value] == 'Amazon' })
344
+ assert(response[:facetHits].any? { |hit| hit[:value] == 'Apple' })
345
+ assert(response[:facetHits].any? { |hit| hit[:value] == 'Arista Networks' })
346
+ end
347
+ end
348
+
349
+ describe 'synonyms' do
350
+ def before_all
351
+ super
352
+ @index = @@search_client.init_index(get_test_index_name('synonyms'))
353
+ end
354
+
355
+ def test_synonyms
356
+ responses = Algolia::MultipleResponse.new
357
+ responses.push(@index.save_objects([
358
+ { console: 'Sony PlayStation <PLAYSTATIONVERSION>' },
359
+ { console: 'Nintendo Switch' },
360
+ { console: 'Nintendo Wii U' },
361
+ { console: 'Nintendo Game Boy Advance' },
362
+ { console: 'Microsoft Xbox' },
363
+ { console: 'Microsoft Xbox 360' },
364
+ { console: 'Microsoft Xbox One' }
365
+ ], { auto_generate_object_id_if_not_exist: true }))
366
+
367
+ synonym1 = {
368
+ objectID: 'gba',
369
+ type: 'synonym',
370
+ synonyms: ['gba', 'gameboy advance', 'game boy advance']
371
+ }
372
+
373
+ responses.push(@index.save_synonym(synonym1))
374
+
375
+ synonym2 = {
376
+ objectID: 'wii_to_wii_u',
377
+ type: 'onewaysynonym',
378
+ input: 'wii',
379
+ synonyms: ['wii U']
380
+ }
381
+
382
+ synonym3 = {
383
+ objectID: 'playstation_version_placeholder',
384
+ type: 'placeholder',
385
+ placeholder: '<PLAYSTATIONVERSION>',
386
+ replacements: ['1', 'One', '2', '3', '4', '4 Pro']
387
+ }
388
+
389
+ synonym4 = {
390
+ objectID: 'ps4',
391
+ type: 'altcorrection1',
392
+ word: 'ps4',
393
+ corrections: ['playstation4']
394
+ }
395
+
396
+ synonym5 = {
397
+ objectID: 'psone',
398
+ type: 'altcorrection2',
399
+ word: 'psone',
400
+ corrections: ['playstationone']
401
+ }
402
+
403
+ responses.push(@index.save_synonyms([synonym2, synonym3, synonym4, synonym5]))
404
+
405
+ responses.wait
406
+
407
+ assert_equal synonym1, @index.get_synonym(synonym1[:objectID])
408
+ assert_equal synonym2, @index.get_synonym(synonym2[:objectID])
409
+ assert_equal synonym3, @index.get_synonym(synonym3[:objectID])
410
+ assert_equal synonym4, @index.get_synonym(synonym4[:objectID])
411
+ assert_equal synonym5, @index.get_synonym(synonym5[:objectID])
412
+
413
+ res = @index.search_synonyms('')
414
+ assert_equal 5, res[:hits].length
415
+
416
+ results = []
417
+ @index.browse_synonyms do |synonym|
418
+ results.push(synonym)
419
+ end
420
+
421
+ synonyms = [
422
+ synonym1,
423
+ synonym2,
424
+ synonym3,
425
+ synonym4,
426
+ synonym5
427
+ ]
428
+
429
+ synonyms.each do |synonym|
430
+ assert_includes results, synonym
431
+ end
432
+
433
+ @index.delete_synonym!('gba')
434
+
435
+ exception = assert_raises Algolia::AlgoliaHttpError do
436
+ @index.get_synonym('gba')
437
+ end
438
+
439
+ assert_equal 'Synonym set does not exist', exception.message
440
+
441
+ @index.clear_synonyms!
442
+
443
+ res = @index.search_synonyms('')
444
+ assert_equal 0, res[:nbHits]
445
+ end
446
+
447
+ describe 'query rules' do
448
+ def before_all
449
+ super
450
+ @index = @@search_client.init_index(get_test_index_name('rules'))
451
+ end
452
+
453
+ def test_rules
454
+ responses = Algolia::MultipleResponse.new
455
+ responses.push(@index.save_objects([
456
+ { objectID: 'iphone_7', brand: 'Apple', model: '7' },
457
+ { objectID: 'iphone_8', brand: 'Apple', model: '8' },
458
+ { objectID: 'iphone_x', brand: 'Apple', model: 'X' },
459
+ { objectID: 'one_plus_one', brand: 'OnePlus',
460
+ model: 'One' },
461
+ { objectID: 'one_plus_two', brand: 'OnePlus',
462
+ model: 'Two' }
463
+ ], { auto_generate_object_id_if_not_exist: true }))
464
+
465
+ responses.push(@index.set_settings({ attributesForFaceting: %w(brand model) }))
466
+
467
+ rule1 = {
468
+ objectID: 'brand_automatic_faceting',
469
+ enabled: false,
470
+ condition: { anchoring: 'is', pattern: '{facet:brand}' },
471
+ consequence: {
472
+ params: {
473
+ automaticFacetFilters: [
474
+ { facet: 'brand', disjunctive: true, score: 42 }
475
+ ]
476
+ }
477
+ },
478
+ validity: [
479
+ {
480
+ from: 1532439300, # 07/24/2018 13:35:00 UTC
481
+ until: 1532525700 # 07/25/2018 13:35:00 UTC
482
+ },
483
+ {
484
+ from: 1532612100, # 07/26/2018 13:35:00 UTC
485
+ until: 1532698500 # 07/27/2018 13:35:00 UTC
486
+ }
487
+ ],
488
+ description: 'Automatic apply the faceting on `brand` if a brand value is found in the query'
489
+ }
490
+
491
+ responses.push(@index.save_rule(rule1))
492
+
493
+ rule2 = {
494
+ objectID: 'query_edits',
495
+ conditions: [{ anchoring: 'is', pattern: 'mobile phone', alternatives: true }],
496
+ consequence: {
497
+ filterPromotes: false,
498
+ params: {
499
+ query: {
500
+ edits: [
501
+ { type: 'remove', delete: 'mobile' },
502
+ { type: 'replace', delete: 'phone', insert: 'iphone' }
503
+ ]
504
+ }
505
+ }
506
+ }
507
+ }
508
+
509
+ rule3 = {
510
+ objectID: 'query_promo',
511
+ consequence: {
512
+ params: {
513
+ filters: 'brand:OnePlus'
514
+ }
515
+ }
516
+ }
517
+
518
+ rule4 = {
519
+ objectID: 'query_promo_summer',
520
+ condition: {
521
+ context: 'summer'
522
+ },
523
+ consequence: {
524
+ params: {
525
+ filters: 'model:One'
526
+ }
527
+ }
528
+ }
529
+
530
+ responses.push(@index.save_rules([rule2, rule3, rule4]))
531
+
532
+ responses.wait
533
+
534
+ assert_equal 1, @index.search('', { ruleContexts: ['summer'] })[:nbHits]
535
+
536
+ assert_equal rule1, rule_without_metadata(@index.get_rule(rule1[:objectID]))
537
+ assert_equal rule2, rule_without_metadata(@index.get_rule(rule2[:objectID]))
538
+ assert_equal rule3, rule_without_metadata(@index.get_rule(rule3[:objectID]))
539
+ assert_equal rule4, rule_without_metadata(@index.get_rule(rule4[:objectID]))
540
+
541
+ assert_equal 4, @index.search_rules('')[:nbHits]
542
+
543
+ results = []
544
+ @index.browse_rules do |rule|
545
+ results.push(rule)
546
+ end
547
+
548
+ rules = [
549
+ rule1,
550
+ rule2,
551
+ rule3,
552
+ rule4
553
+ ]
554
+
555
+ results.each do |rule|
556
+ assert_includes rules, rule_without_metadata(rule)
557
+ end
558
+
559
+ @index.delete_rule!(rule1[:objectID])
560
+
561
+ exception = assert_raises Algolia::AlgoliaHttpError do
562
+ @index.get_rule(rule1[:objectID])
563
+ end
564
+
565
+ assert_equal 'ObjectID does not exist', exception.message
566
+
567
+ @index.clear_rules!
568
+
569
+ res = @index.search_rules('')
570
+ assert_equal 0, res[:nbHits]
571
+ end
572
+ end
573
+
574
+ describe 'batching' do
575
+ def before_all
576
+ super
577
+ @index = @@search_client.init_index(get_test_index_name('index_batching'))
578
+ end
579
+
580
+ def test_index_batching
581
+ @index.save_objects!([
582
+ { objectID: 'one', key: 'value' },
583
+ { objectID: 'two', key: 'value' },
584
+ { objectID: 'three', key: 'value' },
585
+ { objectID: 'four', key: 'value' },
586
+ { objectID: 'five', key: 'value' }
587
+ ])
588
+
589
+ @index.batch!([
590
+ { action: 'addObject', body: { objectID: 'zero', key: 'value' } },
591
+ { action: 'updateObject', body: { objectID: 'one', k: 'v' } },
592
+ { action: 'partialUpdateObject', body: { objectID: 'two', k: 'v' } },
593
+ { action: 'partialUpdateObject', body: { objectID: 'two_bis', key: 'value' } },
594
+ { action: 'partialUpdateObjectNoCreate', body: { objectID: 'three', k: 'v' } },
595
+ { action: 'deleteObject', body: { objectID: 'four' } }
596
+ ])
597
+
598
+ objects = [
599
+ { objectID: 'zero', key: 'value' },
600
+ { objectID: 'one', k: 'v' },
601
+ { objectID: 'two', key: 'value', k: 'v' },
602
+ { objectID: 'two_bis', key: 'value' },
603
+ { objectID: 'three', key: 'value', k: 'v' },
604
+ { objectID: 'five', key: 'value' }
605
+ ]
606
+
607
+ @index.browse_objects do |object|
608
+ assert_includes objects, object
609
+ end
610
+ end
611
+ end
612
+
613
+ describe 'replacing' do
614
+ def before_all
615
+ super
616
+ @index = @@search_client.init_index(get_test_index_name('replacing'))
617
+ end
618
+
619
+ def test_replacing
620
+ responses = Algolia::MultipleResponse.new
621
+ responses.push(@index.save_object({ objectID: 'one' }))
622
+ responses.push(@index.save_rule({
623
+ objectID: 'one',
624
+ condition: { anchoring: 'is', pattern: 'pattern' },
625
+ consequence: {
626
+ params: {
627
+ query: {
628
+ edits: [
629
+ { type: 'remove', delete: 'pattern' }
630
+ ]
631
+ }
632
+ }
633
+ }
634
+ }))
635
+ responses.push(@index.save_synonym({ objectID: 'one', type: 'synonym', synonyms: %w(one two) }))
636
+ responses.wait
637
+
638
+ @index.replace_all_objects!([{ objectID: 'two' }])
639
+ responses.push(@index.replace_all_rules([{
640
+ objectID: 'two',
641
+ condition: { anchoring: 'is', pattern: 'pattern' },
642
+ consequence: {
643
+ params: {
644
+ query: {
645
+ edits: [
646
+ { type: 'remove', delete: 'pattern' }
647
+ ]
648
+ }
649
+ }
650
+ }
651
+ }]))
652
+
653
+ responses.push(@index.replace_all_synonyms([{ objectID: 'two', type: 'synonym', synonyms: %w(one two) }]))
654
+
655
+ responses.wait
656
+
657
+ exception = assert_raises Algolia::AlgoliaHttpError do
658
+ @index.get_object('one')
659
+ end
660
+
661
+ assert_equal 'ObjectID does not exist', exception.message
662
+
663
+ assert_equal 'two', @index.get_object('two')[:objectID]
664
+
665
+ exception = assert_raises Algolia::AlgoliaHttpError do
666
+ @index.get_rule('one')
667
+ end
668
+
669
+ assert_equal 'ObjectID does not exist', exception.message
670
+
671
+ assert_equal 'two', @index.get_rule('two')[:objectID]
672
+
673
+ exception = assert_raises Algolia::AlgoliaHttpError do
674
+ @index.get_synonym('one')
675
+ end
676
+
677
+ assert_equal 'Synonym set does not exist', exception.message
678
+
679
+ assert_equal 'two', @index.get_synonym('two')[:objectID]
680
+ end
681
+ end
682
+
683
+ describe 'exists' do
684
+ def before_all
685
+ super
686
+ @index = @@search_client.init_index(get_test_index_name('exists'))
687
+ end
688
+
689
+ def test_exists
690
+ refute @index.exists?
691
+ @index.save_object!(generate_object('111'))
692
+ assert @index.exists?
693
+ @index.delete!
694
+ refute @index.exists?
695
+ end
696
+ end
697
+ end
698
+ end