diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f36fee..21bb931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ Please mark backwards incompatible changes with an exclamation mark at the start ## [Unreleased] +### Added +- `QueryBuilder#sort` can now receive either the direction of the sorting (`asc` + or `desc`) or a `Hash` with advanced sorting options. These are relayed + directly to Elasticsearch. + ## [29.0.0] - 2025-08-28 ### Changed diff --git a/documentation/source/user_guidelines/elasticsearch/query_builder.rst b/documentation/source/user_guidelines/elasticsearch/query_builder.rst index 2bdd0b8..5cbee84 100644 --- a/documentation/source/user_guidelines/elasticsearch/query_builder.rst +++ b/documentation/source/user_guidelines/elasticsearch/query_builder.rst @@ -101,6 +101,17 @@ aggregated into a single ``sort`` clause, for example: query_builder.sort(name: 'asc') query_builder.sort(age: 'desc') +For special cases or sorting needs it is also possible to pass a ``Hash`` with +advanced sorting options, for example: + +.. code-block:: ruby + + query_builder.sort(price: { order: :desc, missing: :_last }) + +For more information about what options can be passed in this ``Hash``, their +possible values and meanings, please see `Sort search results`_ from +Elasticsearch's documentation. + #collapse --------- @@ -497,3 +508,4 @@ Example: .. _`Range Query`: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html .. _`Terms Query`: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html .. _`Regexp Query`: https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html +.. _`Sort search results`: https://www.elastic.co/docs/reference/elasticsearch/rest-apis/sort-search-results diff --git a/lib/jay_api/elasticsearch/query_builder.rb b/lib/jay_api/elasticsearch/query_builder.rb index a3b821b..7fc822d 100644 --- a/lib/jay_api/elasticsearch/query_builder.rb +++ b/lib/jay_api/elasticsearch/query_builder.rb @@ -64,13 +64,22 @@ def size(size) # query_builder.sort(age: 'desc') # # Both will produce the same +sort+ clause. + # + # It is also possible to pass a Hash with advanced sorting options, for + # example: + # + # query_builder.sort(price: { order: :desc, missing: :_last }) + # # @param [Hash] sort A Hash whose keys are the name of the fields - # and the keys are the direction of the sorting, either +asc+ or - # +desc+. + # and whose values are either the direction of the sorting (+:asc+ or + # +:desc+) or a Hash with advanced sort options. + # @see https://www.elastic.co/docs/reference/elasticsearch/rest-apis/sort-search-results # @return [QueryBuilder] itself so that other methods can be chained. def sort(sort) check_argument(sort, 'sort', Hash) - @sort.merge!(sort) + @sort.merge!( + sort.transform_values { |value| value.is_a?(Hash) ? value : { order: value } } + ) self end @@ -184,11 +193,7 @@ def build_query query_hash[:_source] = @source unless @source.nil? query_hash[:query] = query.to_h - if @sort.any? - query_hash[:sort] = @sort.map do |field, direction| - { field => { order: direction } } - end - end + query_hash[:sort] = @sort.map { |field, ordering| { field => ordering } } if @sort.any? if @collapse query_hash[:collapse] = { diff --git a/spec/jay_api/elasticsearch/query_builder_spec.rb b/spec/jay_api/elasticsearch/query_builder_spec.rb index 5a7493d..7fe02a6 100644 --- a/spec/jay_api/elasticsearch/query_builder_spec.rb +++ b/spec/jay_api/elasticsearch/query_builder_spec.rb @@ -98,6 +98,34 @@ it_behaves_like '#to_query with multiple sort fields' end end + + context 'when sorting options have been given' do + before { query_builder.sort(price: { order: :desc, missing: '_last' }) } + + it 'adds the :sort key with the expected content' do + expect(method_call).to include(sort: [{ price: { order: :desc, missing: '_last' } }]) + end + end + + context 'when multiple fields with sort options have been given' do + before do + query_builder.sort(price: { order: :desc, missing: '_last' }) + query_builder.sort(updated_at: { order: :desc, unmapped_type: 'date' }) + end + + let(:expected_sort_clause) do + { + sort: [ + { price: { order: :desc, missing: '_last' } }, + { updated_at: { order: :desc, unmapped_type: 'date' } } + ] + } + end + + it 'adds the :sort key with the expected content' do + expect(method_call).to include(expected_sort_clause) + end + end end context 'when no collapse clause has been given' do