diff --git a/CHANGELOG.md b/CHANGELOG.md index 634d62f..6068615 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Please mark backwards incompatible changes with an exclamation mark at the start when `active_support/core_ext/string` hadn't been loaded. ### Added +- The `#force_merge` method to the `Elasticsearch::Index` class. This method + starts a Forced Segment Merge on the index. - The `#totals` method to `Elasticsearch::Stats::Index`, this gives the caller access to the index's total metrics. - The `Elasticsearch::Stats::Index::Totals` class. The class contains information diff --git a/lib/jay_api/elasticsearch/errors.rb b/lib/jay_api/elasticsearch/errors.rb index d0f4943..e953a0c 100644 --- a/lib/jay_api/elasticsearch/errors.rb +++ b/lib/jay_api/elasticsearch/errors.rb @@ -6,6 +6,7 @@ require_relative 'errors/query_execution_failure' require_relative 'errors/query_execution_timeout' require_relative 'errors/search_after_error' +require_relative 'errors/writable_index_error' module JayAPI module Elasticsearch diff --git a/lib/jay_api/elasticsearch/errors/writable_index_error.rb b/lib/jay_api/elasticsearch/errors/writable_index_error.rb new file mode 100644 index 0000000..b4463ff --- /dev/null +++ b/lib/jay_api/elasticsearch/errors/writable_index_error.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require_relative '../../errors/error' + +module JayAPI + module Elasticsearch + module Errors + # An error to be raised when an attempt is made to perform force_merge + # over an index which hasn't been set to be read-only. + class WritableIndexError < ::JayAPI::Errors::Error; end + end + end +end diff --git a/lib/jay_api/elasticsearch/index.rb b/lib/jay_api/elasticsearch/index.rb index 0999228..df15a97 100644 --- a/lib/jay_api/elasticsearch/index.rb +++ b/lib/jay_api/elasticsearch/index.rb @@ -2,6 +2,7 @@ require_relative 'indexable' require_relative 'indices/settings' +require_relative 'errors/writable_index_error' module JayAPI module Elasticsearch @@ -57,6 +58,29 @@ def settings # DO NOT MEMOIZE! Leave it to the caller. ::JayAPI::Elasticsearch::Indices::Settings.new(client.transport_client, index_name) end + + # Starts a Forced Segment Merge process on the index. + # + # ⚠️ For big indexes this process can take a very long time, make sure to + # adjust the timeout when creating the client. + # @param [Boolean] only_expunge_deletes Specifies whether the operation + # should only remove deleted documents. + # @raise [JayAPI::Elasticsearch::Errors::WritableIndexError] If the index + # is writable (hasn't been set to read-only). + # @return [Hash] A +Hash+ with the result of the index merging process, + # it looks like this: + # + # { "_shards" => { "total" => 10, "successful" => 10, "failed" => 0 } } + def force_merge(only_expunge_deletes: nil) + unless settings.blocks.write_blocked? + raise ::JayAPI::Elasticsearch::Errors::WritableIndexError, + "Write block for '#{index_name}' has not been enabled. " \ + "Please enable the index's write block before performing a segment merge" + end + + params = { index: index_name, only_expunge_deletes: }.compact + client.transport_client.indices.forcemerge(**params) + end end end end diff --git a/spec/jay_api/elasticsearch/index_spec.rb b/spec/jay_api/elasticsearch/index_spec.rb index d39cf52..c3ba7f7 100644 --- a/spec/jay_api/elasticsearch/index_spec.rb +++ b/spec/jay_api/elasticsearch/index_spec.rb @@ -231,6 +231,135 @@ end end + describe '#force_merge' do + subject(:method_call) { index.force_merge(**method_params) } + + let(:method_params) { {} } + + let(:write_blocked?) { true } + + let(:blocks) do + instance_double( + JayAPI::Elasticsearch::Indices::Settings::Blocks, + write_blocked?: write_blocked? + ) + end + + let(:settings) do + instance_double( + JayAPI::Elasticsearch::Indices::Settings, + blocks: blocks + ) + end + + let(:request_result) do + { '_shards' => { 'total' => 10, 'successful' => 10, 'failed' => 0 } } + end + + let(:indices_client) do + instance_double( + Elasticsearch::API::Indices::IndicesClient, + forcemerge: request_result + ) + end + + before do + allow(JayAPI::Elasticsearch::Indices::Settings).to receive(:new).and_return(settings) + allow(transport_client).to receive(:indices).and_return(indices_client) + end + + context 'when the fetching of the settings raises an HTTP error' do + let(:error) do + [ + Elasticsearch::Transport::Transport::Errors::RequestTimeout, + '408 - Timed out' + ] + end + + before do + allow(blocks).to receive(:write_blocked?).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + context 'when the fetching of the settings raises a KeyError' do + let(:error) { [KeyError, 'key not found: "blocks"'] } + + before do + allow(blocks).to receive(:write_blocked?).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + context 'when the index is not in read-only mode' do + let(:write_blocked?) { false } + + it 'raises a WritableIndexError' do + expect { method_call }.to raise_error( + JayAPI::Elasticsearch::Errors::WritableIndexError, + "Write block for 'elite_unit_tests' has not been enabled. " \ + "Please enable the index's write block before performing a segment merge" + ) + end + end + + context "when 'only_expunge_deletes' is omitted" do + it 'starts a Force Merge process for the index, with no additional parameters' do + expect(indices_client).to receive(:forcemerge).with(index: 'elite_unit_tests') + method_call + end + end + + context "when 'only_expunge_deletes' set to false" do + let(:method_params) { super().merge(only_expunge_deletes: false) } + + it 'starts a Force Merge process for the index, with only_expunge_deletes set to false' do + expect(indices_client).to receive(:forcemerge) + .with(index: 'elite_unit_tests', only_expunge_deletes: false) + + method_call + end + end + + context "when 'only_expunge_deletes' set to true" do + let(:method_params) { super().merge(only_expunge_deletes: true) } + + it 'starts a Force Merge process for the index, with only_expunge_deletes set to true' do + expect(indices_client).to receive(:forcemerge) + .with(index: 'elite_unit_tests', only_expunge_deletes: true) + + method_call + end + end + + context 'when the Force Merge API call fails' do + let(:error) do + [ + Elasticsearch::Transport::Transport::Errors::Forbidden, + '408 - You do not have permissions to perform this action' + ] + end + + before do + allow(indices_client).to receive(:forcemerge).and_raise(*error) + end + + it 're-raises the error' do + expect { method_call }.to raise_error(*error) + end + end + + it 'returns the Hash with the result of the request' do + expect(method_call).to be(request_result) + end + end + describe '#queue_size' do subject(:method_call) { index.queue_size }