elastic_searchable 1.6 → 2.0.0

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.
@@ -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
+