From daf690e054d023886d7b254a64a6a7da4ab779c6 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Mon, 31 Aug 2026 10:58:46 +0200 Subject: [PATCH] [ESRTEST-31237] Add the _source parameter to TopHits aggregation The parameter allows the user to specify which fields should be returned in the top hits aggregation results. This can help reduce the amount of data returned and improve performance when only specific fields are needed. false can also be given to get rid of the _source field in the results, which can be useful when the user is only interested in the document IDs. --- CHANGELOG.md | 1 + .../query_builder/aggregations/top_hits.rb | 16 +++- .../aggregations/top_hits_spec.rb | 92 +++++++++++++++++++ 3 files changed, 105 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e6b67c..803af73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Please mark backwards incompatible changes with an exclamation mark at the start ## [Unreleased] ### Added +- The `_source` parameter for Elasticsearch's `TopHits` aggregation. - The `sort` parameter for Elasticsearch's `TopHits` aggregation. - The `missing` parameter for Elasticsearch's `Terms` aggregation. diff --git a/lib/jay_api/elasticsearch/query_builder/aggregations/top_hits.rb b/lib/jay_api/elasticsearch/query_builder/aggregations/top_hits.rb index 4715e86..2f10596 100644 --- a/lib/jay_api/elasticsearch/query_builder/aggregations/top_hits.rb +++ b/lib/jay_api/elasticsearch/query_builder/aggregations/top_hits.rb @@ -15,7 +15,7 @@ class Aggregations # Information on this type of aggregation can be found here: # https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-top-hits-aggregation class TopHits < ::JayAPI::Elasticsearch::QueryBuilder::Aggregations::Aggregation - attr_reader :size, :sort + attr_reader :size, :sort, :_source # @param [String] name The name used by Elasticsearch to identify each # of the aggregations. @@ -23,16 +23,23 @@ class TopHits < ::JayAPI::Elasticsearch::QueryBuilder::Aggregations::Aggregation # @param [Hash] sort How to sort the documents to determine the top # hits. When not specified the documents are sorted by the score of # the main query. - def initialize(name, size:, sort: nil) + # @param [FalseClass, String, Array, Hash] _source Expression + # used for source filtering. + # @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#source-filtering + # Elasticsearch's documentation for more information on what kind of + # expressions are allowed. + # rubocop:disable-next Lint/UnderscorePrefixedVariableName -- to match Elasticsearch's name + def initialize(name, size:, sort: nil, _source: nil) super(name) @size = size @sort = sort + @_source = _source end # @return [self] A copy of the receiver. def clone - optional_params = { sort: sort.deep_dup }.compact + optional_params = { sort: sort.deep_dup, _source: _source.deep_dup }.compact copy = self.class.new(name, size: size, **optional_params) copy.aggregations = aggregations.clone copy @@ -45,7 +52,8 @@ def to_h { top_hits: { size: size, - sort: sort.deep_dup + sort: sort.deep_dup, + _source: _source.deep_dup }.compact } end diff --git a/spec/jay_api/elasticsearch/query_builder/aggregations/top_hits_spec.rb b/spec/jay_api/elasticsearch/query_builder/aggregations/top_hits_spec.rb index 877d257..0a4a160 100644 --- a/spec/jay_api/elasticsearch/query_builder/aggregations/top_hits_spec.rb +++ b/spec/jay_api/elasticsearch/query_builder/aggregations/top_hits_spec.rb @@ -47,6 +47,50 @@ end end + shared_examples_for "#clone when a '_source' has been given" do + it "has its '_source' set to the same value" do + expect(method_call._source).to eq(_source) + end + end + + shared_examples_for "#clone when a non-scalar '_source' has been given" do + it_behaves_like "#clone when a '_source' has been given" + + it 'is not the same object' do + expect(method_call._source).not_to be(top_hits._source) + end + end + + context "when a '_source' has been given" do + let(:constructor_params) { super().merge(_source:) } + + context "when '_source' is a boolean" do + let(:_source) { false } + + it_behaves_like "#clone when a '_source' has been given" + end + + context "when '_source' is a string" do + let(:_source) { 'meta_data.*' } + + it_behaves_like "#clone when a non-scalar '_source' has been given" + end + + context "when '_source' is an array" do + let(:_source) { %w[meta_data.* timestamp] } + + it_behaves_like "#clone when a non-scalar '_source' has been given" + end + + context "when '_source' is a hash" do + let(:_source) do + { includes: %i[date price] } + end + + it_behaves_like "#clone when a non-scalar '_source' has been given" + end + end + context 'when the original object has nested aggregations' do let(:cloned_aggregations) { instance_double(JayAPI::Elasticsearch::QueryBuilder::Aggregations) } @@ -110,5 +154,53 @@ expect(sort_hash).to eq(sort) end end + + context "when a '_source' has been given" do + let(:constructor_params) { super().merge(_source:) } + + context "when '_source' is a scalar" do + let(:_source) { false } + + let(:expected_hash) do + { + 'an_aggregation_sample' => { + top_hits: { + size: 1, + _source: false + } + } + } + end + + it 'returns the expected Hash' do + expect(method_call).to eq(expected_hash) + end + end + + context "when '_source' is not a scalar" do + let(:_source) { %w[meta_data.* timestamp] } + + let(:expected_hash) do + { + 'an_aggregation_sample' => { + top_hits: { + size: 1, + _source: %w[meta_data.* timestamp] + } + } + } + end + + it 'returns the expected Hash' do + expect(method_call).to eq(expected_hash) + end + + it "does not return a reference to the internal '_source' object" do + source_hash = method_call.dig('an_aggregation_sample', :top_hits, :_source) + expect(source_hash).not_to be(top_hits._source) + expect(source_hash).to eq(_source) + end + end + end end end