elastic_searchable 1.5 → 1.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 07b8465cf211c71d51f8aae0a70050d764ca5b85
4
+ data.tar.gz: 22718204ae3d09fd1e969c1a8410b4d352946ff8
5
+ SHA512:
6
+ metadata.gz: 64be3a3d8766e5a9d3a2d39523172703467572e496b96d92d3167686d04b8522b2aaf8203e32c60d8db4fe40ecedea7eb1fee945d49068ef0de7c47a51c4f490
7
+ data.tar.gz: f7791b7a15d83546061e34773e580d3d4a0fdee66d277f35c793d2515a20d0c0accc4c39f82b0d400a06a2dfd0ff35dc7910a1d3cb35f917c7481af867f1e737
data/.gitignore CHANGED
@@ -18,6 +18,8 @@ pkg
18
18
  # test files
19
19
  test/*.log
20
20
  test/*.sqlite3
21
+ spec/**/*.log
22
+ spec/*.sqlite3
21
23
 
22
24
  # For vim:
23
25
  *.swp
data/.rvmrc CHANGED
@@ -1 +1 @@
1
- rvm use ruby-1.9.3-p125@elastic_searchable --create
1
+ rvm use default@elastic_searchable --create
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.1
4
+ services:
5
+ - elasticsearch
data/CONTRIBUTORS.txt CHANGED
@@ -1,4 +1,5 @@
1
1
  Ryan Sonnek - Original Author
2
+ Geoff Hichborn - Implemented search result autoscrubbing
2
3
 
3
4
 
4
5
  Complete list of contributors:
data/Rakefile CHANGED
@@ -1,13 +1,11 @@
1
- require 'bundler'
2
- Bundler::GemHelper.install_tasks
1
+ require "bundler/gem_tasks"
3
2
 
4
3
  require 'rake'
5
4
 
6
- require 'rake/testtask'
7
- Rake::TestTask.new(:test) do |test|
8
- test.libs << 'lib' << 'test'
9
- test.pattern = 'test/**/test_*.rb'
10
- test.verbose = true
11
- end
12
- task :default => :test
5
+ require 'rspec/core/rake_task'
13
6
 
7
+ desc "Run specs"
8
+ RSpec::Core::RakeTask.new do |t|
9
+ end
10
+ task :default => :spec
11
+ task :test => :spec
@@ -23,9 +23,10 @@ Gem::Specification.new do |s|
23
23
  s.add_runtime_dependency(%q<httparty>, [">= 0.6.0"])
24
24
  s.add_runtime_dependency(%q<backgrounded>, ["~> 0.7.0"])
25
25
  s.add_runtime_dependency(%q<multi_json>, [">= 1.0.0"])
26
- s.add_development_dependency(%q<rake>, ["0.9.2.2"])
26
+ s.add_development_dependency(%q<rake>)
27
27
  s.add_development_dependency(%q<sqlite3>)
28
- s.add_development_dependency(%q<pry>, ["0.9.6.2"])
29
- s.add_development_dependency(%q<shoulda>, ["2.11.3"])
30
- s.add_development_dependency(%q<mocha>)
28
+ s.add_development_dependency(%q<pry>)
29
+ s.add_development_dependency(%q<rspec>)
30
+ s.add_development_dependency(%q<byebug>)
31
+ s.add_development_dependency(%q<prefactory>)
31
32
  end
@@ -111,15 +111,15 @@ module ElasticSearchable
111
111
  # see http://www.elasticsearch.org/guide/reference/api/index_.html
112
112
  def reindex(lifecycle = nil)
113
113
  query = {}
114
- query[:percolate] = "*" if _percolate_callbacks.any?
115
- response = ElasticSearchable.request :put, self.class.index_type_path(self.id), :query => query, :json_body => self.as_json_for_index
114
+ response = ElasticSearchable.request :put, self.class.index_type_path(id), :query => query, :json_body => as_json_for_index
116
115
 
117
116
  self.index_lifecycle = lifecycle ? lifecycle.to_sym : nil
118
117
  run_callbacks :index
119
118
 
120
- self.percolations = response['matches'] || []
121
- run_callbacks :percolate if self.percolations.any?
119
+ self.percolate if _percolate_callbacks.any?
120
+ run_callbacks :percolate if percolations.any?
122
121
  end
122
+
123
123
  # document to index in elasticsearch
124
124
  def as_json_for_index
125
125
  original_include_root_in_json = self.class.include_root_in_json
@@ -128,10 +128,12 @@ module ElasticSearchable
128
128
  ensure
129
129
  self.class.include_root_in_json = original_include_root_in_json
130
130
  end
131
+
131
132
  def should_index?
132
133
  [self.class.elastic_options[:if]].flatten.compact.all? { |m| evaluate_elastic_condition(m) } &&
133
134
  ![self.class.elastic_options[:unless]].flatten.compact.any? { |m| evaluate_elastic_condition(m) }
134
135
  end
136
+
135
137
  # percolate this object to see what registered searches match
136
138
  # can be done on transient/non-persisted objects!
137
139
  # can be done automatically when indexing using :percolate => true config option
@@ -140,7 +142,7 @@ module ElasticSearchable
140
142
  body = {:doc => self.as_json_for_index}
141
143
  body[:query] = percolator_query if percolator_query
142
144
  response = ElasticSearchable.request :get, self.class.index_type_path('_percolate'), :json_body => body
143
- self.percolations = response['matches'] || []
145
+ self.percolations = (response['matches'] || []).map { |match| match['_id'] }
144
146
  self.percolations
145
147
  end
146
148
 
@@ -1,3 +1,3 @@
1
1
  module ElasticSearchable
2
- VERSION = '1.5'
2
+ VERSION = '1.6'
3
3
  end
@@ -0,0 +1,433 @@
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
+