elastictastic 0.5.0 → 0.10.2

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