From 808a80962f9f97387228bcdede5170902cf4eda1 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Fri, 13 Feb 2026 15:41:40 +0100 Subject: [PATCH 1/2] [JAY-737] Add the Stats::Index::Totals class The class contains information about an index's total metrics, for example, total number of documents, total size, etc. At the time of writing it only returns three metrics, total number of documents (#docs_count), total number of deleted documents (#deleted_docs) and the ratio between these two (#deleted_ratio). --- CHANGELOG.md | 3 + .../elasticsearch/stats/index/totals.rb | 57 ++++++++ .../elasticsearch/stats/index/totals_spec.rb | 124 ++++++++++++++++++ 3 files changed, 184 insertions(+) create mode 100644 lib/jay_api/elasticsearch/stats/index/totals.rb create mode 100644 spec/jay_api/elasticsearch/stats/index/totals_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index b53adcb..4a6a3ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ Please mark backwards incompatible changes with an exclamation mark at the start when `active_support/core_ext/string` hadn't been loaded. ### Added +- The `Elasticsearch::Stats::Index::Totals` class. The class contains information + about an index's total metrics, for example, total number of documents, total + size, etc. - The `#settings` method to the `Elasticsearch::Index` class. This gives the caller access to the index's settings. - The `Elasticsearch::Indices::Settings::Blocks` class. The class encapsulates diff --git a/lib/jay_api/elasticsearch/stats/index/totals.rb b/lib/jay_api/elasticsearch/stats/index/totals.rb new file mode 100644 index 0000000..4bfba42 --- /dev/null +++ b/lib/jay_api/elasticsearch/stats/index/totals.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module JayAPI + module Elasticsearch + class Stats + class Index + # Contains information about an index's totals (docs, used space, etc). + class Totals + # @param [Hash] data The data under the index's +total+ key. + def initialize(data) + @data = data + end + + # @return [Integer] The total number of documents in the index. + def docs_count + @docs_count ||= docs.fetch('count') + end + + # @return [Integer] The total number of deleted documents in the index. + def deleted_docs + @deleted_docs ||= docs.fetch('deleted') + end + + # @return [Float] A number between 0 and 1 that represents the ratio + # of between deleted documents and total documents in the index. + def deleted_ratio + @deleted_ratio ||= calculate_deleted_ratio + end + + private + + attr_reader :data + + # @return [Hash] The information about the documents in the index. + # Looks something like this: + # + # { "count" => 530626, "deleted" => 11 } + # + # @raise [KeyError] If the given data doesn't have a +docs+ key. + def docs + @docs ||= data.fetch('docs') + end + + # @return [Float] A number between 0 and 1 that represents the ratio + # of between deleted documents and total documents in the index. + def calculate_deleted_ratio + if docs_count.zero? + return deleted_docs.zero? ? 0.0 : 1.0 + end + + deleted_docs / docs_count.to_f + end + end + end + end + end +end diff --git a/spec/jay_api/elasticsearch/stats/index/totals_spec.rb b/spec/jay_api/elasticsearch/stats/index/totals_spec.rb new file mode 100644 index 0000000..264e92d --- /dev/null +++ b/spec/jay_api/elasticsearch/stats/index/totals_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/stats/index/totals' + +RSpec.describe JayAPI::Elasticsearch::Stats::Index::Totals do + subject(:totals) { described_class.new(data) } + + let(:docs) do + { 'count' => 530_626, 'deleted' => 11 } + end + + let(:data) do + { + 'docs' => docs, + 'store' => { 'size_in_bytes' => 1_001_425_875, 'reserved_in_bytes' => 0 }, + 'flush' => { 'total' => 25, 'periodic' => 0, 'total_time_in_millis' => 1924 }, + 'warmer' => { 'current' => 0, 'total' => 30, 'total_time_in_millis' => 0 }, + 'completion' => { 'size_in_bytes' => 0 } + } + end + + shared_examples_for '#docs' do + context "when the data doesn't have a 'docs' key" do + let(:data) { super().except('docs') } + + it 'raises a KeyError' do + expect { method_call }.to raise_error( + KeyError, 'key not found: "docs"' + ) + end + end + end + + shared_examples_for '#docs_count' do + it_behaves_like '#docs' + + context "when the data doesn't contain the total number of documents" do + let(:docs) { super().except('count') } + + it 'raises a KeyError' do + expect { method_call }.to raise_error( + KeyError, 'key not found: "count"' + ) + end + end + end + + describe '#docs_count' do + subject(:method_call) { totals.docs_count } + + it_behaves_like '#docs_count' + + it 'returns the expected number of documents' do + expect(method_call).to eq(530_626) + end + end + + shared_examples_for '#deleted_docs' do + it_behaves_like '#docs' + + context "when the data doesn't contain the number of deleted documents" do + let(:docs) { super().except('deleted') } + + it 'raises a KeyError' do + expect { method_call }.to raise_error( + KeyError, 'key not found: "deleted"' + ) + end + end + end + + describe '#deleted_docs' do + subject(:method_call) { totals.deleted_docs } + + it_behaves_like '#deleted_docs' + + it 'returns the expected number of deleted documents' do + expect(method_call).to eq(11) + end + end + + describe '#deleted_ratio' do + subject(:method_call) { totals.deleted_ratio } + + it_behaves_like '#docs_count' + it_behaves_like '#deleted_docs' + + context 'when the index has no documents' do + let(:docs) { super().merge('count' => 0) } + + context 'when the index has no deleted documents' do + let(:docs) { super().merge('deleted' => 0) } + + it 'returns 0' do + expect(method_call).to be_zero + end + end + + context 'when the index has deleted documents' do + it 'returns 1' do + expect(method_call).to be(1.0) + end + end + end + + context 'when the index has documents' do + context 'when the index has no deleted documents' do + let(:docs) { super().merge('deleted' => 0) } + + it 'returns 0' do + expect(method_call).to be_zero + end + end + + context 'when the index has deleted documents' do + let(:docs) { super().merge('deleted' => 10_636) } + + it 'returns the expected ratio' do + expect(method_call).to be_within(0.0001).of(0.02) + end + end + end + end +end From 64458f6903bbffea64de8fe5fbf119380638b20f Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Fri, 13 Feb 2026 16:01:09 +0100 Subject: [PATCH 2/2] [JAY-737] Add the Stats::Index#totals method The method returns the index's total metrics like the number of documents, total size, etc. --- CHANGELOG.md | 2 + lib/jay_api/elasticsearch/stats/index.rb | 13 +++++ .../jay_api/elasticsearch/stats/index_spec.rb | 56 ++++++++++++++++++- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a6a3ab..634d62f 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 `#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 about an index's total metrics, for example, total number of documents, total size, etc. diff --git a/lib/jay_api/elasticsearch/stats/index.rb b/lib/jay_api/elasticsearch/stats/index.rb index 7c0eb2b..2915805 100644 --- a/lib/jay_api/elasticsearch/stats/index.rb +++ b/lib/jay_api/elasticsearch/stats/index.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative 'index/totals' + module JayAPI module Elasticsearch class Stats @@ -13,6 +15,17 @@ def initialize(name, data) @name = name @data = data end + + # @return [JayAPI::Elasticsearch::Stats::Index::Totals] Information + # about the index's total metrics. + # @raise [KeyError] If the given data doesn't have a +total+ key. + def totals + @totals ||= ::JayAPI::Elasticsearch::Stats::Index::Totals.new(data.fetch('total')) + end + + private + + attr_reader :data end end end diff --git a/spec/jay_api/elasticsearch/stats/index_spec.rb b/spec/jay_api/elasticsearch/stats/index_spec.rb index 252d14a..823aa0a 100644 --- a/spec/jay_api/elasticsearch/stats/index_spec.rb +++ b/spec/jay_api/elasticsearch/stats/index_spec.rb @@ -6,7 +6,20 @@ subject(:index) { described_class.new(name, data) } let(:name) { 'xyz01_integration_tests' } - let(:data) { {} } + + let(:data) do + { + 'uuid' => 'rouPqkZMSrKHY5bzL7OhTA', + 'primaries' => { + 'docs' => { 'count' => 265_313, 'deleted' => 0 }, + 'store' => { 'size_in_bytes' => 497_335_237, 'reserved_in_bytes' => 0 } + }, + 'total' => { + 'docs' => { 'count' => 530_626, 'deleted' => 11 }, + 'store' => { 'size_in_bytes' => 1_001_425_875, 'reserved_in_bytes' => 0 } + } + } + end describe '#initialize' do subject(:method_call) { index } @@ -16,4 +29,45 @@ expect(index.name).to be(name) end end + + describe '#totals' do + subject(:method_call) { index.totals } + + let(:totals_hash) do + { + 'docs' => { 'count' => 530_626, 'deleted' => 11 }, + 'store' => { 'size_in_bytes' => 1_001_425_875, 'reserved_in_bytes' => 0 } + } + end + + let(:totals) do + instance_double( + JayAPI::Elasticsearch::Stats::Index::Totals + ) + end + + before do + allow(JayAPI::Elasticsearch::Stats::Index::Totals) + .to receive(:new).and_return(totals) + end + + context "when the index's data doesn't contain the totals information" do + let(:data) { super().except('total') } + + it 'raises a KeyError' do + expect { method_call }.to raise_error( + KeyError, 'key not found: "total"' + ) + end + end + + it 'creates a new instance of the Totals class with the expected Hash' do + expect(JayAPI::Elasticsearch::Stats::Index::Totals).to receive(:new).with(totals_hash) + method_call + end + + it 'returns the instance of the Totals class' do + expect(method_call).to be(totals) + end + end end