elastic-app-search 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ module Elastic
2
+ module AppSearch
3
+ module Utils
4
+ extend self
5
+
6
+ def stringify_keys(hash)
7
+ hash.each_with_object({}) do |(key, value), out|
8
+ out[key.to_s] = value
9
+ end
10
+ end
11
+
12
+ def symbolize_keys(hash)
13
+ hash.each_with_object({}) do |(key, value), out|
14
+ new_key = key.respond_to?(:to_sym) ? key.to_sym : key
15
+ out[new_key] = value
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ module Elastic
2
+ module AppSearch
3
+ VERSION = '0.7.0'
4
+ end
5
+ end
Binary file
data/script/console ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'elastic/app-search'
6
+ require 'irb'
7
+ require 'irb/completion'
8
+
9
+ IRB.start
@@ -0,0 +1,500 @@
1
+ require 'config_helper'
2
+
3
+ describe Elastic::AppSearch::Client do
4
+ let(:engine_name) { "ruby-client-test-#{Time.now.to_i}" }
5
+
6
+ include_context 'App Search Credentials'
7
+ let(:client) { Elastic::AppSearch::Client.new(client_options) }
8
+
9
+ before(:all) do
10
+ # Bootstraps a static engine for 'read-only' options that require indexing
11
+ # across the test suite
12
+ @static_engine_name = "ruby-client-test-static-#{Time.now.to_i}"
13
+ as_api_key = ConfigHelper.get_as_api_key
14
+ as_host_identifier = ConfigHelper.get_as_host_identifier
15
+ as_api_endpoint = ConfigHelper.get_as_api_endpoint
16
+ client_options = ConfigHelper.get_client_options(as_api_key, as_host_identifier, as_api_endpoint)
17
+ @static_client = Elastic::AppSearch::Client.new(client_options)
18
+ @static_client.create_engine(@static_engine_name)
19
+
20
+ @document1 = { 'id' => '1', 'title' => 'The Great Gatsby' }
21
+ @document2 = { 'id' => '2', 'title' => 'Catcher in the Rye' }
22
+ @documents = [@document1, @document2]
23
+ @static_client.index_documents(@static_engine_name, @documents)
24
+
25
+ # Wait until documents are indexed
26
+ start = Time.now
27
+ ready = false
28
+ until (ready)
29
+ sleep(3)
30
+ results = @static_client.search(@static_engine_name, '')
31
+ ready = true if results['results'].length == 2
32
+ ready = true if (Time.now - start).to_i >= 120 # Time out after 2 minutes
33
+ end
34
+ end
35
+
36
+ after(:all) do
37
+ @static_client.destroy_engine(@static_engine_name)
38
+ end
39
+
40
+ describe '#create_signed_search_key' do
41
+ let(:key) { 'private-xxxxxxxxxxxxxxxxxxxx' }
42
+ let(:api_key_name) { 'private-key' }
43
+ let(:enforced_options) do
44
+ {
45
+ 'query' => 'cat'
46
+ }
47
+ end
48
+
49
+ subject do
50
+ Elastic::AppSearch::Client.create_signed_search_key(key, api_key_name, enforced_options)
51
+ end
52
+
53
+ it 'should build a valid jwt' do
54
+ decoded_token = JWT.decode subject, key, true, { algorithm: 'HS256' }
55
+ expect(decoded_token[0]['api_key_name']).to eq(api_key_name)
56
+ expect(decoded_token[0]['query']).to eq('cat')
57
+ end
58
+ end
59
+
60
+ describe 'Requests' do
61
+ it 'should include client name and version in headers' do
62
+ stub_request(:any, "#{client_options[:host_identifier]}.api.swiftype.com/api/as/v1/engines")
63
+ client.list_engines
64
+ expect(WebMock).to have_requested(:get, "https://#{client_options[:host_identifier]}.api.swiftype.com/api/as/v1/engines")
65
+ .with(
66
+ :headers => {
67
+ 'X-Swiftype-Client' => 'elastic-app-search-ruby',
68
+ 'X-Swiftype-Client-Version' => Elastic::AppSearch::VERSION
69
+ }
70
+ )
71
+ end
72
+ end
73
+
74
+ context 'Documents' do
75
+ let(:document) { { 'url' => 'http://www.youtube.com/watch?v=v1uyQZNg2vE' } }
76
+
77
+ before do
78
+ client.create_engine(engine_name) rescue Elastic::AppSearch::BadRequest
79
+ end
80
+
81
+ after do
82
+ client.destroy_engine(engine_name) rescue Elastic::AppSearch::NonExistentRecord
83
+ end
84
+
85
+ describe '#index_document' do
86
+ subject { client.index_document(engine_name, document) }
87
+
88
+ it 'should return a processed document status hash' do
89
+ expect(subject).to match('id' => anything)
90
+ end
91
+
92
+ context 'when the document has an id' do
93
+ let(:id) { 'some_id' }
94
+ let(:document) { { 'id' => id, 'url' => 'http://www.youtube.com/watch?v=v1uyQZNg2vE' } }
95
+
96
+ it 'should return a processed document status hash with the same id' do
97
+ expect(subject).to eq('id' => id)
98
+ end
99
+ end
100
+
101
+ context 'when a document has processing errors' do
102
+ let(:document) { { 'id' => 'too long' * 100 } }
103
+
104
+ it 'should raise an error when the API returns errors in the response' do
105
+ expect do
106
+ subject
107
+ end.to raise_error(Elastic::AppSearch::InvalidDocument, /Invalid field/)
108
+ end
109
+ end
110
+
111
+ context 'when a document has a Ruby Time object' do
112
+ let(:time_rfc3339) { '2018-01-01T01:01:01+00:00' }
113
+ let(:time_object) { Time.parse(time_rfc3339) }
114
+ let(:document) { { 'created_at' => time_object } }
115
+
116
+ it 'should serialize the time object in RFC 3339' do
117
+ response = subject
118
+ expect(response).to have_key('id')
119
+ document_id = response.fetch('id')
120
+ expect do
121
+ documents = client.get_documents(engine_name, [document_id])
122
+ expect(documents.size).to eq(1)
123
+ expect(documents.first['created_at']).to eq(time_rfc3339)
124
+ end.to_not raise_error
125
+ end
126
+ end
127
+ end
128
+
129
+ describe '#index_documents' do
130
+ let(:documents) { [document, second_document] }
131
+ let(:second_document_id) { 'another_id' }
132
+ let(:second_document) { { 'id' => second_document_id, 'url' => 'https://www.youtube.com/watch?v=9T1vfsHYiKY' } }
133
+ subject { client.index_documents(engine_name, documents) }
134
+
135
+ it 'should return an array of document status hashes' do
136
+ expect(subject).to match(
137
+ [
138
+ { 'id' => anything, 'errors' => [] },
139
+ { 'id' => second_document_id, 'errors' => [] }
140
+ ]
141
+ )
142
+ end
143
+
144
+ context 'when one of the documents has processing errors' do
145
+ let(:second_document) { { 'id' => 'too long' * 100 } }
146
+
147
+ it 'should return respective errors in an array of document processing hashes' do
148
+ expect(subject).to match(
149
+ [
150
+ { 'id' => anything, 'errors' => [] },
151
+ { 'id' => anything, 'errors' => ['Invalid field type: id must be less than 800 characters'] },
152
+ ]
153
+ )
154
+ end
155
+ end
156
+ end
157
+
158
+ describe '#update_documents' do
159
+ let(:documents) { [document, second_document] }
160
+ let(:second_document_id) { 'another_id' }
161
+ let(:second_document) { { 'id' => second_document_id, 'url' => 'https://www.youtube.com/watch?v=9T1vfsHYiKY' } }
162
+ let(:updates) do
163
+ [
164
+ {
165
+ 'id' => second_document_id,
166
+ 'url' => 'https://www.example.com'
167
+ }
168
+ ]
169
+ end
170
+
171
+ subject { client.update_documents(engine_name, updates) }
172
+
173
+ before do
174
+ client.index_documents(engine_name, documents)
175
+ end
176
+
177
+ # Note that since indexing a document takes up to a minute,
178
+ # we don't expect this to succeed, so we simply verify that
179
+ # the request responded with the correct 'id', even though
180
+ # the 'errors' object likely contains errors.
181
+ it 'should update existing documents' do
182
+ expect(subject).to match(['id' => second_document_id, 'errors' => anything])
183
+ end
184
+ end
185
+
186
+ describe '#get_documents' do
187
+ let(:documents) { [first_document, second_document] }
188
+ let(:first_document_id) { 'id' }
189
+ let(:first_document) { { 'id' => first_document_id, 'url' => 'https://www.youtube.com/watch?v=v1uyQZNg2vE' } }
190
+ let(:second_document_id) { 'another_id' }
191
+ let(:second_document) { { 'id' => second_document_id, 'url' => 'https://www.youtube.com/watch?v=9T1vfsHYiKY' } }
192
+
193
+ subject { client.get_documents(engine_name, [first_document_id, second_document_id]) }
194
+
195
+ before do
196
+ client.index_documents(engine_name, documents)
197
+ end
198
+
199
+ it 'will return documents by id' do
200
+ response = subject
201
+ expect(response.size).to eq(2)
202
+ expect(response[0]['id']).to eq(first_document_id)
203
+ expect(response[1]['id']).to eq(second_document_id)
204
+ end
205
+ end
206
+
207
+ describe '#list_documents' do
208
+ let(:documents) { [first_document, second_document] }
209
+ let(:first_document_id) { 'id' }
210
+ let(:first_document) { { 'id' => first_document_id, 'url' => 'https://www.youtube.com/watch?v=v1uyQZNg2vE' } }
211
+ let(:second_document_id) { 'another_id' }
212
+ let(:second_document) { { 'id' => second_document_id, 'url' => 'https://www.youtube.com/watch?v=9T1vfsHYiKY' } }
213
+
214
+ before do
215
+ client.index_documents(engine_name, documents)
216
+ end
217
+
218
+ context 'when no options are specified' do
219
+ it 'will return all documents' do
220
+ response = client.list_documents(engine_name)
221
+ expect(response['results'].size).to eq(2)
222
+ expect(response['results'][0]['id']).to eq(first_document_id)
223
+ expect(response['results'][1]['id']).to eq(second_document_id)
224
+ end
225
+ end
226
+
227
+ context 'when options are specified' do
228
+ it 'will return all documents' do
229
+ response = client.list_documents(engine_name, :page => { :size => 1, :current => 2 })
230
+ expect(response['results'].size).to eq(1)
231
+ expect(response['results'][0]['id']).to eq(second_document_id)
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ context 'Search' do
238
+ describe '#search' do
239
+ subject { @static_client.search(@static_engine_name, query, options) }
240
+ let(:query) { '' }
241
+ let(:options) { { 'page' => { 'size' => 2 } } }
242
+
243
+ it 'should execute a search query' do
244
+ expect(subject).to match(
245
+ 'meta' => anything,
246
+ 'results' => [anything, anything]
247
+ )
248
+ end
249
+ end
250
+
251
+ describe '#multi_search' do
252
+ subject { @static_client.multi_search(@static_engine_name, queries) }
253
+
254
+ context 'when options are provided' do
255
+ let(:queries) do
256
+ [
257
+ { 'query' => 'gatsby', 'options' => { 'page' => { 'size' => 1 } } },
258
+ { 'query' => 'catcher', 'options' => { 'page' => { 'size' => 1 } } }
259
+ ]
260
+ end
261
+
262
+ it 'should execute a multi search query' do
263
+ response = subject
264
+ expect(response).to match(
265
+ [
266
+ {
267
+ 'meta' => anything,
268
+ 'results' => [{ 'id' => { 'raw' => '1' }, 'title' => anything, '_meta' => anything }]
269
+ },
270
+ {
271
+ 'meta' => anything,
272
+ 'results' => [{ 'id' => { 'raw' => '2' }, 'title' => anything, '_meta' => anything }]
273
+ }
274
+ ]
275
+ )
276
+ end
277
+ end
278
+
279
+ context 'when options are omitted' do
280
+ let(:queries) do
281
+ [
282
+ { 'query' => 'gatsby' },
283
+ { 'query' => 'catcher' }
284
+ ]
285
+ end
286
+
287
+ it 'should execute a multi search query' do
288
+ response = subject
289
+ expect(response).to match(
290
+ [
291
+ {
292
+ 'meta' => anything,
293
+ 'results' => [{ 'id' => { 'raw' => '1' }, 'title' => anything, '_meta' => anything }]
294
+ },
295
+ {
296
+ 'meta' => anything,
297
+ 'results' => [{ 'id' => { 'raw' => '2' }, 'title' => anything, '_meta' => anything }]
298
+ }
299
+ ]
300
+ )
301
+ end
302
+ end
303
+
304
+ context 'when a search is bad' do
305
+ let(:queries) do
306
+ [
307
+ {
308
+ 'query' => 'cat',
309
+ 'options' => { 'search_fields' => { 'taco' => {} } }
310
+ }, {
311
+ 'query' => 'dog',
312
+ 'options' => { 'search_fields' => { 'body' => {} } }
313
+ }
314
+ ]
315
+ end
316
+
317
+ it 'should throw an appropriate error' do
318
+ expect { subject }.to raise_error do |e|
319
+ expect(e).to be_a(Elastic::AppSearch::BadRequest)
320
+ expect(e.errors).to eq(['Search fields contains invalid field: taco', 'Search fields contains invalid field: body'])
321
+ end
322
+ end
323
+ end
324
+ end
325
+ end
326
+
327
+ context 'QuerySuggest' do
328
+ describe '#query_suggestion' do
329
+ let(:query) { 'cat' }
330
+ let(:options) { { :size => 3, :types => { :documents => { :fields => ['title'] } } } }
331
+
332
+ context 'when options are provided' do
333
+ subject { @static_client.query_suggestion(@static_engine_name, query, options) }
334
+
335
+ it 'should request query suggestions' do
336
+ expect(subject).to match(
337
+ 'meta' => anything,
338
+ 'results' => anything
339
+ )
340
+ end
341
+ end
342
+
343
+ context 'when options are omitted' do
344
+ subject { @static_client.query_suggestion(@static_engine_name, query) }
345
+
346
+ it 'should request query suggestions' do
347
+ expect(subject).to match(
348
+ 'meta' => anything,
349
+ 'results' => anything
350
+ )
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ context 'SearchSettings' do
357
+ let(:default_settings) { {
358
+ "search_fields" => {
359
+ "id" => {
360
+ "weight" => 1
361
+ }
362
+ },
363
+ "result_fields" => {"id"=>{"raw"=>{}}},
364
+ "boosts" => {}
365
+ } }
366
+
367
+ let(:updated_settings) { {
368
+ "search_fields" => {
369
+ "id" => {
370
+ "weight" => 3
371
+ }
372
+ },
373
+ "result_fields" => {"id"=>{"raw"=>{}}},
374
+ "boosts" => {}
375
+ } }
376
+
377
+ before(:each) do
378
+ client.create_engine(engine_name) rescue Elastic::AppSearch::BadRequest
379
+ end
380
+
381
+ after(:each) do
382
+ client.destroy_engine(engine_name) rescue Elastic::AppSearch::NonExistentRecord
383
+ end
384
+
385
+ describe '#show_settings' do
386
+ subject { client.show_settings(engine_name) }
387
+
388
+ it 'should return default settings' do
389
+ expect(subject).to match(default_settings)
390
+ end
391
+ end
392
+
393
+ describe '#update_settings' do
394
+ subject { client.show_settings(engine_name) }
395
+
396
+ before do
397
+ client.update_settings(engine_name, updated_settings)
398
+ end
399
+
400
+ it 'should update search settings' do
401
+ expect(subject).to match(updated_settings)
402
+ end
403
+ end
404
+
405
+ describe '#reset_settings' do
406
+ subject { client.show_settings(engine_name) }
407
+
408
+ before do
409
+ client.update_settings(engine_name, updated_settings)
410
+ client.reset_settings(engine_name)
411
+ end
412
+
413
+ it 'should reset search settings' do
414
+ expect(subject).to match(default_settings)
415
+ end
416
+ end
417
+ end
418
+
419
+ context 'Engines' do
420
+ after do
421
+ client.destroy_engine(engine_name) rescue Elastic::AppSearch::NonExistentRecord
422
+ end
423
+
424
+ context '#create_engine' do
425
+ it 'should create an engine when given a right set of parameters' do
426
+ expect { client.get_engine(engine_name) }.to raise_error(Elastic::AppSearch::NonExistentRecord)
427
+ client.create_engine(engine_name)
428
+ expect { client.get_engine(engine_name) }.to_not raise_error
429
+ end
430
+
431
+ it 'should accept an optional language parameter' do
432
+ expect { client.get_engine(engine_name) }.to raise_error(Elastic::AppSearch::NonExistentRecord)
433
+ client.create_engine(engine_name, 'da')
434
+ expect(client.get_engine(engine_name)).to match('name' => anything, 'type' => anything, 'language' => 'da')
435
+ end
436
+
437
+ it 'should return an engine object' do
438
+ engine = client.create_engine(engine_name)
439
+ expect(engine).to be_kind_of(Hash)
440
+ expect(engine['name']).to eq(engine_name)
441
+ end
442
+
443
+ it 'should return an error when the engine name has already been taken' do
444
+ client.create_engine(engine_name)
445
+ expect { client.create_engine(engine_name) }.to raise_error do |e|
446
+ expect(e).to be_a(Elastic::AppSearch::BadRequest)
447
+ expect(e.errors).to eq(['Name is already taken'])
448
+ end
449
+ end
450
+ end
451
+
452
+ context '#list_engines' do
453
+ it 'should return an array with a list of engines' do
454
+ expect(client.list_engines['results']).to be_an(Array)
455
+ end
456
+
457
+ it 'should include the engine name in listed objects' do
458
+ client.create_engine(engine_name)
459
+
460
+ engines = client.list_engines['results']
461
+ expect(engines.find { |e| e['name'] == engine_name }).to_not be_nil
462
+ end
463
+
464
+ it 'should include the engine name in listed objects with pagination' do
465
+ client.create_engine(engine_name)
466
+
467
+ engines = client.list_engines(:current => 1, :size => 20)['results']
468
+ expect(engines.find { |e| e['name'] == engine_name }).to_not be_nil
469
+ end
470
+ end
471
+
472
+ context '#destroy_engine' do
473
+ it 'should destroy the engine if it exists' do
474
+ client.create_engine(engine_name)
475
+ expect { client.get_engine(engine_name) }.to_not raise_error
476
+
477
+ client.destroy_engine(engine_name)
478
+ expect { client.get_engine(engine_name) }.to raise_error(Elastic::AppSearch::NonExistentRecord)
479
+ end
480
+
481
+ it 'should raise an error if the engine does not exist' do
482
+ expect { client.destroy_engine(engine_name) }.to raise_error(Elastic::AppSearch::NonExistentRecord)
483
+ end
484
+ end
485
+ end
486
+
487
+ context 'Configuration' do
488
+ context 'host_identifier' do
489
+ it 'sets the base url correctly' do
490
+ client = Elastic::AppSearch::Client.new(:host_identifier => 'host-asdf', :api_key => 'foo')
491
+ expect(client.api_endpoint).to eq('https://host-asdf.api.swiftype.com/api/as/v1/')
492
+ end
493
+
494
+ it 'sets the base url correctly using deprecated as_host_key' do
495
+ client = Elastic::AppSearch::Client.new(:account_host_key => 'host-asdf', :api_key => 'foo')
496
+ expect(client.api_endpoint).to eq('https://host-asdf.api.swiftype.com/api/as/v1/')
497
+ end
498
+ end
499
+ end
500
+ end