Skip to content

Commit 6d6f6ee

Browse files
[ESRTEST-31237] Add 'sort' parameter to TopHits aggregation
The parameter allows the user to determine the order the documents are put into to determine what documents are the "top hits". By default, they are ordered by the score of the main query, however, that is not always what one might want. This parameter allows possibilities like sorting the top hits in chronological order or by price in ascending order, etc. These are things that cannot be reflected by the score of the main query even if it includes a "corresponding" sort clause.
1 parent 60a38ad commit 6d6f6ee

5 files changed

Lines changed: 89 additions & 16 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Please mark backwards incompatible changes with an exclamation mark at the start
99
## [Unreleased]
1010

1111
### Added
12+
- The `sort` parameter for Elasticsearch's `TopHits` aggregation.
1213
- The `missing` parameter for Elasticsearch's `Terms` aggregation.
1314

1415
## [29.10.0] - 2026-08-21

‎lib/jay_api/elasticsearch/query_builder/aggregations.rb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ def value_count(name, field:)
7070

7171
# Adds a +top_hits+ type aggregation. For more information about the parameters
7272
# @see JayAPI::Elasticsearch::QueryBuilder::Aggregations::TopHits#initialize
73-
def top_hits(name, size:)
74-
add(::JayAPI::Elasticsearch::QueryBuilder::Aggregations::TopHits.new(name, size: size))
73+
def top_hits(name, **params)
74+
add(::JayAPI::Elasticsearch::QueryBuilder::Aggregations::TopHits.new(name, **params))
7575
end
7676

7777
# Adds an +scripted_metric+ type aggregation. For information about the parameters

‎lib/jay_api/elasticsearch/query_builder/aggregations/top_hits.rb‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,25 @@ class Aggregations
1515
# Information on this type of aggregation can be found here:
1616
# https://www.elastic.co/docs/reference/aggregations/search-aggregations-metrics-top-hits-aggregation
1717
class TopHits < ::JayAPI::Elasticsearch::QueryBuilder::Aggregations::Aggregation
18-
attr_reader :size
18+
attr_reader :size, :sort
1919

2020
# @param [String] name The name used by Elasticsearch to identify each
2121
# of the aggregations.
2222
# @param [String] size The number of hits that will be returned.
23-
def initialize(name, size:)
23+
# @param [Hash] sort How to sort the documents to determine the top
24+
# hits. When not specified the documents are sorted by the score of
25+
# the main query.
26+
def initialize(name, size:, sort: nil)
2427
super(name)
2528

2629
@size = size
30+
@sort = sort
2731
end
2832

2933
# @return [self] A copy of the receiver.
3034
def clone
31-
copy = self.class.new(name, size: size)
35+
optional_params = { sort: sort.deep_dup }.compact
36+
copy = self.class.new(name, size: size, **optional_params)
3237
copy.aggregations = aggregations.clone
3338
copy
3439
end
@@ -39,8 +44,9 @@ def to_h
3944
super do
4045
{
4146
top_hits: {
42-
size: size
43-
}
47+
size: size,
48+
sort: sort.deep_dup
49+
}.compact
4450
}
4551
end
4652
end

‎spec/jay_api/elasticsearch/query_builder/aggregations/top_hits_spec.rb‎

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,25 @@
2828
expect(method_call.size).to be(top_hits.size)
2929
end
3030

31+
context "when no 'sort' has been given" do
32+
it "has its 'sort' set to nil" do
33+
expect(method_call.sort).to be_nil
34+
end
35+
end
36+
37+
context "when a 'sort' has been given" do
38+
let(:sort) { { timestamp: :desc } }
39+
let(:constructor_params) { super().merge(sort: sort) }
40+
41+
it "has its 'sort' set to the same value" do
42+
expect(method_call.sort).to eq(sort)
43+
end
44+
45+
it 'is not the same hash' do
46+
expect(method_call.sort).not_to be(top_hits.sort)
47+
end
48+
end
49+
3150
context 'when the original object has nested aggregations' do
3251
let(:cloned_aggregations) { instance_double(JayAPI::Elasticsearch::QueryBuilder::Aggregations) }
3352

@@ -50,18 +69,46 @@
5069
describe '#to_h' do
5170
subject(:method_call) { top_hits.to_h }
5271

53-
let(:expected_hash) do
54-
{
55-
'an_aggregation_sample' => {
56-
top_hits: { size: 1 }
72+
it_behaves_like 'JayAPI::Elasticsearch::QueryBuilder::Aggregations::Aggregation#to_h'
73+
74+
context "when no 'sort' has been given" do
75+
let(:expected_hash) do
76+
{
77+
'an_aggregation_sample' => {
78+
top_hits: { size: 1 }
79+
}
5780
}
58-
}
81+
end
82+
83+
it 'returns the expected Hash (does not include the :sort key)' do
84+
expect(method_call).to eq(expected_hash)
85+
end
5986
end
6087

61-
it_behaves_like 'JayAPI::Elasticsearch::QueryBuilder::Aggregations::Aggregation#to_h'
88+
context "when a 'sort' has been given" do
89+
let(:sort) { { timestamp: :desc } }
90+
let(:constructor_params) { super().merge(sort: sort) }
91+
92+
let(:expected_hash) do
93+
{
94+
'an_aggregation_sample' => {
95+
top_hits: {
96+
size: 1,
97+
sort: { timestamp: :desc }
98+
}
99+
}
100+
}
101+
end
62102

63-
it 'returns the expected Hash' do
64-
expect(method_call).to eq(expected_hash)
103+
it 'returns the expected Hash (includes the expected :sort key)' do
104+
expect(method_call).to eq(expected_hash)
105+
end
106+
107+
it "does not return a reference to the internal 'sort' Hash" do
108+
sort_hash = method_call.dig('an_aggregation_sample', :top_hits, :sort)
109+
expect(sort_hash).not_to be(top_hits.sort)
110+
expect(sort_hash).to eq(sort)
111+
end
65112
end
66113
end
67114
end

‎spec/jay_api/elasticsearch/query_builder/aggregations_spec.rb‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,12 +450,16 @@
450450

451451
describe '#top_hits' do
452452
subject(:method_call) do
453-
aggregations.top_hits(name, size: size)
453+
aggregations.top_hits(name, **method_params)
454454
end
455455

456456
let(:name) { 'recent_logs' }
457457
let(:size) { 10 }
458458

459+
let(:method_params) do
460+
{ size: }
461+
end
462+
459463
let(:top_hits) do
460464
instance_double(
461465
JayAPI::Elasticsearch::QueryBuilder::Aggregations::TopHits,
@@ -475,6 +479,21 @@
475479
method_call
476480
end
477481

482+
context "when a 'sort' parameter is given" do
483+
let(:sort) do
484+
{ date: { order: :desc } }
485+
end
486+
487+
let(:method_params) { super().merge(sort:) }
488+
489+
it 'creates the TopHits instance with the expected parameters' do
490+
expect(JayAPI::Elasticsearch::QueryBuilder::Aggregations::TopHits)
491+
.to receive(:new).with(name, size: size, sort: sort)
492+
493+
method_call
494+
end
495+
end
496+
478497
it 'adds the TopHits instance to the array of aggregations' do
479498
expect { method_call }.to change(aggregations, :to_h).to(aggs: { top_hits: 'TopHits#to_h' })
480499
end

0 commit comments

Comments
 (0)