elastictastic 0.5.0 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/LICENSE +1 -1
  2. data/README.md +161 -10
  3. data/lib/elastictastic/adapter.rb +84 -0
  4. data/lib/elastictastic/association.rb +6 -0
  5. data/lib/elastictastic/basic_document.rb +213 -0
  6. data/lib/elastictastic/bulk_persistence_strategy.rb +64 -19
  7. data/lib/elastictastic/callbacks.rb +18 -12
  8. data/lib/elastictastic/child_collection_proxy.rb +15 -11
  9. data/lib/elastictastic/client.rb +47 -24
  10. data/lib/elastictastic/configuration.rb +59 -4
  11. data/lib/elastictastic/dirty.rb +43 -28
  12. data/lib/elastictastic/discrete_persistence_strategy.rb +48 -23
  13. data/lib/elastictastic/document.rb +1 -85
  14. data/lib/elastictastic/embedded_document.rb +34 -0
  15. data/lib/elastictastic/errors.rb +17 -5
  16. data/lib/elastictastic/field.rb +3 -0
  17. data/lib/elastictastic/mass_assignment_security.rb +2 -4
  18. data/lib/elastictastic/middleware.rb +66 -84
  19. data/lib/elastictastic/multi_get.rb +30 -0
  20. data/lib/elastictastic/multi_search.rb +70 -0
  21. data/lib/elastictastic/nested_document.rb +3 -27
  22. data/lib/elastictastic/new_relic_instrumentation.rb +8 -8
  23. data/lib/elastictastic/observing.rb +8 -6
  24. data/lib/elastictastic/optimistic_locking.rb +57 -0
  25. data/lib/elastictastic/parent_child.rb +56 -54
  26. data/lib/elastictastic/persistence.rb +16 -16
  27. data/lib/elastictastic/properties.rb +136 -96
  28. data/lib/elastictastic/railtie.rb +1 -1
  29. data/lib/elastictastic/rotor.rb +105 -0
  30. data/lib/elastictastic/scope.rb +186 -56
  31. data/lib/elastictastic/server_error.rb +20 -1
  32. data/lib/elastictastic/test_helpers.rb +152 -97
  33. data/lib/elastictastic/thrift/constants.rb +12 -0
  34. data/lib/elastictastic/thrift/rest.rb +83 -0
  35. data/lib/elastictastic/thrift/types.rb +124 -0
  36. data/lib/elastictastic/thrift_adapter.rb +61 -0
  37. data/lib/elastictastic/transport_methods.rb +27 -0
  38. data/lib/elastictastic/validations.rb +11 -13
  39. data/lib/elastictastic/version.rb +1 -1
  40. data/lib/elastictastic.rb +148 -27
  41. data/spec/environment.rb +1 -1
  42. data/spec/examples/bulk_persistence_strategy_spec.rb +151 -23
  43. data/spec/examples/callbacks_spec.rb +65 -34
  44. data/spec/examples/dirty_spec.rb +160 -1
  45. data/spec/examples/document_spec.rb +168 -106
  46. data/spec/examples/middleware_spec.rb +1 -61
  47. data/spec/examples/multi_get_spec.rb +127 -0
  48. data/spec/examples/multi_search_spec.rb +113 -0
  49. data/spec/examples/observing_spec.rb +24 -3
  50. data/spec/examples/optimistic_locking_spec.rb +417 -0
  51. data/spec/examples/parent_child_spec.rb +73 -33
  52. data/spec/examples/properties_spec.rb +53 -0
  53. data/spec/examples/rotor_spec.rb +132 -0
  54. data/spec/examples/scope_spec.rb +78 -18
  55. data/spec/examples/search_spec.rb +26 -0
  56. data/spec/examples/validation_spec.rb +7 -1
  57. data/spec/models/author.rb +1 -1
  58. data/spec/models/blog.rb +2 -0
  59. data/spec/models/comment.rb +1 -1
  60. data/spec/models/photo.rb +9 -0
  61. data/spec/models/post.rb +3 -0
  62. metadata +97 -78
  63. data/lib/elastictastic/resource.rb +0 -4
  64. data/spec/examples/active_model_lint_spec.rb +0 -20
@@ -34,6 +34,59 @@ describe Elastictastic::Properties do
34
34
  it 'should map embedded object fields' do
35
35
  properties['author']['properties']['id']['type'].should == 'integer'
36
36
  end
37
+
38
+ it 'should set boost field' do
39
+ mapping['post']['_boost'].should == { 'name' => 'score', 'null_value' => 1.0 }
40
+ end
41
+
42
+ it 'should set routing param if given' do
43
+ Photo.mapping['photo']['_routing'].should == {
44
+ 'required' => true,
45
+ 'path' => 'post_id'
46
+ }
47
+ end
48
+ end
49
+
50
+ describe ':preset' do
51
+ before do
52
+ Elastictastic.config.presets[:silly] = { :type => 'integer', :store => 'yes', :index => 'no' }
53
+ end
54
+
55
+ let :clazz do
56
+ Class.new do
57
+ include Elastictastic::Document
58
+
59
+ def self.name
60
+ 'Clazz'
61
+ end
62
+
63
+ field :title, :type => 'string', :preset => 'silly'
64
+ field :created, :preset => :silly
65
+ field :multi, :type => 'string' do
66
+ field :searchable, :preset => 'silly', :index => 'yes'
67
+ end
68
+ end
69
+ end
70
+
71
+ let(:properties) { clazz.mapping.values.first['properties'] }
72
+
73
+ it 'should apply preset values' do
74
+ properties['created'].should == {
75
+ 'type' => 'integer', 'store' => 'yes', 'index' => 'no'
76
+ }
77
+ end
78
+
79
+ it 'should override preset values with given values' do
80
+ properties['title'].should == {
81
+ 'type' => 'string', 'store' => 'yes', 'index' => 'no'
82
+ }
83
+ end
84
+
85
+ it 'should apply presets to field alternates' do
86
+ properties['multi']['fields']['searchable'].should == {
87
+ 'store' => 'yes', 'index' => 'yes', 'type' => 'integer'
88
+ }
89
+ end
37
90
  end
38
91
 
39
92
  describe '#elasticsearch_doc' do
@@ -0,0 +1,132 @@
1
+ require File.expand_path('../spec_helper', __FILE__)
2
+
3
+ describe Elastictastic::Rotor do
4
+ let(:client) { Elastictastic::Client.new(config) }
5
+ let(:last_request) { FakeWeb.last_request }
6
+
7
+ context 'without backoff' do
8
+ let(:config) do
9
+ Elastictastic::Configuration.new.tap do |config|
10
+ config.hosts = ['http://es1.local', 'http://es2.local']
11
+ end
12
+ end
13
+
14
+ it 'should alternate requests between hosts' do
15
+ expect do
16
+ 2.times do
17
+ 1.upto 2 do |i|
18
+ host_status(i => true)
19
+ client.get('default', 'post', '1')
20
+ end
21
+ end
22
+ end.not_to raise_error # We can't check the hostname of last_request in Fakeweb
23
+ end
24
+
25
+ context 'if one host fails' do
26
+ let!(:now) { Time.now.tap { |now| Time.stub(:now).and_return(now) }}
27
+
28
+ before do
29
+ host_status(1 => false, 2 => true)
30
+ end
31
+
32
+ it 'should try the next host' do
33
+ client.get('default', 'post', '1').should == { 'success' => true }
34
+ end
35
+
36
+ it 'should pass PUT body to retry' do
37
+ client.update('default', 'post', '1', { 'title' => 'pizza' })
38
+ FakeWeb.last_request.body.should == { 'title' => 'pizza' }.to_json
39
+ end
40
+ end
41
+
42
+ context 'if all hosts fail' do
43
+ let!(:now) { Time.now.tap { |now| Time.stub(:now).and_return(now) }}
44
+
45
+ before do
46
+ host_status(1 => false, 2 => false)
47
+ end
48
+
49
+ it 'should raise error if no hosts respond' do
50
+ expect { client.get('default', 'post', '1') }.to(raise_error Elastictastic::NoServerAvailable)
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'with backoff' do
56
+ let(:config) do
57
+ Elastictastic::Configuration.new.tap do |config|
58
+ config.hosts = ['http://es1.local', 'http://es2.local']
59
+ config.backoff_threshold = 2
60
+ config.backoff_start = 1
61
+ config.backoff_max = 4
62
+ end
63
+ end
64
+ let!(:time) { Time.now.tap { |time| Time.stub(:now).and_return(time) }}
65
+
66
+ before do
67
+ host_status(1 => false, 2 => true)
68
+ end
69
+
70
+ it 'should retry immediately before reaching initial failure count' do
71
+ client.get('default', 'post', '1')
72
+ expect { client.get('default', 'post', '1') }.to change(FakeWeb.requests, :length).by(2)
73
+ end
74
+
75
+ it 'should back off after initial failure count reached' do
76
+ 2.times { client.get('default', 'post', '1') }
77
+ expect { client.get('default', 'post', '1') }.to change(FakeWeb.requests, :length).by(1)
78
+ end
79
+
80
+ it 'should retry after initial backoff period elapses' do
81
+ 3.times { client.get('default', 'post', '1') }
82
+ Time.stub(:now).and_return(time + 1)
83
+ expect { client.get('default', 'post', '1') }.to change(FakeWeb.requests, :length).by(2)
84
+ end
85
+
86
+ it 'should double backoff after another failure' do
87
+ 3.times { client.get('default', 'post', '1') }
88
+ Time.stub(:now).and_return(time + 1)
89
+ client.get('default', 'post', '1')
90
+ Time.stub(:now).and_return(time + 2)
91
+ expect { client.get('default', 'post', '1') }.to change(FakeWeb.requests, :length).by(1)
92
+ end
93
+
94
+ it 'should cap backoff interval at backoff_max' do
95
+ 2.times { client.get('default', 'post', '1') } # first backoff - 1 second
96
+ Time.stub(:now).and_return(time + 1)
97
+ client.get('default', 'post', '1') # second backoff - 2 seconds
98
+ Time.stub(:now).and_return(time + 3)
99
+ client.get('default', 'post', '1') # third backoff - 4 seconds
100
+ Time.stub(:now).and_return(time + 7)
101
+ client.get('default', 'post', '1') # fourth backoff - 4 seconds again
102
+ Time.stub(:now).and_return(time + 11)
103
+ expect { client.get('default', 'post', '1') }.to change(FakeWeb.requests, :length).by(2)
104
+ end
105
+
106
+ it 'should reset backoff after a successful request' do
107
+ 2.times { client.get('default', 'post', '1') } # initial backoff - 1 second
108
+ host_status(1 => true, 2 => true)
109
+ Time.stub(:now).and_return(time + 1)
110
+ 2.times { client.get('default', 'post', '1') } # first one will go to es2 because of rotation. second one has success so es1 should reset
111
+ host_status(1 => false, 2 => true)
112
+ client.get('default', 'post', '1') # should be willing to immediately retry
113
+ host_status(1 => true, 2 => false)
114
+ expect { client.get('default', 'post', '1') }.to_not raise_error # only will succeed if it retries #1
115
+ end
116
+ end
117
+
118
+ private
119
+
120
+ def host_status(statuses)
121
+ FakeWeb.clean_registry
122
+ statuses.each_pair do |i, healthy|
123
+ url = %r(^http://es#{i}.local/)
124
+ if healthy
125
+ options = { :body => '{"success":true}' }
126
+ else
127
+ options = { :exception => Errno::ECONNREFUSED }
128
+ end
129
+ FakeWeb.register_uri(:any, url, options)
130
+ end
131
+ end
132
+ end
@@ -7,7 +7,7 @@ describe Elastictastic::Scope do
7
7
  include Elastictastic::TestHelpers
8
8
 
9
9
  let(:last_request) { FakeWeb.last_request }
10
- let(:last_request_body) { JSON.parse(last_request.body) }
10
+ let(:last_request_body) { Elastictastic.json_decode(last_request.body) }
11
11
  let(:last_request_path) { last_request.path.split('?', 2)[0] }
12
12
  let(:last_request_params) { last_request.path.split('?', 2)[1].try(:split, '&') }
13
13
 
@@ -20,7 +20,7 @@ describe Elastictastic::Scope do
20
20
  let(:scroll_requests) { FakeWeb.requests[1..-1] }
21
21
 
22
22
  before do
23
- @scroll_ids = stub_elasticsearch_scan(
23
+ @scroll_ids = stub_es_scan(
24
24
  'default', 'post', 2, *make_hits(3)
25
25
  )
26
26
  end
@@ -41,7 +41,7 @@ describe Elastictastic::Scope do
41
41
  end
42
42
 
43
43
  it 'should send query in data for initial search' do
44
- scan_request.body.should == scope.params.to_json
44
+ scan_request.body.should == Elastictastic.json_encode(scope.params)
45
45
  end
46
46
 
47
47
  it 'should send POST request initially' do
@@ -81,7 +81,7 @@ describe Elastictastic::Scope do
81
81
  let(:scope) { Post.from(10).size(10) }
82
82
 
83
83
  before do
84
- stub_elasticsearch_search(
84
+ stub_es_search(
85
85
  'default', 'post', 'hits' => {
86
86
  'total' => 2,
87
87
  'hits' => make_hits(2)
@@ -109,18 +109,35 @@ describe Elastictastic::Scope do
109
109
  end
110
110
  end # context 'with from/size'
111
111
 
112
+ describe 'with page out of range' do
113
+ let(:scope) { Post.from(10).size(10) }
114
+
115
+ before do
116
+ stub_es_search(
117
+ 'default', 'post', 'hits' => {
118
+ 'total' => 2,
119
+ 'hits' => []
120
+ }
121
+ )
122
+ end
123
+
124
+ it 'should return empty array of results' do
125
+ scope.to_a.should == []
126
+ end
127
+ end
128
+
112
129
  context 'with sort but no from/size' do
113
130
  let(:scope) { Post.sort(:title => 'asc') }
114
131
  let(:requests) { FakeWeb.requests }
115
132
  let(:request_bodies) do
116
133
  requests.map do |request|
117
- JSON.parse(request.body)
134
+ Elastictastic.json_decode(request.body)
118
135
  end
119
136
  end
120
137
 
121
138
  before do
122
139
  Elastictastic.config.default_batch_size = 2
123
- stub_elasticsearch_search(
140
+ stub_es_search(
124
141
  'default', 'post',
125
142
  make_hits(3).each_slice(2).map { |batch| { 'hits' => { 'hits' => batch, 'total' => 3 }} }
126
143
  )
@@ -169,6 +186,38 @@ describe Elastictastic::Scope do
169
186
  scope.each { |post| post.should be_persisted }
170
187
  end
171
188
  end # context 'with sort but no from/size'
189
+
190
+ describe 'with routing' do
191
+ it 'should send routing param in single-search query' do
192
+ stub_es_search(
193
+ 'default', 'post',
194
+ 'hits' => {'total' => 2, 'hits' => make_hits(2)}
195
+ )
196
+ Post.routing('7').size(10).to_a
197
+ last_request_uri.query.split('&').should include('routing=7')
198
+ end
199
+
200
+ it 'should send routing param in scan query' do
201
+ stub_es_scan(
202
+ 'default', 'post', 2, *make_hits(3)
203
+ )
204
+ Post.routing('7').to_a
205
+ URI.parse(FakeWeb.requests.first.path).query.split('&').
206
+ should include('routing=7')
207
+ end
208
+
209
+ it 'should send routing param in batch-search queries' do
210
+ Elastictastic.config.default_batch_size = 2
211
+ stub_es_search(
212
+ 'default', 'post',
213
+ make_hits(3).each_slice(2).map { |batch| { 'hits' => { 'hits' => batch, 'total' => 3 }} }
214
+ )
215
+ Post.routing('7').sort(:score).to_a
216
+ FakeWeb.requests.each do |request|
217
+ URI.parse(request.path).query.split('&').should include('routing=7')
218
+ end
219
+ end
220
+ end
172
221
  end # describe '#each'
173
222
 
174
223
  describe 'hit metadata' do
@@ -203,7 +252,7 @@ describe Elastictastic::Scope do
203
252
  let(:scope) { Post }
204
253
 
205
254
  before do
206
- stub_elasticsearch_scan('default', 'post', 2, *hits)
255
+ stub_es_scan('default', 'post', 2, *hits)
207
256
  end
208
257
 
209
258
  it_should_behave_like 'enumerator with hit metadata'
@@ -214,7 +263,7 @@ describe Elastictastic::Scope do
214
263
 
215
264
  before do
216
265
  batches = hits.each_slice(2).map { |batch| { 'hits' => { 'hits' => batch, 'total' => 3 }} }
217
- stub_elasticsearch_search('default', 'post', batches)
266
+ stub_es_search('default', 'post', batches)
218
267
  end
219
268
 
220
269
  it_should_behave_like 'enumerator with hit metadata'
@@ -224,7 +273,7 @@ describe Elastictastic::Scope do
224
273
  let(:scope) { Post.size(3) }
225
274
 
226
275
  before do
227
- stub_elasticsearch_search('default', 'post', 'hits' => { 'hits' => hits, 'total' => 3 })
276
+ stub_es_search('default', 'post', 'hits' => { 'hits' => hits, 'total' => 3 })
228
277
  end
229
278
 
230
279
  it_should_behave_like 'enumerator with hit metadata'
@@ -234,7 +283,7 @@ describe Elastictastic::Scope do
234
283
  describe '#count' do
235
284
  context 'with no operations performed yet' do
236
285
  let!(:count) do
237
- stub_elasticsearch_search('default', 'post', 'hits' => { 'total' => 3 })
286
+ stub_es_search('default', 'post', 'hits' => { 'total' => 3 })
238
287
  Post.all.count
239
288
  end
240
289
 
@@ -249,7 +298,7 @@ describe Elastictastic::Scope do
249
298
 
250
299
  context 'with scan search performed' do
251
300
  let!(:count) do
252
- stub_elasticsearch_scan(
301
+ stub_es_scan(
253
302
  'default', 'post', 2, *make_hits(3)
254
303
  )
255
304
  scope = Post.all
@@ -268,7 +317,7 @@ describe Elastictastic::Scope do
268
317
 
269
318
  context 'with paginated search performed' do
270
319
  let!(:count) do
271
- stub_elasticsearch_search(
320
+ stub_es_search(
272
321
  'default', 'post', 'hits' => {
273
322
  'hits' => make_hits(3),
274
323
  'total' => 3
@@ -290,7 +339,7 @@ describe Elastictastic::Scope do
290
339
 
291
340
  context 'with paginated scan performed' do
292
341
  let!(:count) do
293
- stub_elasticsearch_search(
342
+ stub_es_search(
294
343
  'default', 'post', 'hits' => {
295
344
  'hits' => make_hits(2),
296
345
  'total' => 2
@@ -309,6 +358,17 @@ describe Elastictastic::Scope do
309
358
  FakeWeb.should have(1).request
310
359
  end
311
360
  end # context 'with paginated scan performed'
361
+
362
+ context 'with routing specified' do
363
+ let!(:count) do
364
+ stub_es_search('default', 'post', 'hits' => { 'total' => 3 })
365
+ Post.routing(7).count
366
+ end
367
+
368
+ it 'should send routing parameter' do
369
+ last_request_uri.query.split('&').should include('routing=7')
370
+ end
371
+ end
312
372
  end # describe '#count'
313
373
 
314
374
  describe '#all_facets' do
@@ -327,7 +387,7 @@ describe Elastictastic::Scope do
327
387
 
328
388
  context 'with no requests performed' do
329
389
  let!(:facets) do
330
- stub_elasticsearch_search(
390
+ stub_es_search(
331
391
  'default', 'post',
332
392
  'hits' => { 'hits' => [], 'total' => 2 },
333
393
  'facets' => facet_response
@@ -346,7 +406,7 @@ describe Elastictastic::Scope do
346
406
 
347
407
  context 'with count request performed' do
348
408
  let!(:facets) do
349
- stub_elasticsearch_search(
409
+ stub_es_search(
350
410
  'default', 'post',
351
411
  'hits' => { 'hits' => [], 'total' => 2 },
352
412
  'facets' => facet_response
@@ -366,7 +426,7 @@ describe Elastictastic::Scope do
366
426
 
367
427
  context 'with single-page search performed' do
368
428
  let!(:facets) do
369
- stub_elasticsearch_search(
429
+ stub_es_search(
370
430
  'default', 'post',
371
431
  'hits' => { 'hits' => make_hits(2), 'total' => 2 },
372
432
  'facets' => facet_response
@@ -387,7 +447,7 @@ describe Elastictastic::Scope do
387
447
 
388
448
  context 'with multi-page search performed' do
389
449
  let!(:facets) do
390
- stub_elasticsearch_search(
450
+ stub_es_search(
391
451
  'default', 'post',
392
452
  'hits' => { 'hits' => make_hits(2), 'total' => 2 },
393
453
  'facets' => facet_response
@@ -410,7 +470,7 @@ describe Elastictastic::Scope do
410
470
  describe '#first' do
411
471
  shared_examples_for 'first method' do
412
472
  before do
413
- stub_elasticsearch_search(
473
+ stub_es_search(
414
474
  index, 'post', 'hits' => {
415
475
  'total' => 12,
416
476
  'hits' => make_hits(1)
@@ -6,6 +6,8 @@ require File.expand_path('../spec_helper', __FILE__)
6
6
  # search and dealing with the results
7
7
  #
8
8
  describe Elastictastic::Search do
9
+ include Elastictastic::TestHelpers
10
+
9
11
  describe '#to_params' do
10
12
  {
11
13
  'query' => { 'match_all' => {} },
@@ -47,6 +49,30 @@ describe Elastictastic::Search do
47
49
  end
48
50
  end
49
51
 
52
+ describe '#[]' do
53
+ it 'should run #first query with an integer argument' do
54
+ stub_es_search('default', 'post', 'hits' => {
55
+ 'total' => '2',
56
+ 'hits' => [generate_es_hit('post', :id => '1')]
57
+ })
58
+ Post.all[4].id.should == '1'
59
+ last_request_json['from'].should == 4
60
+ last_request_json['size'].should == 1
61
+ end
62
+
63
+ it 'should add from/size to scope with a range argument' do
64
+ params = Post.all[2..4].params
65
+ params['from'].should == 2
66
+ params['size'].should == 3
67
+ end
68
+
69
+ it 'should add from/size to scope with an end-excluded range argument' do
70
+ params = Post.all[2...4].params
71
+ params['from'].should == 2
72
+ params['size'].should == 2
73
+ end
74
+ end
75
+
50
76
  describe 'merging' do
51
77
  let(:scope) { Post }
52
78
 
@@ -21,12 +21,18 @@ describe Elastictastic::Validations do
21
21
  it 'should raise Elastictastic::RecordInvalid for save!' do
22
22
  expect { post.save! }.to raise_error(Elastictastic::RecordInvalid)
23
23
  end
24
+
25
+ it 'should save successfully if validations disabled' do
26
+ stub_es_create('default', 'post')
27
+ post.save(:validate => false)
28
+ FakeWeb.last_request.path.should == '/default/post'
29
+ end
24
30
  end
25
31
 
26
32
  describe 'with valid data' do
27
33
  let(:post) { Post.new }
28
34
 
29
- before { stub_elasticsearch_create('default', 'post') }
35
+ before { stub_es_create('default', 'post') }
30
36
 
31
37
  it 'should be valid' do
32
38
  post.should be_valid
@@ -1,5 +1,5 @@
1
1
  class Author
2
- include Elastictastic::NestedDocument
2
+ include Elastictastic::EmbeddedDocument
3
3
 
4
4
  field :id, :type => 'integer'
5
5
  field :name
data/spec/models/blog.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  class Blog
2
2
  include Elastictastic::Document
3
3
 
4
+ field :name
5
+
4
6
  has_many :posts
5
7
  end
@@ -1,5 +1,5 @@
1
1
  class Comment
2
- include Elastictastic::NestedDocument
2
+ include Elastictastic::EmbeddedDocument
3
3
 
4
4
  field :body
5
5
  end
@@ -0,0 +1,9 @@
1
+ class Photo
2
+ include Elastictastic::Document
3
+
4
+ field :post_id, :type => 'integer'
5
+ field :path
6
+ field :caption
7
+
8
+ route_with :post_id, :required => true
9
+ end
data/spec/models/post.rb CHANGED
@@ -3,12 +3,15 @@ class Post
3
3
 
4
4
  field :title
5
5
  field :comments_count, :type => 'integer'
6
+ field :score, :type => 'integer'
6
7
  field :tags, :index => 'analyzed' do
7
8
  field :non_analyzed, :index => 'not_analyzed'
8
9
  end
9
10
  field :created_at, :type => 'date'
10
11
  field :published_at, :type => 'date'
11
12
 
13
+ boost :score
14
+
12
15
  embed :author
13
16
  embed :comments
14
17