Skip to content

Commit b97491a

Browse files
committed
[STORE] Added a update method for the repository Store module
Allows to use Elasticsearch's "Update API" to update the document either with partial document or a script. Examples in tests and the README.
1 parent c44c343 commit b97491a

File tree

6 files changed

+266
-2
lines changed

6 files changed

+266
-2
lines changed

Diff for: elasticsearch-persistence/README.md

+16
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,22 @@ repository.save(note)
322322
# => {"_index"=>"notes_development", "_type"=>"my_note", "_id"=>"1", "_version"=>1, "created"=>true}
323323
```
324324

325+
The `update` method allows you to perform a partial update of a document in the repository.
326+
Use either a partial document:
327+
328+
```ruby
329+
repository.update id: 1, title: 'UPDATED', tags: []
330+
# => {"_index"=>"notes_development", "_type"=>"note", "_id"=>"1", "_version"=>2}
331+
```
332+
333+
Or a script (optionally with parameters):
334+
335+
```ruby
336+
repository.update 1, script: 'if (!ctx._source.tags.contains(t)) { ctx._source.tags += t }', params: { t: 'foo' }
337+
# => {"_index"=>"notes_development", "_type"=>"note", "_id"=>"1", "_version"=>3}
338+
```
339+
340+
325341
The `delete` method allows to remove objects from the repository (pass either the object itself or its ID):
326342

327343
```ruby

Diff for: elasticsearch-persistence/lib/elasticsearch/persistence/repository/store.rb

+35
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,41 @@ def save(document, options={})
1515
client.index( { index: index_name, type: type, id: id, body: serialized }.merge(options) )
1616
end
1717

18+
# Update the serialized object in Elasticsearch with partial data or script
19+
#
20+
def update(document, options={})
21+
case
22+
when document.is_a?(String) || document.is_a?(Integer)
23+
id = document
24+
when document.respond_to?(:to_hash)
25+
serialized = document.to_hash
26+
id = __extract_id_from_document(serialized)
27+
else
28+
raise ArgumentError, "Expected a document ID or a Hash-like object, #{document.class} given"
29+
end
30+
31+
type = options.delete(:type) || \
32+
(defined?(serialized) && serialized && serialized.delete(:type)) || \
33+
document_type || \
34+
__get_type_from_class(klass)
35+
36+
if defined?(serialized) && serialized
37+
body = if serialized[:script]
38+
serialized.select { |k, v| [:script, :params, :upsert].include? k }
39+
else
40+
{ doc: serialized }
41+
end
42+
else
43+
body = {}
44+
body.update( doc: options.delete(:doc)) if options[:doc]
45+
body.update( script: options.delete(:script)) if options[:script]
46+
body.update( params: options.delete(:params)) if options[:params]
47+
body.update( upsert: options.delete(:upsert)) if options[:upsert]
48+
end
49+
50+
client.update( { index: index_name, type: type, id: id, body: body }.merge(options) )
51+
end
52+
1853
# Remove the serialized object or document with specified ID from Elasticsearch
1954
#
2055
def delete(document, options={})

Diff for: elasticsearch-persistence/test/integration/repository/customized_class_test.rb

+14
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ def to_hash
4444
assert_not_nil Elasticsearch::Persistence.client.get index: 'my_notes', type: 'my_note', id: '1'
4545
end
4646

47+
should "update the document" do
48+
@repository.save Note.new(id: 1, title: 'Test')
49+
50+
@repository.update 1, doc: { title: 'UPDATED' }
51+
assert_equal 'UPDATED', @repository.find(1).attributes['title']
52+
end
53+
54+
should "update the document with a script" do
55+
@repository.save Note.new(id: 1, title: 'Test')
56+
57+
@repository.update 1, script: 'ctx._source.title = "UPDATED"'
58+
assert_equal 'UPDATED', @repository.find(1).attributes['title']
59+
end
60+
4761
should "delete the object" do
4862
note = My::Note.new id: 1, title: 'Test'
4963
@repository.save note

Diff for: elasticsearch-persistence/test/integration/repository/default_class_test.rb

+19
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,25 @@ def to_hash
3434
assert_equal 'Test', @repository.find(response['_id']).attributes['title']
3535
end
3636

37+
should "update the document" do
38+
@repository.save Note.new(id: 1, title: 'Test')
39+
40+
@repository.update 1, type: 'note', doc: { title: 'UPDATED' }
41+
assert_equal 'UPDATED', @repository.find(1).attributes['title']
42+
end
43+
44+
should "update the document with a script" do
45+
@repository.save Note.new(id: 1, title: 'Test')
46+
47+
@repository.update 1, type: 'note', script: 'ctx._source.title = "UPDATED"'
48+
assert_equal 'UPDATED', @repository.find(1).attributes['title']
49+
end
50+
51+
should "save the document with an upsert" do
52+
@repository.update 1, type: 'note', script: 'ctx._source.clicks += 1', upsert: { clicks: 1 }
53+
assert_equal 1, @repository.find(1).attributes['clicks']
54+
end
55+
3756
should "delete an object" do
3857
note = Note.new(id: '1', title: 'Test')
3958

Diff for: elasticsearch-persistence/test/integration/repository/virtus_model_test.rb

+70-1
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,28 @@ class RepositoryWithVirtusIntegrationTest < Elasticsearch::Test::IntegrationTest
99
class ::Page
1010
include Virtus.model
1111

12+
attribute :id, String, writer: :private
1213
attribute :title, String
1314
attribute :views, Integer, default: 0
1415
attribute :published, Boolean, default: false
15-
attribute :slug, String, default: lambda { |page, attribute| page.title.downcase.gsub(' ', '-') }
16+
attribute :slug, String, default: lambda { |page, attr| page.title.downcase.gsub(' ', '-') }
17+
18+
def set_id(id)
19+
self.id = id
20+
end
1621
end
1722

1823
context "The repository with a Virtus model" do
1924
setup do
2025
@repository = Elasticsearch::Persistence::Repository.new do
2126
index :pages
27+
klass Page
28+
29+
def deserialize(document)
30+
page = klass.new document['_source']
31+
page.set_id document['_id']
32+
page
33+
end
2234
end
2335
end
2436

@@ -38,6 +50,63 @@ class ::Page
3850
type: 'page',
3951
id: id
4052
end
53+
54+
should "update the object with a partial document" do
55+
response = @repository.save Page.new(title: 'Test')
56+
id = response['_id']
57+
58+
page = @repository.find(id)
59+
60+
assert_equal 'Test', page.title
61+
62+
@repository.update page.id, doc: { title: 'UPDATE' }
63+
64+
page = @repository.find(id)
65+
assert_equal 'UPDATE', page.title
66+
end
67+
68+
should "update the object with a Hash" do
69+
response = @repository.save Page.new(title: 'Test')
70+
id = response['_id']
71+
72+
page = @repository.find(id)
73+
74+
assert_equal 'Test', page.title
75+
76+
@repository.update id: page.id, title: 'UPDATE'
77+
78+
page = @repository.find(id)
79+
assert_equal 'UPDATE', page.title
80+
end
81+
82+
should "update the object with a script" do
83+
response = @repository.save Page.new(title: 'Test Page')
84+
id = response['_id']
85+
86+
page = @repository.find(id)
87+
88+
assert_not_nil page.id
89+
assert_equal 0, page.views
90+
91+
@repository.update page.id, script: 'ctx._source.views += 1'
92+
93+
page = @repository.find(id)
94+
assert_equal 1, page.views
95+
96+
@repository.update id: page.id, script: 'ctx._source.views += 1'
97+
98+
page = @repository.find(id)
99+
assert_equal 2, page.views
100+
end
101+
102+
should "update the object with a script and params" do
103+
response = @repository.save Page.new(title: 'Test Page')
104+
105+
@repository.update id: response['_id'], script: 'ctx._source.views += count', params: { count: 3 }
106+
107+
page = @repository.find(response['_id'])
108+
assert_equal 3, page.views
109+
end
41110
end
42111

43112
end

Diff for: elasticsearch-persistence/test/unit/repository_store_test.rb

+112-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ class Elasticsearch::Persistence::RepositoryStoreTest < Test::Unit::TestCase
55
class MyDocument; end
66

77
setup do
8-
@shoulda_subject = Class.new() { include Elasticsearch::Persistence::Repository::Store }.new
8+
@shoulda_subject = Class.new() do
9+
include Elasticsearch::Persistence::Repository::Store
10+
include Elasticsearch::Persistence::Repository::Naming
11+
end.new
912
@shoulda_subject.stubs(:index_name).returns('test')
1013
end
1114

@@ -85,6 +88,114 @@ class MyDocument; end
8588
end
8689
end
8790

91+
context "update" do
92+
should "get the ID from first argument and :doc from options" do
93+
subject.expects(:serialize).never
94+
subject.expects(:document_type).returns('mydoc')
95+
subject.expects(:__extract_id_from_document).never
96+
97+
client = mock
98+
client.expects(:update).with do |arguments|
99+
assert_equal '1', arguments[:id]
100+
assert_equal 'mydoc', arguments[:type]
101+
assert_equal({doc: { foo: 'bar' }}, arguments[:body])
102+
end
103+
subject.expects(:client).returns(client)
104+
105+
subject.update('1', doc: { foo: 'bar' })
106+
end
107+
108+
should "get the ID from first argument and :script from options" do
109+
subject.expects(:document_type).returns('mydoc')
110+
subject.expects(:__extract_id_from_document).never
111+
112+
client = mock
113+
client.expects(:update).with do |arguments|
114+
assert_equal '1', arguments[:id]
115+
assert_equal 'mydoc', arguments[:type]
116+
assert_equal({script: 'ctx._source.foo += 1'}, arguments[:body])
117+
end
118+
subject.expects(:client).returns(client)
119+
120+
subject.update('1', script: 'ctx._source.foo += 1')
121+
end
122+
123+
should "get the ID from first argument and :script with :upsert from options" do
124+
subject.expects(:document_type).returns('mydoc')
125+
subject.expects(:__extract_id_from_document).never
126+
127+
client = mock
128+
client.expects(:update).with do |arguments|
129+
assert_equal '1', arguments[:id]
130+
assert_equal 'mydoc', arguments[:type]
131+
assert_equal({script: 'ctx._source.foo += 1', upsert: { foo: 1 }}, arguments[:body])
132+
end
133+
subject.expects(:client).returns(client)
134+
135+
subject.update('1', script: 'ctx._source.foo += 1', upsert: { foo: 1 })
136+
end
137+
138+
should "get the ID and :doc from document" do
139+
subject.expects(:document_type).returns('mydoc')
140+
141+
client = mock
142+
client.expects(:update).with do |arguments|
143+
assert_equal '1', arguments[:id]
144+
assert_equal 'mydoc', arguments[:type]
145+
assert_equal({doc: { foo: 'bar' }}, arguments[:body])
146+
end
147+
subject.expects(:client).returns(client)
148+
149+
subject.update(id: '1', foo: 'bar')
150+
end
151+
152+
should "get the ID and :script from document" do
153+
subject.expects(:document_type).returns('mydoc')
154+
155+
client = mock
156+
client.expects(:update).with do |arguments|
157+
assert_equal '1', arguments[:id]
158+
assert_equal 'mydoc', arguments[:type]
159+
assert_equal({script: 'ctx._source.foo += 1'}, arguments[:body])
160+
end
161+
subject.expects(:client).returns(client)
162+
163+
subject.update(id: '1', script: 'ctx._source.foo += 1')
164+
end
165+
166+
should "get the ID and :script with :upsert from document" do
167+
subject.expects(:document_type).returns('mydoc')
168+
169+
client = mock
170+
client.expects(:update).with do |arguments|
171+
assert_equal '1', arguments[:id]
172+
assert_equal 'mydoc', arguments[:type]
173+
assert_equal({script: 'ctx._source.foo += 1', upsert: { foo: 1 } }, arguments[:body])
174+
end
175+
subject.expects(:client).returns(client)
176+
177+
subject.update(id: '1', script: 'ctx._source.foo += 1', upsert: { foo: 1 })
178+
end
179+
180+
should "override the type from params" do
181+
subject.expects(:document_type).never
182+
183+
client = mock
184+
client.expects(:update).with do |arguments|
185+
assert_equal '1', arguments[:id]
186+
assert_equal 'foo', arguments[:type]
187+
assert_equal({script: 'ctx._source.foo += 1'}, arguments[:body])
188+
end
189+
subject.expects(:client).returns(client)
190+
191+
subject.update(id: '1', script: 'ctx._source.foo += 1', type: 'foo')
192+
end
193+
194+
should "raise an exception when passed incorrect argument" do
195+
assert_raise(ArgumentError) { subject.update(MyDocument.new, foo: 'bar') }
196+
end
197+
end
198+
88199
context "delete" do
89200
should "get type from klass when passed only ID" do
90201
subject.expects(:serialize).never

0 commit comments

Comments
 (0)