tire-erez 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. data/.gitignore +14 -0
  2. data/.travis.yml +32 -0
  3. data/.yardopts +1 -0
  4. data/Gemfile +10 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.markdown +775 -0
  7. data/Rakefile +51 -0
  8. data/examples/rails-application-template.rb +263 -0
  9. data/examples/tire-dsl.rb +932 -0
  10. data/lib/tire.rb +59 -0
  11. data/lib/tire/alias.rb +296 -0
  12. data/lib/tire/configuration.rb +38 -0
  13. data/lib/tire/count.rb +85 -0
  14. data/lib/tire/dsl.rb +114 -0
  15. data/lib/tire/http/client.rb +62 -0
  16. data/lib/tire/http/clients/curb.rb +61 -0
  17. data/lib/tire/http/clients/faraday.rb +71 -0
  18. data/lib/tire/http/response.rb +27 -0
  19. data/lib/tire/index.rb +443 -0
  20. data/lib/tire/logger.rb +60 -0
  21. data/lib/tire/model/callbacks.rb +40 -0
  22. data/lib/tire/model/import.rb +27 -0
  23. data/lib/tire/model/indexing.rb +134 -0
  24. data/lib/tire/model/naming.rb +100 -0
  25. data/lib/tire/model/percolate.rb +99 -0
  26. data/lib/tire/model/persistence.rb +72 -0
  27. data/lib/tire/model/persistence/attributes.rb +148 -0
  28. data/lib/tire/model/persistence/finders.rb +54 -0
  29. data/lib/tire/model/persistence/storage.rb +77 -0
  30. data/lib/tire/model/search.rb +322 -0
  31. data/lib/tire/multi_search.rb +263 -0
  32. data/lib/tire/results/collection.rb +156 -0
  33. data/lib/tire/results/item.rb +94 -0
  34. data/lib/tire/results/pagination.rb +68 -0
  35. data/lib/tire/rubyext/hash.rb +8 -0
  36. data/lib/tire/rubyext/ruby_1_8.rb +1 -0
  37. data/lib/tire/rubyext/symbol.rb +11 -0
  38. data/lib/tire/rubyext/uri_escape.rb +74 -0
  39. data/lib/tire/search.rb +211 -0
  40. data/lib/tire/search/facet.rb +81 -0
  41. data/lib/tire/search/filter.rb +28 -0
  42. data/lib/tire/search/highlight.rb +37 -0
  43. data/lib/tire/search/queries/match.rb +40 -0
  44. data/lib/tire/search/query.rb +250 -0
  45. data/lib/tire/search/scan.rb +114 -0
  46. data/lib/tire/search/script_field.rb +23 -0
  47. data/lib/tire/search/sort.rb +25 -0
  48. data/lib/tire/tasks.rb +138 -0
  49. data/lib/tire/utils.rb +17 -0
  50. data/lib/tire/version.rb +18 -0
  51. data/test/fixtures/articles/1.json +1 -0
  52. data/test/fixtures/articles/2.json +1 -0
  53. data/test/fixtures/articles/3.json +1 -0
  54. data/test/fixtures/articles/4.json +1 -0
  55. data/test/fixtures/articles/5.json +1 -0
  56. data/test/integration/active_model_indexing_test.rb +51 -0
  57. data/test/integration/active_model_searchable_test.rb +114 -0
  58. data/test/integration/active_record_searchable_test.rb +620 -0
  59. data/test/integration/boolean_queries_test.rb +43 -0
  60. data/test/integration/boosting_queries_test.rb +32 -0
  61. data/test/integration/bulk_test.rb +86 -0
  62. data/test/integration/count_test.rb +64 -0
  63. data/test/integration/custom_score_queries_test.rb +89 -0
  64. data/test/integration/dis_max_queries_test.rb +68 -0
  65. data/test/integration/dsl_search_test.rb +30 -0
  66. data/test/integration/explanation_test.rb +44 -0
  67. data/test/integration/facets_test.rb +311 -0
  68. data/test/integration/filtered_queries_test.rb +66 -0
  69. data/test/integration/filters_test.rb +75 -0
  70. data/test/integration/fuzzy_queries_test.rb +20 -0
  71. data/test/integration/highlight_test.rb +64 -0
  72. data/test/integration/index_aliases_test.rb +122 -0
  73. data/test/integration/index_mapping_test.rb +43 -0
  74. data/test/integration/index_store_test.rb +112 -0
  75. data/test/integration/index_update_document_test.rb +121 -0
  76. data/test/integration/match_query_test.rb +79 -0
  77. data/test/integration/mongoid_searchable_test.rb +309 -0
  78. data/test/integration/multi_search_test.rb +114 -0
  79. data/test/integration/nested_query_test.rb +135 -0
  80. data/test/integration/percolator_test.rb +111 -0
  81. data/test/integration/persistent_model_test.rb +205 -0
  82. data/test/integration/prefix_query_test.rb +43 -0
  83. data/test/integration/query_return_version_test.rb +70 -0
  84. data/test/integration/query_string_test.rb +52 -0
  85. data/test/integration/range_queries_test.rb +36 -0
  86. data/test/integration/reindex_test.rb +56 -0
  87. data/test/integration/results_test.rb +58 -0
  88. data/test/integration/scan_test.rb +56 -0
  89. data/test/integration/script_fields_test.rb +38 -0
  90. data/test/integration/sort_test.rb +52 -0
  91. data/test/integration/text_query_test.rb +39 -0
  92. data/test/models/active_model_article.rb +31 -0
  93. data/test/models/active_model_article_with_callbacks.rb +49 -0
  94. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  95. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  96. data/test/models/active_record_models.rb +131 -0
  97. data/test/models/article.rb +15 -0
  98. data/test/models/mongoid_models.rb +85 -0
  99. data/test/models/persistent_article.rb +11 -0
  100. data/test/models/persistent_article_in_index.rb +16 -0
  101. data/test/models/persistent_article_in_namespace.rb +12 -0
  102. data/test/models/persistent_article_with_casting.rb +28 -0
  103. data/test/models/persistent_article_with_defaults.rb +12 -0
  104. data/test/models/persistent_article_with_percolation.rb +5 -0
  105. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  106. data/test/models/supermodel_article.rb +17 -0
  107. data/test/models/validated_model.rb +11 -0
  108. data/test/test_helper.rb +118 -0
  109. data/test/unit/active_model_lint_test.rb +17 -0
  110. data/test/unit/configuration_test.rb +84 -0
  111. data/test/unit/count_test.rb +67 -0
  112. data/test/unit/http_client_test.rb +79 -0
  113. data/test/unit/http_response_test.rb +49 -0
  114. data/test/unit/index_alias_test.rb +335 -0
  115. data/test/unit/index_test.rb +1098 -0
  116. data/test/unit/logger_test.rb +125 -0
  117. data/test/unit/model_callbacks_test.rb +116 -0
  118. data/test/unit/model_import_test.rb +75 -0
  119. data/test/unit/model_initialization_test.rb +31 -0
  120. data/test/unit/model_persistence_test.rb +548 -0
  121. data/test/unit/model_search_test.rb +964 -0
  122. data/test/unit/multi_search_test.rb +304 -0
  123. data/test/unit/results_collection_test.rb +372 -0
  124. data/test/unit/results_item_test.rb +173 -0
  125. data/test/unit/rubyext_test.rb +66 -0
  126. data/test/unit/search_facet_test.rb +186 -0
  127. data/test/unit/search_filter_test.rb +42 -0
  128. data/test/unit/search_highlight_test.rb +46 -0
  129. data/test/unit/search_query_test.rb +419 -0
  130. data/test/unit/search_scan_test.rb +113 -0
  131. data/test/unit/search_script_field_test.rb +26 -0
  132. data/test/unit/search_sort_test.rb +50 -0
  133. data/test/unit/search_test.rb +556 -0
  134. data/test/unit/tire_test.rb +144 -0
  135. data/tire.gemspec +83 -0
  136. metadata +586 -0
@@ -0,0 +1,335 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+
5
+ class IndexAliasTest < Test::Unit::TestCase
6
+
7
+ context "Index Alias" do
8
+ teardown { Configuration.reset }
9
+
10
+ context "initialization" do
11
+
12
+ should "have a name and defaults" do
13
+ @alias = Alias.new :name => 'dummy'
14
+ assert_equal 'dummy', @alias.name
15
+ assert @alias.indices.empty?
16
+ end
17
+
18
+ should "have indices" do
19
+ @alias = Alias.new :indices => ['index_A', 'index_B']
20
+ assert_equal ['index_A', 'index_B'], @alias.indices.to_a
21
+ end
22
+
23
+ should "have single index" do
24
+ @alias = Alias.new :index => 'index_A'
25
+ assert_equal ['index_A'], @alias.indices.to_a
26
+ assert_equal ['index_A'], @alias.index.to_a
27
+ end
28
+
29
+ should "have properties" do
30
+ @alias = Alias.new :routing => '1'
31
+ assert_equal '1', @alias.routing
32
+ end
33
+
34
+ should "allow to add indices in a block" do
35
+ @alias = Alias.new do
36
+ index 'index_A'
37
+ index 'index_B'
38
+ end
39
+ assert_equal ['index_A', 'index_B'], @alias.indices.to_a
40
+
41
+ @alias = Alias.new do |a|
42
+ a.index 'index_A'
43
+ a.index 'index_B'
44
+ end
45
+ assert_equal ['index_A', 'index_B'], @alias.indices.to_a
46
+ end
47
+
48
+ should "allow to define filter with DSL" do
49
+ @alias = Alias.new do
50
+ filter :terms, :username => 'mary'
51
+ end
52
+
53
+ assert_equal( { :terms => { :username => 'mary' } }, @alias.filter )
54
+ end
55
+
56
+ should "allow chaining" do
57
+ @alias = Alias.new.name('my_alias').index('index_1').index('index_2').routing('1')
58
+
59
+ assert_equal 'my_alias', @alias.name
60
+ assert_equal ['index_1', 'index_2'], @alias.indices.to_a
61
+ assert_equal '1', @alias.routing
62
+ end
63
+
64
+ should "be converted to JSON" do
65
+ @alias = Alias.new :name => 'index_anne',
66
+ :indices => ['index_2012_04', 'index_2012_03', 'index_2012_02'],
67
+ :routing => 1,
68
+ :filter => { :terms => { :user => 'anne' } }
69
+ # p @alias.to_json
70
+ result = MultiJson.decode @alias.to_json
71
+
72
+ assert_equal 3, result['actions'].size
73
+ assert_equal 'index_2012_04', result['actions'][0]['add']['index']
74
+ end
75
+
76
+ should "be converted to string" do
77
+ @alias = Alias.new :name => 'my_alias'
78
+ assert_equal 'my_alias', @alias.to_s
79
+ end
80
+
81
+ end
82
+
83
+ context "updating" do
84
+ setup do
85
+ Configuration.client.expects(:get).
86
+ returns( mock_response( %q|{"index_A":{"aliases":{}},"index_B":{"aliases":{"alias_john":{"filter":{"term":{"user":"john"}}},"alias_martha":{"filter":{"term":{"user":"martha"}}}}},"mongoid_articles":{"aliases":{}},"index_C":{"aliases":{"alias_martha":{"filter":{"term":{"user":"martha"}}}}}}|), 200 ).at_least_once
87
+ end
88
+
89
+ should "add indices to alias" do
90
+ Configuration.client.expects(:post).with do |url, json|
91
+ # puts json
92
+ MultiJson.decode(json)['actions'].any? do |a|
93
+ a['add']['index'] == 'index_A' &&
94
+ a['add']['alias'] == 'alias_martha'
95
+ end
96
+ end.returns(mock_response('{}'), 200)
97
+
98
+ a = Alias.find('alias_martha')
99
+ a.indices.push 'index_A'
100
+ a.save
101
+ end
102
+
103
+ should "remove indices from alias" do
104
+ Configuration.client.expects(:post).with do |url, json|
105
+ # puts json
106
+ MultiJson.decode(json)['actions'].any? do |a|
107
+ a['remove'] &&
108
+ a['remove']['index'] == 'index_A' &&
109
+ a['remove']['alias'] == 'alias_martha'
110
+ end
111
+ end.returns(mock_response('{}'), 200)
112
+
113
+ a = Alias.find('alias_martha')
114
+ a.indices.delete 'index_A'
115
+ a.save
116
+ end
117
+
118
+ should "change alias configuration" do
119
+ Configuration.client.expects(:post).with do |url, json|
120
+ # puts json
121
+ MultiJson.decode(json)['actions'].all? { |a| a['add']['routing'] == 'martha' }
122
+ end.returns(mock_response('{}'), 200)
123
+
124
+ a = Alias.find('alias_martha')
125
+ a.routing('martha')
126
+ a.save
127
+ end
128
+
129
+ end
130
+
131
+ context "saving" do
132
+
133
+ should "send data to Elasticsearch" do
134
+ Configuration.client.expects(:post).with do |url, json|
135
+ url == "#{Configuration.url}/_aliases" &&
136
+ json =~ /"index":"index_2012_05"/ &&
137
+ json =~ /"alias":"index_current"/
138
+ end.returns(mock_response('{}'), 200)
139
+
140
+ @alias = Alias.new :name => 'index_current', :index => 'index_2012_05'
141
+ @alias.save
142
+ end
143
+
144
+ should "log request" do
145
+ Tire.configure { logger '/dev/null' }
146
+
147
+ Configuration.client.expects(:post)
148
+ Configuration.logger.expects(:log_request)
149
+ Alias.new( :name => 'index_current', :index => 'index_2012_05' ).save
150
+ end
151
+
152
+ end
153
+
154
+ context "finding" do
155
+ setup do
156
+ Configuration.client.expects(:get).with do |url, json|
157
+ url == "#{Configuration.url}/_aliases"
158
+ end.returns( mock_response( %q|{"index_A":{"aliases":{}},"index_B":{"aliases":{"alias_john":{"filter":{"term":{"user":"john"}}},"alias_martha":{"filter":{"term":{"user":"martha"}}}}},"index_C":{"aliases":{"alias_martha":{"filter":{"term":{"user":"martha"}}}}}}|), 200 )
159
+ end
160
+
161
+ should "find all aliases" do
162
+ aliases = Alias.all
163
+ # p aliases
164
+ assert_equal 2, aliases.size
165
+ assert_equal ['index_B', 'index_C'], aliases.select { |a| a.name == 'alias_martha'}.first.indices.to_a.sort
166
+ end
167
+
168
+ should "find aliases for a specific index" do
169
+ Configuration.client.unstub(:get)
170
+ Configuration.client.expects(:get).with do |url, json|
171
+ url == "#{Configuration.url}/index_C/_aliases"
172
+ end.returns( mock_response( %q|{"index_C":{"aliases":{"alias_martha":{"filter":{"term":{"user":"martha"}}}}}}|), 200 )
173
+
174
+ aliases = Alias.all('index_C')
175
+ # p aliases
176
+ assert_equal 1, aliases.size
177
+ assert_equal ['index_C'], aliases.last.indices.to_a
178
+ end
179
+
180
+ should "find an alias" do
181
+ a = Alias.find('alias_martha')
182
+ assert_instance_of Alias, a
183
+ assert_equal ['index_B', 'index_C'], a.indices.to_a.sort
184
+ end
185
+
186
+ should "find an alias and configure it with a block" do
187
+ a = Alias.find('alias_martha') do |a|
188
+ a.indices.delete 'index_A'
189
+ a.indices.add 'index_D'
190
+ end
191
+
192
+ assert_equal ['index_B', 'index_C', 'index_D'], a.indices.to_a.sort
193
+ end
194
+
195
+ end
196
+
197
+ context "creating" do
198
+ setup do
199
+ Configuration.client.expects(:post).with do |url, json|
200
+ url == "#{Configuration.url}/_aliases" &&
201
+ json =~ /"index":"index_2012_05"/ &&
202
+ json =~ /"alias":"index_current"/
203
+ end.returns(mock_response('{}'), 200)
204
+ end
205
+
206
+ should "create the alias" do
207
+ Alias.create :name => 'index_current', :index => 'index_2012_05'
208
+ end
209
+
210
+ should "create the alias with a block" do
211
+ Alias.create :name => 'index_current' do
212
+ index 'index_2012_05'
213
+ end
214
+ end
215
+
216
+ end
217
+
218
+ end
219
+
220
+ context "IndexCollection" do
221
+
222
+ should "be intialized with an array or arguments list" do
223
+ c1 = Alias::IndexCollection.new ['1', '2', '3']
224
+ c2 = Alias::IndexCollection.new '1', '2', '3'
225
+ assert_equal c1.to_a, c2.to_a
226
+ end
227
+
228
+ should "be iterable" do
229
+ c = Alias::IndexCollection.new '1', '2', '3'
230
+ assert_respond_to c, :each
231
+ assert_respond_to c, :size
232
+ assert_equal [1, 2, 3], c.map(&:to_i)
233
+ end
234
+
235
+ should "allow adding values" do
236
+ c = Alias::IndexCollection.new '1', '2'
237
+ c.add '3'
238
+ assert_equal 3, c.size
239
+ assert_equal ['1', '2', '3'], c.add_indices
240
+ assert_equal [], c.remove_indices
241
+ end
242
+
243
+ should "allow removing values" do
244
+ c = Alias::IndexCollection.new '1', '2'
245
+ c.remove '1'
246
+ assert_equal 1, c.size
247
+ assert_equal ['2'], c.add_indices
248
+ assert_equal ['1'], c.remove_indices
249
+ end
250
+
251
+ should "clear everything" do
252
+ c = Alias::IndexCollection.new '1', '2'
253
+ c.clear
254
+ assert_equal 0, c.size
255
+ assert_equal [], c.add_indices
256
+ assert_equal ['1', '2'], c.remove_indices
257
+ end
258
+
259
+ should "respond to empty" do
260
+ c = Alias::IndexCollection.new
261
+ assert c.empty?, "#{c.inspect} should be empty"
262
+ end
263
+
264
+ should "remove values with a block" do
265
+ c = Alias::IndexCollection.new '1', '2', '3'
266
+
267
+ c.delete_if { |a| a.to_i > 1 }
268
+ assert_equal 1, c.size
269
+ assert_equal ['1'], c.add_indices
270
+ assert_equal ['2', '3'], c.remove_indices
271
+ end
272
+
273
+ end
274
+
275
+ context "aliases with index and search routing values" do
276
+ setup do
277
+ json =<<-JSON
278
+ {
279
+ "index_A": {
280
+ "aliases": {}
281
+ },
282
+ "index_B": {
283
+ "aliases": {
284
+ "alias_john": {
285
+ "filter": {
286
+ "term": {
287
+ "user": "john"
288
+ }
289
+ }
290
+ },
291
+ "alias_martha": {
292
+ "filter": {
293
+ "term": {
294
+ "user": "martha"
295
+ }
296
+ }
297
+ }
298
+ }
299
+ },
300
+ "index_C": {
301
+ "aliases": {
302
+ "alias_martha": {
303
+ "filter": {
304
+ "term": {
305
+ "user": "martha"
306
+ }
307
+ },
308
+ "index_routing": "1",
309
+ "search_routing": "2"
310
+ }
311
+ }
312
+ }
313
+ }
314
+ JSON
315
+ Configuration.client.expects(:get).
316
+ returns( mock_response(json), 200).
317
+ at_least_once
318
+ end
319
+
320
+ should "find all aliases" do
321
+ aliases = Alias.all
322
+ # p aliases
323
+ assert_equal 2, aliases.size
324
+ assert_equal ['index_B', 'index_C'], aliases.select { |a| a.name == 'alias_martha'}.first.indices.to_a.sort
325
+ end
326
+
327
+ should "find an alias" do
328
+ a = Alias.find('alias_martha')
329
+ assert_instance_of Alias, a
330
+ assert_equal ['index_B', 'index_C'], a.indices.to_a.sort
331
+ end
332
+ end
333
+
334
+ end
335
+ end
@@ -0,0 +1,1098 @@
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 for analyzer" do
130
+ Configuration.client.expects(:get).with do |url, payload|
131
+ assert_equal "#{@index.url}/_analyze?analyzer=whitespace&pretty=true", url
132
+ end.returns(mock_response(@mock_analyze_response))
133
+
134
+ @index.analyze("foo bar", :analyzer => 'whitespace')
135
+
136
+ end
137
+
138
+ should "properly encode parameters for field" do
139
+ Configuration.client.expects(:get).with do |url, payload|
140
+ assert_equal "#{@index.url}/_analyze?field=title&pretty=true", url
141
+ end.returns(mock_response(@mock_analyze_response))
142
+
143
+ @index.analyze("foo bar", :field => 'title')
144
+ end
145
+
146
+ should "properly encode format parameter" do
147
+ Configuration.client.expects(:get).with do |url, payload|
148
+ assert_equal "#{@index.url}/_analyze?analyzer=keyword&format=text&pretty=true", url
149
+ end.returns(mock_response(@mock_analyze_response))
150
+
151
+ @index.analyze("foo bar", :analyzer => 'keyword', :format => 'text')
152
+ end
153
+
154
+ end
155
+
156
+ context "mapping" do
157
+
158
+ should "create index with mapping" do
159
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"acknowledged":true}'))
160
+
161
+ assert @index.create :settings => { :number_of_shards => 1 },
162
+ :mappings => { :article => {
163
+ :properties => {
164
+ :title => { :boost => 2.0,
165
+ :type => 'string',
166
+ :store => 'yes',
167
+ :analyzer => 'snowball' }
168
+ }
169
+ }
170
+ }
171
+ end
172
+
173
+ should "return the mapping" do
174
+ json =<<-JSON
175
+ {
176
+ "dummy" : {
177
+ "article" : {
178
+ "properties" : {
179
+ "title" : { "type" : "string", "boost" : 2.0 },
180
+ "category" : { "type" : "string", "analyzed" : "no" }
181
+ }
182
+ }
183
+ }
184
+ }
185
+ JSON
186
+ Configuration.client.stubs(:get).returns(mock_response(json))
187
+
188
+ assert_equal 'string', @index.mapping['article']['properties']['title']['type']
189
+ assert_equal 2.0, @index.mapping['article']['properties']['title']['boost']
190
+ end
191
+
192
+ end
193
+
194
+ context "settings" do
195
+
196
+ should "return index settings" do
197
+ json =<<-JSON
198
+ {
199
+ "dummy" : {
200
+ "settings" : {
201
+ "index.number_of_shards" : "20",
202
+ "index.number_of_replicas" : "0"
203
+ }
204
+ }
205
+ }
206
+ JSON
207
+ Configuration.client.stubs(:get).returns(mock_response(json))
208
+
209
+ assert_equal '20', @index.settings['index.number_of_shards']
210
+ end
211
+
212
+ end
213
+
214
+ context "when storing" do
215
+
216
+ should "set type from Hash :type property" do
217
+ Configuration.client.expects(:post).with do |url,document|
218
+ assert_equal "#{@index.url}/article/", url
219
+ end.returns(mock_response('{"ok":true,"_id":"test"}'))
220
+ @index.store :type => 'article', :title => 'Test'
221
+ end
222
+
223
+ should "set type from Hash :_type property" do
224
+ Configuration.client.expects(:post).with do |url,document|
225
+ assert_equal "#{@index.url}/article/", url
226
+ end.returns(mock_response('{"ok":true,"_id":"test"}'))
227
+ @index.store :_type => 'article', :title => 'Test'
228
+ end
229
+
230
+ should "set type from Object _type method" do
231
+ Configuration.client.expects(:post).with do |url,document|
232
+ assert_equal "#{@index.url}/article/", url
233
+ end.returns(mock_response('{"ok":true,"_id":"test"}'))
234
+
235
+ article = Class.new do
236
+ def _type; 'article'; end
237
+ def to_indexed_json; "{}"; end
238
+ end.new
239
+ @index.store article
240
+ end
241
+
242
+ should "set type from Object type method" do
243
+ Configuration.client.expects(:post).with do |url,document|
244
+ assert_equal "#{@index.url}/article/", url
245
+ end.returns(mock_response('{"ok":true,"_id":"test"}'))
246
+
247
+ article = Class.new do
248
+ def type; 'article'; end
249
+ def to_indexed_json; "{}"; end
250
+ end.new
251
+ @index.store article
252
+ end
253
+
254
+ should "properly encode namespaced document types" do
255
+ Configuration.client.expects(:post).with do |url,document|
256
+ assert_equal "#{@index.url}/my_namespace%2Fmy_model/", url
257
+ end.returns(mock_response('{"ok":true,"_id":"123"}'))
258
+
259
+ module MyNamespace
260
+ class MyModel
261
+ def document_type; "my_namespace/my_model"; end
262
+ def to_indexed_json; "{}"; end
263
+ end
264
+ end
265
+
266
+ @index.store MyNamespace::MyModel.new
267
+ end
268
+
269
+ should "set default type" do
270
+ Configuration.client.expects(:post).with("#{@index.url}/document/", '{"title":"Test"}').returns(mock_response('{"ok":true,"_id":"test"}'))
271
+ @index.store :title => 'Test'
272
+ end
273
+
274
+ should "call #to_indexed_json on non-String documents" do
275
+ document = { :title => 'Test' }
276
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_id":"test"}'))
277
+ document.expects(:to_indexed_json)
278
+ @index.store document
279
+ end
280
+
281
+ should "raise error when storing neither String nor object with #to_indexed_json method" do
282
+ class MyDocument;end; document = MyDocument.new
283
+ assert_raise(ArgumentError) { @index.store document }
284
+ end
285
+
286
+ should "raise deprecation warning when trying to store a JSON string" do
287
+ Configuration.client.expects(:post).returns(mock_response('{"ok":true,"_id":"test"}'))
288
+ @index.store '{"foo" : "bar"}'
289
+ end
290
+
291
+ should "extract the routing information from options" do
292
+ Configuration.client.expects(:post).with do |url, payload|
293
+ assert_match /routing=abc/, url
294
+ end.returns(mock_response('{"ok":true,"_id":"123"}'))
295
+
296
+ @index.store( {:id => 123, :title => 'Test'}, {:routing => 'abc'} )
297
+ end
298
+
299
+ should "extract the replication type from options" do
300
+ Configuration.client.expects(:post).
301
+ with do |url, payload|
302
+ assert_equal "#{Configuration.url}/dummy/document/?replication=async", url
303
+ end.
304
+ returns(mock_response('{"ok":true,"_id":"test"}'))
305
+
306
+ @index.store({:title => 'Test'}, {:replication => 'async'})
307
+ end
308
+
309
+ context "document with ID" do
310
+
311
+ should "store Hash it under its ID property" do
312
+ Configuration.client.expects(:post).with("#{@index.url}/document/123",
313
+ MultiJson.encode({:id => 123, :title => 'Test'})).
314
+ returns(mock_response('{"ok":true,"_id":"123"}'))
315
+ @index.store :id => 123, :title => 'Test'
316
+ end
317
+
318
+ should "store a custom class under its ID property" do
319
+ Configuration.client.expects(:post).with("#{@index.url}/document/123",
320
+ {:id => 123, :title => 'Test', :body => 'Lorem'}.to_json).
321
+ returns(mock_response('{"ok":true,"_id":"123"}'))
322
+ @index.store Article.new(:id => 123, :title => 'Test', :body => 'Lorem')
323
+ end
324
+
325
+ should "convert document ID to string or number" do
326
+ # This is related to issues #529, #535:
327
+ # When using Mongoid and the Yajl gem, document IDs from Mongo (Moped::BSON::ObjectId)
328
+ # are incorrectly serialized to JSON, and documents are stored with incorrect, auto-generated IDs.
329
+ class Document1; def id; "one"; end; end
330
+ class Document2; def id; 1; end; end
331
+ class Document3; class ID; def as_json; 'c'; end; end
332
+ def id; ID.new; end
333
+ end
334
+
335
+ document_1 = Document1.new
336
+ document_2 = Document2.new
337
+ document_3 = Document3.new
338
+
339
+ assert_equal 'one', @index.get_id_from_document(document_1)
340
+ assert_equal 1, @index.get_id_from_document(document_2)
341
+ assert_equal 'c', @index.get_id_from_document(document_3)
342
+ end
343
+
344
+ end
345
+
346
+ end
347
+
348
+ context "when retrieving" do
349
+
350
+ setup do
351
+ Configuration.reset :wrapper
352
+
353
+ Configuration.client.stubs(:post).with do |url, payload|
354
+ url == "#{@index.url}/article/" &&
355
+ payload =~ /"title":"Test"/
356
+ end.
357
+ returns(mock_response('{"ok":true,"_id":"id-1"}'))
358
+ @index.store :type => 'article', :title => 'Test'
359
+ end
360
+
361
+ should "return document in default wrapper" do
362
+ Configuration.client.expects(:get).with("#{@index.url}/article/id-1").
363
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
364
+ article = @index.retrieve :article, 'id-1'
365
+ assert_instance_of Results::Item, article
366
+ assert_equal 'Test', article.title
367
+ assert_equal 'Test', article[:title]
368
+ end
369
+
370
+ should "return document as a hash" do
371
+ Configuration.wrapper Hash
372
+
373
+ Configuration.client.expects(:get).with("#{@index.url}/article/id-1").
374
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
375
+ article = @index.retrieve :article, 'id-1'
376
+ assert_instance_of Hash, article
377
+ end
378
+
379
+ should "return document in custom wrapper" do
380
+ Configuration.wrapper Article
381
+
382
+ Configuration.client.expects(:get).with("#{@index.url}/article/id-1").
383
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
384
+ article = @index.retrieve :article, 'id-1'
385
+ assert_instance_of Article, article
386
+ assert_equal 'Test', article.title
387
+ end
388
+
389
+ should "return nil for missing document" do
390
+ Configuration.client.expects(:get).with("#{@index.url}/article/id-1").
391
+ returns(mock_response('{"_id":"id-1","exists":false}'))
392
+ article = @index.retrieve :article, 'id-1'
393
+ assert_equal nil, article
394
+ end
395
+
396
+ should "raise error when no ID passed" do
397
+ assert_raise ArgumentError do
398
+ @index.retrieve 'article', nil
399
+ end
400
+ end
401
+
402
+ should "properly encode document type" do
403
+ Configuration.client.expects(:get).with("#{@index.url}/my_namespace%2Fmy_model/id-1").
404
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
405
+ article = @index.retrieve 'my_namespace/my_model', 'id-1'
406
+ end
407
+
408
+ should "allow to set routing" do
409
+ Configuration.client.expects(:get).with("#{@index.url}/article/id-1?routing=foo").
410
+ returns(mock_response('{"_id":"id-1"}'))
411
+ article = @index.retrieve :article, 'id-1', :routing => 'foo'
412
+ end
413
+
414
+ should "allow to set routing and fields" do
415
+ Configuration.client.expects(:get).with do |url|
416
+ assert url.include?('routing=foo'), url
417
+ assert url.include?('fields=name'), url
418
+ end.returns(mock_response('{"_id":"id-1"}'))
419
+
420
+ article = @index.retrieve :article, 'id-1', :routing => 'foo', :fields => 'name'
421
+ end
422
+
423
+ should "allow to set preference" do
424
+ Configuration.client.expects(:get).with do |url|
425
+ assert url.include?('preference=foo'), url
426
+ end.returns(mock_response('{"_id":"id-1"}'))
427
+
428
+ article = @index.retrieve :article, 'id-1', :preference => 'foo'
429
+ end
430
+
431
+ end
432
+
433
+ context "when removing" do
434
+
435
+ should "get type from document" do
436
+ Configuration.client.expects(:delete).with("#{@index.url}/article/1").
437
+ returns(mock_response('{"ok":true,"_id":"1"}')).twice
438
+ @index.remove :id => 1, :type => 'article', :title => 'Test'
439
+ @index.remove :id => 1, :type => 'article', :title => 'Test'
440
+ end
441
+
442
+ should "get namespaced type from document" do
443
+ Configuration.client.expects(:delete).with("#{@index.url}/articles%2Farticle/1").
444
+ returns(mock_response('{"ok":true,"_id":"1"}')).twice
445
+ @index.remove :id => 1, :type => 'articles/article', :title => 'Test'
446
+ @index.remove :id => 1, :type => 'articles/article', :title => 'Test'
447
+ end
448
+
449
+ should "set default type" do
450
+ Configuration.client.expects(:delete).with("#{@index.url}/document/1").
451
+ returns(mock_response('{"ok":true,"_id":"1"}'))
452
+ @index.remove :id => 1, :title => 'Test'
453
+ end
454
+
455
+ should "get ID from hash" do
456
+ Configuration.client.expects(:delete).with("#{@index.url}/document/1").
457
+ returns(mock_response('{"ok":true,"_id":"1"}'))
458
+ @index.remove :id => 1
459
+ end
460
+
461
+ should "get ID from method" do
462
+ document = stub('document', :id => 1)
463
+ Configuration.client.expects(:delete).with("#{@index.url}/document/1").
464
+ returns(mock_response('{"ok":true,"_id":"1"}'))
465
+ @index.remove document
466
+ end
467
+
468
+ should "get type and ID from arguments" do
469
+ Configuration.client.expects(:delete).with("#{@index.url}/article/1").
470
+ returns(mock_response('{"ok":true,"_id":"1"}'))
471
+ @index.remove :article, 1
472
+ end
473
+
474
+ should "raise error when no ID passed" do
475
+ assert_raise ArgumentError do
476
+ @index.remove :document, nil
477
+ end
478
+ end
479
+
480
+ should "properly encode document type" do
481
+ Configuration.client.expects(:delete).with("#{@index.url}/my_namespace%2Fmy_model/id-1").
482
+ returns(mock_response('{"_id":"id-1","_version":1, "_source" : {"title":"Test"}}'))
483
+ article = @index.remove 'my_namespace/my_model', 'id-1'
484
+ end
485
+
486
+ end
487
+
488
+ context "when updating" do
489
+
490
+ should "send script payload" do
491
+ Configuration.client.expects(:post).with do |url,payload|
492
+ payload = MultiJson.decode(payload)
493
+ # p [url, payload]
494
+ assert_equal( "#{@index.url}/document/42/_update", url ) &&
495
+ assert_not_nil( payload['script'] ) &&
496
+ assert_not_nil( payload['params'] ) &&
497
+ assert_equal( '21', payload['params']['bar'] )
498
+ end.
499
+ returns(
500
+ mock_response('{"ok":"true","_index":"dummy","_type":"document","_id":"42","_version":"2"}'))
501
+
502
+ assert @index.update('document', '42', {:script => "ctx._source.foo = bar;", :params => { :bar => '21' }})
503
+ end
504
+
505
+ should "send partial doc payload" do
506
+ Configuration.client.expects(:post).with do |url,payload|
507
+ payload = MultiJson.decode(payload)
508
+ # p [url, payload]
509
+ assert_equal( "#{@index.url}/document/42/_update", url ) &&
510
+ assert_not_nil( payload['doc'] ) &&
511
+ assert_equal('bar', payload['doc']['foo'])
512
+ end.
513
+ returns(
514
+ mock_response('{"ok":"true","_index":"dummy","_type":"document","_id":"42","_version":"2"}'))
515
+
516
+ assert @index.update('document', '42', {:doc => {:foo => 'bar'}})
517
+ end
518
+
519
+ should "send options" do
520
+ Configuration.client.expects(:post).with do |url,payload|
521
+ payload = MultiJson.decode(payload)
522
+ # p [url, payload]
523
+ assert_equal( "#{@index.url}/document/42/_update?timeout=1000", url ) &&
524
+ assert_nil( payload['timeout'] )
525
+ end.
526
+ returns(
527
+ mock_response('{"ok":"true","_index":"dummy","_type":"document","_id":"42","_version":"2"}'))
528
+ assert @index.update('document', '42', {:script => "ctx._source.foo = 'bar'"}, {:timeout => 1000})
529
+ end
530
+
531
+ should "raise error when no type or ID is passed" do
532
+ assert_raise(ArgumentError) { @index.update('article', nil, :script => 'foobar') }
533
+ assert_raise(ArgumentError) { @index.update(nil, '123', :script => 'foobar') }
534
+ end
535
+
536
+ should "raise an error when no script or partial document is passed" do
537
+ assert_raise ArgumentError do
538
+ @index.update('article', "42", {:foo => 'bar'})
539
+ end
540
+ end
541
+
542
+ end
543
+
544
+ context "when performing a bulk api action" do
545
+ # Possible Bulk API actions are `index`, `create`, `delete`
546
+ #
547
+ # The expected JSON looks like this:
548
+ #
549
+ # {"index":{"_index":"dummy","_type":"document","_id":"1"}}
550
+ # {"id":"1","title":"One"}
551
+ # {"create":{"_index":"dummy","_type":"document","_id":"2"}}
552
+ # {"id":"2","title":"Two"}
553
+ # {"delete":{"_index":"dummy","_type":"document","_id":"2"}}
554
+ #
555
+ # See http://www.elasticsearch.org/guide/reference/api/bulk.html
556
+
557
+ should "serialize payload for index action" do
558
+ Configuration.client.
559
+ expects(:post).
560
+ with do |url, payload|
561
+ assert_equal "#{@index.url}/_bulk", url
562
+ assert_match /"index"/, payload
563
+ assert_match /"_index":"dummy"/, payload
564
+ assert_match /"_type":"document"/, payload
565
+ assert_match /"_id":"1"/, payload
566
+ assert_match /"_id":"2"/, payload
567
+ assert_match /"title":"One"/, payload
568
+ assert_match /"title":"Two"/, payload
569
+ end.
570
+ returns(mock_response('{}'), 200)
571
+
572
+ @index.bulk :index, [ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ]
573
+ end
574
+
575
+ should "serialize payload for create action" do
576
+ Configuration.client.
577
+ expects(:post).
578
+ with do |url, payload|
579
+ assert_equal "#{@index.url}/_bulk", url
580
+ assert_match /"create"/, payload
581
+ assert_match /"_index":"dummy"/, payload
582
+ assert_match /"_type":"document"/, payload
583
+ assert_match /"_id":"1"/, payload
584
+ assert_match /"_id":"2"/, payload
585
+ assert_match /"title":"One"/, payload
586
+ assert_match /"title":"Two"/, payload
587
+ end.
588
+ returns(mock_response('{}'), 200)
589
+
590
+ @index.bulk :create, [ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ]
591
+ end
592
+
593
+ should "serialize payload for delete action" do
594
+ Configuration.client.
595
+ expects(:post).
596
+ with do |url, payload|
597
+ assert_equal "#{@index.url}/_bulk", url
598
+ assert_match /"delete"/, payload
599
+ assert_match /"_index":"dummy"/, payload
600
+ assert_match /"_type":"document"/, payload
601
+ assert_match /"_id":"1"/, payload
602
+ assert_match /"_id":"2"/, payload
603
+ assert ! payload.include?('"title"')
604
+ end.
605
+ returns(mock_response('{}'), 200)
606
+
607
+ @index.bulk :delete, [ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ]
608
+ end
609
+
610
+ should "serialize meta parameters such as routing into payload header" do
611
+ Configuration.client.
612
+ expects(:post).
613
+ with do |url, payload|
614
+ # print payload
615
+ lines = payload.split("\n")
616
+ assert_match /"_routing":"A"/, lines[0]
617
+ assert_match /"_routing":"B"/, lines[2]
618
+ assert_match /"_ttl":"1d"/, lines[2]
619
+ assert ! lines[4].include?('"_routing"')
620
+ end.
621
+ returns(mock_response('{}'), 200)
622
+
623
+ @index.bulk :index,
624
+ [
625
+ {:id => '1', :title => 'One', :_routing => 'A'},
626
+ {:id => '2', :title => 'Two', :_routing => 'B', :_ttl => '1d'},
627
+ {:id => '3', :title => 'Three'}
628
+ ]
629
+
630
+ end
631
+
632
+ should "pass URL parameters such as refresh or consistency" do
633
+ Configuration.client.
634
+ expects(:post).
635
+ with do |url, payload|
636
+ # p url
637
+ assert_match /\?consistency=one/, url
638
+ assert_match /&refresh=true/, url
639
+ end.
640
+ returns(mock_response('{}'), 200)
641
+
642
+ @index.bulk :index,
643
+ [ {:id => '1', :title => 'One' } ],
644
+ :consistency => 'one',
645
+ :refresh => true
646
+
647
+ end
648
+
649
+ should "serialize ActiveModel instances as payload" do
650
+ Configuration.client.
651
+ expects(:post).
652
+ with do |url, payload|
653
+ assert_equal "#{ActiveModelArticle.index.url}/_bulk", url
654
+ assert_match /"index"/, payload
655
+ assert_match /"_index":"active_model_articles"/, payload
656
+ assert_match /"_type":"active_model_article"/, payload
657
+ assert_match /"_id":"1"/, payload
658
+ assert_match /"_id":"2"/, payload
659
+ assert_match /"title":"One"/, payload
660
+ assert_match /"title":"Two"/, payload
661
+ end.
662
+ returns(mock_response('{}'), 200)
663
+
664
+ one = ActiveModelArticle.new 'title' => 'One'; one.id = '1'
665
+ two = ActiveModelArticle.new 'title' => 'Two'; two.id = '2'
666
+
667
+ ActiveModelArticle.index.bulk :index, [ one, two ]
668
+ end
669
+
670
+ should "extract meta information from document objects" do
671
+ Configuration.client.
672
+ expects(:post).
673
+ with do |url, payload|
674
+ print payload
675
+ lines = payload.split("\n")
676
+ assert_match /"_routing":"A"/, lines[0]
677
+ end.
678
+ returns(mock_response('{}'), 200)
679
+
680
+ class MyModel
681
+ def document_type; "my_model"; end
682
+ def to_hash; { :id => 1, :title => 'Foo', :_routing => 'A' }; end
683
+ def to_indexed_json; MultiJson.encode(to_hash); end
684
+ end
685
+
686
+ Tire.index('my_models').bulk_store [ MyModel.new ]
687
+ end
688
+
689
+ context "with namespaced models" do
690
+
691
+ should "not URL-escape the document_type" do
692
+ Configuration.client.expects(:post).with do |url, payload|
693
+ # puts url, payload
694
+ assert_equal "#{Configuration.url}/my_namespace_my_models/_bulk", url
695
+ assert_match %r|"_index":"my_namespace_my_models"|, payload
696
+ assert_match %r|"_type":"my_namespace/my_model"|, payload
697
+ end.returns(mock_response('{}', 200))
698
+
699
+ module MyNamespace
700
+ class MyModel
701
+ def document_type; "my_namespace/my_model"; end
702
+ def to_indexed_json; "{}"; end
703
+ end
704
+ end
705
+
706
+ Tire.index('my_namespace_my_models').bulk :index, [ MyNamespace::MyModel.new ]
707
+ end
708
+ end
709
+
710
+ should "try again when an exception occurs" do
711
+ Configuration.client.expects(:post).returns(mock_response('Server error', 503)).at_least(2)
712
+
713
+ assert !@index.bulk(:index, [ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ])
714
+ end
715
+
716
+ should "try again and the raise when an exception occurs" do
717
+ Configuration.client.expects(:post).returns(mock_response('Server error', 503)).at_least(2)
718
+
719
+ assert_raise(RuntimeError) do
720
+ @index.bulk :index,
721
+ [ {:id => '1', :title => 'One'}, {:id => '2', :title => 'Two'} ],
722
+ :raise => true
723
+ end
724
+ end
725
+
726
+ should "try again when a connection error occurs" do
727
+ Configuration.client.expects(:post).raises(Errno::ECONNREFUSED, "Connection refused - connect(2)").at_least(2)
728
+
729
+ assert !@index.bulk(:index, [ {:id => '1', :title => 'One'} ])
730
+ end
731
+
732
+ should "retry on SIGINT type of exceptions" do
733
+ Configuration.client.expects(:post).raises(Interrupt, "abort then interrupt!")
734
+
735
+ assert_raise Interrupt do
736
+ @index.bulk :index, [ {:id => '1', :title => 'One'} ]
737
+ end
738
+ end
739
+
740
+ should "display error message when collection item does not have ID" do
741
+ Configuration.client.expects(:post).with do |url, json|
742
+ url == "#{ActiveModelArticle.index.url}/_bulk"
743
+ end.returns(mock_response('success', 200))
744
+
745
+ STDERR.expects(:puts).once
746
+
747
+ documents = [ { :title => 'Bogus' }, { :title => 'Real', :id => 1 } ]
748
+ ActiveModelArticle.index.bulk :index, documents
749
+ end
750
+
751
+ should "log the response code" do
752
+ Tire.configure { logger STDERR }
753
+ Configuration.client.expects(:post).returns(mock_response('{}'), 200)
754
+
755
+ Configuration.logger.expects(:log_response).with do |status, took, body|
756
+ status == 200
757
+ end
758
+
759
+ @index.bulk :index, [ {:id => '1', :title => 'One'} ]
760
+ end
761
+
762
+
763
+ should "return immediately with empty collection" do
764
+ Configuration.client.expects(:post).never
765
+ @index.bulk_store []
766
+ end
767
+
768
+ end
769
+
770
+ context "when importing" do
771
+ setup do
772
+ @index = Tire::Index.new 'import'
773
+ end
774
+
775
+ class ::ImportData
776
+ DATA = (1..4).to_a
777
+
778
+ def self.paginate(options={})
779
+ options = {:page => 1, :per_page => 1000}.update options
780
+ DATA.slice( (options[:page]-1)*options[:per_page]...options[:page]*options[:per_page] )
781
+ end
782
+
783
+ def self.each(&block); DATA.each &block; end
784
+ def self.map(&block); DATA.map &block; end
785
+ def self.count; DATA.size; end
786
+ end
787
+
788
+ should "be initialized with a collection" do
789
+ @index.expects(:bulk_store).returns(:true)
790
+
791
+ assert_nothing_raised { @index.import [{ :id => 1, :title => 'Article' }] }
792
+ end
793
+
794
+ should "be initialized with a class and params" do
795
+ @index.expects(:bulk_store).returns(:true)
796
+
797
+ assert_nothing_raised { @index.import ImportData }
798
+ end
799
+
800
+ context "plain collection" do
801
+
802
+ should "just store it in bulk" do
803
+ collection = [{ :id => 1, :title => 'Article' }]
804
+ @index.expects(:bulk_store).with(collection, {} ).returns(true)
805
+
806
+ @index.import collection
807
+ end
808
+
809
+ end
810
+
811
+ context "class" do
812
+
813
+ should "call the passed method and bulk store the results" do
814
+ @index.expects(:bulk_store).with { |c, o| c == [1, 2, 3, 4] }.returns(true)
815
+
816
+ @index.import ImportData, :method => 'paginate'
817
+ end
818
+
819
+ should "pass the params to the passed method and bulk store the results" do
820
+ @index.expects(:bulk_store).with { |c,o| c == [1, 2] }.returns(true)
821
+ @index.expects(:bulk_store).with { |c,o| c == [3, 4] }.returns(true)
822
+
823
+ @index.import ImportData, :method => 'paginate', :page => 1, :per_page => 2
824
+ end
825
+
826
+ should "pass the class when method not passed" do
827
+ @index.expects(:bulk_store).with { |c,o| c == ImportData }.returns(true)
828
+
829
+ @index.import ImportData
830
+ end
831
+
832
+ end
833
+
834
+ context "with passed block" do
835
+
836
+ context "and plain collection" do
837
+
838
+ should "allow to manipulate the collection in the block" do
839
+ Tire::Index.any_instance.expects(:bulk_store).with([{ :id => 1, :title => 'ARTICLE' }], {})
840
+
841
+
842
+ @index.import [{ :id => 1, :title => 'Article' }] do |articles|
843
+ articles.map { |article| article.update :title => article[:title].upcase }
844
+ end
845
+ end
846
+
847
+ end
848
+
849
+ context "and object" do
850
+
851
+ should "call the passed block on every batch" do
852
+ Tire::Index.any_instance.expects(:bulk_store).with { |collection, options| collection == [1, 2] }
853
+ Tire::Index.any_instance.expects(:bulk_store).with { |collection, options| collection == [3, 4] }
854
+
855
+ runs = 0
856
+ @index.import ImportData, :method => 'paginate', :per_page => 2 do |documents|
857
+ runs += 1
858
+ # Don't forget to return the documents at the end of the block
859
+ documents
860
+ end
861
+
862
+ assert_equal 2, runs
863
+ end
864
+
865
+ should "allow to manipulate the documents in passed block" do
866
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [2, 3] }
867
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [4, 5] }
868
+
869
+ @index.import ImportData, :method => :paginate, :per_page => 2 do |documents|
870
+ # Add 1 to every "document" and return them
871
+ documents.map { |d| d + 1 }
872
+ end
873
+ end
874
+
875
+ end
876
+
877
+ end
878
+
879
+ end
880
+
881
+ context "when percolating" do
882
+
883
+ should "register percolator query as a Hash" do
884
+ query = { :query => { :query_string => { :query => 'foo' } } }
885
+ Configuration.client.expects(:put).with do |url, payload|
886
+ payload = MultiJson.decode(payload)
887
+ assert_equal "#{Configuration.url}/_percolator/dummy/my-query",
888
+ url
889
+ assert_equal 'foo', payload['query']['query_string']['query']
890
+ end.
891
+ returns(mock_response('{
892
+ "ok" : true,
893
+ "_index" : "_percolator",
894
+ "_type" : "dummy",
895
+ "_id" : "my-query",
896
+ "_version" : 1
897
+ }'))
898
+
899
+ @index.register_percolator_query 'my-query', query
900
+ end
901
+
902
+ should "register percolator query as a block" do
903
+ Configuration.client.expects(:put).with do |url, payload|
904
+ payload = MultiJson.decode(payload)
905
+ assert_equal "#{Configuration.url}/_percolator/dummy/my-query",
906
+ url
907
+ assert_equal 'foo', payload['query']['query_string']['query']
908
+ end.
909
+ returns(mock_response('{
910
+ "ok" : true,
911
+ "_index" : "_percolator",
912
+ "_type" : "dummy",
913
+ "_id" : "my-query",
914
+ "_version" : 1
915
+ }'))
916
+
917
+ @index.register_percolator_query 'my-query' do
918
+ string 'foo'
919
+ end
920
+ end
921
+
922
+ should "register percolator query with a key" do
923
+ query = { :query => { :query_string => { :query => 'foo' } },
924
+ :tags => ['alert'] }
925
+
926
+ Configuration.client.expects(:put).with do |url, payload|
927
+ payload = MultiJson.decode(payload)
928
+ assert_equal "#{Configuration.url}/_percolator/dummy/my-query",
929
+ url
930
+ assert_equal 'foo', payload['query']['query_string']['query']
931
+ assert_equal ['alert'], payload['tags']
932
+ end.
933
+ returns(mock_response('{
934
+ "ok" : true,
935
+ "_index" : "_percolator",
936
+ "_type" : "dummy",
937
+ "_id" : "my-query",
938
+ "_version" : 1
939
+ }'))
940
+
941
+ assert @index.register_percolator_query('my-query', query)
942
+ end
943
+
944
+ should "unregister percolator query" do
945
+ Configuration.client.expects(:delete).with("#{Configuration.url}/_percolator/dummy/my-query").
946
+ returns(mock_response('{"ok":true,"acknowledged":true}'))
947
+ assert @index.unregister_percolator_query('my-query')
948
+ end
949
+
950
+ should "percolate document against all registered queries" do
951
+ Configuration.client.expects(:get).with do |url,payload|
952
+ payload = MultiJson.decode(payload)
953
+ assert_equal "#{@index.url}/document/_percolate", url
954
+ assert_equal 'Test', payload['doc']['title']
955
+ end.
956
+ returns(mock_response('{"ok":true,"_id":"test","tire_matches":["alerts"]}'))
957
+
958
+ tire_matches = @index.percolate :title => 'Test'
959
+ assert_equal ["alerts"], tire_matches
960
+ end
961
+
962
+ should "percolate a typed document against all registered queries" do
963
+ Configuration.client.expects(:get).with do |url,payload|
964
+ payload = MultiJson.decode(payload)
965
+ assert_equal "#{@index.url}/article/_percolate", url
966
+ assert_equal 'Test', payload['doc']['title']
967
+ end.
968
+ returns(mock_response('{"ok":true,"_id":"test","tire_matches":["alerts"]}'))
969
+
970
+ tire_matches = @index.percolate :type => 'article', :title => 'Test'
971
+ assert_equal ["alerts"], tire_matches
972
+ end
973
+
974
+ should "percolate document against specific queries" do
975
+ Configuration.client.expects(:get).with do |url,payload|
976
+ payload = MultiJson.decode(payload)
977
+ # p [url, payload]
978
+ assert_equal "#{@index.url}/document/_percolate", url
979
+ assert_equal 'Test', payload['doc']['title']
980
+ assert_equal 'tag:alerts', payload['query']['query_string']['query']
981
+ end.
982
+ returns(mock_response('{"ok":true,"_id":"test","tire_matches":["alerts"]}'))
983
+
984
+ tire_matches = @index.percolate(:title => 'Test') { string 'tag:alerts' }
985
+ assert_equal ["alerts"], tire_matches
986
+ end
987
+
988
+ context "while storing document" do
989
+
990
+ should "percolate document against all registered queries" do
991
+ Configuration.client.expects(:post).
992
+ with do |url, payload|
993
+ assert_equal "#{@index.url}/article/?percolate=%2A", url
994
+ end.
995
+ returns(mock_response('{"ok":true,"_id":"test","tire_matches":["alerts"]}'))
996
+ @index.store( {:type => 'article', :title => 'Test'}, {:percolate => true} )
997
+ end
998
+
999
+ should "percolate document against specific queries" do
1000
+ Configuration.client.expects(:post).
1001
+ with do |url, payload|
1002
+ assert_equal "#{@index.url}/article/?percolate=tag%3Aalerts", url
1003
+ end.
1004
+ returns(mock_response('{"ok":true,"_id":"test","tire_matches":["alerts"]}'))
1005
+ response = @index.store( {:type => 'article', :title => 'Test'}, {:percolate => 'tag:alerts'} )
1006
+ assert_equal response['tire_matches'], ['alerts']
1007
+ end
1008
+
1009
+ end
1010
+
1011
+ end
1012
+
1013
+ context "when passing parent document ID" do
1014
+
1015
+ should "set the :parent option in the request parameters" do
1016
+ Configuration.client.expects(:post).
1017
+ with do |url, payload|
1018
+ assert_equal "#{Configuration.url}/dummy/document/?parent=1234", url
1019
+ end.
1020
+ returns(mock_response('{"ok":true,"_id":"test"}'))
1021
+
1022
+ @index.store({:title => 'Test'}, {:parent => 1234})
1023
+ end
1024
+ end
1025
+
1026
+ context "reindexing" do
1027
+ setup do
1028
+ @results = {
1029
+ "_scroll_id" => "abc123",
1030
+ "took" => 3,
1031
+ "hits" => {
1032
+ "total" => 10,
1033
+ "hits" => [
1034
+ { "_id" => "1", "_source" => { "title" => "Test" } }
1035
+ ]
1036
+ }
1037
+ }
1038
+ end
1039
+
1040
+ should "perform bulk store in the new index" do
1041
+ Index.any_instance.stubs(:exists?).returns(true)
1042
+ Search::Scan.any_instance.stubs(:__perform)
1043
+ Search::Scan.any_instance.
1044
+ expects(:results).
1045
+ returns(Results::Collection.new(@results)).
1046
+ then.
1047
+ returns(Results::Collection.new(@results.merge('hits' => {'hits' => []}))).
1048
+ at_least_once
1049
+
1050
+ Index.any_instance.expects(:bulk_store).once
1051
+
1052
+ @index.reindex 'whammy'
1053
+ end
1054
+
1055
+ should "create the new index if it does not exist" do
1056
+ options = { :settings => { :number_of_shards => 1 } }
1057
+
1058
+ Index.any_instance.stubs(:exists?).returns(false)
1059
+ Search::Scan.any_instance.stubs(:__perform)
1060
+ Search::Scan.any_instance.
1061
+ expects(:results).
1062
+ returns(Results::Collection.new(@results)).
1063
+ then.
1064
+ returns(Results::Collection.new(@results.merge('hits' => {'hits' => []}))).
1065
+ at_least_once
1066
+
1067
+ Index.any_instance.expects(:create).with(options).once
1068
+
1069
+ @index.reindex 'whammy', options
1070
+ end
1071
+
1072
+ end
1073
+
1074
+ context "when accessing the variables from outer scope" do
1075
+
1076
+ should "access the variables" do
1077
+ @my_title = 'Title From Outer Space'
1078
+
1079
+ def index_something
1080
+ @tags = ['block', 'scope', 'revenge']
1081
+
1082
+ Index.any_instance.expects(:store).with(:title => 'Title From Outer Space', :tags => ['block', 'scope', 'revenge'])
1083
+
1084
+ Tire::Index.new 'outer-space' do |index|
1085
+ index.store :title => @my_title, :tags => @tags
1086
+ end
1087
+ end
1088
+
1089
+ index_something
1090
+ end
1091
+
1092
+ end
1093
+
1094
+ end
1095
+
1096
+ end
1097
+
1098
+ end