algolia 2.0.0.pre.alpha.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 (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