From 17259602dd83c0e87fa6f1d52386f16cbc5288d8 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Fri, 23 May 2025 17:18:22 +0200 Subject: [PATCH 1/5] [JAY-625] Add the Sources::Terms class The class represents a "Terms" value source for Elasticsearch's composite aggregations. How this value source works is explained here: https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-composite-aggregation#_terms The class will be used in an upcoming commit by the, yet to be added, composite aggregation class. --- .../aggregations/sources/terms.rb | 56 +++++++++ .../aggregations/sources/terms_spec.rb | 108 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 lib/jay_api/elasticsearch/query_builder/aggregations/sources/terms.rb create mode 100644 spec/jay_api/elasticsearch/query_builder/aggregations/sources/terms_spec.rb diff --git a/lib/jay_api/elasticsearch/query_builder/aggregations/sources/terms.rb b/lib/jay_api/elasticsearch/query_builder/aggregations/sources/terms.rb new file mode 100644 index 0000000..fcdf38c --- /dev/null +++ b/lib/jay_api/elasticsearch/query_builder/aggregations/sources/terms.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module JayAPI + module Elasticsearch + class QueryBuilder + class Aggregations + module Sources + # Represents a "Terms" value source for a Composite aggregation. + # More information about this type of value source can be found here: + # https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-composite-aggregation#_terms + class Terms + attr_reader :name, :field, :order, :missing_bucket, :missing_order + + # @param [String] name The name for the value source. + # @param [String] field The field for the value source. + # @param [String, nil] order The order in which the values coming + # from this data source should be ordered, this can be either + # "asc" or "desc" + # @param [Boolean] missing_bucket Whether or not a bucket for the + # documents without a value in +field+ should be created. + # @param [String] missing_order Where to put the bucket for the + # documents with a missing value, either "first" or "last". + def initialize(name, field:, order: nil, missing_bucket: nil, missing_order: nil) + @name = name + @field = field + @order = order + @missing_bucket = missing_bucket + @missing_order = missing_order + end + + # @return [self] A copy of the receiver. + def clone + self.class.new( + name, field: field, order: order, missing_bucket: missing_bucket, missing_order: missing_order + ) + end + + # @return [Hash] The hash representation for the value source. + def to_h + { + name => { + terms: { + field: field, + order: order, + missing_bucket: missing_bucket, + missing_order: missing_order + }.compact + } + } + end + end + end + end + end + end +end diff --git a/spec/jay_api/elasticsearch/query_builder/aggregations/sources/terms_spec.rb b/spec/jay_api/elasticsearch/query_builder/aggregations/sources/terms_spec.rb new file mode 100644 index 0000000..7ee8501 --- /dev/null +++ b/spec/jay_api/elasticsearch/query_builder/aggregations/sources/terms_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/query_builder/aggregations/sources/terms' + +RSpec.describe JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Terms do + subject(:terms) { described_class.new(name, **constructor_params) } + + let(:name) { 'product' } + let(:constructor_params) { { field: 'product.name' } } + + describe '#clone' do + subject(:method_call) { terms.clone } + + it "returns an instance of #{described_class}" do + expect(method_call).to be_a(described_class) + end + + it 'does not return the same object' do + expect(method_call).not_to be(terms) + end + + it "has the same 'name'" do + expect(method_call.name).to eq('product') + end + + it "has the same 'field'" do + expect(method_call.field).to eq('product.name') + end + + context "when no 'order' has been given" do + it 'has no order' do + expect(method_call.order).to be_nil + end + end + + context 'when an order has been given' do + let(:constructor_params) { super().merge(order: 'desc') } + + it 'has the same order' do + expect(method_call.order).to eq('desc') + end + end + + context 'when no configuration for missing values has been given' do + it 'has its "missing values" attributes set to nil' do + expect(method_call.missing_bucket).to be_nil + expect(method_call.missing_order).to be_nil + end + end + + context 'when missing values configuration has been given' do + let(:constructor_params) { super().merge(missing_bucket: true, missing_order: 'first') } + + it 'has the sam values in its "missing values" attributes' do + expect(method_call.missing_bucket).to be(true) + expect(method_call.missing_order).to eq('first') + end + end + end + + describe '#to_h' do + subject(:method_call) { terms.to_h } + + let(:expected_hash) do + { 'product' => { terms: { field: 'product.name' } } } + end + + it 'returns the expected hash' do + expect(method_call).to eq(expected_hash) + end + + context "when an 'order' has been given" do + let(:constructor_params) { super().merge(order: 'asc') } + + let(:expected_hash) do + { 'product' => { terms: { field: 'product.name', order: 'asc' } } } + end + + it 'returns the expected hash' do + expect(method_call).to eq(expected_hash) + end + end + + context "when 'missing_bucket' has been set to true" do + let(:constructor_params) { super().merge(missing_bucket: true) } + + let(:expected_hash) do + { 'product' => { terms: { field: 'product.name', missing_bucket: true } } } + end + + it 'returns the expected hash' do + expect(method_call).to eq(expected_hash) + end + end + + context "when 'missing_bucket' and 'missing_order' have been given" do + let(:constructor_params) { super().merge(missing_bucket: true, missing_order: 'last') } + + let(:expected_hash) do + { 'product' => { terms: { field: 'product.name', missing_bucket: true, missing_order: 'last' } } } + end + + it 'returns the expected hash' do + expect(method_call).to eq(expected_hash) + end + end + end +end From b6a2f24c0dcdca2f3c0aab4dc7ae61c09a58fc26 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Fri, 30 May 2025 15:45:53 +0200 Subject: [PATCH 2/5] [JAY-625] Add the Aggregations::Sources class This class holds a collection of sources for a Composite aggregation for Elasticsearch. The class will be used in an upcoming commit by said aggregation to hold its sources and to allow the user to add sources to a composite aggregation by passing a block to its constructor. For the moment the class can only handle a Terms source. Additional sources might be added in the future. --- .../aggregations/sources/sources.rb | 46 ++++++++ .../aggregations/sources/sources_spec.rb | 107 ++++++++++++++++++ 2 files changed, 153 insertions(+) create mode 100644 lib/jay_api/elasticsearch/query_builder/aggregations/sources/sources.rb create mode 100644 spec/jay_api/elasticsearch/query_builder/aggregations/sources/sources_spec.rb diff --git a/lib/jay_api/elasticsearch/query_builder/aggregations/sources/sources.rb b/lib/jay_api/elasticsearch/query_builder/aggregations/sources/sources.rb new file mode 100644 index 0000000..408491b --- /dev/null +++ b/lib/jay_api/elasticsearch/query_builder/aggregations/sources/sources.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require_relative 'terms' + +module JayAPI + module Elasticsearch + class QueryBuilder + class Aggregations + module Sources + # Represents the collection of sources for a Composite aggregation in + # Elasticsearch + class Sources + # Adds a +terms+ source to the collection. + # For information about the parameters: + # @see Sources::Terms#initialize + def terms(name, **kw_args) + sources.push(::JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Terms.new(name, **kw_args)) + end + + # @return [Array] Array representation of the collection of + # sources of the composite aggregation. + def to_a + sources.map(&:to_h) + end + + # @return [self] A copy of the receiver (not a shallow clone, it + # clones all of the elements of the collection). + def clone + self.class.new.tap do |copy| + copy.sources.concat(sources.map(&:clone)) + end + end + + protected + + # @return [Array] The array used to hold the collection of + # sources. + def sources + @sources ||= [] + end + end + end + end + end + end +end diff --git a/spec/jay_api/elasticsearch/query_builder/aggregations/sources/sources_spec.rb b/spec/jay_api/elasticsearch/query_builder/aggregations/sources/sources_spec.rb new file mode 100644 index 0000000..774825f --- /dev/null +++ b/spec/jay_api/elasticsearch/query_builder/aggregations/sources/sources_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/query_builder/aggregations/sources/sources' + +RSpec.describe JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Sources do + subject(:sources) { described_class.new } + + let(:terms) do + [ + instance_double( + JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Terms, + to_h: { 'product' => { field: 'product.name' } } + ), + instance_double( + JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Terms, + to_h: { 'brand' => { field: 'brand.name' } } + ) + ] + end + + before do + allow(JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Terms) + .to receive(:new).and_return(*terms) + end + + describe '#terms' do + subject(:method_call) { sources.terms('product', field: 'product.name') } + + it 'creates an instance of the Terms source passing down the given parameters' do + expect(JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Terms) + .to receive(:new).with('product', field: 'product.name') + + method_call + end + + it 'adds the Terms instance to the sources collection' do + expect { method_call }.to change(sources, :to_a).from([]).to(['product' => { field: 'product.name' }]) + end + end + + shared_context 'with elements in the sources collection' do + before do + sources.terms('product', field: 'product.name') + sources.terms('brand', field: 'brand.name') + end + end + + describe '#to_a' do + subject(:method_call) { sources.to_a } + + context 'when no sources have been added to the collection' do + it 'returns an empty array' do + expect(method_call).to be_an(Array).and be_empty + end + end + + context 'when some sources have been added to the collection' do + let(:expected_array) do + [ + { 'product' => { field: 'product.name' } }, + { 'brand' => { field: 'brand.name' } } + ] + end + + include_context 'with elements in the sources collection' + + it 'returns the expected array' do + expect(method_call).to eq(expected_array) + end + end + end + + shared_examples_for '#clone' do + it 'returns a new instance of the class' do + expect(method_call).to be_a(described_class) + end + + it 'does not return the same object' do + expect(method_call).not_to be(sources) + end + end + + describe '#clone' do + subject(:method_call) { sources.clone } + + context 'when there are no elements in the collection' do + it_behaves_like '#clone' + + it 'returns a collection which is also empty' do + expect(method_call.to_a).to eq([]) + end + end + + context 'when there are elements in the collection' do + include_context 'with elements in the sources collection' + + it 'clones each of the sources' do + expect(terms).to all(receive(:clone)) + method_call + end + + it 'returns an equivalent collection' do + expect(method_call.to_a).to eq(sources.to_a) + end + end + end +end From 851233e129bb9d1e030814dbd9c3ffc46d04bbc0 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Fri, 30 May 2025 17:10:12 +0200 Subject: [PATCH 3/5] [JAY-625] Add the Aggregations::Composite class The class model a composite Elasticsearch aggregation. Tests for the proper handling of nested aggregations were added as integration tests because creating said tests as unit tests would require a great deal of mocking logic. --- CHANGELOG.md | 3 + .../query_builder/aggregations/composite.rb | 77 ++++++++++ .../aggregations/composite_spec.rb | 85 +++++++++++ .../aggregations/composite_spec.rb | 132 ++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 lib/jay_api/elasticsearch/query_builder/aggregations/composite.rb create mode 100644 spec/integration/jay_api/elasticsearch/query_builder/aggregations/composite_spec.rb create mode 100644 spec/jay_api/elasticsearch/query_builder/aggregations/composite_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index b477c53..1d35a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Please mark backwards incompatible changes with an exclamation mark at the start ## [Unreleased] +### Added +- The `Aggregations::Composite` class. + ## [28.2.0] - 2025-05-30 ### Added diff --git a/lib/jay_api/elasticsearch/query_builder/aggregations/composite.rb b/lib/jay_api/elasticsearch/query_builder/aggregations/composite.rb new file mode 100644 index 0000000..92e06e9 --- /dev/null +++ b/lib/jay_api/elasticsearch/query_builder/aggregations/composite.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'active_support' +require 'active_support/core_ext/string/inflections' + +require_relative 'aggregation' +require_relative 'sources/sources' +require_relative 'errors/aggregations_error' + +module JayAPI + module Elasticsearch + class QueryBuilder + class Aggregations + # Represents a Composite aggregation in Elasticsearch. For more + # information about this type of aggregation: + # @see https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-composite-aggregation + class Composite < ::JayAPI::Elasticsearch::QueryBuilder::Aggregations::Aggregation + attr_reader :size + + # @param [String] name The name of the composite aggregation. + # @param [Integer] size The number of composite buckets to return. + # @yieldparam [JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Sources] + # The collection of sources for the composite aggregation. This + # should be used by the caller to add sources to the composite + # aggregation. + # @raise [JayAPI::Elasticsearch::QueryBuilder::Aggregations::Errors::AggregationsError] + # If the method is called without a block. + def initialize(name, size: nil, &block) + unless block + raise(::JayAPI::Elasticsearch::QueryBuilder::Aggregations::Errors::AggregationsError, + "The #{self.class.name.demodulize} aggregation must be initialized with a block") + end + + super(name) + @size = size + block.call(sources) + end + + # @return [self] A copy of the receiver. Sources and nested + # aggregations are also cloned. + def clone + # rubocop:disable Lint/EmptyBlock (The sources will be assigned later) + copy = self.class.new(name, size: size) {} + # rubocop:enable Lint/EmptyBlock + + copy.aggregations = aggregations.clone + copy.sources = sources.clone + copy + end + + # @return [Hash] The Hash representation of the +Aggregation+. + # Properly formatted for Elasticsearch. + def to_h + super do + { + composite: { + sources: sources.to_a, + size: size + }.compact + } + end + end + + protected + + attr_writer :sources # Used by the #clone method + + # @return [JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Sources] + # The collection of sources of the composite aggregation. + def sources + @sources ||= ::JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Sources.new + end + end + end + end + end +end diff --git a/spec/integration/jay_api/elasticsearch/query_builder/aggregations/composite_spec.rb b/spec/integration/jay_api/elasticsearch/query_builder/aggregations/composite_spec.rb new file mode 100644 index 0000000..84541ca --- /dev/null +++ b/spec/integration/jay_api/elasticsearch/query_builder/aggregations/composite_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/query_builder/aggregations/composite' + +RSpec.describe JayAPI::Elasticsearch::QueryBuilder::Aggregations::Composite do + subject(:composite) do + described_class.new('products_by_brand', **constructor_params) do |sources| + sources.terms('product', field: 'product.name', order: 'asc') + sources.terms('brand', field: 'brand.name') + end + end + + let(:constructor_params) { {} } + + describe '#to_h' do + subject(:method_call) { composite.to_h } + + let(:expected_hash) do + { + 'products_by_brand' => { + composite: { + sources: [ + { 'product' => { terms: { field: 'product.name', order: 'asc' } } }, + { 'brand' => { terms: { field: 'brand.name' } } } + ] + } + } + } + end + + it 'returns the expected Hash' do + expect(method_call).to eq(expected_hash) + end + + context "when a 'size' has been specified" do + let(:constructor_params) { { size: 10 } } + + let(:expected_hash) do + { + 'products_by_brand' => { + composite: { + sources: [ + { 'product' => { terms: { field: 'product.name', order: 'asc' } } }, + { 'brand' => { terms: { field: 'brand.name' } } } + ], + size: 10 + } + } + } + end + + it 'returns the expected Hash' do + expect(method_call).to eq(expected_hash) + end + end + + context 'with nested aggregations' do + before do + composite.aggs do |aggs| + aggs.avg('avg_price', field: 'product.price') + end + end + + let(:expected_hash) do + { + 'products_by_brand' => { + composite: { + sources: [ + { 'product' => { terms: { field: 'product.name', order: 'asc' } } }, + { 'brand' => { terms: { field: 'brand.name' } } } + ] + }, + aggs: { + 'avg_price' => { avg: { field: 'product.price' } } + } + } + } + end + + it 'returns the expected Hash' do + expect(method_call).to eq(expected_hash) + end + end + end +end diff --git a/spec/jay_api/elasticsearch/query_builder/aggregations/composite_spec.rb b/spec/jay_api/elasticsearch/query_builder/aggregations/composite_spec.rb new file mode 100644 index 0000000..13a776f --- /dev/null +++ b/spec/jay_api/elasticsearch/query_builder/aggregations/composite_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +require 'jay_api/elasticsearch/query_builder/aggregations/composite' + +require_relative 'aggregation_shared' + +RSpec.describe JayAPI::Elasticsearch::QueryBuilder::Aggregations::Composite do + # rubocop:disable Lint/EmptyBlock (the code inside the block is not relevant for the tests) + subject(:composite) { described_class.new(name, **constructor_params) {} } + # rubocop:enable Lint/EmptyBlock + + let(:name) { 'products_by_brand' } + let(:constructor_params) { {} } + + let(:sources) do + instance_double( + JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Sources, + to_a: 'Sources#to_a' + ) + end + + before do + allow(JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Sources) + .to receive(:new).and_return(sources) + end + + describe '#initialize' do + context 'when no block is given' do + subject(:composite) { described_class.new(name, **constructor_params) } + + it 'raises a JayAPI::Elasticsearch::QueryBuilder::Aggregations::Errors::AggregationsError' do + expect { composite }.to raise_error( + JayAPI::Elasticsearch::QueryBuilder::Aggregations::Errors::AggregationsError, + 'The Composite aggregation must be initialized with a block' + ) + end + end + + context 'when a block is given' do + it 'creates a new instance of the Sources class and yields it to the block' do + expect(JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Sources).to receive(:new) + expect { |block| described_class.new(name, **constructor_params, &block) }.to yield_with_args(sources) + end + end + end + + describe '#clone' do + subject(:method_call) { aggregation.clone } + + let(:aggregation) { composite } + + let(:sources_clone) do + instance_double( + JayAPI::Elasticsearch::QueryBuilder::Aggregations::Sources::Sources, + to_a: 'Sources#clone#to_a' + ) + end + + before do + allow(sources).to receive(:clone).and_return(sources_clone) + end + + it 'returns an instance of the same class' do + expect(method_call).to be_an_instance_of(described_class) + end + + it 'does not return the same object' do + expect(method_call).not_to be(aggregation) + end + + it "returns an aggregation with the same 'name'" do + expect(method_call.name).to be(name) + end + + it 'calls #clone on the underlying Sources object' do + expect(sources).to receive(:clone) + method_call + end + + it 'returns an aggregation with the cloned Source object' do + expect(method_call.to_h).to eq('products_by_brand' => { composite: { sources: 'Sources#clone#to_a' } }) + end + + context "when no 'size' has been given to the constructor" do + it "leaves the clone's 'size' as nil" do + expect(method_call.size).to be_nil + end + end + + context "when a 'size' has been given to the constructor" do + let(:size) { 10 } + let(:constructor_params) { { size: size } } + + it "sets the clone's 'size' to the same value" do + expect(method_call.size).to be(size) + end + end + + it_behaves_like 'JayAPI::Elasticsearch::QueryBuilder::Aggregations::Terms::#clone' + end + + describe '#to_h' do + subject(:method_call) { composite.to_h } + + context "when no 'size' has been given to the constructor" do + let(:expected_hash) do + { + 'products_by_brand' => { composite: { sources: 'Sources#to_a' } } + } + end + + it 'returns the expected Hash' do + expect(method_call.to_h).to eq(expected_hash) + end + end + + context "when 'size' has been given to the constructor" do + let(:size) { 10 } + let(:constructor_params) { { size: size } } + + let(:expected_hash) do + { + 'products_by_brand' => { composite: { sources: 'Sources#to_a', size: 10 } } + } + end + + it 'returns the expected Hash' do + expect(method_call.to_h).to eq(expected_hash) + end + end + end +end From dee3febaf5b0c5b6234a2249dbc617f1a300513e Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Fri, 23 May 2025 14:40:07 +0200 Subject: [PATCH 4/5] [JAY-625] Add the Aggregations#composite method The method allows the user to add composite aggregations. It relies on the Aggregations::Composite class for that. --- CHANGELOG.md | 3 ++- .../elasticsearch/query_builder/aggregations.rb | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d35a2d..a546d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ Please mark backwards incompatible changes with an exclamation mark at the start ## [Unreleased] ### Added -- The `Aggregations::Composite` class. +- The `Aggregations::Composite` class and the `Aggregations#composite` method. + They make it possible to use Elasticsearch's `composite` aggregations. ## [28.2.0] - 2025-05-30 diff --git a/lib/jay_api/elasticsearch/query_builder/aggregations.rb b/lib/jay_api/elasticsearch/query_builder/aggregations.rb index f792488..d6d192b 100644 --- a/lib/jay_api/elasticsearch/query_builder/aggregations.rb +++ b/lib/jay_api/elasticsearch/query_builder/aggregations.rb @@ -5,6 +5,7 @@ require_relative 'aggregations/aggregation' require_relative 'aggregations/avg' require_relative 'aggregations/cardinality' +require_relative 'aggregations/composite' require_relative 'aggregations/date_histogram' require_relative 'aggregations/filter' require_relative 'aggregations/scripted_metric' @@ -121,6 +122,16 @@ def date_histogram(name, field:, calendar_interval:, format: nil) ) end + # Adds a +composite+ aggregation. For more information about the parameters: + # @see JayAPI::Elasticsearch::QueryBuilder::Aggregations::Composite#initialize + def composite(name, size: nil, &block) + add( + ::JayAPI::Elasticsearch::QueryBuilder::Aggregations::Composite.new( + name, size: size, &block + ) + ) + end + # Returns a Hash with the correct format for the current list of # aggregations. For example: # From e187774b9344c1eb8b831daa2c7fbe75a47d2f29 Mon Sep 17 00:00:00 2001 From: Sergio Bobillier Date: Fri, 30 May 2025 17:33:25 +0200 Subject: [PATCH 5/5] [JAY-625] Document Elasticsearch's composite aggregation Adds documentation on how to use the newly added composite aggregation. --- .../elasticsearch/aggregations.rst | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/documentation/source/user_guidelines/elasticsearch/aggregations.rst b/documentation/source/user_guidelines/elasticsearch/aggregations.rst index c52c9a5..bd75b4c 100644 --- a/documentation/source/user_guidelines/elasticsearch/aggregations.rst +++ b/documentation/source/user_guidelines/elasticsearch/aggregations.rst @@ -329,6 +329,54 @@ The code above would produce the following query: ``QueryBuilder::Script`` objects here. Their use will produce unintended results. +composite +--------- + +This is a multi-bucket aggregation that aggregates the set of documents using a +compound value made out of all the existing combinations of values from the +specified sources. Currently Jay API only allows one type of source: ``terms``. + +Using the ``terms`` source it is possible to create a bucket for each existing +combination of values from a set of fields. + +Detailed information on how to use this type of aggregation can be found on +`Elasticsearch's documentation on the Composite aggregation`_ + +Code example: + +.. code-block:: ruby + + query_builder = JayAPI::Elasticsearch::QueryBuilder.new + query_builder.aggregations.composite('products_by_brand') do |sources| + sources.terms('product', field: 'product.name') + sources.terms('brand', field: 'brand.name') + end + +This would generate the following query: + +.. code-block:: json + + { + "query": { + "match_all": {} + }, + "aggs": { + "products_by_brand": { + "composite": { + "sources": [ + { "product": { "terms": { "field": "product.name" } } }, + { "brand": { "terms": { "field": "brand.name" } } } + ] + } + } + } + } + +This will create one bucket for each existing combination of ``product.name`` +and ``brand.name`` in the index. The buckets will only say how many documents +(``doc_count``) exist for each combination. Nested aggregations could be added +to get other information out of the documents in each bucket. + .. _`Elasticsearch's documentation on the Terms aggregation`: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html .. _`Elasticsearch's documentation on the Avg aggregation`: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html .. _`Elasticsearch's documentation on the Sum aggregation`: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html @@ -337,4 +385,5 @@ The code above would produce the following query: .. _`Elasticsearch's documentation on the Cardinality aggregation`: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html .. _`Elasticsearch's documentation on the Date Histogram aggregation`: https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-datehistogram-aggregation .. _`Elasticsearch's documentation on the Scripted Metric aggregation`: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-scripted-metric-aggregation.html +.. _`Elasticsearch's documentation on the Composite aggregation`: https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-composite-aggregation .. _`Painless`: https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-scripting-painless.html