module SearchFlip # @api private # # The SearchFlip::Bulk class implements the bulk support, ie it collects # single requests and emits batches of requests. # # @example # SearchFlip::Bulk.new "http://127.0.0.1:9200/index/type/_bulk" do |bulk| # bulk.create record.id, MyIndex.serialize(record) # bulk.index record.id, MyIndex.serialize(record), version: record.version, version_type: "external" # bulk.delete record.id, routing: record.user_id # bulk.update record.id, doc: MyIndex.serialize(record) # end class Bulk class Error < StandardError; end attr_reader :url, :options, :ignore_errors # @api private # # Builds and yields a new Bulk object, ie initiates the buffer, yields, # sends batches of records each time the buffer is full, and sends a final # batch after the yielded code returns and there are still documents # present within the buffer. # # @example Basic use # SearchFlip::Bulk.new "http://127.0.0.1:9200/index/type/_bulk" do |bulk| # # ... # end # # @example Ignore certain errors # SearchFlip::Bulk.new "http://127.0.0.1:9200/index/type/_bulk", 1_000, ignore_errors: [409] do |bulk| # # ... # end # # @param url [String] The endpoint to send bulk requests to # @param options [Hash] Options for the bulk requests # @option options bulk_limit [Fixnum] The maximum number of documents per bulk # request # @option bulk_max_mb [Fixnum] The maximum payload size in megabytes per # bulk request # @option options ignore_errors [Array, Fixnum] Errors that should be # ignored. If you eg want to ignore errors resulting from conflicts, # you can specify to ignore 409 here. # @option options raise [Boolean] If you want the bulk requests to never # raise any exceptions (fire and forget), you can pass false here. # Default is true. # @option options http_client [SearchFlip::HTTPClient] An optional http # client instance def initialize(url, options = {}) @url = url @options = options @http_client = options[:http_client] || SearchFlip::HTTPClient.new @ignore_errors = Array(options[:ignore_errors] || []).to_set @bulk_limit = options[:bulk_limit] || SearchFlip::Config[:bulk_limit] @bulk_max_mb = options[:bulk_max_mb] || SearchFlip::Config[:bulk_max_mb] @bulk_max_bytes = @bulk_max_mb * 1024 * 1024 init yield self upload if @num > 0 end # @api private # # Adds an index request to the bulk batch. # # @param id [Fixnum, String] The document/record id # @param json [String] The json document # @param options [options] Options for the index request, like eg routing # and versioning def index(id, object, options = {}) perform(:index, id, SearchFlip::JSON.generate(object), options) end # @api private # # Adds an index request to the bulk batch # # @see #index ruby2_keywords def import(*args) index(*args) end # @api private # # Adds a create request to the bulk batch. # # @param id [Fixnum, String] The document/record id # @param json [String] The json document # @param options [options] Options for the index request, like eg routing # and versioning def create(id, object, options = {}) perform(:create, id, SearchFlip::JSON.generate(object), options) end # @api private # # Adds a update request to the bulk batch. # # @param id [Fixnum, String] The document/record id # @param json [String] The json document # @param options [options] Options for the index request, like eg routing # and versioning def update(id, object, options = {}) perform(:update, id, SearchFlip::JSON.generate(object), options) end # @api private # # Adds a delete request to the bulk batch. # # @param id [Fixnum, String] The document/record id # @param options [options] Options for the index request, like eg routing # and versioning def delete(id, options = {}) perform(:delete, id, nil, options) end private def init @payload = "" @num = 0 end def upload response = @http_client .headers(accept: "application/json", content_type: "application/x-ndjson") .put(url, body: @payload) return if options[:raise] == false parsed_response = SearchFlip::JSON.parse(response.to_s) return unless parsed_response["errors"] parsed_response["items"].each do |item| item.each_value do |element| status = element["status"] next if status.between?(200, 299) next if ignore_errors.include?(status) raise SearchFlip::Bulk::Error, SearchFlip::JSON.generate(element) end end ensure init end def perform(action, id, json = nil, options = {}) new_payload = SearchFlip::JSON.generate(action => options.merge(_id: id)) new_payload << "\n" if json new_payload << json new_payload << "\n" end upload if @num > 0 && @payload.bytesize + new_payload.bytesize >= @bulk_max_bytes @payload << new_payload @num += 1 upload if @num >= @bulk_limit || @payload.bytesize >= @bulk_max_bytes end end end