load_balanced_tire 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. data/.gitignore +14 -0
  2. data/.travis.yml +29 -0
  3. data/Gemfile +4 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.markdown +760 -0
  6. data/Rakefile +78 -0
  7. data/examples/rails-application-template.rb +249 -0
  8. data/examples/tire-dsl.rb +876 -0
  9. data/lib/tire.rb +55 -0
  10. data/lib/tire/alias.rb +296 -0
  11. data/lib/tire/configuration.rb +30 -0
  12. data/lib/tire/dsl.rb +43 -0
  13. data/lib/tire/http/client.rb +62 -0
  14. data/lib/tire/http/clients/curb.rb +61 -0
  15. data/lib/tire/http/clients/faraday.rb +71 -0
  16. data/lib/tire/http/response.rb +27 -0
  17. data/lib/tire/index.rb +361 -0
  18. data/lib/tire/logger.rb +60 -0
  19. data/lib/tire/model/callbacks.rb +40 -0
  20. data/lib/tire/model/import.rb +26 -0
  21. data/lib/tire/model/indexing.rb +128 -0
  22. data/lib/tire/model/naming.rb +100 -0
  23. data/lib/tire/model/percolate.rb +99 -0
  24. data/lib/tire/model/persistence.rb +71 -0
  25. data/lib/tire/model/persistence/attributes.rb +143 -0
  26. data/lib/tire/model/persistence/finders.rb +66 -0
  27. data/lib/tire/model/persistence/storage.rb +69 -0
  28. data/lib/tire/model/search.rb +307 -0
  29. data/lib/tire/results/collection.rb +114 -0
  30. data/lib/tire/results/item.rb +86 -0
  31. data/lib/tire/results/pagination.rb +54 -0
  32. data/lib/tire/rubyext/hash.rb +8 -0
  33. data/lib/tire/rubyext/ruby_1_8.rb +7 -0
  34. data/lib/tire/rubyext/symbol.rb +11 -0
  35. data/lib/tire/search.rb +188 -0
  36. data/lib/tire/search/facet.rb +74 -0
  37. data/lib/tire/search/filter.rb +28 -0
  38. data/lib/tire/search/highlight.rb +37 -0
  39. data/lib/tire/search/query.rb +186 -0
  40. data/lib/tire/search/scan.rb +114 -0
  41. data/lib/tire/search/script_field.rb +23 -0
  42. data/lib/tire/search/sort.rb +25 -0
  43. data/lib/tire/tasks.rb +135 -0
  44. data/lib/tire/utils.rb +17 -0
  45. data/lib/tire/version.rb +22 -0
  46. data/test/fixtures/articles/1.json +1 -0
  47. data/test/fixtures/articles/2.json +1 -0
  48. data/test/fixtures/articles/3.json +1 -0
  49. data/test/fixtures/articles/4.json +1 -0
  50. data/test/fixtures/articles/5.json +1 -0
  51. data/test/integration/active_model_indexing_test.rb +51 -0
  52. data/test/integration/active_model_searchable_test.rb +114 -0
  53. data/test/integration/active_record_searchable_test.rb +446 -0
  54. data/test/integration/boolean_queries_test.rb +43 -0
  55. data/test/integration/count_test.rb +34 -0
  56. data/test/integration/custom_score_queries_test.rb +88 -0
  57. data/test/integration/dis_max_queries_test.rb +68 -0
  58. data/test/integration/dsl_search_test.rb +22 -0
  59. data/test/integration/explanation_test.rb +44 -0
  60. data/test/integration/facets_test.rb +259 -0
  61. data/test/integration/filtered_queries_test.rb +66 -0
  62. data/test/integration/filters_test.rb +63 -0
  63. data/test/integration/fuzzy_queries_test.rb +20 -0
  64. data/test/integration/highlight_test.rb +64 -0
  65. data/test/integration/index_aliases_test.rb +122 -0
  66. data/test/integration/index_mapping_test.rb +43 -0
  67. data/test/integration/index_store_test.rb +96 -0
  68. data/test/integration/index_update_document_test.rb +111 -0
  69. data/test/integration/mongoid_searchable_test.rb +309 -0
  70. data/test/integration/percolator_test.rb +111 -0
  71. data/test/integration/persistent_model_test.rb +130 -0
  72. data/test/integration/prefix_query_test.rb +43 -0
  73. data/test/integration/query_return_version_test.rb +70 -0
  74. data/test/integration/query_string_test.rb +52 -0
  75. data/test/integration/range_queries_test.rb +36 -0
  76. data/test/integration/reindex_test.rb +46 -0
  77. data/test/integration/results_test.rb +39 -0
  78. data/test/integration/scan_test.rb +56 -0
  79. data/test/integration/script_fields_test.rb +38 -0
  80. data/test/integration/sort_test.rb +36 -0
  81. data/test/integration/text_query_test.rb +39 -0
  82. data/test/models/active_model_article.rb +31 -0
  83. data/test/models/active_model_article_with_callbacks.rb +49 -0
  84. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  85. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  86. data/test/models/active_record_models.rb +122 -0
  87. data/test/models/article.rb +15 -0
  88. data/test/models/mongoid_models.rb +97 -0
  89. data/test/models/persistent_article.rb +11 -0
  90. data/test/models/persistent_article_in_namespace.rb +12 -0
  91. data/test/models/persistent_article_with_casting.rb +28 -0
  92. data/test/models/persistent_article_with_defaults.rb +11 -0
  93. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  94. data/test/models/supermodel_article.rb +17 -0
  95. data/test/models/validated_model.rb +11 -0
  96. data/test/test_helper.rb +93 -0
  97. data/test/unit/active_model_lint_test.rb +17 -0
  98. data/test/unit/configuration_test.rb +74 -0
  99. data/test/unit/http_client_test.rb +76 -0
  100. data/test/unit/http_response_test.rb +49 -0
  101. data/test/unit/index_alias_test.rb +275 -0
  102. data/test/unit/index_test.rb +894 -0
  103. data/test/unit/logger_test.rb +125 -0
  104. data/test/unit/model_callbacks_test.rb +116 -0
  105. data/test/unit/model_import_test.rb +71 -0
  106. data/test/unit/model_persistence_test.rb +528 -0
  107. data/test/unit/model_search_test.rb +913 -0
  108. data/test/unit/results_collection_test.rb +281 -0
  109. data/test/unit/results_item_test.rb +162 -0
  110. data/test/unit/rubyext_test.rb +66 -0
  111. data/test/unit/search_facet_test.rb +153 -0
  112. data/test/unit/search_filter_test.rb +42 -0
  113. data/test/unit/search_highlight_test.rb +46 -0
  114. data/test/unit/search_query_test.rb +301 -0
  115. data/test/unit/search_scan_test.rb +113 -0
  116. data/test/unit/search_script_field_test.rb +26 -0
  117. data/test/unit/search_sort_test.rb +50 -0
  118. data/test/unit/search_test.rb +499 -0
  119. data/test/unit/tire_test.rb +126 -0
  120. data/tire.gemspec +90 -0
  121. metadata +549 -0
@@ -0,0 +1,913 @@
1
+ require 'test_helper'
2
+
3
+ class ModelWithIndexCallbacks
4
+ extend ActiveModel::Naming
5
+ extend ActiveModel::Callbacks
6
+
7
+ include Tire::Model::Search
8
+ include Tire::Model::Callbacks
9
+
10
+ def destroyed?; false; end
11
+ def serializable_hash; {:one => 1}; end
12
+ end
13
+
14
+ module Tire
15
+ module Model
16
+
17
+ class SearchTest < Test::Unit::TestCase
18
+
19
+ context "Model::Search" do
20
+
21
+ setup do
22
+ @stub = stub('search') { stubs(:query).returns(self); stubs(:perform).returns(self); stubs(:results).returns([]) }
23
+ Tire::Index.any_instance.stubs(:exists?).returns(false)
24
+ end
25
+
26
+ teardown do
27
+ ActiveModelArticleWithCustomIndexName.index_name 'custom-index-name'
28
+ end
29
+
30
+ should "have the search method" do
31
+ assert_respond_to ActiveModelArticle, :search
32
+ end
33
+
34
+ should "have the callback methods for update index defined" do
35
+ assert_respond_to ::ModelWithIndexCallbacks, :before_update_elasticsearch_index
36
+ assert_respond_to ::ModelWithIndexCallbacks, :after_update_elasticsearch_index
37
+ end
38
+
39
+ should "limit searching in index for documents matching the model 'document_type'" do
40
+ Tire::Search::Search.
41
+ expects(:new).
42
+ with(ActiveModelArticle.index_name, { :type => ActiveModelArticle.document_type }).
43
+ returns(@stub).
44
+ twice
45
+
46
+ ActiveModelArticle.search 'foo'
47
+ ActiveModelArticle.search { query { string 'foo' } }
48
+ end
49
+
50
+ should "search in custom name" do
51
+ first = 'custom-index-name'
52
+ second = 'another-custom-index-name'
53
+ expected_options = { :type => ActiveModelArticleWithCustomIndexName.document_type }
54
+
55
+ Tire::Search::Search.expects(:new).with(first, expected_options).returns(@stub).twice
56
+ ActiveModelArticleWithCustomIndexName.index_name first
57
+ ActiveModelArticleWithCustomIndexName.search 'foo'
58
+ ActiveModelArticleWithCustomIndexName.search { query { string 'foo' } }
59
+
60
+ Tire::Search::Search.expects(:new).with(second, expected_options).returns(@stub).twice
61
+ ActiveModelArticleWithCustomIndexName.index_name second
62
+ ActiveModelArticleWithCustomIndexName.search 'foo'
63
+ ActiveModelArticleWithCustomIndexName.search { query { string 'foo' } }
64
+
65
+ Tire::Search::Search.expects(:new).with(first, expected_options).returns(@stub).twice
66
+ ActiveModelArticleWithCustomIndexName.index_name first
67
+ ActiveModelArticleWithCustomIndexName.search 'foo'
68
+ ActiveModelArticleWithCustomIndexName.search { query { string 'foo' } }
69
+ end
70
+
71
+ should "search in custom type" do
72
+ name = ActiveModelArticleWithCustomDocumentType.index_name
73
+ Tire::Search::Search.expects(:new).with(name, { :type => 'my_custom_type' }).returns(@stub).twice
74
+
75
+ ActiveModelArticleWithCustomDocumentType.search 'foo'
76
+ ActiveModelArticleWithCustomDocumentType.search { query { string 'foo' } }
77
+ end
78
+
79
+ should "allow to pass custom document type" do
80
+ Tire::Search::Search.
81
+ expects(:new).
82
+ with(ActiveModelArticle.index_name, { :type => 'custom_type' }).
83
+ returns(@stub).
84
+ twice
85
+
86
+ ActiveModelArticle.search 'foo', :type => 'custom_type'
87
+ ActiveModelArticle.search( :type => 'custom_type' ) { query { string 'foo' } }
88
+ end
89
+
90
+ should "allow to pass custom index name" do
91
+ Tire::Search::Search.
92
+ expects(:new).
93
+ with('custom_index', { :type => ActiveModelArticle.document_type }).
94
+ returns(@stub).
95
+ twice
96
+
97
+ ActiveModelArticle.search 'foo', :index => 'custom_index'
98
+ ActiveModelArticle.search( :index => 'custom_index' ) do
99
+ query { string 'foo' }
100
+ end
101
+ end
102
+
103
+ should "allow to refresh index" do
104
+ Index.any_instance.expects(:refresh)
105
+
106
+ ActiveModelArticle.index.refresh
107
+ end
108
+
109
+ should "wrap results in instances of the wrapper class" do
110
+ response = { 'hits' => { 'hits' => [{'_id' => 1, '_score' => 0.8, '_source' => { 'title' => 'Article' }}] } }
111
+ Configuration.client.expects(:get).returns(mock_response(response.to_json))
112
+
113
+ collection = ActiveModelArticle.search 'foo'
114
+ assert_instance_of Results::Collection, collection
115
+
116
+ document = collection.first
117
+
118
+ assert_instance_of Results::Item, document
119
+ assert_not_nil document._score
120
+ assert_equal 1, document.id
121
+ assert_equal 'Article', document.title
122
+ end
123
+
124
+ context "searching with a block" do
125
+ setup do
126
+ Tire::Search::Search.any_instance.expects(:perform).returns(@stub)
127
+ end
128
+
129
+ should "pass on whatever block it received" do
130
+ Tire::Search::Query.any_instance.expects(:string).with('foo').returns(@stub)
131
+
132
+ ActiveModelArticle.search { query { string 'foo' } }
133
+ end
134
+
135
+ should "allow to pass block with argument to query, allowing to use local variables from outer scope" do
136
+ Tire::Search::Query.any_instance.expects(:instance_eval).never
137
+ Tire::Search::Query.any_instance.expects(:string).with('foo').returns(@stub)
138
+
139
+ my_query = 'foo'
140
+ ActiveModelArticle.search do
141
+ query do |query|
142
+ query.string(my_query)
143
+ end
144
+ end
145
+ end
146
+
147
+ should "allow to pass :page and :per_page options" do
148
+ Tire::Search::Search.any_instance.expects(:size).with(10)
149
+ Tire::Search::Search.any_instance.expects(:from).with(20)
150
+
151
+ ActiveModelArticle.search :per_page => 10, :page => 3 do
152
+ query { string 'foo' }
153
+ end
154
+ end
155
+
156
+ should "allow to pass :version option" do
157
+ Tire::Search::Search.any_instance.expects(:version).with(true)
158
+
159
+ ActiveModelArticle.search :version => true do
160
+ query { all }
161
+ end
162
+ end
163
+
164
+ end
165
+
166
+ context "searching with query string" do
167
+
168
+ setup do
169
+ @q = 'foo AND bar'
170
+
171
+ Tire::Search::Query.any_instance.expects(:string).at_least_once.with(@q).returns(@stub)
172
+ Tire::Search::Search.any_instance.expects(:perform).at_least_once.returns(@stub)
173
+ end
174
+
175
+ should "search for query string" do
176
+ ActiveModelArticle.search @q
177
+ end
178
+
179
+ should "allow to pass :order option" do
180
+ Tire::Search::Sort.any_instance.expects(:by).with('title', nil)
181
+
182
+ ActiveModelArticle.search @q, :order => 'title'
183
+ end
184
+
185
+ should "allow to pass :sort option as :order option" do
186
+ Tire::Search::Sort.any_instance.expects(:by).with('title', nil)
187
+
188
+ ActiveModelArticle.search @q, :sort => 'title'
189
+ end
190
+
191
+ should "allow to specify sort direction" do
192
+ Tire::Search::Sort.any_instance.expects(:by).with('title', 'DESC')
193
+
194
+ ActiveModelArticle.search @q, :order => 'title DESC'
195
+ end
196
+
197
+ should "allow to specify more fields to sort on" do
198
+ Tire::Search::Sort.any_instance.expects(:by).with('title', 'DESC')
199
+ Tire::Search::Sort.any_instance.expects(:by).with('author.name', nil)
200
+
201
+ ActiveModelArticle.search @q, :order => ['title DESC', 'author.name']
202
+ end
203
+
204
+ should "allow to specify number of results per page" do
205
+ Tire::Search::Search.any_instance.expects(:size).with(20)
206
+
207
+ ActiveModelArticle.search @q, :per_page => 20
208
+ end
209
+
210
+ should "allow to specify first page in paginated results" do
211
+ Tire::Search::Search.any_instance.expects(:size).with(10)
212
+ Tire::Search::Search.any_instance.expects(:from).with(0)
213
+
214
+ ActiveModelArticle.search @q, :per_page => 10, :page => 1
215
+ end
216
+
217
+ should "allow to specify page further in paginated results" do
218
+ Tire::Search::Search.any_instance.expects(:size).with(10)
219
+ Tire::Search::Search.any_instance.expects(:from).with(20)
220
+
221
+ ActiveModelArticle.search @q, :per_page => 10, :page => 3
222
+ end
223
+
224
+ should "allow to limit returned fields" do
225
+ Tire::Search::Search.any_instance.expects(:fields).with(["id"])
226
+ ActiveModelArticle.search @q, :fields => 'id'
227
+
228
+ Tire::Search::Search.any_instance.expects(:fields).with(["id", "title"])
229
+ ActiveModelArticle.search @q, :fields => ['id', 'title']
230
+ end
231
+
232
+ should "allow to pass :version option" do
233
+ Tire::Search::Search.any_instance.expects(:version).with(true)
234
+
235
+ ActiveModelArticle.search @q, :version => true
236
+ end
237
+
238
+ end
239
+
240
+ should "not set callback when hooks are missing" do
241
+ @model = ActiveModelArticle.new
242
+ @model.expects(:update_elasticsearch_index).never
243
+
244
+ @model.save
245
+ end
246
+
247
+ should_eventually "not define destroyed? if class already implements it" do
248
+ load File.expand_path('../../models/active_model_article_with_callbacks.rb', __FILE__)
249
+
250
+ # TODO: Find a way how to break the old implementation:
251
+ # if base.respond_to?(:before_destroy) && !base.respond_to?(:destroyed?)
252
+ ActiveModelArticleWithCallbacks.expects(:class_eval).never
253
+ end
254
+
255
+ should "fire :after_save callbacks" do
256
+ @model = ActiveModelArticleWithCallbacks.new
257
+ @model.tire.expects(:update_index)
258
+
259
+ @model.save
260
+ end
261
+
262
+ should "fire :after_destroy callbacks" do
263
+ @model = ActiveModelArticleWithCallbacks.new
264
+ @model.tire.expects(:update_index)
265
+
266
+ @model.destroy
267
+ end
268
+
269
+ should "store the record in index on :update_elasticsearch_index when saved" do
270
+ @model = ActiveModelArticleWithCallbacks.new
271
+ Tire::Index.any_instance.expects(:store).returns({})
272
+
273
+ @model.save
274
+ end
275
+
276
+ should "remove the record from index on :update_elasticsearch_index when destroyed" do
277
+ @model = ActiveModelArticleWithCallbacks.new
278
+ i = mock('index') { expects(:remove) }
279
+ Tire::Index.expects(:new).with('active_model_article_with_callbacks').returns(i)
280
+
281
+ @model.destroy
282
+ end
283
+
284
+ context "with custom mapping" do
285
+
286
+ should "create the index with mapping" do
287
+ expected = {
288
+ :settings => {},
289
+ :mappings => { :model_with_custom_mapping => {
290
+ :properties => { :title => { :type => 'string', :analyzer => 'snowball', :boost => 10 } }
291
+ }}
292
+ }
293
+
294
+ Tire::Index.any_instance.expects(:create).with(expected)
295
+
296
+ class ::ModelWithCustomMapping
297
+ extend ActiveModel::Naming
298
+ extend ActiveModel::Callbacks
299
+
300
+ include Tire::Model::Search
301
+ include Tire::Model::Callbacks
302
+
303
+ mapping do
304
+ indexes :title, :type => 'string', :analyzer => 'snowball', :boost => 10
305
+ end
306
+
307
+ end
308
+
309
+ assert_equal 'snowball', ModelWithCustomMapping.mapping[:title][:analyzer]
310
+ end
311
+
312
+ should "create the index with proper mapping options" do
313
+ expected = {
314
+ :settings => {},
315
+ :mappings => {
316
+ :model_with_custom_mapping_and_options => {
317
+ :_source => { :compress => true },
318
+ :_all => { :enabled => false },
319
+ :properties => { :title => { :type => 'string', :analyzer => 'snowball', :boost => 10 } }
320
+ }
321
+ }
322
+ }
323
+
324
+ Tire::Index.any_instance.expects(:create).with(expected)
325
+
326
+ class ::ModelWithCustomMappingAndOptions
327
+ extend ActiveModel::Naming
328
+ extend ActiveModel::Callbacks
329
+
330
+ include Tire::Model::Search
331
+ include Tire::Model::Callbacks
332
+
333
+ mapping :_source => { :compress => true }, :_all => { :enabled => false } do
334
+ indexes :title, :type => 'string', :analyzer => 'snowball', :boost => 10
335
+ end
336
+
337
+ end
338
+
339
+ assert_equal 'snowball', ModelWithCustomMappingAndOptions.mapping[:title][:analyzer]
340
+ assert_equal true, ModelWithCustomMappingAndOptions.mapping_options[:_source][:compress]
341
+ assert_equal false, ModelWithCustomMappingAndOptions.mapping_options[:_all][:enabled]
342
+ end
343
+
344
+ should "not raise an error when defining mapping" do
345
+ Tire::Index.any_instance.unstub(:exists?)
346
+ Configuration.client.expects(:head).raises(Errno::ECONNREFUSED)
347
+
348
+ assert_nothing_raised do
349
+ class ::ModelWithCustomMapping
350
+ extend ActiveModel::Naming
351
+ extend ActiveModel::Callbacks
352
+
353
+ include Tire::Model::Search
354
+ include Tire::Model::Callbacks
355
+
356
+ mapping do
357
+ indexes :title, :type => 'string', :analyzer => 'snowball', :boost => 10
358
+ end
359
+
360
+ end
361
+ end
362
+ end
363
+
364
+ should "define mapping for nested properties with a block" do
365
+ expected = {
366
+ :settings => {},
367
+ :mappings => { :model_with_nested_mapping => {
368
+ :properties => {
369
+ :title => { :type => 'string' },
370
+ :author => {
371
+ :type => 'object',
372
+ :properties => {
373
+ :first_name => { :type => 'string' },
374
+ :last_name => { :type => 'string', :boost => 100 },
375
+ :posts => {
376
+ :type => 'object',
377
+ :properties => {
378
+ :title => { :type => 'string', :boost => 10 }
379
+ }
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ }}
386
+
387
+ Tire::Index.any_instance.expects(:create).with(expected)
388
+
389
+ class ::ModelWithNestedMapping
390
+ extend ActiveModel::Naming
391
+ extend ActiveModel::Callbacks
392
+
393
+ include Tire::Model::Search
394
+ include Tire::Model::Callbacks
395
+
396
+ mapping do
397
+ indexes :title, :type => 'string'
398
+ indexes :author do
399
+ indexes :first_name, :type => 'string'
400
+ indexes :last_name, :type => 'string', :boost => 100
401
+
402
+ indexes :posts do
403
+ indexes :title, :type => 'string', :boost => 10
404
+ end
405
+ end
406
+ end
407
+
408
+ end
409
+
410
+ assert_not_nil ModelWithNestedMapping.mapping[:author][:properties][:last_name]
411
+ assert_equal 100, ModelWithNestedMapping.mapping[:author][:properties][:last_name][:boost]
412
+ assert_equal 10, ModelWithNestedMapping.mapping[:author][:properties][:posts][:properties][:title][:boost]
413
+ end
414
+
415
+ should "define mapping for nested documents" do
416
+ class ::ModelWithNestedDocuments
417
+ extend ActiveModel::Naming
418
+ extend ActiveModel::Callbacks
419
+
420
+ include Tire::Model::Search
421
+ include Tire::Model::Callbacks
422
+
423
+ mapping do
424
+ indexes :comments, :type => 'nested', :include_in_parent => true do
425
+ indexes :author_name
426
+ indexes :body, :boost => 100
427
+ end
428
+ end
429
+
430
+ end
431
+
432
+ assert_equal 'nested', ModelWithNestedDocuments.mapping[:comments][:type]
433
+ assert_not_nil ModelWithNestedDocuments.mapping[:comments][:properties][:author_name]
434
+ assert_equal 100, ModelWithNestedDocuments.mapping[:comments][:properties][:body][:boost]
435
+ end
436
+
437
+ end
438
+
439
+ context "with settings" do
440
+
441
+ should "create the index with settings and mappings" do
442
+ expected_settings = {
443
+ :settings => { :number_of_shards => 1, :number_of_replicas => 1 }
444
+ }
445
+
446
+ Tire::Index.any_instance.expects(:create).with do |expected|
447
+ expected[:settings][:number_of_shards] == 1 &&
448
+ expected[:mappings].size > 0
449
+ end
450
+
451
+ class ::ModelWithCustomSettings
452
+ extend ActiveModel::Naming
453
+ extend ActiveModel::Callbacks
454
+
455
+ include Tire::Model::Search
456
+ include Tire::Model::Callbacks
457
+
458
+ settings :number_of_shards => 1, :number_of_replicas => 1 do
459
+ mapping do
460
+ indexes :title, :type => 'string'
461
+ end
462
+ end
463
+
464
+ end
465
+
466
+ assert_instance_of Hash, ModelWithCustomSettings.settings
467
+ assert_equal 1, ModelWithCustomSettings.settings[:number_of_shards]
468
+ end
469
+
470
+ end
471
+
472
+ context "with index update callbacks" do
473
+ setup do
474
+ class ::ModelWithIndexCallbacks
475
+ _update_elasticsearch_index_callbacks.clear
476
+ def notify; end
477
+ end
478
+
479
+ response = { 'ok' => true,
480
+ '_id' => 1,
481
+ 'matches' => ['foo'] }
482
+ Configuration.client.expects(:post).returns(mock_response(response.to_json))
483
+ end
484
+
485
+ should "run the callback defined as block" do
486
+ class ::ModelWithIndexCallbacks
487
+ after_update_elasticsearch_index { self.go! }
488
+ end
489
+
490
+ @model = ::ModelWithIndexCallbacks.new
491
+ @model.expects(:go!)
492
+
493
+ @model.update_elasticsearch_index
494
+ end
495
+
496
+ should "run the callback defined as symbol" do
497
+ class ::ModelWithIndexCallbacks
498
+ after_update_elasticsearch_index :notify
499
+
500
+ def notify; self.go!; end
501
+ end
502
+
503
+ @model = ::ModelWithIndexCallbacks.new
504
+ @model.expects(:go!)
505
+
506
+ @model.update_elasticsearch_index
507
+ end
508
+
509
+ should "set the 'matches' property from percolated response" do
510
+ @model = ::ModelWithIndexCallbacks.new
511
+ @model.update_elasticsearch_index
512
+
513
+ assert_equal ['foo'], @model.matches
514
+ end
515
+
516
+ end
517
+
518
+ context "serialization" do
519
+ setup { Tire::Index.any_instance.stubs(:create).returns(true) }
520
+
521
+ should "have to_hash" do
522
+ assert_equal( {'title' => 'Test'}, ActiveModelArticle.new( 'title' => 'Test' ).to_hash )
523
+ end
524
+
525
+ should "not redefine to_hash if already defined" do
526
+ class ::ActiveModelArticleWithToHash < ActiveModelArticle
527
+ def to_hash; { :foo => 'bar' }; end
528
+ end
529
+ assert_equal 'bar', ::ActiveModelArticleWithToHash.new(:title => 'Test').to_hash[:foo]
530
+
531
+ class ::ActiveModelArticleWithToHashFromSuperclass < Hash
532
+ include Tire::Model::Search
533
+ include Tire::Model::Callbacks
534
+ end
535
+ assert_equal( {}, ::ActiveModelArticleWithToHashFromSuperclass.new(:title => 'Test').to_hash)
536
+ end
537
+
538
+ should "serialize itself into JSON without 'root'" do
539
+ @model = ActiveModelArticle.new 'title' => 'Test'
540
+ assert_equal({'title' => 'Test'}.to_json, @model.to_indexed_json)
541
+ end
542
+
543
+ should "not include the ID property in serialized document (_source)" do
544
+ @model = ActiveModelArticle.new 'id' => 1, 'title' => 'Test'
545
+ assert_nil MultiJson.decode(@model.to_indexed_json)[:id]
546
+ assert_nil MultiJson.decode(@model.to_indexed_json)['id']
547
+ end
548
+
549
+ should "not include the type property in serialized document (_source)" do
550
+ @model = ActiveModelArticle.new 'type' => 'foo', 'title' => 'Test'
551
+ assert_nil MultiJson.decode(@model.to_indexed_json)[:type]
552
+ assert_nil MultiJson.decode(@model.to_indexed_json)['type']
553
+ end
554
+
555
+ should "serialize itself with serializable_hash when no mapping is set" do
556
+
557
+ class ::ModelWithoutMapping
558
+ extend ActiveModel::Naming
559
+ extend ActiveModel::Callbacks
560
+ include ActiveModel::Serialization
561
+ include Tire::Model::Search
562
+ include Tire::Model::Callbacks
563
+
564
+ # Do NOT configure any mapping
565
+
566
+ attr_reader :attributes
567
+
568
+ def initialize(attributes = {}); @attributes = attributes; end
569
+
570
+ def method_missing(name, *args, &block)
571
+ attributes[name.to_sym] || attributes[name.to_s] || super
572
+ end
573
+ end
574
+
575
+ model = ::ModelWithoutMapping.new :one => 1, :two => 2
576
+ assert_equal( {:one => 1, :two => 2}, model.serializable_hash )
577
+
578
+ # Bot properties are returned
579
+ assert_equal( {:one => 1, :two => 2}.to_json, model.to_indexed_json )
580
+ end
581
+
582
+ should "serialize only mapped properties when mapping is set" do
583
+
584
+ class ::ModelWithMapping
585
+ extend ActiveModel::Naming
586
+ extend ActiveModel::Callbacks
587
+ include ActiveModel::Serialization
588
+ include Tire::Model::Search
589
+ include Tire::Model::Callbacks
590
+
591
+ mapping do
592
+ # ONLY index the 'one' attribute
593
+ indexes :one, :type => 'string', :analyzer => 'keyword'
594
+ end
595
+
596
+ attr_reader :attributes
597
+
598
+ def initialize(attributes = {}); @attributes = attributes; end
599
+
600
+ def method_missing(name, *args, &block)
601
+ attributes[name.to_sym] || attributes[name.to_s] || super
602
+ end
603
+ end
604
+
605
+ model = ::ModelWithMapping.new :one => 1, :two => 2
606
+ assert_equal( {:one => 1, :two => 2}, model.serializable_hash )
607
+
608
+ # Only the mapped property is returned
609
+ assert_equal( {:one => 1}.to_json, model.to_indexed_json )
610
+
611
+ end
612
+
613
+ should "serialize mapped properties when mapping procs are set" do
614
+ class ::ModelWithMappingProcs
615
+ extend ActiveModel::Naming
616
+ extend ActiveModel::Callbacks
617
+ include ActiveModel::Serialization
618
+ include Tire::Model::Search
619
+ include Tire::Model::Callbacks
620
+
621
+ mapping do
622
+ indexes :one, :type => 'string', :analyzer => 'keyword'
623
+ indexes :two, :type => 'string', :analyzer => 'keyword', :as => proc { one * 2 }
624
+ indexes :three, :type => 'string', :analyzer => 'keyword', :as => 'one + 2'
625
+ end
626
+
627
+ attr_reader :attributes
628
+
629
+ def initialize(attributes = {}); @attributes = attributes; end
630
+
631
+ def method_missing(name, *args, &block)
632
+ attributes[name.to_sym] || attributes[name.to_s] || super
633
+ end
634
+ end
635
+
636
+ model = ::ModelWithMappingProcs.new :one => 1, :two => 1, :three => 1
637
+ hash = model.serializable_hash
638
+ document = MultiJson.decode(model.to_indexed_json)
639
+
640
+ assert_equal 1, hash[:one]
641
+ assert_equal 1, hash[:two]
642
+ assert_equal 1, hash[:three]
643
+
644
+ assert_equal 1, document['one']
645
+ assert_equal 2, document['two']
646
+ assert_equal 3, document['three']
647
+ end
648
+
649
+ end
650
+
651
+ context "with percolation" do
652
+ setup do
653
+ class ::ActiveModelArticleWithCallbacks; percolate!(false); end
654
+ @article = ::ActiveModelArticleWithCallbacks.new :title => 'Test'
655
+ end
656
+
657
+ should "return matching queries on percolate" do
658
+ Tire::Index.any_instance.expects(:percolate).returns(["alert"])
659
+
660
+ assert_equal ['alert'], @article.percolate
661
+ end
662
+
663
+ should "pass the arguments to percolate" do
664
+ filter = lambda { string 'tag:alerts' }
665
+
666
+ Tire::Index.any_instance.expects(:percolate).with do |doc,query|
667
+ # p [doc,query]
668
+ doc == @article &&
669
+ query == filter
670
+ end.returns(["alert"])
671
+
672
+ assert_equal ['alert'], @article.percolate(&filter)
673
+ end
674
+
675
+ should "mark the instance for percolation on index update" do
676
+ @article.percolate = true
677
+
678
+ Tire::Index.any_instance.expects(:store).with do |doc,options|
679
+ # p [doc,options]
680
+ options[:percolate] == true
681
+ end.returns(MultiJson.decode('{"ok":true,"_id":"test","matches":["alerts"]}'))
682
+
683
+ @article.update_elasticsearch_index
684
+ end
685
+
686
+ should "not percolate document on index update when not set for percolation" do
687
+ Tire::Index.any_instance.expects(:store).with do |doc,options|
688
+ # p [doc,options]
689
+ options[:percolate] == nil
690
+ end.returns(MultiJson.decode('{"ok":true,"_id":"test"}'))
691
+
692
+ @article.update_elasticsearch_index
693
+ end
694
+
695
+ should "set the default percolator pattern" do
696
+ class ::ActiveModelArticleWithPercolation < ::ActiveModelArticleWithCallbacks
697
+ percolate!
698
+ end
699
+
700
+ assert_equal true, ::ActiveModelArticleWithCallbacks.percolator
701
+ end
702
+
703
+ should "set the percolator pattern" do
704
+ class ::ActiveModelArticleWithPercolation < ::ActiveModelArticleWithCallbacks
705
+ percolate! 'tags:alert'
706
+ end
707
+
708
+ assert_equal 'tags:alert', ::ActiveModelArticleWithCallbacks.percolator
709
+ end
710
+
711
+ should "mark the class for percolation on index update" do
712
+ class ::ActiveModelArticleWithPercolation < ::ActiveModelArticleWithCallbacks
713
+ percolate!
714
+ end
715
+
716
+ Tire::Index.any_instance.expects(:store).with do |doc,options|
717
+ # p [doc,options]
718
+ options[:percolate] == true
719
+ end.returns(MultiJson.decode('{"ok":true,"_id":"test","matches":["alerts"]}'))
720
+
721
+ percolated = ActiveModelArticleWithPercolation.new :title => 'Percolate me!'
722
+ percolated.update_elasticsearch_index
723
+ end
724
+
725
+ should "execute the 'on_percolate' callback" do
726
+ $test__matches = nil
727
+ class ::ActiveModelArticleWithPercolation < ::ActiveModelArticleWithCallbacks
728
+ on_percolate { $test__matches = matches }
729
+ end
730
+ percolated = ActiveModelArticleWithPercolation.new :title => 'Percolate me!'
731
+
732
+ Tire::Index.any_instance.expects(:store).
733
+ with do |doc,options|
734
+ doc == percolated &&
735
+ options[:percolate] == true
736
+ end.
737
+ returns(MultiJson.decode('{"ok":true,"_id":"test","matches":["alerts"]}'))
738
+
739
+ percolated.update_elasticsearch_index
740
+
741
+ assert_equal ['alerts'], $test__matches
742
+ end
743
+
744
+ should "execute the 'on_percolate' callback for specific pattern" do
745
+ $test__matches = nil
746
+ class ::ActiveModelArticleWithPercolation < ::ActiveModelArticleWithCallbacks
747
+ on_percolate('tags:alert') { $test__matches = self.matches }
748
+ end
749
+ percolated = ActiveModelArticleWithPercolation.new :title => 'Percolate me!'
750
+
751
+ Tire::Index.any_instance.expects(:store).
752
+ with do |doc,options|
753
+ doc == percolated &&
754
+ options[:percolate] == 'tags:alert'
755
+ end.
756
+ returns(MultiJson.decode('{"ok":true,"_id":"test","matches":["alerts"]}'))
757
+
758
+ percolated.update_elasticsearch_index
759
+
760
+ assert_equal ['alerts'], $test__matches
761
+ end
762
+
763
+ end
764
+
765
+ context "proxy" do
766
+
767
+ should "have the proxy to class methods" do
768
+ assert_respond_to ActiveModelArticle, :tire
769
+ assert_instance_of Tire::Model::Search::ClassMethodsProxy, ActiveModelArticle.tire
770
+ end
771
+
772
+ should "have the proxy to instance methods" do
773
+ assert_respond_to ActiveModelArticle.new, :tire
774
+ assert_instance_of Tire::Model::Search::InstanceMethodsProxy, ActiveModelArticle.new.tire
775
+ end
776
+
777
+ should "NOT overload existing top-level class methods" do
778
+ assert_equal "THIS IS MY MAPPING!", ActiveRecordClassWithTireMethods.mapping
779
+ assert_equal 'snowball', ActiveRecordClassWithTireMethods.tire.mapping[:title][:analyzer]
780
+ end
781
+
782
+ should "NOT overload existing top-level instance methods" do
783
+ ActiveRecordClassWithTireMethods.stubs(:columns).returns([])
784
+ ActiveRecordClassWithTireMethods.stubs(:column_defaults).returns({})
785
+ assert_equal "THIS IS MY INDEX!", ActiveRecordClassWithTireMethods.new.index
786
+ assert_equal 'active_record_class_with_tire_methods',
787
+ ActiveRecordClassWithTireMethods.new.tire.index.name
788
+ end
789
+
790
+ end
791
+
792
+ context "with index prefix" do
793
+ class ::ModelWithoutPrefix
794
+ extend ActiveModel::Naming
795
+ extend ActiveModel::Callbacks
796
+
797
+ include Tire::Model::Search
798
+ include Tire::Model::Callbacks
799
+ end
800
+ class ::ModelWithPrefix
801
+ extend ActiveModel::Naming
802
+ extend ActiveModel::Callbacks
803
+
804
+ include Tire::Model::Search
805
+ include Tire::Model::Callbacks
806
+
807
+ tire.index_prefix 'custom_prefix'
808
+ end
809
+
810
+ class ::OtherModelWithPrefix
811
+ extend ActiveModel::Naming
812
+ extend ActiveModel::Callbacks
813
+
814
+ include Tire::Model::Search
815
+ include Tire::Model::Callbacks
816
+
817
+ index_prefix 'other_custom_prefix'
818
+ end
819
+
820
+ teardown do
821
+ # FIXME: Depends on the interface itself
822
+ Model::Search.index_prefix nil
823
+ end
824
+
825
+ should "return nil by default" do
826
+ assert_nil Model::Search.index_prefix
827
+ end
828
+
829
+ should "allow to set and retrieve the value" do
830
+ assert_nothing_raised { Model::Search.index_prefix 'app_environment' }
831
+ assert_equal 'app_environment', Model::Search.index_prefix
832
+ end
833
+
834
+ should "allow to reset the value" do
835
+ Model::Search.index_prefix 'prefix'
836
+ Model::Search.index_prefix nil
837
+ assert_nil Model::Search.index_prefix
838
+ end
839
+
840
+ should "not add any prefix by default" do
841
+ assert_equal 'model_without_prefixes', ModelWithoutPrefix.index_name
842
+ end
843
+
844
+ should "add general and custom prefixes to model index names" do
845
+ Model::Search.index_prefix 'general_prefix'
846
+ assert_equal 'general_prefix_model_without_prefixes', ModelWithoutPrefix.index_name
847
+ assert_equal 'custom_prefix_model_with_prefixes', ModelWithPrefix.index_name
848
+ assert_equal 'other_custom_prefix_other_model_with_prefixes', OtherModelWithPrefix.index_name
849
+ end
850
+
851
+ end
852
+
853
+ context "with dynamic index name" do
854
+ class ::ModelWithDynamicIndexName
855
+ extend ActiveModel::Naming
856
+ extend ActiveModel::Callbacks
857
+
858
+ include Tire::Model::Search
859
+ include Tire::Model::Callbacks
860
+
861
+ index_name do
862
+ "dynamic" + '_' + "index"
863
+ end
864
+ end
865
+
866
+ should "have index name as a proc" do
867
+ assert_kind_of Proc, ::ModelWithDynamicIndexName.index_name
868
+ end
869
+
870
+ should "evaluate the proc in Model.index" do
871
+ assert_equal 'dynamic_index', ::ModelWithDynamicIndexName.index.name
872
+ end
873
+
874
+ end
875
+
876
+ end
877
+
878
+ context "Results::Item" do
879
+
880
+ setup do
881
+ module ::Rails
882
+ end
883
+
884
+ class ::FakeRailsModel
885
+ extend ActiveModel::Naming
886
+ include ActiveModel::Conversion
887
+ def self.find(*args); new; end
888
+ end
889
+
890
+ @document = Results::Item.new :id => 1, :_type => 'fake_rails_model', :title => 'Test'
891
+ end
892
+
893
+ should "load the 'real' instance from the corresponding model" do
894
+ assert_respond_to @document, :load
895
+ assert_instance_of FakeRailsModel, @document.load
896
+ end
897
+
898
+ should "pass the ID to the corresponding model's find method" do
899
+ FakeRailsModel.expects(:find).with(1).returns(FakeRailsModel.new)
900
+ @document.load
901
+ end
902
+
903
+ should "pass the options to the corresponding model's find method" do
904
+ FakeRailsModel.expects(:find).with(1, {:include => 'everything'}).returns(FakeRailsModel.new)
905
+ @document.load :include => 'everything'
906
+ end
907
+
908
+ end
909
+
910
+ end
911
+
912
+ end
913
+ end