load_balanced_tire 0.1

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 (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