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