Skip to content

Commit c4b42b5

Browse files
AaronRustadkarmi
authored andcommitted
[MODEL] Added the ability to transform models during indexing
For instance, to add the `_parent` field to indexed documents: transform = lambda do |a| { index: {_id: a.id, _parent: a.author_id, data: a.__elasticsearch__.as_indexed_json} } end Article.import transform: transform Closes elastic#113
1 parent 750ccad commit c4b42b5

File tree

8 files changed

+114
-17
lines changed

8 files changed

+114
-17
lines changed

elasticsearch-model/lib/elasticsearch/model/adapters/active_record.rb

+5-4
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,15 @@ def __find_in_batches(options={}, &block)
8888
scope = named_scope ? self.__send__(named_scope) : self
8989

9090
scope.find_in_batches(options) do |batch|
91-
batch_for_bulk = batch.map { |a| { index: { _id: a.id, data: a.__elasticsearch__.as_indexed_json } } }
92-
yield batch_for_bulk
91+
yield batch
9392
end
9493
end
95-
end
9694

95+
def __transform
96+
lambda {|model| { index: { _id: model.id, data: model.__elasticsearch__.as_indexed_json } }}
97+
end
98+
end
9799
end
98-
99100
end
100101
end
101102
end

elasticsearch-model/lib/elasticsearch/model/adapters/default.rb

+6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ module Importing
3636
def __find_in_batches(options={}, &block)
3737
raise NotImplemented, "Method not implemented for default adapter"
3838
end
39+
40+
# @abstract Implement this method in your adapter
41+
#
42+
def __transform
43+
raise NotImplemented, "Method not implemented for default adapter"
44+
end
3945
end
4046

4147
end

elasticsearch-model/lib/elasticsearch/model/adapters/mongoid.rb

+6-4
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,19 @@ def __find_in_batches(options={}, &block)
7070
items << item
7171

7272
if items.length % options[:batch_size] == 0
73-
batch_for_bulk = items.map { |a| { index: { _id: a.id.to_s, data: a.as_indexed_json } } }
74-
yield batch_for_bulk
73+
yield items
7574
items = []
7675
end
7776
end
7877

7978
unless items.empty?
80-
batch_for_bulk = items.map { |a| { index: { _id: a.id.to_s, data: a.as_indexed_json } } }
81-
yield batch_for_bulk
79+
yield items
8280
end
8381
end
82+
83+
def __transform
84+
lambda {|a| { index: { _id: a.id.to_s, data: a.as_indexed_json } }}
85+
end
8486
end
8587

8688
end

elasticsearch-model/lib/elasticsearch/model/importing.rb

+20-4
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,24 @@ module ClassMethods
6868
#
6969
# Article.import scope: 'published'
7070
#
71+
# @example Customize how each record is [bulk imported](https://github.com/elasticsearch/elasticsearch-ruby/blob/master/elasticsearch-api/lib/elasticsearch/api/actions/bulk.rb)
72+
#
73+
# transform = lambda do |article|
74+
# {index: {_id: article.id, _parent: article.author_id, data: article.__elasticsearch__.as_indexed_json}}
75+
# end
76+
#
77+
# Article.import transform: transform
78+
#
7179
def import(options={}, &block)
7280
errors = 0
73-
refresh = options.delete(:refresh) || false
74-
target_index = options.delete(:index) || index_name
75-
target_type = options.delete(:type) || document_type
81+
refresh = options.delete(:refresh) || false
82+
target_index = options.delete(:index) || index_name
83+
target_type = options.delete(:type) || document_type
84+
transform = options.delete(:transform) || __transform
85+
86+
if !transform.respond_to?(:call)
87+
raise ArgumentError, "You must pass an object that supports #call method, #{transform.class} given"
88+
end
7689

7790
if options.delete(:force)
7891
self.create_index! force: true, index: target_index
@@ -82,7 +95,7 @@ def import(options={}, &block)
8295
response = client.bulk \
8396
index: target_index,
8497
type: target_type,
85-
body: batch
98+
body: __batch_to_bulk(batch, transform)
8699

87100
yield response if block_given?
88101

@@ -94,6 +107,9 @@ def import(options={}, &block)
94107
return errors
95108
end
96109

110+
def __batch_to_bulk(batch, transform)
111+
batch.map {|model| transform.call(model)}
112+
end
97113
end
98114

99115
end

elasticsearch-model/test/unit/adapter_active_record_test.rb

+14
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,20 @@ def ids
104104
DummyClassForActiveRecord.__find_in_batches(scope: :published) do; end
105105
end
106106

107+
context "when transforming models" do
108+
setup do
109+
@transform = DummyClassForActiveRecord.__transform
110+
end
111+
112+
should "provide an object that responds to #call" do
113+
assert_respond_to @transform, :call
114+
end
115+
116+
should "provide basic transformation" do
117+
model = mock("model", id: 1, __elasticsearch__: stub(as_indexed_json: {}))
118+
assert_equal @transform.call(model), { index: { _id: 1, data: {} } }
119+
end
120+
end
107121
end
108122
end
109123
end

elasticsearch-model/test/unit/adapter_default_test.rb

+14-4
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,21 @@ class ::DummyClassForDefaultAdapter; end
1919
assert_instance_of Module, Elasticsearch::Model::Adapter::Default::Callbacks
2020
end
2121

22-
should "have the default Importing implementation" do
23-
DummyClassForDefaultAdapter.__send__ :include, Elasticsearch::Model::Adapter::Default::Importing
22+
context "concerning abstract methods" do
23+
setup do
24+
DummyClassForDefaultAdapter.__send__ :include, Elasticsearch::Model::Adapter::Default::Importing
25+
end
26+
27+
should "have the default Importing implementation" do
28+
assert_raise Elasticsearch::Model::NotImplemented do
29+
DummyClassForDefaultAdapter.new.__find_in_batches
30+
end
31+
end
2432

25-
assert_raise Elasticsearch::Model::NotImplemented do
26-
DummyClassForDefaultAdapter.new.__find_in_batches
33+
should "have the default transform implementation" do
34+
assert_raise Elasticsearch::Model::NotImplemented do
35+
DummyClassForDefaultAdapter.new.__transform
36+
end
2737
end
2838
end
2939

elasticsearch-model/test/unit/adapter_mongoid_test.rb

+15
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@ def ids
8181
DummyClassForMongoid.__send__ :extend, Elasticsearch::Model::Adapter::Mongoid::Importing
8282
DummyClassForMongoid.__find_in_batches do; end
8383
end
84+
85+
context "when transforming models" do
86+
setup do
87+
@transform = DummyClassForMongoid.__transform
88+
end
89+
90+
should "provide an object that responds to #call" do
91+
assert_respond_to @transform, :call
92+
end
93+
94+
should "provide basic transformation" do
95+
model = mock("model", id: 1, as_indexed_json: {})
96+
assert_equal @transform.call(model), { index: { _id: "1", data: {} } }
97+
end
98+
end
8499
end
85100

86101
end

elasticsearch-model/test/unit/importing_test.rb

+34-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ module ImportingMixin
1010
def __find_in_batches(options={}, &block)
1111
yield if block_given?
1212
end
13+
def __transform
14+
lambda {|a|}
15+
end
1316
end
1417

1518
def importing_mixin
@@ -41,7 +44,7 @@ def importing_mixin
4144
DummyImportingModel.expects(:client).returns(client)
4245
DummyImportingModel.expects(:index_name).returns('foo')
4346
DummyImportingModel.expects(:document_type).returns('foo')
44-
47+
DummyImportingModel.stubs(:__batch_to_bulk)
4548
assert_equal 0, DummyImportingModel.import
4649
end
4750

@@ -58,6 +61,7 @@ def importing_mixin
5861
DummyImportingModel.stubs(:client).returns(client)
5962
DummyImportingModel.stubs(:index_name).returns('foo')
6063
DummyImportingModel.stubs(:document_type).returns('foo')
64+
DummyImportingModel.stubs(:__batch_to_bulk)
6165

6266
assert_equal 1, DummyImportingModel.import
6367
end
@@ -75,6 +79,7 @@ def importing_mixin
7579
DummyImportingModel.stubs(:client).returns(client)
7680
DummyImportingModel.stubs(:index_name).returns('foo')
7781
DummyImportingModel.stubs(:document_type).returns('foo')
82+
DummyImportingModel.stubs(:__batch_to_bulk)
7883

7984
DummyImportingModel.import do |response|
8085
assert_equal 2, response['items'].size
@@ -116,8 +121,36 @@ def importing_mixin
116121
.returns({'items' => [ {'index' => {} }]})
117122

118123
DummyImportingModel.stubs(:client).returns(client)
124+
DummyImportingModel.stubs(:__batch_to_bulk)
119125

120126
DummyImportingModel.import index: 'my-new-index', type: 'my-other-type'
121127
end
128+
129+
should "default to the adapter's bulk transform" do
130+
client = mock('client', bulk: {'items' => []})
131+
transform = lambda {|a|}
132+
133+
DummyImportingModel.stubs(:client).returns(client)
134+
DummyImportingModel.expects(:__transform).returns(transform)
135+
DummyImportingModel.expects(:__batch_to_bulk).with(anything, transform)
136+
137+
DummyImportingModel.import index: 'foo', type: 'bar'
138+
end
139+
140+
should "use the optioned transform" do
141+
client = mock('client', bulk: {'items' => []})
142+
transform = lambda {|a|}
143+
144+
DummyImportingModel.stubs(:client).returns(client)
145+
DummyImportingModel.expects(:__batch_to_bulk).with(anything, transform)
146+
147+
DummyImportingModel.import index: 'foo', type: 'bar', transform: transform
148+
end
149+
150+
should "raise an ArgumentError if transform is an object that doesn't respond to #call" do
151+
assert_raise ArgumentError do
152+
DummyImportingModel.import index: 'foo', type: 'bar', transform: "not_callable"
153+
end
154+
end
122155
end
123156
end

0 commit comments

Comments
 (0)