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.
- data/lib/tire.rb +18 -3
- data/lib/tire/alias.rb +11 -35
- data/lib/tire/index.rb +34 -76
- data/lib/tire/model/callbacks.rb +40 -0
- data/lib/tire/model/import.rb +26 -0
- data/lib/tire/model/indexing.rb +128 -0
- data/lib/tire/model/naming.rb +100 -0
- data/lib/tire/model/percolate.rb +99 -0
- data/lib/tire/model/persistence.rb +72 -0
- data/lib/tire/model/persistence/attributes.rb +143 -0
- data/lib/tire/model/persistence/finders.rb +66 -0
- data/lib/tire/model/persistence/storage.rb +71 -0
- data/lib/tire/model/search.rb +305 -0
- data/lib/tire/results/collection.rb +38 -13
- data/lib/tire/results/item.rb +19 -0
- data/lib/tire/rubyext/hash.rb +8 -0
- data/lib/tire/rubyext/ruby_1_8.rb +54 -0
- data/lib/tire/rubyext/symbol.rb +11 -0
- data/lib/tire/search.rb +7 -8
- data/lib/tire/search/scan.rb +8 -8
- data/lib/tire/search/sort.rb +1 -1
- data/lib/tire/utils.rb +17 -0
- data/lib/tire/version.rb +7 -38
- data/test/integration/active_model_indexing_test.rb +51 -0
- data/test/integration/active_model_searchable_test.rb +114 -0
- data/test/integration/active_record_searchable_test.rb +446 -0
- data/test/integration/mongoid_searchable_test.rb +309 -0
- data/test/integration/persistent_model_test.rb +117 -0
- data/test/integration/reindex_test.rb +2 -2
- data/test/integration/scan_test.rb +1 -1
- data/test/models/active_model_article.rb +31 -0
- data/test/models/active_model_article_with_callbacks.rb +49 -0
- data/test/models/active_model_article_with_custom_document_type.rb +7 -0
- data/test/models/active_model_article_with_custom_index_name.rb +7 -0
- data/test/models/active_record_models.rb +122 -0
- data/test/models/mongoid_models.rb +97 -0
- data/test/models/persistent_article.rb +11 -0
- data/test/models/persistent_article_in_namespace.rb +12 -0
- data/test/models/persistent_article_with_casting.rb +28 -0
- data/test/models/persistent_article_with_defaults.rb +11 -0
- data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
- data/test/models/supermodel_article.rb +17 -0
- data/test/models/validated_model.rb +11 -0
- data/test/test_helper.rb +27 -3
- data/test/unit/active_model_lint_test.rb +17 -0
- data/test/unit/index_alias_test.rb +3 -17
- data/test/unit/index_test.rb +30 -18
- data/test/unit/model_callbacks_test.rb +116 -0
- data/test/unit/model_import_test.rb +71 -0
- data/test/unit/model_persistence_test.rb +516 -0
- data/test/unit/model_search_test.rb +899 -0
- data/test/unit/results_collection_test.rb +60 -0
- data/test/unit/results_item_test.rb +37 -0
- data/test/unit/rubyext_test.rb +3 -3
- data/test/unit/search_test.rb +1 -6
- data/test/unit/tire_test.rb +15 -0
- data/tire.gemspec +30 -13
- metadata +153 -41
- 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
|