Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.7.0 (2015-09-18)
- Add streaming bulk functionality via `bulk_stream_items`
- Make Delete by Query compatible with Elasticsearch 2.0

## 0.6.0 (2015-09-11)
- Support all URL parameters when using `Client.#scroll`
- BREAKING: Moved some `Scroller` reader methods into `Scroller.opts`
Expand Down
101 changes: 99 additions & 2 deletions lib/elastomer/client/bulk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,103 @@ def bulk( body = nil, params = nil )
end
end

# Stream bulk actions from an Enumerator.
#
# Examples
#
# ops = [
# [:index, document1, {:_type => "foo", :_id => 1}],
# [:create, document2],
# [:delete, {:_type => "bar", :_id => 42}]
# ]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a huge fan of the Bulk::index / Bulk::delete syntax here. My proposal is this:

ops = [
  [:index, document1, {:_type => "foo", :_id => 1}],
  [:create, document2],
  [:delete, {:_type => "bar", :_id => 42}]
]

The first item in the list is the bulk action to take. This corresponds to one of the action methods on the Bulk instance. The second item in the list is a document (or the params in the case of :delete). And thirdly comes the optional params.

Inside the Enumerator we have:

Enumerator.new do |yielder|
  ops.each do |action, *args|
    response = bulk_obj.send(action, *args)
    yielder.yield response unless response.nil?
  end
  response = bulk_obj.call
  yielder.yield response unless response.nil?
end

The Ruby send method is being used here to call the action method on the bulk_obj and passing the *args to the method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a pretty clever use of send to reuse the existing methods! I'll implement that now.

# bulk_stream_responses(ops, :index => 'default-index').each do |response|
# puts response
# end
#
# Returns an Enumerator of responses.
def bulk_stream_responses(ops, params = {})
bulk_obj = Bulk.new(self, params)

Enumerator.new do |yielder|
ops.each do |action, *args|
response = bulk_obj.send(action, *args)
yielder.yield response unless response.nil?
end

response = bulk_obj.call
yielder.yield response unless response.nil?
end
end

# Internal: Determine whether or not a response item has an HTTP status code
# in the range 200 to 299.
#
# item - The bulk response item
#
# Returns a boolean
def is_ok?(item)
item.values.first["status"].between?(200, 299)
end

# Stream bulk actions from an Enumerator and passes the response items to
# the given block.
#
# Examples
#
# ops = [
# [:index, document1, {:_type => "foo", :_id => 1}],
# [:create, document2],
# [:delete, {:_type => "bar", :_id => 42}]
# ]
# bulk_stream_items(ops, :index => 'default-index') do |item|
# puts item

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we document what the item typically looks like?

# end
#
# # return value:
# # {
# # "took" => 256,
# # "errors" => false,
# # "success" => 3,
# # "failure" => 0
# # }
#
# # sample response item:
# # {
# # "delete": {
# # "_index": "foo",
# # "_type": "bar",
# # "_id": "42",
# # "_version": 3,
# # "status": 200,
# # "found": true
# # }
# # }
#
# Returns a Hash of stats about items from the responses.
def bulk_stream_items(ops, params = {})
stats = {
"took" => 0,
"errors" => false,
"success" => 0,
"failure" => 0
}

bulk_stream_responses(ops, params).each do |response|
stats["took"] += response["took"]
stats["errors"] |= response["errors"]

response["items"].each do |item|
if is_ok?(item)
stats["success"] += 1
else
stats["failure"] += 1
end
yield item
end
end

stats
end

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a simpler way to essentially map over an Enumerator than by constructing a new one and yielding each element of the original, as I did here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thought on this method is to not return an Enumerator. Instead, accept a block and perform the iteration directly with this method.

def bulk_stream_items(ops, params = {})
  stats = {
    "took" => 0,
    "errors" => false,
    "success" => 0,
    "failure" => 0
  }

  bulk_stream_responses(ops, params).each do |response|
    stats["took"] += response["took"]
    stats["errors"] |= response["errors"]

    response["items"].each do |item|
      if is_ok?(item)
        stats["success"] += 1
      else
        stats["failure"] += 1
      end
      yield item
    end
  end

  return stats
end

I've also added a hash of interesting numbers as the return value from this method. The is_ok? method does not exist yet, but it would check the HTTP status returned with each item.

def is_ok?(item)
  item["status"].between?(200, 299)
end

Thoughts on this approach?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client might want to retry failed documents, in which case they would need access to at least the failed response items. Maybe it's worth having a separate convenient method called bulk_stream_stats which does exactly what you suggest.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client does have access to the failed items during the iteration. When yield item is called, the client can also check the status code of that particular operation.

Returning the stats hash is there just to be nice to the end user.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My apologies, I read over your code too quickly and missed that yield item 🙇

I like the convenience of returning stats, and the block syntax would look almost the same as calling each on the Enumerator. I'll implement it like you showed.


# The Bulk class provides some abstractions and helper methods for working
# with the ElasticSearch bulk API command. Instances of the Bulk class
Expand Down Expand Up @@ -156,7 +253,7 @@ def create( document, params )
end

# Add an update action to the list of bulk actions to be performed when
# the bulk API call is made. Parameters can be provided in the parameters
# the bulk API call is made. Parameters can be provided in the parameters
# hash (underscore prefix optional) or in the document hash (underscore
# prefix required).
#
Expand All @@ -178,7 +275,7 @@ def update( document, params )
# the bulk API call is made.
#
# params - Parameters for the delete action (as a Hash)
#
#
# Examples
# delete(:_id => 1, :_type => 'foo')
#
Expand Down
31 changes: 12 additions & 19 deletions lib/elastomer/client/delete_by_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,35 +90,28 @@ def categorize(item)
}
end

# Internal: Combine the items in a response with the existing statistics
# Internal: Combine a response item with the existing statistics
#
# response - A bulk response
def accumulate(response)
unless response.nil?
@response_stats['took'] += response['took']

response['items'].each do |item|
item = item['delete']
(@response_stats['_indices'][item['_index']] ||= {}).merge!(categorize(item)) { |_, n, m| n + m }
@response_stats['_indices']['_all'].merge!(categorize(item)) { |_, n, m| n + m }
@response_stats['failures'] << item unless is_ok? item['status']
end
end
# item - A bulk response item
def accumulate(item)
item = item["delete"]
(@response_stats['_indices'][item['_index']] ||= {}).merge!(categorize(item)) { |_, n, m| n + m }
@response_stats['_indices']['_all'].merge!(categorize(item)) { |_, n, m| n + m }
@response_stats['failures'] << item unless is_ok? item['status']
end

# Perform the Delete by Query action
#
# Returns a Hash of statistics about the bulk operation
def execute
# accumulate is called both inside and outside the bulk block in order
# to capture bulk responses returned from calls to `delete` and the call
# to `bulk`
accumulate(@client.bulk(@params) do |bulk|
ops = Enumerator.new do |yielder|
@client.scan(@query, @params).each_document do |hit|
accumulate(bulk.delete(_id: hit["_id"], _type: hit["_type"], _index: hit["_index"]))
yielder.yield([:delete, { _id: hit["_id"], _type: hit["_type"], _index: hit["_index"] }])
end
end)
end

stats = @client.bulk_stream_items(ops, @params) { |item| accumulate(item) }
@response_stats['took'] = stats['took']
@response_stats
end

Expand Down
2 changes: 1 addition & 1 deletion lib/elastomer/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module Elastomer
VERSION = '0.6.0'
VERSION = '0.7.0'

def self.version
VERSION
Expand Down
27 changes: 27 additions & 0 deletions test/client/bulk_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -293,4 +293,31 @@
assert_equal 'tweet', items[0]['index']['_type']
assert_equal 1, items[0]['index']['_version']
end

it 'streams bulk responses' do
ops = [
[:index, { :message => 'tweet 1' }, { :_id => 1, :_type => 'book', :_index => @index.name }],
[:index, { :message => 'tweet 2' }, { :_id => 2, :_type => 'book', :_index => @index.name }],
[:index, { :message => 'tweet 3' }, { :_id => 3, :_type => 'book', :_index => @index.name }]
]
responses = $client.bulk_stream_responses(ops, { :action_count => 2 }).to_a
assert_equal(2, responses.length)
assert_bulk_index(responses[0]["items"][0])
assert_bulk_index(responses[0]["items"][1])
assert_bulk_index(responses[1]["items"][0])
end

it 'streams bulk items' do
ops = [
[:index, { :message => 'tweet 1' }, { :_id => 1, :_type => 'book', :_index => @index.name }],
[:index, { :message => 'tweet 2' }, { :_id => 2, :_type => 'book', :_index => @index.name }],
[:index, { :message => 'tweet 3' }, { :_id => 3, :_type => 'book', :_index => @index.name }]
]
items = []
stats = $client.bulk_stream_items(ops, { :action_count => 2 }) { |item| items << item }
assert_equal(3, items.length)
assert_bulk_index(items[0])
assert_bulk_index(items[1])
assert_bulk_index(items[2])
end
end