elastic_searchable 1.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,558 @@
1
+ require File.join(File.dirname(__FILE__), 'helper')
2
+
3
+ class TestElasticSearchable < Test::Unit::TestCase
4
+ def setup
5
+ delete_index
6
+ ElasticSearchable.index_settings = {'number_of_replicas' => 0, 'number_of_shards' => 1}
7
+ # ElasticSearchable.debug_output
8
+ end
9
+
10
+ context 'non elastic activerecord class' do
11
+ class Parent < ActiveRecord::Base
12
+ end
13
+ setup do
14
+ @clazz = Parent
15
+ end
16
+ should 'not respond to elastic_options' do
17
+ assert !@clazz.respond_to?(:elastic_options)
18
+ end
19
+ end
20
+ context 'instance of non-elastic_searchable activerecord class' do
21
+ class Parent < ActiveRecord::Base
22
+ end
23
+ setup do
24
+ @instance = Parent.new
25
+ end
26
+ should 'not respond to percolations' do
27
+ assert !@instance.respond_to?(:percolations)
28
+ end
29
+ end
30
+
31
+ class Post < ActiveRecord::Base
32
+ elastic_searchable
33
+ after_index :indexed
34
+ after_index :indexed_on_create, :on => :create
35
+ after_index :indexed_on_update, :on => :update
36
+ def indexed
37
+ @indexed = true
38
+ end
39
+ def indexed?
40
+ @indexed
41
+ end
42
+ def indexed_on_create
43
+ @indexed_on_create = true
44
+ end
45
+ def indexed_on_create?
46
+ @indexed_on_create
47
+ end
48
+ def indexed_on_update
49
+ @indexed_on_update = true
50
+ end
51
+ def indexed_on_update?
52
+ @indexed_on_update
53
+ end
54
+ end
55
+ context 'activerecord class with default elastic_searchable config' do
56
+ setup do
57
+ @clazz = Post
58
+ end
59
+ should 'respond to :search' do
60
+ assert @clazz.respond_to?(:search)
61
+ end
62
+ should 'define elastic_options' do
63
+ assert @clazz.elastic_options
64
+ end
65
+ should 'respond to :percolations' do
66
+ assert @clazz.new.respond_to?(:percolations)
67
+ assert_equal [], @clazz.new.percolations
68
+ end
69
+ end
70
+
71
+ context 'Model.request with invalid url' do
72
+ should 'raise error' do
73
+ assert_raises ElasticSearchable::ElasticError do
74
+ ElasticSearchable.request :get, '/elastic_searchable/foobar/notfound'
75
+ end
76
+ end
77
+ end
78
+
79
+ context 'ElasticSearchable.create_index' do
80
+ setup do
81
+ ElasticSearchable.create_index
82
+ ElasticSearchable.refresh_index
83
+ @status = ElasticSearchable.request :get, '/elastic_searchable/_status'
84
+ end
85
+ should 'have created index' do
86
+ assert @status['ok']
87
+ end
88
+ end
89
+
90
+ context 'Model.create' do
91
+ setup do
92
+ @post = Post.create :title => 'foo', :body => "bar"
93
+ end
94
+ should 'have fired after_index callback' do
95
+ assert @post.indexed?
96
+ end
97
+ should 'have fired after_index_on_create callback' do
98
+ assert @post.indexed_on_create?
99
+ end
100
+ should 'not have fired after_index_on_update callback' do
101
+ assert !@post.indexed_on_update?
102
+ end
103
+ end
104
+
105
+ context 'Model.update' do
106
+ setup do
107
+ Post.create :title => 'foo', :body => 'bar'
108
+ @post = Post.last
109
+ @post.title = 'baz'
110
+ @post.save
111
+ end
112
+ should 'have fired after_index callback' do
113
+ assert @post.indexed?
114
+ end
115
+ should 'not have fired after_index_on_create callback' do
116
+ assert !@post.indexed_on_create?
117
+ end
118
+ should 'have fired after_index_on_update callback' do
119
+ assert @post.indexed_on_update?
120
+ end
121
+ end
122
+
123
+ context 'Model.create within ElasticSearchable.offline block' do
124
+ setup do
125
+ ElasticSearchable.offline do
126
+ @post = Post.create :title => 'foo', :body => "bar"
127
+ end
128
+ end
129
+ should 'not have fired after_index callback' do
130
+ assert !@post.indexed?
131
+ end
132
+ should 'not have fired after_index_on_create callback' do
133
+ assert !@post.indexed_on_create?
134
+ end
135
+ should 'not have fired after_index_on_update callback' do
136
+ assert !@post.indexed_on_update?
137
+ end
138
+ end
139
+
140
+ context 'with empty index when multiple database records' do
141
+ setup do
142
+ Post.delete_all
143
+ ElasticSearchable.create_index
144
+ @first_post = Post.create :title => 'foo', :body => "first bar"
145
+ @second_post = Post.create :title => 'foo', :body => "second bar"
146
+ ElasticSearchable.delete_index
147
+ ElasticSearchable.create_index
148
+ end
149
+ should 'not raise error if error occurs reindexing model' do
150
+ ElasticSearchable.expects(:request).raises(ElasticSearchable::ElasticError.new('faux error'))
151
+ assert_nothing_raised do
152
+ Post.reindex
153
+ end
154
+ end
155
+ should 'not raise error if destroying one instance' do
156
+ Logger.any_instance.expects(:warn)
157
+ assert_nothing_raised do
158
+ @first_post.destroy
159
+ end
160
+ end
161
+ context 'Model.reindex' do
162
+ setup do
163
+ Post.reindex :per_page => 1, :scope => Post.scoped(:order => 'body desc')
164
+ ElasticSearchable.refresh_index
165
+ end
166
+ should 'have reindexed both records' do
167
+ assert_nothing_raised do
168
+ ElasticSearchable.request :get, "/elastic_searchable/posts/#{@first_post.id}"
169
+ ElasticSearchable.request :get, "/elastic_searchable/posts/#{@second_post.id}"
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ context 'with index containing multiple results' do
176
+ setup do
177
+ ElasticSearchable.create_index
178
+ @first_post = Post.create :title => 'foo', :body => "first bar"
179
+ @second_post = Post.create :title => 'foo', :body => "second bar"
180
+ ElasticSearchable.refresh_index
181
+ end
182
+
183
+ context 'searching on a term that returns one result' do
184
+ setup do
185
+ @results = Post.search 'first'
186
+ end
187
+ should 'find created object' do
188
+ assert_contains @results, @first_post
189
+ end
190
+ should 'be paginated' do
191
+ assert_equal 1, @results.current_page
192
+ assert_equal Post.per_page, @results.per_page
193
+ assert_nil @results.previous_page
194
+ assert_nil @results.next_page
195
+ end
196
+ should 'have populated hit' do
197
+ assert_equal @results.first.hit['_id'], @first_post.id.to_s
198
+ end
199
+ end
200
+ context 'searching on a term that returns multiple results' do
201
+ setup do
202
+ @results = Post.search 'foo'
203
+ end
204
+ should 'have populated hit on each record with the correct hit json' do
205
+ assert_equal @results.first.hit['_id'], @first_post.id.to_s
206
+ assert_equal @results.last.hit['_id'], @second_post.id.to_s
207
+ end
208
+ end
209
+
210
+ context 'searching for results using a query Hash' do
211
+ setup do
212
+ @results = Post.search({
213
+ :filtered => {
214
+ :query => {
215
+ :term => {:title => 'foo'},
216
+ },
217
+ :filter => {
218
+ :or => [
219
+ {:term => {:body => 'second'}},
220
+ {:term => {:body => 'third'}}
221
+ ]
222
+ }
223
+ }
224
+ })
225
+ end
226
+ should 'find only the object which ' do
227
+ assert_does_not_contain @results, @first_post
228
+ assert_contains @results, @second_post
229
+ end
230
+ end
231
+
232
+ context 'when per_page is a string' do
233
+ setup do
234
+ @results = Post.search 'foo', :per_page => 1.to_s, :sort => 'id'
235
+ end
236
+ should 'find first object' do
237
+ assert_contains @results, @first_post
238
+ end
239
+ end
240
+
241
+ context 'searching for second page using will_paginate params' do
242
+ setup do
243
+ @results = Post.search 'foo', :page => 2, :per_page => 1, :sort => 'id'
244
+ end
245
+ should 'not find objects from first page' do
246
+ assert_does_not_contain @results, @first_post
247
+ end
248
+ should 'find second object' do
249
+ assert_contains @results, @second_post
250
+ end
251
+ should 'be paginated' do
252
+ assert_equal 2, @results.current_page
253
+ assert_equal 1, @results.per_page
254
+ assert_equal 1, @results.previous_page
255
+ assert_nil @results.next_page
256
+ end
257
+ end
258
+
259
+ context 'sorting search results' do
260
+ setup do
261
+ @results = Post.search 'foo', :sort => 'id:desc'
262
+ end
263
+ should 'sort results correctly' do
264
+ assert_equal @second_post, @results.first
265
+ assert_equal @first_post, @results.last
266
+ end
267
+ end
268
+
269
+ context 'advanced sort options' do
270
+ setup do
271
+ @results = Post.search 'foo', :sort => [{:id => 'desc'}]
272
+ end
273
+ should 'sort results correctly' do
274
+ assert_equal @second_post, @results.first
275
+ assert_equal @first_post, @results.last
276
+ end
277
+ end
278
+
279
+ context 'destroying one object' do
280
+ setup do
281
+ @first_post.destroy
282
+ ElasticSearchable.refresh_index
283
+ end
284
+ should 'be removed from the index' do
285
+ @request = ElasticSearchable.get "/elastic_searchable/posts/#{@first_post.id}"
286
+ assert @request.response.is_a?(Net::HTTPNotFound), @request.inspect
287
+ end
288
+ end
289
+ end
290
+
291
+
292
+ class Blog < ActiveRecord::Base
293
+ elastic_searchable :if => proc {|b| b.should_index? }
294
+ def should_index?
295
+ false
296
+ end
297
+ end
298
+ context 'activerecord class with optional :if=>proc configuration' do
299
+ context 'when creating new instance' do
300
+ setup do
301
+ Blog.any_instance.expects(:reindex).never
302
+ @blog = Blog.create! :title => 'foo'
303
+ end
304
+ should 'not index record' do end #see expectations
305
+ should 'not be found in elasticsearch' do
306
+ @request = ElasticSearchable.get "/elastic_searchable/blogs/#{@blog.id}"
307
+ assert @request.response.is_a?(Net::HTTPNotFound), @request.inspect
308
+ end
309
+ end
310
+ end
311
+
312
+ class Friend < ActiveRecord::Base
313
+ belongs_to :book
314
+ elastic_searchable :json => {:include => {:book => {:only => :title}}, :only => :name}
315
+ end
316
+ context 'activerecord class with optional :json config' do
317
+ context 'creating index' do
318
+ setup do
319
+ ElasticSearchable.create_index
320
+ @book = Book.create! :isbn => '123abc', :title => 'another world'
321
+ @friend = Friend.new :name => 'bob', :favorite_color => 'red'
322
+ @friend.book = @book
323
+ @friend.save!
324
+ ElasticSearchable.refresh_index
325
+ end
326
+ should 'index json with configuration' do
327
+ @response = ElasticSearchable.request :get, "/elastic_searchable/friends/#{@friend.id}"
328
+ # should not index:
329
+ # friend.favorite_color
330
+ # book.isbn
331
+ expected = {
332
+ "name" => 'bob',
333
+ 'book' => {'title' => 'another world'}
334
+ }
335
+ assert_equal expected, @response['_source'], @response.inspect
336
+ end
337
+ end
338
+ end
339
+
340
+ context '.index_name' do
341
+ setup do
342
+ @orig_index_name = ElasticSearchable.index_name
343
+ ElasticSearchable.index_name = 'my_new_index'
344
+ end
345
+ teardown do
346
+ ElasticSearchable.index_name = @orig_index_name
347
+ end
348
+ should 'change default index' do
349
+ assert_equal 'my_new_index', ElasticSearchable.index_name
350
+ end
351
+ end
352
+
353
+ class Book < ActiveRecord::Base
354
+ elastic_searchable
355
+ after_percolate :on_percolated
356
+ def on_percolated
357
+ @percolated = percolations
358
+ end
359
+ def percolated
360
+ @percolated
361
+ end
362
+ end
363
+ context 'Book class with after_percolate callback' do
364
+ context 'with created index' do
365
+ setup do
366
+ ElasticSearchable.create_index
367
+ end
368
+ context "when index has configured percolation" do
369
+ setup do
370
+ ElasticSearchable.request :put, '/_percolator/elastic_searchable/myfilter', :json_body => {:query => {:query_string => {:query => 'foo' }}}
371
+ ElasticSearchable.request :post, '/_percolator/_refresh'
372
+ end
373
+ context 'creating an object that does not match the percolation' do
374
+ setup do
375
+ Book.any_instance.expects(:on_percolated).never
376
+ @book = Book.create! :title => 'bar'
377
+ end
378
+ should 'not percolate the record' do end #see expectations
379
+ end
380
+ context 'creating an object that matches the percolation' do
381
+ setup do
382
+ @book = Book.create :title => "foo"
383
+ end
384
+ should 'return percolated matches in the callback' do
385
+ assert_equal ['myfilter'], @book.percolated
386
+ end
387
+ end
388
+ context 'percolating a non-persisted object' do
389
+ setup do
390
+ @matches = Book.new(:title => 'foo').percolate
391
+ end
392
+ should 'return percolated matches' do
393
+ assert_equal ['myfilter'], @matches
394
+ end
395
+ end
396
+ context "with multiple percolators" do
397
+ setup do
398
+ ElasticSearchable.request :put, '/_percolator/elastic_searchable/greenfilter', :json_body => { :color => 'green', :query => {:query_string => {:query => 'foo' }}}
399
+ ElasticSearchable.request :put, '/_percolator/elastic_searchable/bluefilter', :json_body => { :color => 'blue', :query => {:query_string => {:query => 'foo' }}}
400
+ ElasticSearchable.request :post, '/_percolator/_refresh'
401
+ end
402
+ context 'percolating a non-persisted object with no limitation' do
403
+ setup do
404
+ @matches = Book.new(:title => 'foo').percolate
405
+ end
406
+ should 'return all percolated matches' do
407
+ assert_equal ['bluefilter','greenfilter','myfilter'], @matches.sort
408
+ end
409
+ end
410
+ context 'percolating a non-persisted object with limitations' do
411
+ setup do
412
+ @matches = Book.new(:title => 'foo').percolate( { :term => { :color => 'green' }} )
413
+ end
414
+ should 'return limited percolated matches' do
415
+ assert_equal ['greenfilter'], @matches
416
+ end
417
+ end
418
+ end
419
+ end
420
+ end
421
+ end
422
+
423
+ class MaxPageSizeClass < ActiveRecord::Base
424
+ elastic_searchable
425
+ def self.max_per_page
426
+ 1
427
+ end
428
+ end
429
+ context 'with 2 MaxPageSizeClass instances' do
430
+ setup do
431
+ ElasticSearchable.create_index
432
+ @first = MaxPageSizeClass.create! :name => 'foo one'
433
+ @second = MaxPageSizeClass.create! :name => 'foo two'
434
+ ElasticSearchable.refresh_index
435
+ end
436
+ context 'MaxPageSizeClass.search with default options and WillPaginate' do
437
+ setup do
438
+ ElasticSearchable::Paginator.handler = ElasticSearchable::Pagination::WillPaginate
439
+ @results = MaxPageSizeClass.search 'foo'
440
+ end
441
+ should 'have one per page' do
442
+ assert_equal 1, @results.per_page
443
+ end
444
+ should 'return one instance' do
445
+ assert_equal 1, @results.length
446
+ end
447
+ should 'have second page' do
448
+ assert_equal 2, @results.total_entries
449
+ end
450
+ end
451
+
452
+ context 'MaxPageSizeClass.search with default options and Kaminari' do
453
+ setup do
454
+ ElasticSearchable::Paginator.handler = ElasticSearchable::Pagination::Kaminari
455
+ @results = MaxPageSizeClass.search 'foo'
456
+ end
457
+ should 'have one per page' do
458
+ assert_equal 1, @results.per_page
459
+ end
460
+ should 'return one instance' do
461
+ assert_equal 1, @results.length
462
+ end
463
+ should 'have second page' do
464
+ assert_equal 2, @results.total_entries
465
+ end
466
+ should 'have a total of 2 pages' do
467
+ assert_equal 2, @results.num_pages
468
+ end
469
+ end
470
+
471
+ context "escape_query" do
472
+ should "escape exclamation marks" do
473
+ queryString = '!'
474
+ result = ElasticSearchable.escape_query(queryString)
475
+ assert_equal '\!', result
476
+ end
477
+
478
+ should "escape ^" do
479
+ queryString = '^'
480
+ result = ElasticSearchable.escape_query(queryString)
481
+ assert_equal '\^', result
482
+ end
483
+
484
+ should "escape +" do
485
+ queryString = '+'
486
+ result = ElasticSearchable.escape_query(queryString)
487
+ assert_equal '\+', result
488
+ end
489
+
490
+ should "escape -" do
491
+ queryString = '-'
492
+ result = ElasticSearchable.escape_query(queryString)
493
+ assert_equal '\-', result
494
+ end
495
+
496
+ should "escape (" do
497
+ queryString = '('
498
+ result = ElasticSearchable.escape_query(queryString)
499
+ assert_equal '\(', result
500
+ end
501
+
502
+ should "escape )" do
503
+ queryString = ')'
504
+ result = ElasticSearchable.escape_query(queryString)
505
+ assert_equal '\)', result
506
+ end
507
+
508
+ should "escape {" do
509
+ queryString = '}'
510
+ result = ElasticSearchable.escape_query(queryString)
511
+ assert_equal '\}', result
512
+ end
513
+
514
+ should "escape [" do
515
+ queryString = '['
516
+ result = ElasticSearchable.escape_query(queryString)
517
+ assert_equal '\[', result
518
+ end
519
+
520
+ should "escape ]" do
521
+ queryString = ']'
522
+ result = ElasticSearchable.escape_query(queryString)
523
+ assert_equal '\]', result
524
+ end
525
+
526
+ should 'escape "' do
527
+ queryString = '"'
528
+ result = ElasticSearchable.escape_query(queryString)
529
+ assert_equal '\"', result
530
+ end
531
+
532
+ should "escape ~" do
533
+ queryString = '~'
534
+ result = ElasticSearchable.escape_query(queryString)
535
+ assert_equal '\~', result
536
+ end
537
+
538
+ should "escape *" do
539
+ queryString = '*'
540
+ result = ElasticSearchable.escape_query(queryString)
541
+ assert_equal '\*', result
542
+ end
543
+
544
+ should "escape :" do
545
+ queryString = ':'
546
+ result = ElasticSearchable.escape_query(queryString)
547
+ assert_equal '\:', result
548
+ end
549
+
550
+ should "escape ?" do
551
+ queryString = '?'
552
+ result = ElasticSearchable.escape_query(queryString)
553
+ assert_equal '\?', result
554
+ end
555
+ end
556
+ end
557
+ end
558
+