elastictastic 0.5.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/LICENSE +19 -0
- data/README.md +326 -0
- data/lib/elastictastic/association.rb +21 -0
- data/lib/elastictastic/bulk_persistence_strategy.rb +70 -0
- data/lib/elastictastic/callbacks.rb +30 -0
- data/lib/elastictastic/child_collection_proxy.rb +56 -0
- data/lib/elastictastic/client.rb +101 -0
- data/lib/elastictastic/configuration.rb +35 -0
- data/lib/elastictastic/dirty.rb +130 -0
- data/lib/elastictastic/discrete_persistence_strategy.rb +52 -0
- data/lib/elastictastic/document.rb +98 -0
- data/lib/elastictastic/errors.rb +7 -0
- data/lib/elastictastic/field.rb +38 -0
- data/lib/elastictastic/index.rb +19 -0
- data/lib/elastictastic/mass_assignment_security.rb +15 -0
- data/lib/elastictastic/middleware.rb +119 -0
- data/lib/elastictastic/nested_document.rb +29 -0
- data/lib/elastictastic/new_relic_instrumentation.rb +26 -0
- data/lib/elastictastic/observer.rb +3 -0
- data/lib/elastictastic/observing.rb +21 -0
- data/lib/elastictastic/parent_child.rb +115 -0
- data/lib/elastictastic/persistence.rb +67 -0
- data/lib/elastictastic/properties.rb +236 -0
- data/lib/elastictastic/railtie.rb +35 -0
- data/lib/elastictastic/resource.rb +4 -0
- data/lib/elastictastic/scope.rb +283 -0
- data/lib/elastictastic/scope_builder.rb +32 -0
- data/lib/elastictastic/scoped.rb +20 -0
- data/lib/elastictastic/search.rb +180 -0
- data/lib/elastictastic/server_error.rb +15 -0
- data/lib/elastictastic/test_helpers.rb +172 -0
- data/lib/elastictastic/util.rb +63 -0
- data/lib/elastictastic/validations.rb +45 -0
- data/lib/elastictastic/version.rb +3 -0
- data/lib/elastictastic.rb +82 -0
- data/spec/environment.rb +6 -0
- data/spec/examples/active_model_lint_spec.rb +20 -0
- data/spec/examples/bulk_persistence_strategy_spec.rb +233 -0
- data/spec/examples/callbacks_spec.rb +96 -0
- data/spec/examples/dirty_spec.rb +238 -0
- data/spec/examples/document_spec.rb +600 -0
- data/spec/examples/mass_assignment_security_spec.rb +13 -0
- data/spec/examples/middleware_spec.rb +92 -0
- data/spec/examples/observing_spec.rb +141 -0
- data/spec/examples/parent_child_spec.rb +308 -0
- data/spec/examples/properties_spec.rb +92 -0
- data/spec/examples/scope_spec.rb +491 -0
- data/spec/examples/search_spec.rb +382 -0
- data/spec/examples/spec_helper.rb +15 -0
- data/spec/examples/validation_spec.rb +65 -0
- data/spec/models/author.rb +9 -0
- data/spec/models/blog.rb +5 -0
- data/spec/models/comment.rb +5 -0
- data/spec/models/post.rb +41 -0
- data/spec/models/post_observer.rb +11 -0
- data/spec/support/fakeweb_request_history.rb +13 -0
- metadata +227 -0
@@ -0,0 +1,382 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
#
|
4
|
+
# Here we describe the functionality for constructing queries, which is (sort of)
|
5
|
+
# what the Search module does. See spec/examples/scope_spec to test running the
|
6
|
+
# search and dealing with the results
|
7
|
+
#
|
8
|
+
describe Elastictastic::Search do
|
9
|
+
describe '#to_params' do
|
10
|
+
{
|
11
|
+
'query' => { 'match_all' => {} },
|
12
|
+
'filter' => { 'ids' => { 'values' => 1 }},
|
13
|
+
'from' => 10,
|
14
|
+
'sort' => { 'created_at' => 'desc' },
|
15
|
+
'highlight' => { 'fields' => { 'body' => {}}},
|
16
|
+
'fields' => %w(title body),
|
17
|
+
'script_fields' => { 'rtitle' => { 'script' => "_source.title.reverse()" }},
|
18
|
+
'preference' => '_local',
|
19
|
+
'facets' => { 'tags' => { 'terms' => { 'field' => 'tags' }}}
|
20
|
+
}.each_pair do |method, value|
|
21
|
+
it "should build scope for #{method} param" do
|
22
|
+
Post.__send__(method, value).params.should == { method => value }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should raise ArgumentError if no value passed' do
|
27
|
+
expect { Post.query }.to raise_error(ArgumentError)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should not cast single arg to an array' do
|
31
|
+
Post.fields('title').params.should == { 'fields' => 'title' }
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should take multiple args and convert them to an array' do
|
35
|
+
Post.fields('title', 'body').params.should ==
|
36
|
+
{ 'fields' => %w(title body) }
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should chain scopes' do
|
40
|
+
Post.from(30).size(15).params.should == { 'from' => 30, 'size' => 15 }
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should not chain destructively' do
|
44
|
+
scope = Post.from(20)
|
45
|
+
scope.size(10)
|
46
|
+
scope.params.should == { 'from' => 20 }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe 'merging' do
|
51
|
+
let(:scope) { Post }
|
52
|
+
|
53
|
+
it 'should merge two simple queries into a bool' do
|
54
|
+
scope.query { term(:title => 'Pizza') }.query { term(:comments_count => 0) }.params.should == {
|
55
|
+
'query' => {
|
56
|
+
'bool' => {
|
57
|
+
'must' => [
|
58
|
+
{ 'term' => { 'title' => 'Pizza' }},
|
59
|
+
{ 'term' => { 'comments_count' => 0 }}
|
60
|
+
]
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should merge three simple queries into a bool' do
|
67
|
+
scope = self.scope.query { term(:title => 'pizza') }
|
68
|
+
scope = scope.query { term(:comments_count => 1) }
|
69
|
+
scope = scope.query { term(:tags => 'delicious') }
|
70
|
+
scope.params.should == {
|
71
|
+
'query' => {
|
72
|
+
'bool' => {
|
73
|
+
'must' => [
|
74
|
+
{ 'term' => { 'title' => 'pizza' }},
|
75
|
+
{ 'term' => { 'comments_count' => 1 }},
|
76
|
+
{ 'term' => { 'tags' => 'delicious' }}
|
77
|
+
]
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should merge a new query into an existing boolean query' do
|
84
|
+
scope = self.scope.query { bool { must({ 'term' => { 'title' => 'pizza' }}, { 'term' => { 'comments_count' => 1 }}) }}
|
85
|
+
scope = scope.query { term(:tags => 'delicious') }
|
86
|
+
scope.params.should == {
|
87
|
+
'query' => {
|
88
|
+
'bool' => {
|
89
|
+
'must' => [
|
90
|
+
{ 'term' => { 'title' => 'pizza' }},
|
91
|
+
{ 'term' => { 'comments_count' => 1 }},
|
92
|
+
{ 'term' => { 'tags' => 'delicious' }}
|
93
|
+
]
|
94
|
+
}
|
95
|
+
}
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should merge a new query into an existing filtered query' do
|
100
|
+
scope = self.scope.query do
|
101
|
+
filtered do
|
102
|
+
query { term('title' => 'pizza') }
|
103
|
+
filter { term('comments_count' => 1) }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
scope = scope.query { term('tags' => 'delicious') }
|
107
|
+
scope.params['query'].should == {
|
108
|
+
'filtered' => {
|
109
|
+
'query' => { 'bool' => { 'must' => [{ 'term' => { 'title' => 'pizza' }}, { 'term' => { 'tags' => 'delicious' }}] }},
|
110
|
+
'filter' => { 'term' => { 'comments_count' => 1 }}
|
111
|
+
}
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'should merge two filtered queries' do
|
116
|
+
scope = self.scope.query do
|
117
|
+
filtered do
|
118
|
+
query { term('title' => 'pizza') }
|
119
|
+
filter { term('comments_count' => 1) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
scope = scope.query do
|
123
|
+
filtered do
|
124
|
+
query { term('title' => 'pepperoni') }
|
125
|
+
filter { term('tags' => 'delicious') }
|
126
|
+
end
|
127
|
+
end
|
128
|
+
scope.params.should == {
|
129
|
+
'query' => {
|
130
|
+
'filtered' => {
|
131
|
+
'query' => { 'bool' => { 'must' => [
|
132
|
+
{ 'term' => { 'title' => 'pizza' }},
|
133
|
+
{ 'term' => { 'title' => 'pepperoni' }}
|
134
|
+
]}},
|
135
|
+
'filter' => { 'and' => [
|
136
|
+
{ 'term' => { 'comments_count' => 1 }},
|
137
|
+
{ 'term' => { 'tags' => 'delicious' }}
|
138
|
+
]}
|
139
|
+
}
|
140
|
+
}
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'should merge a regular query with a constant-score filter query' do
|
145
|
+
scope = self.scope.query('term' => { 'title' => 'pizza' })
|
146
|
+
scope = scope.query { constant_score { filter { term('tags' => 'delicious') }}}
|
147
|
+
scope.params.should == {
|
148
|
+
'query' => {
|
149
|
+
'filtered' => {
|
150
|
+
'query' => { 'term' => { 'title' => 'pizza' }},
|
151
|
+
'filter' => { 'term' => { 'tags' => 'delicious' }}
|
152
|
+
}
|
153
|
+
}
|
154
|
+
}
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'should merge two constant-score filter queries' do
|
158
|
+
scope = self.scope.query { constant_score { filter { term('tags' => 'delicious') }}}
|
159
|
+
scope = scope.query { constant_score { filter { term('comments_count' => 0) }}}
|
160
|
+
scope.params.should == {
|
161
|
+
'query' => {
|
162
|
+
'constant_score' => {
|
163
|
+
'filter' => {
|
164
|
+
'and' => [
|
165
|
+
{ 'term' => { 'tags' => 'delicious' }},
|
166
|
+
{ 'term' => { 'comments_count' => 0 }}
|
167
|
+
]
|
168
|
+
}
|
169
|
+
}
|
170
|
+
}
|
171
|
+
}
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'should merge a constant-score filter query into a constant-score conjunction query' do
|
175
|
+
scope = self.scope.query { constant_score { filter('and' => [{ 'term' => { 'title' => 'pizza' }}, { 'term' => { 'tags' => 'delicious' }}]) }}
|
176
|
+
scope = scope.query { constant_score { filter { term('comments_count' => 0) }}}
|
177
|
+
scope.params.should == {
|
178
|
+
'query' => {
|
179
|
+
'constant_score' => {
|
180
|
+
'filter' => {
|
181
|
+
'and' => [
|
182
|
+
{ 'term' => { 'title' => 'pizza' }},
|
183
|
+
{ 'term' => { 'tags' => 'delicious' }},
|
184
|
+
{ 'term' => { 'comments_count' => 0 }}
|
185
|
+
]
|
186
|
+
}
|
187
|
+
}
|
188
|
+
}
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'should merge two filters' do
|
193
|
+
scope = self.scope.filter('term' => { 'comments_count' => 0 })
|
194
|
+
scope = scope.filter('term' => { 'tags' => 'delicious' })
|
195
|
+
scope.params.should == {
|
196
|
+
'filter' => { 'and' => [
|
197
|
+
{ 'term' => { 'comments_count' => 0 }},
|
198
|
+
{ 'term' => { 'tags' => 'delicious' }}
|
199
|
+
]}
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'should merge filter into conjunction' do
|
204
|
+
scope = self.scope.filter('and' => [{ 'term' => { 'title' => 'pizza' }}, { 'term' => { 'comments_count' => 0 }}])
|
205
|
+
scope = scope.filter('term' => { 'tags' => 'delicious' })
|
206
|
+
scope.params.should == {
|
207
|
+
'filter' => { 'and' => [
|
208
|
+
{ 'term' => { 'title' => 'pizza' }},
|
209
|
+
{ 'term' => { 'comments_count' => 0 }},
|
210
|
+
{ 'term' => { 'tags' => 'delicious' }}
|
211
|
+
]}
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'should override from when chaining' do
|
216
|
+
scope.from(10).from(20).params.should == { 'from' => 20 }
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'should not override from when not given' do
|
220
|
+
scope.from(10).size(10).params.should == { 'from' => 10, 'size' => 10 }
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'should override size when chaining' do
|
224
|
+
scope.size(10).size(15).params.should == { 'size' => 15 }
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'should concatenate chained sorts' do
|
228
|
+
scope.sort('title' => 'asc').sort('comments_count' => 'desc').params.should == {
|
229
|
+
'sort' => [{ 'title' => 'asc' }, { 'comments_count' => 'desc' }]
|
230
|
+
}
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'should concatenate chained sort onto multiple sorts' do
|
234
|
+
scope.sort({ 'title' => 'asc' }, { 'comments_count' => 'desc' }).sort('created_at' => 'desc').params.should == {
|
235
|
+
'sort' => [{ 'title' => 'asc' }, { 'comments_count' => 'desc' }, { 'created_at' => 'desc' }]
|
236
|
+
}
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'should merge highlight fields' do
|
240
|
+
scope.highlight { fields('title' => {}) }.highlight { fields('tags' => {}) }.params.should == {
|
241
|
+
'highlight' => { 'fields' => { 'title' => {}, 'tags' => {} }}
|
242
|
+
}
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'should move global highlight settings into fields when merging' do
|
246
|
+
scope = self.scope.highlight do
|
247
|
+
fields('title' => {})
|
248
|
+
number_of_fragments(0)
|
249
|
+
end
|
250
|
+
scope = scope.highlight do
|
251
|
+
fields('tags' => {})
|
252
|
+
number_of_fragments(1)
|
253
|
+
end
|
254
|
+
scope.params.should == {
|
255
|
+
'highlight' => {
|
256
|
+
'fields' => {
|
257
|
+
'title' => { 'number_of_fragments' => 0 },
|
258
|
+
'tags' => { 'number_of_fragments' => 1 }
|
259
|
+
}
|
260
|
+
}
|
261
|
+
}
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'should not override field-specific highlight settings when moving global settings' do
|
265
|
+
scope = self.scope.highlight do
|
266
|
+
fields(:title => { :number_of_fragments => 0 }, :tags => {})
|
267
|
+
number_of_fragments 1
|
268
|
+
end
|
269
|
+
scope = scope.highlight do
|
270
|
+
fields('comments.body' => {})
|
271
|
+
number_of_fragments 2
|
272
|
+
end
|
273
|
+
scope.params.should == {
|
274
|
+
'highlight' => {
|
275
|
+
'fields' => {
|
276
|
+
'title' => { 'number_of_fragments' => 0 },
|
277
|
+
'tags' => { 'number_of_fragments' => 1 },
|
278
|
+
'comments.body' => { 'number_of_fragments' => 2 }
|
279
|
+
}
|
280
|
+
}
|
281
|
+
}
|
282
|
+
end
|
283
|
+
|
284
|
+
it 'should concatenate fields' do
|
285
|
+
scope.fields('title').fields('tags').params.should == {
|
286
|
+
'fields' => %w(title tags)
|
287
|
+
}
|
288
|
+
end
|
289
|
+
|
290
|
+
it 'should concatenate onto arrays of fields' do
|
291
|
+
scope.fields('title', 'comments.body').fields('tags').params.should == {
|
292
|
+
'fields' => %w(title comments.body tags)
|
293
|
+
}
|
294
|
+
end
|
295
|
+
|
296
|
+
it 'should merge script fields' do
|
297
|
+
scope.script_fields(:test1 => { 'script' => '1' }).script_fields(:test2 => { 'script' => '2' }).params.should == {
|
298
|
+
'script_fields' => { 'test1' => { 'script' => '1' }, 'test2' => { 'script' => '2' }}
|
299
|
+
}
|
300
|
+
end
|
301
|
+
|
302
|
+
it 'should overwrite chained preference' do
|
303
|
+
scope.preference('_local').preference('_primary').params.should ==
|
304
|
+
{ 'preference' => '_primary' }
|
305
|
+
end
|
306
|
+
|
307
|
+
it 'should merge facets' do
|
308
|
+
scope = self.scope.facets(:title => { :terms => { :field => 'title' }})
|
309
|
+
scope = scope.facets(:tags => { :terms => { :field => 'tags' }})
|
310
|
+
scope.params.should == {
|
311
|
+
'facets' => {
|
312
|
+
'title' => { 'terms' => { 'field' => 'title' }},
|
313
|
+
'tags' => { 'terms' => { 'field' => 'tags' }}
|
314
|
+
}
|
315
|
+
}
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
describe 'block builder' do
|
320
|
+
it 'should build scopes with a block' do
|
321
|
+
Post.sort { title 'asc' }.params.should ==
|
322
|
+
{ 'sort' => { 'title' => 'asc' }}
|
323
|
+
end
|
324
|
+
|
325
|
+
it 'should accept multiple calls within a block' do
|
326
|
+
scope = Post.sort do
|
327
|
+
title 'asc'
|
328
|
+
created_at 'desc'
|
329
|
+
end
|
330
|
+
scope.params.should ==
|
331
|
+
{ 'sort' => { 'title' => 'asc', 'created_at' => 'desc' }}
|
332
|
+
end
|
333
|
+
|
334
|
+
it 'should turn varargs into array' do
|
335
|
+
Post.highlight { fields 'body', 'comments.body' }.params.should ==
|
336
|
+
{ 'highlight' => { 'fields' => %w(body comments.body) }}
|
337
|
+
end
|
338
|
+
|
339
|
+
it 'should accept nested calls within a block' do
|
340
|
+
scope = Post.query do
|
341
|
+
query_string { query 'test testor' }
|
342
|
+
end
|
343
|
+
scope.params.should ==
|
344
|
+
{ 'query' => { 'query_string' => { 'query' => 'test testor' }}}
|
345
|
+
end
|
346
|
+
|
347
|
+
it 'should set value to empty object (Hash) if none passed' do
|
348
|
+
scope = Post.query { match_all }.params.should ==
|
349
|
+
{ 'query' => { 'match_all' => {} }}
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
describe 'class methods' do
|
354
|
+
let(:named_scope) do
|
355
|
+
Post.from(10).search_keywords('hey guy')
|
356
|
+
end
|
357
|
+
|
358
|
+
it 'should delegate to class singleton' do
|
359
|
+
named_scope.params['query'].should == {
|
360
|
+
'query_string' => { 'query' => 'hey guy', 'fields' => %w(title body) }
|
361
|
+
}
|
362
|
+
end
|
363
|
+
|
364
|
+
it 'should retain current scope' do
|
365
|
+
named_scope.params['from'].should == 10
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
describe 'class methods directly on type-in-index' do
|
370
|
+
let(:named_scope) { Post.in_index('my_index').search_keywords('hey guy') }
|
371
|
+
|
372
|
+
it 'should delegate to class singleton when called on type_in_index' do
|
373
|
+
named_scope.params['query'].should == {
|
374
|
+
'query_string' => { 'query' => 'hey guy', 'fields' => %w(title body) }
|
375
|
+
}
|
376
|
+
end
|
377
|
+
|
378
|
+
it 'should use proper index in scope' do
|
379
|
+
named_scope.index.name.should == 'my_index'
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.expand_path('../../environment', __FILE__)
|
2
|
+
require 'fakeweb'
|
3
|
+
|
4
|
+
require File.expand_path('../../support/fakeweb_request_history', __FILE__)
|
5
|
+
|
6
|
+
RSpec.configure do |config|
|
7
|
+
config.before(:all) do
|
8
|
+
FakeWeb.allow_net_connect = false
|
9
|
+
end
|
10
|
+
|
11
|
+
config.after(:each) do
|
12
|
+
FakeWeb.requests.clear
|
13
|
+
FakeWeb.clean_registry
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe Elastictastic::Validations do
|
4
|
+
include Elastictastic::TestHelpers
|
5
|
+
|
6
|
+
let(:post) { Post.new(:title => 'INVALID') }
|
7
|
+
|
8
|
+
describe 'with invalid data' do
|
9
|
+
it 'should not be valid' do
|
10
|
+
post.should_not be_valid
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should not persist to ElasticSearch on save' do
|
14
|
+
expect { post.save }.to_not raise_error(FakeWeb::NetConnectNotAllowedError)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should return false for save' do
|
18
|
+
post.save.should be_false
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should raise Elastictastic::RecordInvalid for save!' do
|
22
|
+
expect { post.save! }.to raise_error(Elastictastic::RecordInvalid)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe 'with valid data' do
|
27
|
+
let(:post) { Post.new }
|
28
|
+
|
29
|
+
before { stub_elasticsearch_create('default', 'post') }
|
30
|
+
|
31
|
+
it 'should be valid' do
|
32
|
+
post.should be_valid
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should persist to ElasticSearch on save' do
|
36
|
+
post.save
|
37
|
+
FakeWeb.last_request.should be
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should return true from save' do
|
41
|
+
post.save.should be_true
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'should persist to ElasticSearch without error on save!' do
|
45
|
+
post.save!
|
46
|
+
FakeWeb.last_request.should be
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'with invalid nested document' do
|
51
|
+
let(:post) do
|
52
|
+
Post.new.tap do |post|
|
53
|
+
post.author = Author.new(:name => 'INVALID')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should not be valid' do
|
58
|
+
post.should_not be_valid
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'should have an error for author' do
|
62
|
+
post.errors['author'].should be
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/spec/models/blog.rb
ADDED
data/spec/models/post.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
class Post
|
2
|
+
include Elastictastic::Document
|
3
|
+
|
4
|
+
field :title
|
5
|
+
field :comments_count, :type => 'integer'
|
6
|
+
field :tags, :index => 'analyzed' do
|
7
|
+
field :non_analyzed, :index => 'not_analyzed'
|
8
|
+
end
|
9
|
+
field :created_at, :type => 'date'
|
10
|
+
field :published_at, :type => 'date'
|
11
|
+
|
12
|
+
embed :author
|
13
|
+
embed :comments
|
14
|
+
|
15
|
+
belongs_to :blog
|
16
|
+
|
17
|
+
attr_accessible :title
|
18
|
+
|
19
|
+
validates :title, :exclusion => %w(INVALID)
|
20
|
+
|
21
|
+
def observers_that_ran
|
22
|
+
@observers_that_ran ||= Set[]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.search_keywords(keywords)
|
26
|
+
query do
|
27
|
+
query_string do
|
28
|
+
query(keywords)
|
29
|
+
fields 'title', 'body'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from_hash(hash)
|
35
|
+
new.tap do |post|
|
36
|
+
hash.each_pair do |field, value|
|
37
|
+
post.__send__("#{field}=", value)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class PostObserver < Elastictastic::Observer
|
2
|
+
%w(create update save destroy).each do |lifecycle|
|
3
|
+
%w(before after).each do |phase|
|
4
|
+
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
5
|
+
def #{phase}_#{lifecycle}(post)
|
6
|
+
post.observers_that_ran << :#{phase}_#{lifecycle}
|
7
|
+
end
|
8
|
+
RUBY
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|