# Create custom Aggregation

## Create aggregation class

To create a custom Aggregation, create an aggregation class. In the following example, an aggregation groups the location query results by the location priority:

```
<?php

declare(strict_types=1);

namespace App\Query\Aggregation;

use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\AbstractRangeAggregation;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\LocationAggregation;

/**
 * @phpstan-template TValue
 *
 * @phpstan-extends \Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\AbstractRangeAggregation<TValue>
 */
final class PriorityRangeAggregation extends AbstractRangeAggregation implements LocationAggregation
{
}
```

The `PriorityRangeAggregation` class extends `AbstractRangeAggregation`. The name of the class indicates that it aggregates the results by using the Range aggregation.

An aggregation must implement the [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation.html) interface or inherit one of following abstract classes:

- [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\AbstractRangeAggregation`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-AbstractRangeAggregation.html)
- [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\AbstractStatsAggregation`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-AbstractStatsAggregation.html)
- [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\AbstractTermAggregation`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-AbstractTermAggregation.html)

An aggregation can also implement one of the following interfaces:

- [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\FieldAggregation`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-FieldAggregation.html), based on content field
- [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\LocationAggregation`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-LocationAggregation.html), based on content location
- [`Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\RawAggregation`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Core-Repository-Values-Content-Query-Aggregation-RawAggregation.html), based on details of the index structure

Aggregation definition

An aggregation definition must contain at least the name of an aggregation and optional aggregation parameters, such as, for example, the path (string) that is used to limit aggregation results to a specific subtree, content type identifier, or field definition identifier, which is mapped to the search index field name.

Aggregation definition must be independent of the search engine used.

A custom aggregation requires that the following elements are provided:

- An aggregation visitor that returns an array of results
- A result extractor that transforms raw aggregation results from the search engine into `AggregationResult` objects

In simpler cases, you can apply one of the built-in visitors that correspond to the aggregation type. The example below uses `RangeAggregationVisitor`:

**Solr**

```
services:
    app.search.solr.query.aggregation_visitor.priority_range_aggregation:
    class: Ibexa\Solr\Query\Common\AggregationVisitor\RangeAggregationVisitor
    factory: [ '@Ibexa\Solr\Query\Common\AggregationVisitor\Factory\SearchFieldAggregationVisitorFactory', 'createRangeAggregationVisitor' ]
    arguments:
        $aggregationClass: 'App\Query\Aggregation\Solr\PriorityRangeAggregation'
        $searchIndexFieldName: 'priority_i'
    tags:
        - { name: ibexa.search.solr.query.location.aggregation.visitor }
```

**Elasticsearch**

```
services:
    app.search.elasticsearch.query.aggregation_visitor.priority_range_aggregation:
    class: Ibexa\Elasticsearch\Query\AggregationVisitor\RangeAggregationVisitor
    factory: [ '@Ibexa\Elasticsearch\Query\AggregationVisitor\Factory\SearchFieldAggregationVisitorFactory', 'createRangeAggregationVisitor' ]
    arguments:
        $aggregationClass: 'App\Query\Aggregation\Elasticsearch\PriorityRangeAggregation'
        $searchIndexFieldName: 'priority_i'
    tags:
        - { name: ibexa.search.elasticsearch.query.location.aggregation.visitor }
```

The visitor is created by `SearchFieldAggregationVisitorFactory`. You provide it with two arguments:

- The aggregation class in `aggregationClass`
- The field name in search index in `searchIndexFieldName`

In this example, the field is `priority_i` which exists only for locations.

**Solr**

Tag the service with `ibexa.search.solr.query.location.aggregation.visitor`.

For content-based aggregations, use the `ibexa.search.solr.query.content.aggregation.visitor` tag.

**Elasticsearch**

Tag the service with `ibexa.search.elasticsearch.query.location.aggregation_visitor`.

For content-based aggregations, use the `ibexa.search.elasticsearch.query.content.aggregation.visitor` tag.

For the result extractor, you can use the built-in `RangeAggregationResultExtractor` and provide it with the aggregation class in the `aggregationClass` parameter.

**Solr**

Tag the service with `ibexa.search.solr.query.location.aggregation.result.extractor`.

As `$keyMapper` to transform raw keys into more usable objects or scalar values, use `IntRangeAggregationKeyMapper` or create your own implementing [`RangeAggregationKeyMapper`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-ResultExtractor-AggregationResultExtractor-RangeAggregationKeyMapper.html).

```
services:
    app.search.solr.query.aggregation_result_extractor.priority_range_aggregation:
    class: Ibexa\Solr\ResultExtractor\AggregationResultExtractor\RangeAggregationResultExtractor
    arguments:
        $aggregationClass: 'App\Query\Aggregation\Solr\PriorityRangeAggregation'
        $keyMapper: 'Ibexa\Solr\ResultExtractor\AggregationResultExtractor\RangeAggregationKeyMapper\IntRangeAggregationKeyMapper'
    tags:
        - { name: ibexa.search.solr.query.location.aggregation.result.extractor }
```

For other cases, a [`TermAggregationKeyMapper`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-ResultExtractor-AggregationResultExtractor-TermAggregationKeyMapper.html) interface is also available.

**Elasticsearch**

Tag the service with `ibexa.search.elasticsearch.query.location.aggregation.result.extractor`.

```
services:
    app.search.elasticsearch.query.aggregation_result_extractor.priority_range_aggregation:
    class: Ibexa\Elasticsearch\Query\ResultExtractor\AggregationResultExtractor\RangeAggregationResultExtractor
    arguments:
        $aggregationClass: 'App\Query\Aggregation\Elasticsearch\PriorityRangeAggregation'
    tags:
        - { name: ibexa.search.elasticsearch.query.location.aggregation.result.extractor }
```

You can use a different type of aggregation, followed by respective visitor and extractor classes:

**Solr**

- Range
  - `Ibexa\Solr\Query\Common\AggregationVisitor\RangeAggregationVisitor`
  - `Ibexa\Solr\ResultExtractor\AggregationResultExtractor\RangeAggregationResultExtractor`
- Stats (count, minimum, maximum, average, sum)
  - `Ibexa\Solr\Query\Common\AggregationVisitor\StatsAggregationVisitor`
  - `Ibexa\Solr\ResultExtractor\AggregationResultExtractor\StatsAggregationResultExtractor`
- Term
  - `Ibexa\Solr\Query\Common\AggregationVisitor\TermAggregationVisitor`
  - `Ibexa\Solr\ResultExtractor\AggregationResultExtractor\TermAggregationResultExtractor`

**Elasticsearch**

- Range
  - `Ibexa\ElasticSearchEngine\Query\AggregationVisitor\RangeAggregationVisitor`
  - `Ibexa\ElasticSearchEngine\Query\ResultExtractor\AggregationResultExtractor\RangeAggregationResultExtractor`
- Stats (count, minimum, maximum, average, sum)
  - `Ibexa\ElasticSearchEngine\Query\AggregationVisitor\StatsAggregationVisitor`
  - `Ibexa\ElasticSearchEngine\Query\ResultExtractor\AggregationResultExtractor\StatsAggregationResultExtractor`
- Term
  - `Ibexa\ElasticSearchEngine\Query\AggregationVisitor\TermAggregationVisitor`
  - `Ibexa\ElasticSearchEngine\Query\ResultExtractor\AggregationResultExtractor\TermAggregationResultExtractor`

In a more complex use case, you must create your own visitor and extractor.

### Create aggregation visitor

**Solr**

The aggregation visitor must implement [`Ibexa\Contracts\Solr\Query\AggregationVisitor`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-Query-AggregationVisitor.html):

```
<?php

declare(strict_types=1);

namespace App\Query\Aggregation\Solr;

use App\Query\Aggregation\PriorityRangeAggregation;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation;
use Ibexa\Contracts\Solr\Query\AggregationVisitor;

/**
 * @phpstan-template TRangeAggregation of \Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\AbstractRangeAggregation
 */
final class PriorityRangeAggregationVisitor implements AggregationVisitor
{
    public function canVisit(Aggregation $aggregation, array $languageFilter): bool
    {
        return $aggregation instanceof PriorityRangeAggregation;
    }

    /**
     * @param \App\Query\Aggregation\PriorityRangeAggregation<TRangeAggregation> $aggregation
     */
    public function visit(
        AggregationVisitor $dispatcherVisitor,
        Aggregation $aggregation,
        array $languageFilter
    ): array {
        $rangeFacets = [];
        foreach ($aggregation->getRanges() as $range) {
            $from = $this->formatRangeValue($range->getFrom());
            $to = $this->formatRangeValue($range->getTo());
            $rangeFacets["{$from}_{$to}"] = [
                'type' => 'query',
                'q' => sprintf('priority_i:[%s TO %s}', $from, $to),
            ];
        }

        return [
            'type' => 'query',
            'q' => '*:*',
            'facet' => $rangeFacets,
        ];
    }

    private function formatRangeValue($value): string
    {
        if ($value === null) {
            return '*';
        }

        return (string)$value;
    }
}
```

**Elasticsearch**

The aggregation visitor must implement [`Ibexa\Contracts\ElasticSearchEngine\Query\AggregationVisitor`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Elasticsearch-Query-AggregationVisitor.html):

```
<?php

declare(strict_types=1);

namespace App\Query\Aggregation\Elasticsearch;

use App\Query\Aggregation\PriorityRangeAggregation;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation;
use Ibexa\Contracts\Elasticsearch\Query\AggregationVisitor;
use Ibexa\Contracts\Elasticsearch\Query\LanguageFilter;

/**
 * @phpstan-template TRangeAggregation of \Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\AbstractRangeAggregation
 */
final class PriorityRangeAggregationVisitor implements AggregationVisitor
{
    public function supports(Aggregation $aggregation, LanguageFilter $languageFilter): bool
    {
        return $aggregation instanceof PriorityRangeAggregation;
    }

    /**
     * @param \App\Query\Aggregation\PriorityRangeAggregation<TRangeAggregation> $aggregation
     *
     * @return array<string, array<string, mixed>>
     */
    public function visit(AggregationVisitor $dispatcher, Aggregation $aggregation, LanguageFilter $languageFilter): array
    {
        $ranges = [];

        foreach ($aggregation->getRanges() as $range) {
            if ($range->getFrom() !== null && $range->getTo() !== null) {
                $ranges[] = [
                    'from' => $range->getFrom(),
                    'to' => $range->getTo(),
                ];
            } elseif ($range->getFrom() === null && $range->getTo() !== null) {
                $ranges[] = [
                    'to' => $range->getTo(),
                ];
            } elseif ($range->getFrom() !== null && $range->getTo() === null) {
                $ranges[] = [
                    'from' => $range->getFrom(),
                ];
            } else {
                // invalid range
            }
        }

        return [
            'range' => [
                'field' => 'priority_i',
                'ranges' => $ranges,
            ],
        ];
    }
}
```

The `canVisit()` method checks whether the provided aggregation is of the supported type (in this case, your custom `PriorityRangeAggregation`).

The `visit()` method returns an array of results.

Finally, register the aggregation visitor as a service.

**Solr**

Tag the aggregation visitor with `ibexa.search.solr.query.location.aggregation.visitor`:

```
services:
    App\Query\Aggregation\Solr\PriorityRangeAggregationVisitor:
    tags:
        - { name: ibexa.search.solr.query.location.aggregation.visitor }
```

For content-based aggregations, use the `ibexa.search.solr.query.content.aggregation.visitor` tag.

**Elasticsearch**

Tag the aggregation visitor with `ibexa.elasticsearch.query.location.aggregation_visitor`:

```
services:
    App\Query\Aggregation\Elasticsearch\PriorityRangeAggregationVisitor:
    tags:
        - { name: ibexa.search.elasticsearch.query.location.aggregation.visitor }
```

For content-based aggregations, use the `ibexa.search.elasticsearch.query.content.aggregation.visitor` tag.

### Create result extractor

**Solr**

You must also create a result extractor, which implements [`Ibexa\Contracts\Solr\ResultExtractor\AggregationResultExtractor`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Solr-ResultExtractor-AggregationResultExtractor.html) that transforms raw aggregation results from Solr into `AggregationResult` objects:

```
<?php

declare(strict_types=1);

namespace App\Query\Aggregation\Solr;

use App\Query\Aggregation\PriorityRangeAggregation;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation\Range;
use Ibexa\Contracts\Core\Repository\Values\Content\Search\AggregationResult;
use Ibexa\Contracts\Core\Repository\Values\Content\Search\AggregationResult\RangeAggregationResult;
use Ibexa\Contracts\Core\Repository\Values\Content\Search\AggregationResult\RangeAggregationResultEntry;
use Ibexa\Contracts\Solr\ResultExtractor\AggregationResultExtractor;
use stdClass;

final class PriorityRangeAggregationResultExtractor implements AggregationResultExtractor
{
    public function canVisit(Aggregation $aggregation, array $languageFilter): bool
    {
        return $aggregation instanceof PriorityRangeAggregation;
    }

    public function extract(Aggregation $aggregation, array $languageFilter, stdClass $data): AggregationResult
    {
        $entries = [];
        foreach ($data as $key => $bucket) {
            if ($key === 'count' || !str_contains($key, '_')) {
                continue;
            }
            [$from, $to] = explode('_', $key, 2);
            $entries[] = new RangeAggregationResultEntry(
                new Range(
                    $from !== '*' ? $from : null,
                    $to !== '*' ? $to : null
                ),
                $bucket->count
            );
        }

        return new RangeAggregationResult($aggregation->getName(), $entries);
    }
}
```

The `canVisit()` method checks whether the provided aggregation is of the supported type (in this case, your custom `PriorityRangeAggregation`).

The `extract()` method converts the [raw data provided by the search engine](https://solr.apache.org/guide/8_8/search-sample.html#aggregation) to a `RangeAggregationResult` object.

**Elasticsearch**

You must also create a result extractor, which implements [`Ibexa\Contracts\ElasticSearchEngine\Query\AggregationResultExtractor`](https://doc.ibexa.co/en/latest/api/php_api/php_api_reference/classes/Ibexa-Contracts-Elasticsearch-Query-AggregationResultExtractor.html) that transforms raw aggregation results from Elasticsearch into `AggregationResult` objects:

```
<?php

declare(strict_types=1);

namespace App\Query\Aggregation\Elasticsearch;

use App\Query\Aggregation\PriorityRangeAggregation;
use Ibexa\Contracts\Core\Repository\Values\Content\Query\Aggregation;
use Ibexa\Contracts\Core\Repository\Values\Content\Search\AggregationResult;
use Ibexa\Contracts\Elasticsearch\Query\AggregationResultExtractor;
use Ibexa\Contracts\Elasticsearch\Query\LanguageFilter;

final class PriorityRangeAggregationResultExtractor implements AggregationResultExtractor
{
    public function supports(Aggregation $aggregation, LanguageFilter $languageFilter): bool
    {
        return $aggregation instanceof PriorityRangeAggregation;
    }

    public function extract(Aggregation $aggregation, LanguageFilter $languageFilter, array $data): AggregationResult
    {
        $entries = [];

        foreach ($data['buckets'] as $bucket) {
            $entries[] = new AggregationResult\RangeAggregationResultEntry(
                new Aggregation\Range($bucket['from'] ?? null, $bucket['to'] ?? null),
                $bucket['doc_count']
            );
        }

        return new AggregationResult\RangeAggregationResult($aggregation->getName(), $entries);
    }
}
```

The `supports()` method checks whether the provided aggregation is of the supported type (in this case, your custom `PriorityRangeAggregation`).

The `extract()` method converts the [raw data provided by the search engine](https://www.elastic.co/docs/explore-analyze/query-filter/aggregations) to a `RangeAggregationResult` object.

Finally, register the result extractor as a service.

**Solr**

Tag the result extractor with `ibexa.search.solr.query.location.aggregation.result.extractor`:

```
services:
    App\Query\Aggregation\Solr\PriorityRangeAggregationResultExtractor:
    tags:
        - { name: ibexa.search.solr.query.location.aggregation.result.extractor }
```

For content-based aggregations, use the `ibexa.search.solr.query.content.aggregation.result.extractor` tag.

**Elasticsearch**

Tag the result extractor with `ibexa.elasticsearch.query.location.aggregation_result_extractor`:

```
services:
    App\Query\Aggregation\Elasticsearch\PriorityRangeAggregationResultExtractor:
    tags:
        - { name: ibexa.search.elasticsearch.query.location.aggregation.result.extractor }
```

For content-based aggregations, use the `ibexa.search.elasticsearch.query.content.aggregation.result.extractor` tag.
