tire-erez 0.5.4

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