Skip to content

Commit

Permalink
Merge pull request #64 from chrispappas/multi-builder
Browse files Browse the repository at this point in the history
Add MultiBuilder to support multi-search API
  • Loading branch information
freekmurze authored Feb 10, 2025
2 parents 248f10e + cf2ca1b commit 7399509
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 0 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ $builder->addQuery(RangeQuery::create('age')->gte(18));
$results = $builder->search(); // raw response from ElasticSearch
```

#### Multi-Search Queries

Multi-Search queries are also available using the [`MultiBuilder` class](#multi-search-query-builder).

## Adding queries

The `$builder->addQuery()` method can be used to add any of the available `Query` types to the builder. The available query types can be found below or in the `src/Queries` directory of this repo. Every `Query` has a static `create()` method to pass its most important parameters.
Expand Down Expand Up @@ -418,6 +422,42 @@ $pageResults = (new Builder(Elastic\Elasticsearch\ClientBuilder::create()))
->search();
```

## Multi-Search Query Builder

Elasticsearch provides a ["multi-search" API](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html) that allows for multiple query bodies to be included in a single request.

Use the `MultiBuilder` class and [add builders](#add-builders) to add builders to your query request. The response will include a `responses` array of the query results, in the same order the requests are added. Use the `$multiBuilder->search()` to execute the queries, or `$multiBuilder->getPayload()` for the raw request payload.

```php
use Spatie\ElasticsearchQueryBuilder\MultiBuilder;
use Spatie\ElasticsearchQueryBuilder\Builder;

$client = Elastic\Elasticsearch\ClientBuilder::create();
$multiBuilder = (new MultiBuilder($client));

$multiBuilder->addBuilder(
(new Builder($client))->index('custom_index')->size(10)
);
// you can pass the index name to the addBuilder method second param
$multiBuilder->addBuilder(
(new Builder($client))->size(10)
'different_index'
);

$multiResults = $multiBuilder->search();
```

Returns the following response JSON shape:
```
{
"took": 2,
"responses": [
{... first query result ...},
{... second query result ...},
]
}
```

## Testing

```bash
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"require-dev": {
"elasticsearch/elasticsearch": "^8.0",
"friendsofphp/php-cs-fixer": "^2.17",
"php-http/mock-client": "^1.5",
"phpunit/phpunit": "^9.5",
"spatie/ray": "^1.10",
"vimeo/psalm": "^4.3"
Expand Down
5 changes: 5 additions & 0 deletions src/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ public function index(string $searchIndex): static
return $this;
}

public function getIndex(): ?string
{
return $this->searchIndex;
}

public function trackTotalHits(bool $value = true): static
{
$this->trackTotalHits = $value;
Expand Down
50 changes: 50 additions & 0 deletions src/MultiBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Spatie\ElasticsearchQueryBuilder;

use Elastic\Elasticsearch\Client;
use Elastic\Elasticsearch\Response\Elasticsearch;
use Http\Promise\Promise;

class MultiBuilder
{
protected ?array $builders = [];

public function __construct(protected Client $client)
{
}

public function addBuilder(Builder $builder, ?string $indexName = null): static
{
$this->builders[] = [
'index' => $indexName ?? $builder->getIndex(),
'builder' => $builder,
];

return $this;
}

public function getPayload(): array
{
$payload = [];

foreach ($this->builders as $builderInstance) {
['index' => $index, 'builder' => $builder] = $builderInstance;
$payload[] = $index ? ['index' => $index] : [];
$payload[] = $builder->getPayload();
}

return $payload;
}

public function search(): Elasticsearch|Promise
{
$payload = $this->getPayload();

$params = [
'body' => $payload,
];

return $this->client->msearch($params);
}
}
104 changes: 104 additions & 0 deletions tests/Builders/MultiBuilderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace Spatie\ElasticsearchQueryBuilder\Tests\Builders;

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

class MultiBuilderTest extends TestCase
{
private MultiBuilder $multiBuilder;

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->multiBuilder = new MultiBuilder($this->client);
}

public function testEmptyPayloadGeneratesCorrectly(): void
{
$this->assertEmpty($this->multiBuilder->getPayload());
}

public function testSingleBuilderPayloadGeneratesCorrectly(): void
{
$this->multiBuilder->addBuilder(
(new Builder($this->client))->addQuery(TermQuery::create('test', 'value'))
);

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

$this->assertNotEmpty($payload);

$this->assertCount(2, $payload);

$this->assertEquals([], $payload[0]);

$this->assertEquals([
'query' => [
'bool' => [
'must' => [
['term' => ['test' => 'value']],
],
],
],
], $payload[1]);
}

public function testMultipleBuilderPayloadGeneratesCorrectly(): void
{
$this->multiBuilder->addBuilder(
(new Builder($this->client))
->index('firstIndex')
->addQuery(TermQuery::create('keyword', 'value'), 'filter'),
);
$this->multiBuilder->addBuilder(
(new Builder($this->client))
->addQuery(TermQuery::create('keyword', 'value'), 'filter'),
'secondIndex'
);

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

$this->assertNotEmpty($payload);
$this->assertCount(4, $payload);

$index = $payload[0];
$this->assertEquals(['index' => 'firstIndex'], $index);

$body = $payload[1];
$this->assertEquals([
'query' => [
'bool' => [
'filter' => [['term' => ['keyword' => 'value']]],
],
],
], $body);

$index = $payload[2];
$this->assertEquals(['index' => 'secondIndex'], $index);

$body = $payload[3];
$this->assertEquals([
'query' => [
'bool' => [
'filter' => [['term' => ['keyword' => 'value']]],
],
],
], $body);
}
}

0 comments on commit 7399509

Please sign in to comment.