elastictastic 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|