load_balanced_tire 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. data/.gitignore +14 -0
  2. data/.travis.yml +29 -0
  3. data/Gemfile +4 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.markdown +760 -0
  6. data/Rakefile +78 -0
  7. data/examples/rails-application-template.rb +249 -0
  8. data/examples/tire-dsl.rb +876 -0
  9. data/lib/tire.rb +55 -0
  10. data/lib/tire/alias.rb +296 -0
  11. data/lib/tire/configuration.rb +30 -0
  12. data/lib/tire/dsl.rb +43 -0
  13. data/lib/tire/http/client.rb +62 -0
  14. data/lib/tire/http/clients/curb.rb +61 -0
  15. data/lib/tire/http/clients/faraday.rb +71 -0
  16. data/lib/tire/http/response.rb +27 -0
  17. data/lib/tire/index.rb +361 -0
  18. data/lib/tire/logger.rb +60 -0
  19. data/lib/tire/model/callbacks.rb +40 -0
  20. data/lib/tire/model/import.rb +26 -0
  21. data/lib/tire/model/indexing.rb +128 -0
  22. data/lib/tire/model/naming.rb +100 -0
  23. data/lib/tire/model/percolate.rb +99 -0
  24. data/lib/tire/model/persistence.rb +71 -0
  25. data/lib/tire/model/persistence/attributes.rb +143 -0
  26. data/lib/tire/model/persistence/finders.rb +66 -0
  27. data/lib/tire/model/persistence/storage.rb +69 -0
  28. data/lib/tire/model/search.rb +307 -0
  29. data/lib/tire/results/collection.rb +114 -0
  30. data/lib/tire/results/item.rb +86 -0
  31. data/lib/tire/results/pagination.rb +54 -0
  32. data/lib/tire/rubyext/hash.rb +8 -0
  33. data/lib/tire/rubyext/ruby_1_8.rb +7 -0
  34. data/lib/tire/rubyext/symbol.rb +11 -0
  35. data/lib/tire/search.rb +188 -0
  36. data/lib/tire/search/facet.rb +74 -0
  37. data/lib/tire/search/filter.rb +28 -0
  38. data/lib/tire/search/highlight.rb +37 -0
  39. data/lib/tire/search/query.rb +186 -0
  40. data/lib/tire/search/scan.rb +114 -0
  41. data/lib/tire/search/script_field.rb +23 -0
  42. data/lib/tire/search/sort.rb +25 -0
  43. data/lib/tire/tasks.rb +135 -0
  44. data/lib/tire/utils.rb +17 -0
  45. data/lib/tire/version.rb +22 -0
  46. data/test/fixtures/articles/1.json +1 -0
  47. data/test/fixtures/articles/2.json +1 -0
  48. data/test/fixtures/articles/3.json +1 -0
  49. data/test/fixtures/articles/4.json +1 -0
  50. data/test/fixtures/articles/5.json +1 -0
  51. data/test/integration/active_model_indexing_test.rb +51 -0
  52. data/test/integration/active_model_searchable_test.rb +114 -0
  53. data/test/integration/active_record_searchable_test.rb +446 -0
  54. data/test/integration/boolean_queries_test.rb +43 -0
  55. data/test/integration/count_test.rb +34 -0
  56. data/test/integration/custom_score_queries_test.rb +88 -0
  57. data/test/integration/dis_max_queries_test.rb +68 -0
  58. data/test/integration/dsl_search_test.rb +22 -0
  59. data/test/integration/explanation_test.rb +44 -0
  60. data/test/integration/facets_test.rb +259 -0
  61. data/test/integration/filtered_queries_test.rb +66 -0
  62. data/test/integration/filters_test.rb +63 -0
  63. data/test/integration/fuzzy_queries_test.rb +20 -0
  64. data/test/integration/highlight_test.rb +64 -0
  65. data/test/integration/index_aliases_test.rb +122 -0
  66. data/test/integration/index_mapping_test.rb +43 -0
  67. data/test/integration/index_store_test.rb +96 -0
  68. data/test/integration/index_update_document_test.rb +111 -0
  69. data/test/integration/mongoid_searchable_test.rb +309 -0
  70. data/test/integration/percolator_test.rb +111 -0
  71. data/test/integration/persistent_model_test.rb +130 -0
  72. data/test/integration/prefix_query_test.rb +43 -0
  73. data/test/integration/query_return_version_test.rb +70 -0
  74. data/test/integration/query_string_test.rb +52 -0
  75. data/test/integration/range_queries_test.rb +36 -0
  76. data/test/integration/reindex_test.rb +46 -0
  77. data/test/integration/results_test.rb +39 -0
  78. data/test/integration/scan_test.rb +56 -0
  79. data/test/integration/script_fields_test.rb +38 -0
  80. data/test/integration/sort_test.rb +36 -0
  81. data/test/integration/text_query_test.rb +39 -0
  82. data/test/models/active_model_article.rb +31 -0
  83. data/test/models/active_model_article_with_callbacks.rb +49 -0
  84. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  85. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  86. data/test/models/active_record_models.rb +122 -0
  87. data/test/models/article.rb +15 -0
  88. data/test/models/mongoid_models.rb +97 -0
  89. data/test/models/persistent_article.rb +11 -0
  90. data/test/models/persistent_article_in_namespace.rb +12 -0
  91. data/test/models/persistent_article_with_casting.rb +28 -0
  92. data/test/models/persistent_article_with_defaults.rb +11 -0
  93. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  94. data/test/models/supermodel_article.rb +17 -0
  95. data/test/models/validated_model.rb +11 -0
  96. data/test/test_helper.rb +93 -0
  97. data/test/unit/active_model_lint_test.rb +17 -0
  98. data/test/unit/configuration_test.rb +74 -0
  99. data/test/unit/http_client_test.rb +76 -0
  100. data/test/unit/http_response_test.rb +49 -0
  101. data/test/unit/index_alias_test.rb +275 -0
  102. data/test/unit/index_test.rb +894 -0
  103. data/test/unit/logger_test.rb +125 -0
  104. data/test/unit/model_callbacks_test.rb +116 -0
  105. data/test/unit/model_import_test.rb +71 -0
  106. data/test/unit/model_persistence_test.rb +528 -0
  107. data/test/unit/model_search_test.rb +913 -0
  108. data/test/unit/results_collection_test.rb +281 -0
  109. data/test/unit/results_item_test.rb +162 -0
  110. data/test/unit/rubyext_test.rb +66 -0
  111. data/test/unit/search_facet_test.rb +153 -0
  112. data/test/unit/search_filter_test.rb +42 -0
  113. data/test/unit/search_highlight_test.rb +46 -0
  114. data/test/unit/search_query_test.rb +301 -0
  115. data/test/unit/search_scan_test.rb +113 -0
  116. data/test/unit/search_script_field_test.rb +26 -0
  117. data/test/unit/search_sort_test.rb +50 -0
  118. data/test/unit/search_test.rb +499 -0
  119. data/test/unit/tire_test.rb +126 -0
  120. data/tire.gemspec +90 -0
  121. metadata +549 -0
@@ -0,0 +1,894 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class IndexTest < Test::Unit::TestCase
6
+
7
+ context "Index" do
8
+
9
+ setup do @index = Tire::Index.new 'dummy' end
10
+ teardown do Tire.configure { reset } end
11
+
12
+ should "have a name" do
13
+ assert_equal 'dummy', @index.name
14
+ end
15
+
16
+ should "have an URL endpoint" do
17
+ assert_equal "#{Configuration.url}/#{@index.name}", @index.url
18
+ end
19
+
20
+ should "return HTTP response" do
21
+ assert_respond_to @index, :response
22
+
23
+ Configuration.client.expects(:head).returns(mock_response('OK'))
24
+ @index.exists?
25
+ assert_equal 'OK', @index.response.body
26
+ end
27
+
28
+ should "return true when exists" do
29
+ Configuration.client.expects(:head).returns(mock_response(''))
30
+ assert @index.exists?
31
+ end
32
+
33
+ should "return false when does not exist" do
34
+ Configuration.client.expects(:head).returns(mock_response('', 404))
35
+ assert ! @index.exists?
36
+ end
37
+
38
+ should "create new index" do
39
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"acknowledged":true}'))
40
+ assert @index.create
41
+ end
42
+
43
+ should "not raise exception and just return false when trying to create existing index" do
44
+ Configuration.client.expects(:post).returns(mock_response('{"error":"IndexAlreadyExistsException[\'dummy\']"}', 400))
45
+ assert_nothing_raised { assert ! @index.create }
46
+ end
47
+
48
+ should "delete index" do
49
+ Configuration.client.expects(:delete).returns(mock_response('{"ok":true,"acknowledged":true}'))
50
+ assert @index.delete
51
+ end
52
+
53
+ should "not raise exception and just return false when deleting non-existing index" do
54
+ Configuration.client.expects(:delete).returns(mock_response('{"error":"[articles] missing"}', 404))
55
+ assert_nothing_raised { assert ! @index.delete }
56
+ end
57
+
58
+ should "add an index alias" do
59
+ Configuration.client.expects(:post).with do |url, payload|
60
+ url =~ /_aliases/ &&
61
+ MultiJson.decode(payload)['actions'][0]['add'] == {'index' => 'dummy', 'alias' => 'foo'}
62
+ end.returns(mock_response('{"ok":true}'))
63
+
64
+ @index.add_alias 'foo'
65
+ end
66
+
67
+ should "add an index alias with configuration" do
68
+ Configuration.client.expects(:post).with do |url, payload|
69
+ url =~ /_aliases/ &&
70
+ MultiJson.decode(payload)['actions'][0]['add'] == {'index' => 'dummy', 'alias' => 'foo', 'routing' => 1 }
71
+ end.returns(mock_response('{"ok":true}'))
72
+
73
+ @index.add_alias 'foo', :routing => 1
74
+ end
75
+
76
+ should "delete an index alias" do
77
+ Configuration.client.expects(:get).returns(mock_response({'dummy' => {'aliases' => {'foo' => {}}}}.to_json))
78
+ Configuration.client.expects(:post).with do |url, payload|
79
+ url =~ /_aliases/ &&
80
+ MultiJson.decode(payload)['actions'][0]['remove'] == {'index' => 'dummy', 'alias' => 'foo'}
81
+ end.returns(mock_response('{"ok":true}'))
82
+
83
+ @index.remove_alias 'foo'
84
+ end
85
+
86
+ should "list aliases for an index" do
87
+ json = {'dummy' => {'aliases' => {'foo' => {}}}}.to_json
88
+ Configuration.client.expects(:get).returns(mock_response(json))
89
+
90
+ assert_equal ['foo'], @index.aliases.map(&:name)
91
+ end
92
+
93
+ should "return properties of an alias" do
94
+ json = {'dummy' => { 'aliases' => {'foo' => { 'filter' => { 'term' => { 'user' => 'john' } }}} }}.to_json
95
+ Configuration.client.expects(:get).returns(mock_response(json))
96
+
97
+ assert_equal( { 'term' => {'user' => 'john'} }, @index.aliases('foo').filter )
98
+ end
99
+
100
+ should "refresh the index" do
101
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_shards":{}}'))
102
+ assert_nothing_raised { assert @index.refresh }
103
+ end
104
+
105
+ should "open the index" do
106
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_shards":{}}'))
107
+ assert_nothing_raised { assert @index.open }
108
+ end
109
+
110
+ should "close the index" do
111
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_shards":{}}'))
112
+ assert_nothing_raised { assert @index.close }
113
+ end
114
+
115
+ context "analyze support" do
116
+ setup do
117
+ @mock_analyze_response = '{"tokens":[{"token":"foo","start_offset":0,"end_offset":4,"type":"<ALPHANUM>","position":1}]}'
118
+ end
119
+
120
+ should "send text to the Analyze API" do
121
+ Configuration.client.expects(:get).
122
+ with("#{@index.url}/_analyze?pretty=true", "foo bar").
123
+ returns(mock_response(@mock_analyze_response))
124
+
125
+ response = @index.analyze("foo bar")
126
+ assert_equal 1, response['tokens'].size
127
+ end
128
+
129
+ should "properly encode parameters" do
130
+ Configuration.client.expects(:get).with do |url, payload|
131
+ url == "#{@index.url}/_analyze?analyzer=whitespace&pretty=true"
132
+ end.returns(mock_response(@mock_analyze_response))
133
+
134
+ @index.analyze("foo bar", :analyzer => 'whitespace')
135
+
136
+ Configuration.client.expects(:get).with do |url, payload|
137
+ url == "#{@index.url}/_analyze?field=title&pretty=true"
138
+ end.returns(mock_response(@mock_analyze_response))
139
+
140
+ @index.analyze("foo bar", :field => 'title')
141
+
142
+ Configuration.client.expects(:get).with do |url, payload|
143
+ url == "#{@index.url}/_analyze?analyzer=keyword&format=text&pretty=true"
144
+ end.returns(mock_response(@mock_analyze_response))
145
+
146
+ @index.analyze("foo bar", :analyzer => 'keyword', :format => 'text')
147
+ end
148
+
149
+ end
150
+
151
+ context "mapping" do
152
+
153
+ should "create index with mapping" do
154
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"acknowledged":true}'))
155
+
156
+ assert @index.create :settings => { :number_of_shards => 1 },
157
+ :mappings => { :article => {
158
+ :properties => {
159
+ :title => { :boost => 2.0,
160
+ :type => 'string',
161
+ :store => 'yes',
162
+ :analyzer => 'snowball' }
163
+ }
164
+ }
165
+ }
166
+ end
167
+
168
+ should "return the mapping" do
169
+ json =<<-JSON
170
+ {
171
+ "dummy" : {
172
+ "article" : {
173
+ "properties" : {
174
+ "title" : { "type" : "string", "boost" : 2.0 },
175
+ "category" : { "type" : "string", "analyzed" : "no" }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ JSON
181
+ Configuration.client.stubs(:get).returns(mock_response(json))
182
+
183
+ assert_equal 'string', @index.mapping['article']['properties']['title']['type']
184
+ assert_equal 2.0, @index.mapping['article']['properties']['title']['boost']
185
+ end
186
+
187
+ end
188
+
189
+ context "settings" do
190
+
191
+ should "return index settings" do
192
+ json =<<-JSON
193
+ {
194
+ "dummy" : {
195
+ "settings" : {
196
+ "index.number_of_shards" : "20",
197
+ "index.number_of_replicas" : "0"
198
+ }
199
+ }
200
+ }
201
+ JSON
202
+ Configuration.client.stubs(:get).returns(mock_response(json))
203
+
204
+ assert_equal '20', @index.settings['index.number_of_shards']
205
+ end
206
+
207
+ end
208
+
209
+ context "when storing" do
210
+
211
+ should "set type from Hash :type property" do
212
+ Configuration.client.expects(:post).with do |url,document|
213
+ url == "#{@index.url}/article/"
214
+ end.returns(mock_response('{"ok":true,"_id":"test"}'))
215
+ @index.store :type => 'article', :title => 'Test'
216
+ end
217
+
218
+ should "set type from Hash :_type property" do
219
+ Configuration.client.expects(:post).with do |url,document|
220
+ url == "#{@index.url}/article/"
221
+ end.returns(mock_response('{"ok":true,"_id":"test"}'))
222
+ @index.store :_type => 'article', :title => 'Test'
223
+ end
224
+
225
+ should "set type from Object _type method" do
226
+ Configuration.client.expects(:post).with do |url,document|
227
+ url == "#{@index.url}/article/"
228
+ end.returns(mock_response('{"ok":true,"_id":"test"}'))
229
+
230
+ article = Class.new do
231
+ def _type; 'article'; end
232
+ def to_indexed_json; "{}"; end
233
+ end.new
234
+ @index.store article
235
+ end
236
+
237
+ should "set type from Object type method" do
238
+ Configuration.client.expects(:post).with do |url,document|
239
+ url == "#{@index.url}/article/"
240
+ end.returns(mock_response('{"ok":true,"_id":"test"}'))
241
+
242
+ article = Class.new do
243
+ def type; 'article'; end
244
+ def to_indexed_json; "{}"; end
245
+ end.new
246
+ @index.store article
247
+ end
248
+
249
+ should "properly encode namespaced document types" do
250
+ Configuration.client.expects(:post).with do |url,document|
251
+ url == "#{@index.url}/my_namespace%2Fmy_model/"
252
+ end.returns(mock_response('{"ok":true,"_id":"123"}'))
253
+
254
+ module MyNamespace
255
+ class MyModel
256
+ def document_type; "my_namespace/my_model"; end
257
+ def to_indexed_json; "{}"; end
258
+ end
259
+ end
260
+
261
+ @index.store MyNamespace::MyModel.new
262
+ end
263
+
264
+ should "set default type" do
265
+ Configuration.client.expects(:post).with("#{@index.url}/document/", '{"title":"Test"}').returns(mock_response('{"ok":true,"_id":"test"}'))
266
+ @index.store :title => 'Test'
267
+ end
268
+
269
+ should "call #to_indexed_json on non-String documents" do
270
+ document = { :title => 'Test' }
271
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_id":"test"}'))
272
+ document.expects(:to_indexed_json)
273
+ @index.store document
274
+ end
275
+
276
+ should "raise error when storing neither String nor object with #to_indexed_json method" do
277
+ class MyDocument;end; document = MyDocument.new
278
+ assert_raise(ArgumentError) { @index.store document }
279
+ end
280
+
281
+ should "raise deprecation warning when trying to store a JSON string" do
282
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_id":"test"}'))
283
+ @index.store '{"foo" : "bar"}'
284
+ end
285
+
286
+ context "document with ID" do
287
+
288
+ should "store Hash it under its ID property" do
289
+ Configuration.client.expects(:post).with("#{@index.url}/document/123",
290
+ MultiJson.encode({:id => 123, :title => 'Test'})).
291
+ returns(mock_response('{"ok":true,"_id":"123"}'))
292
+ @index.store :id => 123, :title => 'Test'
293
+ end
294
+
295
+ should "store a custom class under its ID property" do
296
+ Configuration.client.expects(:post).with("#{@index.url}/document/123",
297
+ {:id => 123, :title => 'Test', :body => 'Lorem'}.to_json).
298
+ returns(mock_response('{"ok":true,"_id":"123"}'))
299
+ @index.store Article.new(:id => 123, :title => 'Test', :body => 'Lorem')
300
+ end
301
+
302
+ end
303
+
304
+ end
305
+
306
+ context "when retrieving" do
307
+
308
+ setup do
309
+ Configuration.reset :wrapper
310
+
311
+ Configuration.client.stubs(:post).with do |url, payload|
312
+ url == "#{@index.url}/article/" &&
313
+ payload =~ /"title":"Test"/
314
+ end.
315
+ returns(mock_response('{"ok":true,"_id":"id-1"}'))
316
+ @index.store :type => 'article', :title => 'Test'
317
+ end
318
+
319
+ should "return document in default wrapper" do
320
+ Configuration.client.expects(:get).with("#{@index.url}/article/id-1").
321
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
322
+ article = @index.retrieve :article, 'id-1'
323
+ assert_instance_of Results::Item, article
324
+ assert_equal 'Test', article.title
325
+ assert_equal 'Test', article[:title]
326
+ end
327
+
328
+ should "return document as a hash" do
329
+ Configuration.wrapper Hash
330
+
331
+ Configuration.client.expects(:get).with("#{@index.url}/article/id-1").
332
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
333
+ article = @index.retrieve :article, 'id-1'
334
+ assert_instance_of Hash, article
335
+ end
336
+
337
+ should "return document in custom wrapper" do
338
+ Configuration.wrapper Article
339
+
340
+ Configuration.client.expects(:get).with("#{@index.url}/article/id-1").
341
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
342
+ article = @index.retrieve :article, 'id-1'
343
+ assert_instance_of Article, article
344
+ assert_equal 'Test', article.title
345
+ end
346
+
347
+ should "return nil for missing document" do
348
+ Configuration.client.expects(:get).with("#{@index.url}/article/id-1").
349
+ returns(mock_response('{"_id":"id-1","exists":false}'))
350
+ article = @index.retrieve :article, 'id-1'
351
+ assert_equal nil, article
352
+ end
353
+
354
+ should "raise error when no ID passed" do
355
+ assert_raise ArgumentError do
356
+ @index.retrieve 'article', nil
357
+ end
358
+ end
359
+
360
+ should "properly encode document type" do
361
+ Configuration.client.expects(:get).with("#{@index.url}/my_namespace%2Fmy_model/id-1").
362
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
363
+ article = @index.retrieve 'my_namespace/my_model', 'id-1'
364
+ end
365
+
366
+ end
367
+
368
+ context "when removing" do
369
+
370
+ should "get type from document" do
371
+ Configuration.client.expects(:delete).with("#{@index.url}/article/1").
372
+ returns(mock_response('{"ok":true,"_id":"1"}')).twice
373
+ @index.remove :id => 1, :type => 'article', :title => 'Test'
374
+ @index.remove :id => 1, :type => 'article', :title => 'Test'
375
+ end
376
+
377
+ should "get namespaced type from document" do
378
+ Configuration.client.expects(:delete).with("#{@index.url}/articles%2Farticle/1").
379
+ returns(mock_response('{"ok":true,"_id":"1"}')).twice
380
+ @index.remove :id => 1, :type => 'articles/article', :title => 'Test'
381
+ @index.remove :id => 1, :type => 'articles/article', :title => 'Test'
382
+ end
383
+
384
+ should "set default type" do
385
+ Configuration.client.expects(:delete).with("#{@index.url}/document/1").
386
+ returns(mock_response('{"ok":true,"_id":"1"}'))
387
+ @index.remove :id => 1, :title => 'Test'
388
+ end
389
+
390
+ should "get ID from hash" do
391
+ Configuration.client.expects(:delete).with("#{@index.url}/document/1").
392
+ returns(mock_response('{"ok":true,"_id":"1"}'))
393
+ @index.remove :id => 1
394
+ end
395
+
396
+ should "get ID from method" do
397
+ document = stub('document', :id => 1)
398
+ Configuration.client.expects(:delete).with("#{@index.url}/document/1").
399
+ returns(mock_response('{"ok":true,"_id":"1"}'))
400
+ @index.remove document
401
+ end
402
+
403
+ should "get type and ID from arguments" do
404
+ Configuration.client.expects(:delete).with("#{@index.url}/article/1").
405
+ returns(mock_response('{"ok":true,"_id":"1"}'))
406
+ @index.remove :article, 1
407
+ end
408
+
409
+ should "raise error when no ID passed" do
410
+ assert_raise ArgumentError do
411
+ @index.remove :document, nil
412
+ end
413
+ end
414
+
415
+ should "properly encode document type" do
416
+ Configuration.client.expects(:delete).with("#{@index.url}/my_namespace%2Fmy_model/id-1").
417
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
418
+ article = @index.remove 'my_namespace/my_model', 'id-1'
419
+ end
420
+
421
+ end
422
+
423
+ context "when updating" do
424
+
425
+ should "send payload" do
426
+ Configuration.client.expects(:post).with do |url,payload|
427
+ payload = MultiJson.decode(payload)
428
+ # p [url, payload]
429
+ assert_equal( "#{@index.url}/document/42/_update", url ) &&
430
+ assert_not_nil( payload['script'] ) &&
431
+ assert_not_nil( payload['params'] ) &&
432
+ assert_equal( '21', payload['params']['bar'] )
433
+ end.
434
+ returns(
435
+ mock_response('{"ok":"true","_index":"dummy","_type":"document","_id":"42","_version":"2"}'))
436
+
437
+ assert @index.update('document', '42', {:script => "ctx._source.foo = bar;", :params => { :bar => '21' }})
438
+ end
439
+
440
+ should "send options" do
441
+ Configuration.client.expects(:post).with do |url,payload|
442
+ payload = MultiJson.decode(payload)
443
+ # p [url, payload]
444
+ assert_equal( "#{@index.url}/document/42/_update?timeout=1000", url ) &&
445
+ assert_nil( payload['timeout'] )
446
+ end.
447
+ returns(
448
+ mock_response('{"ok":"true","_index":"dummy","_type":"document","_id":"42","_version":"2"}'))
449
+ assert @index.update('document', '42', {:script => "ctx._source.foo = 'bar'"}, {:timeout => 1000})
450
+ end
451
+
452
+ should "raise error when no type or ID is passed" do
453
+ assert_raise(ArgumentError) { @index.update('article', nil, :script => 'foobar') }
454
+ assert_raise(ArgumentError) { @index.update(nil, '123', :script => 'foobar') }
455
+ end
456
+
457
+ should "raise an error when no script is passed" do
458
+ assert_raise ArgumentError do
459
+ @index.update('article', "42", {:params => {"foo" => "bar"}})
460
+ end
461
+ end
462
+
463
+ end
464
+
465
+ context "when storing in bulk" do
466
+ # The expected JSON looks like this:
467
+ #
468
+ # {"index":{"_index":"dummy","_type":"document","_id":"1"}}
469
+ # {"id":"1","title":"One"}
470
+ # {"index":{"_index":"dummy","_type":"document","_id":"2"}}
471
+ # {"id":"2","title":"Two"}
472
+ #
473
+ # See http://www.elasticsearch.org/guide/reference/api/bulk.html
474
+
475
+ should "serialize Hashes" do
476
+ Configuration.client.expects(:post).with do |url, json|
477
+ url == "#{@index.url}/_bulk" &&
478
+ json =~ /"_index":"dummy"/ &&
479
+ json =~ /"_type":"document"/ &&
480
+ json =~ /"_id":"1"/ &&
481
+ json =~ /"_id":"2"/ &&
482
+ json =~ /"id":"1"/ &&
483
+ json =~ /"id":"2"/ &&
484
+ json =~ /"title":"One"/ &&
485
+ json =~ /"title":"Two"/
486
+ end.returns(mock_response('{}'), 200)
487
+
488
+ @index.bulk_store [ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ]
489
+ end
490
+
491
+ should "serialize ActiveModel instances" do
492
+ Configuration.client.expects(:post).with do |url, json|
493
+ url == "#{ActiveModelArticle.index.url}/_bulk" &&
494
+ json =~ /"_index":"active_model_articles"/ &&
495
+ json =~ /"_type":"active_model_article"/ &&
496
+ json =~ /"_id":"1"/ &&
497
+ json =~ /"_id":"2"/ &&
498
+ json =~ /"title":"One"/ &&
499
+ json =~ /"title":"Two"/
500
+ end.returns(mock_response('{}', 200))
501
+
502
+ one = ActiveModelArticle.new 'title' => 'One'; one.id = '1'
503
+ two = ActiveModelArticle.new 'title' => 'Two'; two.id = '2'
504
+
505
+ ActiveModelArticle.index.bulk_store [ one, two ]
506
+ end
507
+
508
+ context "namespaced models" do
509
+ should "not URL-escape the document_type" do
510
+ Configuration.client.expects(:post).with do |url, json|
511
+ # puts url, json
512
+ url == "#{Configuration.url}/my_namespace_my_models/_bulk" &&
513
+ json =~ %r|"_index":"my_namespace_my_models"| &&
514
+ json =~ %r|"_type":"my_namespace/my_model"|
515
+ end.returns(mock_response('{}', 200))
516
+
517
+ module MyNamespace
518
+ class MyModel
519
+ def document_type; "my_namespace/my_model"; end
520
+ def to_indexed_json; "{}"; end
521
+ end
522
+ end
523
+
524
+ Tire.index('my_namespace_my_models').bulk_store [ MyNamespace::MyModel.new ]
525
+ end
526
+ end
527
+
528
+ should "try again when an exception occurs" do
529
+ Configuration.client.expects(:post).returns(mock_response('Server error', 503)).at_least(2)
530
+
531
+ assert !@index.bulk_store([ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ])
532
+ end
533
+
534
+ should "try again and the raise when an exception occurs" do
535
+ Configuration.client.expects(:post).returns(mock_response('Server error', 503)).at_least(2)
536
+
537
+ assert_raise(RuntimeError) do
538
+ @index.bulk_store([ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ], {:raise => true})
539
+ end
540
+ end
541
+
542
+ should "try again when a connection error occurs" do
543
+ Configuration.client.expects(:post).raises(Errno::ECONNREFUSED, "Connection refused - connect(2)").at_least(2)
544
+
545
+ assert !@index.bulk_store([ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ])
546
+ end
547
+
548
+ should "signal exceptions should not be caught" do
549
+ Configuration.client.expects(:post).raises(Interrupt, "abort then interrupt!")
550
+
551
+ assert_raise Interrupt do
552
+ @index.bulk_store([ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ])
553
+ end
554
+ end
555
+
556
+ should "display error message when collection item does not have ID" do
557
+ Configuration.client.expects(:post).with do |url, json|
558
+ url == "#{ActiveModelArticle.index.url}/_bulk"
559
+ end.returns(mock_response('success', 200))
560
+
561
+ STDERR.expects(:puts).once
562
+
563
+ documents = [ { :title => 'Bogus' }, { :title => 'Real', :id => 1 } ]
564
+ ActiveModelArticle.index.bulk_store documents
565
+ end
566
+
567
+ should "log the response code" do
568
+ Tire.configure { logger STDERR }
569
+ Configuration.client.expects(:post).returns(mock_response('{}'), 200)
570
+
571
+ Configuration.logger.expects(:log_response).with do |status, took, body|
572
+ status == 200
573
+ end
574
+
575
+ @index.bulk_store [ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ]
576
+ end
577
+
578
+ end
579
+
580
+ context "when importing" do
581
+ setup do
582
+ @index = Tire::Index.new 'import'
583
+ end
584
+
585
+ class ::ImportData
586
+ DATA = (1..4).to_a
587
+
588
+ def self.paginate(options={})
589
+ options = {:page => 1, :per_page => 1000}.update options
590
+ DATA.slice( (options[:page]-1)*options[:per_page]...options[:page]*options[:per_page] )
591
+ end
592
+
593
+ def self.each(&block); DATA.each &block; end
594
+ def self.map(&block); DATA.map &block; end
595
+ def self.count; DATA.size; end
596
+ end
597
+
598
+ should "be initialized with a collection" do
599
+ @index.expects(:bulk_store).returns(:true)
600
+
601
+ assert_nothing_raised { @index.import [{ :id => 1, :title => 'Article' }] }
602
+ end
603
+
604
+ should "be initialized with a class and params" do
605
+ @index.expects(:bulk_store).returns(:true)
606
+
607
+ assert_nothing_raised { @index.import ImportData }
608
+ end
609
+
610
+ context "plain collection" do
611
+
612
+ should "just store it in bulk" do
613
+ collection = [{ :id => 1, :title => 'Article' }]
614
+ @index.expects(:bulk_store).with(collection, {} ).returns(true)
615
+
616
+ @index.import collection
617
+ end
618
+
619
+ end
620
+
621
+ context "class" do
622
+
623
+ should "call the passed method and bulk store the results" do
624
+ @index.expects(:bulk_store).with { |c, o| c == [1, 2, 3, 4] }.returns(true)
625
+
626
+ @index.import ImportData, :method => 'paginate'
627
+ end
628
+
629
+ should "pass the params to the passed method and bulk store the results" do
630
+ @index.expects(:bulk_store).with { |c,o| c == [1, 2] }.returns(true)
631
+ @index.expects(:bulk_store).with { |c,o| c == [3, 4] }.returns(true)
632
+
633
+ @index.import ImportData, :method => 'paginate', :page => 1, :per_page => 2
634
+ end
635
+
636
+ should "pass the class when method not passed" do
637
+ @index.expects(:bulk_store).with { |c,o| c == ImportData }.returns(true)
638
+
639
+ @index.import ImportData
640
+ end
641
+
642
+ end
643
+
644
+ context "with passed block" do
645
+
646
+ context "and plain collection" do
647
+
648
+ should "allow to manipulate the collection in the block" do
649
+ Tire::Index.any_instance.expects(:bulk_store).with([{ :id => 1, :title => 'ARTICLE' }], {})
650
+
651
+
652
+ @index.import [{ :id => 1, :title => 'Article' }] do |articles|
653
+ articles.map { |article| article.update :title => article[:title].upcase }
654
+ end
655
+ end
656
+
657
+ end
658
+
659
+ context "and object" do
660
+
661
+ should "call the passed block on every batch" do
662
+ Tire::Index.any_instance.expects(:bulk_store).with { |collection, options| collection == [1, 2] }
663
+ Tire::Index.any_instance.expects(:bulk_store).with { |collection, options| collection == [3, 4] }
664
+
665
+ runs = 0
666
+ @index.import ImportData, :method => 'paginate', :per_page => 2 do |documents|
667
+ runs += 1
668
+ # Don't forget to return the documents at the end of the block
669
+ documents
670
+ end
671
+
672
+ assert_equal 2, runs
673
+ end
674
+
675
+ should "allow to manipulate the documents in passed block" do
676
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [2, 3] }
677
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [4, 5] }
678
+
679
+ @index.import ImportData, :method => :paginate, :per_page => 2 do |documents|
680
+ # Add 1 to every "document" and return them
681
+ documents.map { |d| d + 1 }
682
+ end
683
+ end
684
+
685
+ end
686
+
687
+ end
688
+
689
+ end
690
+
691
+ context "when percolating" do
692
+
693
+ should "register percolator query as a Hash" do
694
+ query = { :query => { :query_string => { :query => 'foo' } } }
695
+ Configuration.client.expects(:put).with do |url, payload|
696
+ payload = MultiJson.decode(payload)
697
+ url == "#{Configuration.url}/_percolator/dummy/my-query" &&
698
+ payload['query']['query_string']['query'] == 'foo'
699
+ end.
700
+ returns(mock_response('{
701
+ "ok" : true,
702
+ "_index" : "_percolator",
703
+ "_type" : "dummy",
704
+ "_id" : "my-query",
705
+ "_version" : 1
706
+ }'))
707
+
708
+ @index.register_percolator_query 'my-query', query
709
+ end
710
+
711
+ should "register percolator query as a block" do
712
+ Configuration.client.expects(:put).with do |url, payload|
713
+ payload = MultiJson.decode(payload)
714
+ url == "#{Configuration.url}/_percolator/dummy/my-query" &&
715
+ payload['query']['query_string']['query'] == 'foo'
716
+ end.
717
+ returns(mock_response('{
718
+ "ok" : true,
719
+ "_index" : "_percolator",
720
+ "_type" : "dummy",
721
+ "_id" : "my-query",
722
+ "_version" : 1
723
+ }'))
724
+
725
+ @index.register_percolator_query 'my-query' do
726
+ string 'foo'
727
+ end
728
+ end
729
+
730
+ should "register percolator query with a key" do
731
+ query = { :query => { :query_string => { :query => 'foo' } },
732
+ :tags => ['alert'] }
733
+
734
+ Configuration.client.expects(:put).with do |url, payload|
735
+ payload = MultiJson.decode(payload)
736
+ url == "#{Configuration.url}/_percolator/dummy/my-query" &&
737
+ payload['query']['query_string']['query'] == 'foo' &&
738
+ payload['tags'] == ['alert']
739
+ end.
740
+ returns(mock_response('{
741
+ "ok" : true,
742
+ "_index" : "_percolator",
743
+ "_type" : "dummy",
744
+ "_id" : "my-query",
745
+ "_version" : 1
746
+ }'))
747
+
748
+ assert @index.register_percolator_query('my-query', query)
749
+ end
750
+
751
+ should "unregister percolator query" do
752
+ Configuration.client.expects(:delete).with("#{Configuration.url}/_percolator/dummy/my-query").
753
+ returns(mock_response('{"ok":true,"acknowledged":true}'))
754
+ assert @index.unregister_percolator_query('my-query')
755
+ end
756
+
757
+ should "percolate document against all registered queries" do
758
+ Configuration.client.expects(:get).with do |url,payload|
759
+ payload = MultiJson.decode(payload)
760
+ url == "#{@index.url}/document/_percolate" &&
761
+ payload['doc']['title'] == 'Test'
762
+ end.
763
+ returns(mock_response('{"ok":true,"_id":"test","matches":["alerts"]}'))
764
+
765
+ matches = @index.percolate :title => 'Test'
766
+ assert_equal ["alerts"], matches
767
+ end
768
+
769
+ should "percolate a typed document against all registered queries" do
770
+ Configuration.client.expects(:get).with do |url,payload|
771
+ payload = MultiJson.decode(payload)
772
+ url == "#{@index.url}/article/_percolate" &&
773
+ payload['doc']['title'] == 'Test'
774
+ end.
775
+ returns(mock_response('{"ok":true,"_id":"test","matches":["alerts"]}'))
776
+
777
+ matches = @index.percolate :type => 'article', :title => 'Test'
778
+ assert_equal ["alerts"], matches
779
+ end
780
+
781
+ should "percolate document against specific queries" do
782
+ Configuration.client.expects(:get).with do |url,payload|
783
+ payload = MultiJson.decode(payload)
784
+ # p [url, payload]
785
+ url == "#{@index.url}/document/_percolate" &&
786
+ payload['doc']['title'] == 'Test' &&
787
+ payload['query']['query_string']['query'] == 'tag:alerts'
788
+ end.
789
+ returns(mock_response('{"ok":true,"_id":"test","matches":["alerts"]}'))
790
+
791
+ matches = @index.percolate(:title => 'Test') { string 'tag:alerts' }
792
+ assert_equal ["alerts"], matches
793
+ end
794
+
795
+ context "while storing document" do
796
+
797
+ should "percolate document against all registered queries" do
798
+ Configuration.client.expects(:post).
799
+ with do |url, payload|
800
+ url == "#{@index.url}/article/?percolate=*" &&
801
+ payload =~ /"title":"Test"/
802
+ end.
803
+ returns(mock_response('{"ok":true,"_id":"test","matches":["alerts"]}'))
804
+ @index.store( {:type => 'article', :title => 'Test'}, {:percolate => true} )
805
+ end
806
+
807
+ should "percolate document against specific queries" do
808
+ Configuration.client.expects(:post).
809
+ with do |url, payload|
810
+ url == "#{@index.url}/article/?percolate=tag:alerts" &&
811
+ payload =~ /"title":"Test"/
812
+ end.
813
+ returns(mock_response('{"ok":true,"_id":"test","matches":["alerts"]}'))
814
+ response = @index.store( {:type => 'article', :title => 'Test'}, {:percolate => 'tag:alerts'} )
815
+ assert_equal response['matches'], ['alerts']
816
+ end
817
+
818
+ end
819
+
820
+ end
821
+
822
+ context "reindexing" do
823
+ setup do
824
+ @results = {
825
+ "_scroll_id" => "abc123",
826
+ "took" => 3,
827
+ "hits" => {
828
+ "total" => 10,
829
+ "hits" => [
830
+ { "_id" => "1", "_source" => { "title" => "Test" } }
831
+ ]
832
+ }
833
+ }
834
+ end
835
+
836
+ should "perform bulk store in the new index" do
837
+ Index.any_instance.stubs(:exists?).returns(true)
838
+ Search::Scan.any_instance.stubs(:__perform)
839
+ Search::Scan.any_instance.
840
+ expects(:results).
841
+ returns(Results::Collection.new(@results)).
842
+ then.
843
+ returns(Results::Collection.new(@results.merge('hits' => {'hits' => []}))).
844
+ at_least_once
845
+
846
+ Index.any_instance.expects(:bulk_store).once
847
+
848
+ @index.reindex 'whammy'
849
+ end
850
+
851
+ should "create the new index if it does not exist" do
852
+ options = { :settings => { :number_of_shards => 1 } }
853
+
854
+ Index.any_instance.stubs(:exists?).returns(false)
855
+ Search::Scan.any_instance.stubs(:__perform)
856
+ Search::Scan.any_instance.
857
+ expects(:results).
858
+ returns(Results::Collection.new(@results)).
859
+ then.
860
+ returns(Results::Collection.new(@results.merge('hits' => {'hits' => []}))).
861
+ at_least_once
862
+
863
+ Index.any_instance.expects(:create).with(options).once
864
+
865
+ @index.reindex 'whammy', options
866
+ end
867
+
868
+ end
869
+
870
+ context "when accessing the variables from outer scope" do
871
+
872
+ should "access the variables" do
873
+ @my_title = 'Title From Outer Space'
874
+
875
+ def index_something
876
+ @tags = ['block', 'scope', 'revenge']
877
+
878
+ Index.any_instance.expects(:store).with(:title => 'Title From Outer Space', :tags => ['block', 'scope', 'revenge'])
879
+
880
+ Tire::Index.new 'outer-space' do |index|
881
+ index.store :title => @my_title, :tags => @tags
882
+ end
883
+ end
884
+
885
+ index_something
886
+ end
887
+
888
+ end
889
+
890
+ end
891
+
892
+ end
893
+
894
+ end