Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add elasticsearch collapse on query builder #65

Merged
merged 3 commits into from
Feb 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 47 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ composer require spatie/elasticsearch-query-builder

## Basic usage

The only class you really need to interact with is the `Spatie\ElasticsearchQueryBuilder\Builder` class. It requires an `\Elastic\Elasticsearch\Client` passed in the constructor. Take a look at the [ElasticSearch SDK docs](https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/installation.html) to learn more about connecting to your ElasticSearch cluster.
The only class you really need to interact with is the `Spatie\ElasticsearchQueryBuilder\Builder` class. It requires an `\Elastic\Elasticsearch\Client` passed in the constructor. Take a look at the [ElasticSearch SDK docs](https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/installation.html) to learn more about connecting to your ElasticSearch cluster.

The `Builder` class contains some methods to [add queries](#adding-queries), [aggregations](#adding-aggregations), [sorts](#adding-sorts), [fields](#retrieve-specific-fields) and some extras for [pagination](#pagination). You can read more about these methods below. Once you've fully built-up the query you can use `$builder->search()` to execute the query or `$builder->getPayload()` to get the raw payload for ElasticSearch.

Expand Down Expand Up @@ -84,11 +84,11 @@ The following query types are available:

```php
\Spatie\ElasticsearchQueryBuilder\Queries\GeoshapeQuery::create(
'location',
\Spatie\ElasticsearchQueryBuilder\Queries\GeoshapeQuery::TYPE_POLYGON,
[[1.0, 2.0]],
'location',
\Spatie\ElasticsearchQueryBuilder\Queries\GeoshapeQuery::TYPE_POLYGON,
[[1.0, 2.0]],
\Spatie\ElasticsearchQueryBuilder\Queries\GeoShapeQuery::RELATION_INTERSECTS,
);
);
```

#### `MatchQuery`
Expand All @@ -100,6 +100,7 @@ The following query types are available:
```

#### `MatchPhraseQuery`

[https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html)

```php
Expand All @@ -120,7 +121,7 @@ The following query types are available:

```php
\Spatie\ElasticsearchQueryBuilder\Queries\NestedQuery::create(
'user',
'user',
new \Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery('name', 'john')
);
```
Expand All @@ -140,7 +141,7 @@ $nestedQuery->innerHits(
->size(3)
->addSort(
\Spatie\ElasticsearchQueryBuilder\Sorts\Sort::create(
'comments.likes',
'comments.likes',
\Spatie\ElasticsearchQueryBuilder\Sorts\Sort::DESC
)
)
Expand Down Expand Up @@ -200,14 +201,45 @@ $nestedQuery->innerHits(
->add($existsQuery, 'must_not');
```

#### `collapse`

The `collapse` feature allows grouping search results by a specific field while retrieving top documents from each group using `inner_hits`. This is useful for avoiding duplicate entities in search results while still accessing grouped data.

[https://www.elastic.co/guide/en/elasticsearch/reference/current/collapse-search-results.html](https://www.elastic.co/guide/en/elasticsearch/reference/current/collapse-search-results.html)

```php
use Spatie\ElasticsearchQueryBuilder\Sorts\Sort;
use Spatie\ElasticsearchQueryBuilder\Builder;

// Initialize ExtendedBuilder with an Elasticsearch client
$builder = new Builder($client);

// Apply collapse to group by 'user_id'
$builder->collapse(
'user_id', // Field to collapse on
[
'name' => 'top_three_liked_posts',
'size' => 3, // Retrieve top 3 posts per user
'sort' => [
Sort::create('post.likes', Sort::DESC), // Sort posts by likes (descending)
],
'fields' => ['post.title', 'post.content', 'post.likes'], // Select specific fields
],
10, // Max concurrent group searches
);

// Execute the search
$response = $builder->search();
```

### Chaining multiple queries

Multiple `addQuery()` calls can be chained on one `Builder`. Under the hood they'll be added to a `BoolQuery` with occurrence type `must`. By passing a second argument to the `addQuery()` method you can select a different occurrence type:

```php
$builder
->addQuery(
MatchQuery::create('name', 'billie'),
MatchQuery::create('name', 'billie'),
'must_not' // available types: must, must_not, should, filter
)
->addQuery(
Expand Down Expand Up @@ -344,7 +376,7 @@ $builder

### Nested sort

[https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html#_nested_sorting_examples](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html#_nested_sorting_examples)
[https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html#\_nested_sorting_examples](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html#_nested_sorting_examples)

```php
use Spatie\ElasticsearchQueryBuilder\Sorts\NestedSort;
Expand All @@ -365,8 +397,8 @@ use Spatie\ElasticsearchQueryBuilder\Queries\TermQuery;
$builder
->addSort(
NestedSort::create(
'books',
'books.rating',
'books',
'books.rating',
NestedSort::ASC
)->filter(BoolQuery::create()->add(TermQuery::create('books.category', 'comedy'))
);
Expand Down Expand Up @@ -448,6 +480,7 @@ $multiResults = $multiBuilder->search();
```

Returns the following response JSON shape:

```
{
"took": 2,
Expand Down Expand Up @@ -478,9 +511,9 @@ Please review [our security policy](../../security/policy) on how to report secu

## Credits

- [Alex Vanderbist](https://github.com/alexvanderbist)
- [Ruben Van Assche](https://github.com/rubenvanassche)
- [All Contributors](../../contributors)
- [Alex Vanderbist](https://github.com/alexvanderbist)
- [Ruben Van Assche](https://github.com/rubenvanassche)
- [All Contributors](../../contributors)

## License

Expand Down
26 changes: 23 additions & 3 deletions src/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ class Builder

protected ?BoolQuery $postFilterQuery = null;

public function __construct(protected Client $client)
{
}
protected ?array $collapse = null;

public function __construct(protected Client $client) {}

public function addQuery(Query $query, string $boolType = 'must'): static
{
Expand Down Expand Up @@ -172,6 +172,22 @@ public function addPostFilterQuery(Query $query, string $boolType = 'must'): sta
return $this;
}


public function collapse(string $field, ?array $innerHits = null, ?int $maxConcurrentGroupRequests = null): static
{
$this->collapse = ['field' => $field];

if ($innerHits) {
$this->collapse['inner_hits'] = $innerHits;
}

if ($maxConcurrentGroupRequests !== null) {
$this->collapse['max_concurrent_group_searches'] = $maxConcurrentGroupRequests;
}

return $this;
}

public function getPayload(): array
{
$payload = [];
Expand Down Expand Up @@ -212,6 +228,10 @@ public function getPayload(): array
$payload['post_filter'] = $this->postFilterQuery->toArray();
}

if ($this->collapse) {
$payload['collapse'] = $this->collapse;
}

return $payload;
}
}
66 changes: 66 additions & 0 deletions tests/Queries/CollapseTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

namespace Spatie\ElasticsearchQueryBuilder\Tests\Queries;

use PHPUnit\Framework\TestCase;
use Elastic\Elasticsearch\Client;
use Spatie\ElasticsearchQueryBuilder\Builder;
use Elastic\Transport\TransportBuilder;
use Psr\Log\LoggerInterface;

class CollapseTest extends TestCase
{

private Builder $builder;

private Client $client;

protected function setUp(): void
{
$transport = TransportBuilder::create()
->setClient(new \Http\Mock\Client())
->build();

$logger = $this->createStub(LoggerInterface::class);

$this->client = new Client($transport, $logger);

$this->builder = new Builder($this->client);
}

public function testCollapseIsAddedToPayload()
{
$this->builder->collapse(
'user_id',
[
'name' => 'top_comments',
'size' => 3,
'sort' => [
[
'timestamp' => 'desc'
]
]
],
10,
);

$payload = $this->builder->getPayload();

$expectedCollapse = [
'field' => 'user_id',
'inner_hits' => [
'name' => 'top_comments',
'size' => 3,
'sort' => [
[
'timestamp' => 'desc'
]
],
],
'max_concurrent_group_searches' => 10,
];

$this->assertArrayHasKey('collapse', $payload);
$this->assertEquals($expectedCollapse, $payload['collapse']);
}
}