ssickles-tire 0.4.2.7 → 0.4.3

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