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.
- data/.gitignore +0 -2
- data/.rvmrc +1 -1
- data/CONTRIBUTORS.txt +0 -1
- data/Rakefile +9 -7
- data/elastic_searchable.gemspec +6 -7
- data/lib/elastic_searchable/active_record_extensions.rb +7 -2
- data/lib/elastic_searchable/index.rb +19 -53
- data/lib/elastic_searchable/queries.rb +8 -41
- data/lib/elastic_searchable/version.rb +1 -1
- data/lib/elastic_searchable.rb +30 -4
- data/test/database.yml +3 -0
- data/{spec/spec_helper.rb → test/helper.rb} +10 -16
- data/{spec/support → test}/setup_database.rb +0 -0
- data/test/test_elastic_searchable.rb +558 -0
- metadata +69 -115
- checksums.yaml +0 -7
- data/.travis.yml +0 -5
- data/spec/elastic_searchable_spec.rb +0 -433
- data/spec/support/blog.rb +0 -6
- data/spec/support/book.rb +0 -10
- data/spec/support/database.yml +0 -3
- data/spec/support/friend.rb +0 -4
- data/spec/support/max_page_size_class.rb +0 -6
- data/spec/support/post.rb +0 -26
- data/spec/support/user.rb +0 -8
@@ -1,433 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe ElasticSearchable do
|
4
|
-
before do
|
5
|
-
begin
|
6
|
-
ElasticSearchable.delete '/elastic_searchable'
|
7
|
-
rescue
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
describe 'an ActiveRecord class that has not invoked elastic_searchable' do
|
12
|
-
before do
|
13
|
-
stub_const('Parent', Class.new(ActiveRecord::Base))
|
14
|
-
end
|
15
|
-
let(:clazz) { Parent }
|
16
|
-
let(:instance) { clazz.new }
|
17
|
-
it do
|
18
|
-
expect(clazz).to_not respond_to :elastic_options
|
19
|
-
expect(instance).to_not respond_to :percolations
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
describe 'with an ActiveRecord class with elastic_searchable config' do
|
24
|
-
let(:clazz) { Post }
|
25
|
-
let(:instance) { Post.new }
|
26
|
-
it do
|
27
|
-
expect(clazz).to respond_to :search
|
28
|
-
expect(clazz).to respond_to :elastic_options
|
29
|
-
expect(clazz.elastic_options[:unless]).to include :elasticsearch_offline?
|
30
|
-
expect(instance).to respond_to :percolations
|
31
|
-
expect(instance.percolations).to eq []
|
32
|
-
end
|
33
|
-
|
34
|
-
describe '.request' do
|
35
|
-
subject(:sending_request) { ElasticSearchable.request method, url }
|
36
|
-
context 'GET' do
|
37
|
-
let(:method) { :get }
|
38
|
-
context 'with invalid url' do
|
39
|
-
let(:url) { '/elastic_searchable/foobar/notfound' }
|
40
|
-
it { expect { sending_request }.to raise_error ElasticSearchable::ElasticError }
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
describe '.create_index' do
|
46
|
-
let(:index_status) { ElasticSearchable.request :get, '/elastic_searchable/_status' }
|
47
|
-
context 'when it has not been called' do
|
48
|
-
it { expect { index_status }.to raise_error }
|
49
|
-
end
|
50
|
-
context 'when it has been called' do
|
51
|
-
before do
|
52
|
-
Post.create_index
|
53
|
-
Post.refresh_index
|
54
|
-
end
|
55
|
-
it { expect { index_status }.not_to raise_error }
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
describe 'create callbacks' do
|
60
|
-
let!(:post) { Post.create :title => 'foo', :body => "bar" }
|
61
|
-
it 'fires index callbacks' do
|
62
|
-
expect(post).to be_indexed
|
63
|
-
expect(post).to be_indexed_on_create
|
64
|
-
expect(post).not_to be_indexed_on_update
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
describe 'update callbacks' do
|
69
|
-
before do
|
70
|
-
Post.create :title => 'foo', :body => 'bar'
|
71
|
-
end
|
72
|
-
let(:post) do
|
73
|
-
Post.last.tap do |post|
|
74
|
-
post.title = 'baz'
|
75
|
-
post.save
|
76
|
-
end
|
77
|
-
end
|
78
|
-
it do
|
79
|
-
expect(post).to be_indexed
|
80
|
-
expect(post).not_to be_indexed_on_create
|
81
|
-
expect(post).to be_indexed_on_update
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
describe 'ElasticSearchable.offline' do
|
86
|
-
let!(:post) do
|
87
|
-
ElasticSearchable.offline do
|
88
|
-
Post.create :title => 'foo', :body => "bar"
|
89
|
-
end
|
90
|
-
end
|
91
|
-
it do
|
92
|
-
expect(post).not_to be_indexed
|
93
|
-
expect(post).not_to be_indexed_on_create
|
94
|
-
expect(post).not_to be_indexed_on_update
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
context 'with an empty index and multiple database records' do
|
99
|
-
before do
|
100
|
-
Post.delete_all
|
101
|
-
Post.create_index
|
102
|
-
Post.create :title => 'foo', :body => "first bar"
|
103
|
-
Post.create :title => 'foo', :body => "second bar"
|
104
|
-
Post.delete_index
|
105
|
-
Post.create_index
|
106
|
-
Post.refresh_index
|
107
|
-
end
|
108
|
-
let!(:first_post) { Post.where(:body => "first bar").first }
|
109
|
-
let!(:second_post) { Post.where(:body => "second bar").first }
|
110
|
-
it 'does not raise error if an error occurs when reindexing model' do
|
111
|
-
expect_any_instance_of(Logger).to receive(:warn).at_least(:once)
|
112
|
-
expect(ElasticSearchable).to receive(:request).and_raise(ElasticSearchable::ElasticError.new('faux error'))
|
113
|
-
expect { Post.reindex }.not_to raise_error
|
114
|
-
end
|
115
|
-
it 'does not raise error when destroying one instance' do
|
116
|
-
expect_any_instance_of(Logger).to receive(:warn).at_least(:once)
|
117
|
-
expect { first_post.destroy }.not_to raise_error
|
118
|
-
end
|
119
|
-
describe ".reindex" do
|
120
|
-
before do
|
121
|
-
Post.reindex :per_page => 1, :scope => Post.order('body desc')
|
122
|
-
Post.refresh_index
|
123
|
-
end
|
124
|
-
it do
|
125
|
-
expect { ElasticSearchable.request :get, "/elastic_searchable/posts/#{first_post.id}" }.to_not raise_error
|
126
|
-
expect { ElasticSearchable.request :get, "/elastic_searchable/posts/#{second_post.id}" }.to_not raise_error
|
127
|
-
end
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
context 'with the index containing multiple results' do
|
132
|
-
before do
|
133
|
-
Post.create_index
|
134
|
-
Post.create :title => 'foo', :body => "first bar"
|
135
|
-
Post.create :title => 'foo', :body => "second bar"
|
136
|
-
Post.refresh_index
|
137
|
-
end
|
138
|
-
let!(:first_post) { Post.where(:body => "first bar").first }
|
139
|
-
let!(:second_post) { Post.where(:body => "second bar").first }
|
140
|
-
|
141
|
-
context 'searching on a term that returns one result' do
|
142
|
-
subject(:results) { Post.search 'first' }
|
143
|
-
it do
|
144
|
-
is_expected.to include first_post
|
145
|
-
expect(results.current_page).to eq 1
|
146
|
-
expect(results.per_page).to eq Post.per_page
|
147
|
-
expect(results.previous_page).to be_nil
|
148
|
-
expect(results.next_page).to be_nil
|
149
|
-
expect(results.first.hit['_id']).to eq first_post.id.to_s
|
150
|
-
end
|
151
|
-
end
|
152
|
-
context 'searching on a term that returns multiple results' do
|
153
|
-
subject(:results) { Post.search 'foo' }
|
154
|
-
it do
|
155
|
-
is_expected.to include first_post
|
156
|
-
is_expected.to include second_post
|
157
|
-
expect(results.current_page).to eq 1
|
158
|
-
expect(results.per_page).to eq Post.per_page
|
159
|
-
expect(results.previous_page).to be_nil
|
160
|
-
expect(results.next_page).to be_nil
|
161
|
-
expect(results.first.hit['_id']).to eq first_post.id.to_s
|
162
|
-
expect(results.last.hit['_id']).to eq second_post.id.to_s
|
163
|
-
end
|
164
|
-
end
|
165
|
-
context 'searching for results using a query Hash' do
|
166
|
-
subject(:results) do
|
167
|
-
Post.search({
|
168
|
-
:filtered => {
|
169
|
-
:query => {
|
170
|
-
:term => {:title => 'foo'},
|
171
|
-
},
|
172
|
-
:filter => {
|
173
|
-
:or => [
|
174
|
-
{:term => {:body => 'second'}},
|
175
|
-
{:term => {:body => 'third'}}
|
176
|
-
]
|
177
|
-
}
|
178
|
-
}
|
179
|
-
})
|
180
|
-
end
|
181
|
-
it do
|
182
|
-
is_expected.to_not include first_post
|
183
|
-
is_expected.to include second_post
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
context 'when per_page is a string' do
|
188
|
-
subject(:results) { Post.search 'foo', :per_page => 1.to_s, :sort => 'id' }
|
189
|
-
it { expect(results).to include first_post }
|
190
|
-
end
|
191
|
-
|
192
|
-
context 'searching for second page using will_paginate params' do
|
193
|
-
subject(:results) { Post.search 'foo', :page => 2, :per_page => 1, :sort => 'id' }
|
194
|
-
it do
|
195
|
-
expect(results).not_to include first_post
|
196
|
-
expect(results).to include second_post
|
197
|
-
expect(results.current_page).to eq 2
|
198
|
-
expect(results.per_page).to eq 1
|
199
|
-
expect(results.previous_page).to eq 1
|
200
|
-
expect(results.next_page).to be_nil
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
context 'sorting search results' do
|
205
|
-
subject(:results) { Post.search 'foo', :sort => 'id:desc' }
|
206
|
-
it 'sorts results correctly' do
|
207
|
-
expect(results).to eq [second_post, first_post]
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
context 'advanced sort options' do
|
212
|
-
subject(:results) { Post.search 'foo', :sort => [{:id => 'desc'}] }
|
213
|
-
it 'sorts results correctly' do
|
214
|
-
expect(results).to eq [second_post, first_post]
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
context 'destroying one object' do
|
219
|
-
before do
|
220
|
-
first_post.destroy
|
221
|
-
Post.refresh_index
|
222
|
-
end
|
223
|
-
it 'is removed from the index' do
|
224
|
-
expect(ElasticSearchable.get("/elastic_searchable/posts/#{first_post.id}").response).to be_a Net::HTTPNotFound
|
225
|
-
end
|
226
|
-
end
|
227
|
-
end
|
228
|
-
|
229
|
-
context 'deleting a record without updating the index' do
|
230
|
-
|
231
|
-
context 'backfilling partial result pages' do
|
232
|
-
let!(:posts) do
|
233
|
-
posts = (1..8).map do |i|
|
234
|
-
Post.create :title => 'foo', :body => "#{i} bar"
|
235
|
-
end
|
236
|
-
Post.refresh_index
|
237
|
-
posts
|
238
|
-
end
|
239
|
-
subject(:results) { Post.search 'foo', :size => 4, :sort => 'id:desc' }
|
240
|
-
it 'backfills the first page with results from other pages' do
|
241
|
-
removed_posts = []
|
242
|
-
posts.each_with_index do |post, i|
|
243
|
-
if i % 2 == 1
|
244
|
-
removed_posts << post
|
245
|
-
expect(Post).to receive(:delete_id_from_index_backgrounded).with(post.id)
|
246
|
-
post.delete
|
247
|
-
end
|
248
|
-
end
|
249
|
-
expect(results).to match_array(posts - removed_posts)
|
250
|
-
expect(results.total_entries).to eq 4
|
251
|
-
end
|
252
|
-
end
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
context 'activerecord class with optional :if=>proc configuration' do
|
257
|
-
context 'when creating new instance' do
|
258
|
-
it do
|
259
|
-
expect_any_instance_of(Blog).to_not receive(:reindex)
|
260
|
-
blog = Blog.create! :title => 'foo'
|
261
|
-
expect(ElasticSearchable.get("/elastic_searchable/blogs/#{blog.id}").response).to be_a Net::HTTPNotFound
|
262
|
-
end
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
context 'activerecord class with :index_options and :mapping' do
|
267
|
-
context 'creating index' do
|
268
|
-
before do
|
269
|
-
User.create_index
|
270
|
-
end
|
271
|
-
it 'uses custom index_options' do
|
272
|
-
settings = ElasticSearchable.request(:get, '/elastic_searchable/_settings')['elastic_searchable']['settings']['index']
|
273
|
-
settings.delete('version')
|
274
|
-
settings.delete('uuid')
|
275
|
-
expect(settings).to eq(
|
276
|
-
"analysis" => {
|
277
|
-
"analyzer" => {
|
278
|
-
"default"=> {
|
279
|
-
"filter" => [ "standard", "lowercase", "porterStem"],
|
280
|
-
"tokenizer" => "standard"
|
281
|
-
}
|
282
|
-
}
|
283
|
-
},
|
284
|
-
"number_of_shards"=>"1",
|
285
|
-
"number_of_replicas"=>"0"
|
286
|
-
)
|
287
|
-
end
|
288
|
-
it 'has set mapping' do
|
289
|
-
status = ElasticSearchable.request :get, '/elastic_searchable/users/_mapping'
|
290
|
-
expect(status['elastic_searchable']['mappings']['users']['properties']).to eq(
|
291
|
-
"name"=> {"type"=>"string", "index"=>"not_analyzed"}
|
292
|
-
)
|
293
|
-
end
|
294
|
-
end
|
295
|
-
end
|
296
|
-
|
297
|
-
context 'activerecord class with optional :json config' do
|
298
|
-
context 'creating index' do
|
299
|
-
let!(:friend) do
|
300
|
-
Friend.create_index
|
301
|
-
book = Book.create! :isbn => '123abc', :title => 'another world'
|
302
|
-
friend = Friend.new :name => 'bob', :favorite_color => 'red'
|
303
|
-
friend.book = book
|
304
|
-
friend.save!
|
305
|
-
Friend.refresh_index
|
306
|
-
friend
|
307
|
-
end
|
308
|
-
subject(:json) { ElasticSearchable.request(:get, "/elastic_searchable/friends/#{friend.id}")['_source'] }
|
309
|
-
it 'indexes json with configuration' do
|
310
|
-
expect(json['favorite_color']).to be_nil
|
311
|
-
expect(json['book'].key?('isbn')).to be_falsey
|
312
|
-
expect(json).to eq(
|
313
|
-
"name" => 'bob',
|
314
|
-
'book' => { 'title' => 'another world' }
|
315
|
-
)
|
316
|
-
end
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
|
-
context 'updating ElasticSearchable.default_index' do
|
321
|
-
before do
|
322
|
-
ElasticSearchable.default_index = 'my_new_index'
|
323
|
-
end
|
324
|
-
after do
|
325
|
-
ElasticSearchable.default_index = ElasticSearchable::DEFAULT_INDEX
|
326
|
-
end
|
327
|
-
it { expect(ElasticSearchable.default_index).to eq 'my_new_index' }
|
328
|
-
end
|
329
|
-
|
330
|
-
context 'Book class with after_percolate callback' do
|
331
|
-
context 'with created index and populated fields' do
|
332
|
-
before do
|
333
|
-
Book.create_index
|
334
|
-
Book.create! :title => 'baz'
|
335
|
-
end
|
336
|
-
context "when index has configured percolation" do
|
337
|
-
before do
|
338
|
-
ElasticSearchable.request :put, '/elastic_searchable/.percolator/myfilter', :json_body => {:query => {:query_string => {:query => 'foo' }}}
|
339
|
-
ElasticSearchable.request :post, '/elastic_searchable/_refresh'
|
340
|
-
end
|
341
|
-
context 'creating an object that does not match the percolation' do
|
342
|
-
it 'does not percolate the record' do
|
343
|
-
expect_any_instance_of(Book).to_not receive(:on_percolated)
|
344
|
-
Book.create! :title => 'bar'
|
345
|
-
end
|
346
|
-
end
|
347
|
-
context 'creating an object that matches the percolation' do
|
348
|
-
let!(:book) do
|
349
|
-
Book.create :title => "foo"
|
350
|
-
end
|
351
|
-
it do
|
352
|
-
expect(book.percolated).to eq ['myfilter']
|
353
|
-
end
|
354
|
-
end
|
355
|
-
context 'percolating a non-persisted object' do
|
356
|
-
let!(:matches) { Book.new(:title => 'foo').percolate }
|
357
|
-
it do
|
358
|
-
expect(matches).to eq ['myfilter']
|
359
|
-
end
|
360
|
-
end
|
361
|
-
context "with multiple percolators in the index" do
|
362
|
-
before do
|
363
|
-
ElasticSearchable.request :put, '/elastic_searchable/.percolator/greenfilter', :json_body => { :color => 'green', :query => {:query_string => {:query => 'foo' }}}
|
364
|
-
ElasticSearchable.request :put, '/elastic_searchable/.percolator/bluefilter', :json_body => { :color => 'blue', :query => {:query_string => {:query => 'foo' }}}
|
365
|
-
ElasticSearchable.request :post, '/elastic_searchable/_refresh'
|
366
|
-
end
|
367
|
-
context 'percolating a non-persisted object with no limitation' do
|
368
|
-
let!(:matches) { Book.new(:title => 'foo').percolate }
|
369
|
-
it 'returns all percolated matches' do
|
370
|
-
expect(matches).to match_array ['myfilter', 'greenfilter', 'bluefilter']
|
371
|
-
expect(matches.size).to eq 3
|
372
|
-
end
|
373
|
-
end
|
374
|
-
context 'percolating a non-persisted object with limitations' do
|
375
|
-
let!(:matches) { Book.new(:title => 'foo').percolate(:term => { :color => 'green' }) }
|
376
|
-
it 'returns limited percolated matches' do
|
377
|
-
expect(matches).to eq ['greenfilter']
|
378
|
-
end
|
379
|
-
end
|
380
|
-
end
|
381
|
-
end
|
382
|
-
end
|
383
|
-
end
|
384
|
-
|
385
|
-
context 'with 2 MaxPageSizeClass instances' do
|
386
|
-
before do
|
387
|
-
MaxPageSizeClass.create_index
|
388
|
-
MaxPageSizeClass.create! :name => 'foo one'
|
389
|
-
MaxPageSizeClass.create! :name => 'foo two'
|
390
|
-
MaxPageSizeClass.refresh_index
|
391
|
-
end
|
392
|
-
let!(:first) { MaxPageSizeClass.where(:name => 'foo one').first }
|
393
|
-
let!(:second) { MaxPageSizeClass.where(:name => 'foo two').first }
|
394
|
-
subject(:results) { MaxPageSizeClass.search 'foo' }
|
395
|
-
context 'MaxPageSizeClass.search with default options and WillPaginate' do
|
396
|
-
before do
|
397
|
-
ElasticSearchable::Paginator.handler = ElasticSearchable::Pagination::WillPaginate
|
398
|
-
end
|
399
|
-
it do
|
400
|
-
expect(results.per_page).to eq 1
|
401
|
-
expect(results.length).to eq 1
|
402
|
-
expect(results.total_entries).to eq 2
|
403
|
-
end
|
404
|
-
end
|
405
|
-
|
406
|
-
context 'MaxPageSizeClass.search with default options and Kaminari' do
|
407
|
-
before do
|
408
|
-
ElasticSearchable::Paginator.handler = ElasticSearchable::Pagination::Kaminari
|
409
|
-
@results = MaxPageSizeClass.search 'foo'
|
410
|
-
end
|
411
|
-
it do
|
412
|
-
expect(results.per_page).to eq 1
|
413
|
-
expect(results.length).to eq 1
|
414
|
-
expect(results.total_entries).to eq 2
|
415
|
-
expect(results.num_pages).to eq 2
|
416
|
-
end
|
417
|
-
end
|
418
|
-
|
419
|
-
describe '.escape_query' do
|
420
|
-
let(:backslash) { "\\" }
|
421
|
-
shared_examples_for "escaped" do
|
422
|
-
it { expect(ElasticSearchable.escape_query(queryString)).to eq(backslash + queryString) }
|
423
|
-
end
|
424
|
-
%w| ! ^ + - { } [ ] ~ * : ? ( ) "|.each do |mark|
|
425
|
-
context "escaping '#{mark}'" do
|
426
|
-
let(:queryString) { mark }
|
427
|
-
it_behaves_like "escaped"
|
428
|
-
end
|
429
|
-
end
|
430
|
-
end
|
431
|
-
end
|
432
|
-
end
|
433
|
-
|
data/spec/support/blog.rb
DELETED
data/spec/support/book.rb
DELETED
data/spec/support/database.yml
DELETED
data/spec/support/friend.rb
DELETED
data/spec/support/post.rb
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
class Post < ActiveRecord::Base
|
2
|
-
Post.class_eval do
|
3
|
-
elastic_searchable :index_options => SINGLE_NODE_CLUSTER_CONFIG
|
4
|
-
after_index :indexed
|
5
|
-
after_index :indexed_on_create, :on => :create
|
6
|
-
after_index :indexed_on_update, :on => :update
|
7
|
-
def indexed
|
8
|
-
@indexed = true
|
9
|
-
end
|
10
|
-
def indexed?
|
11
|
-
@indexed
|
12
|
-
end
|
13
|
-
def indexed_on_create
|
14
|
-
@indexed_on_create = true
|
15
|
-
end
|
16
|
-
def indexed_on_create?
|
17
|
-
@indexed_on_create
|
18
|
-
end
|
19
|
-
def indexed_on_update
|
20
|
-
@indexed_on_update = true
|
21
|
-
end
|
22
|
-
def indexed_on_update?
|
23
|
-
@indexed_on_update
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
data/spec/support/user.rb
DELETED
@@ -1,8 +0,0 @@
|
|
1
|
-
class User < ActiveRecord::Base
|
2
|
-
elastic_searchable :index_options => {
|
3
|
-
'number_of_replicas' => 0,
|
4
|
-
'number_of_shards' => 1,
|
5
|
-
"analysis.analyzer.default.tokenizer" => 'standard',
|
6
|
-
"analysis.analyzer.default.filter" => ["standard", "lowercase", 'porterStem']},
|
7
|
-
:mapping => {:properties => {:name => {:type => 'string', :index => 'not_analyzed'}}}
|
8
|
-
end
|