From 53b5fbd824d50cdafdc94acbb719cc8ba8cf9a66 Mon Sep 17 00:00:00 2001 From: Chris Pappas Date: Tue, 4 Feb 2025 17:05:40 -0600 Subject: [PATCH 1/7] Add MultiBuilder to support multi-search API - add getIndex method to Builder, need to extract for the multisearch body if not specified - create MultiBuilder class following the Builder patterns - `multiSearch` method runs the `msearch` method in ES PHP --- src/Builder.php | 5 ++ src/MultiBuilder.php | 50 +++++++++++++++ tests/Builders/MultiBuilderTest.php | 95 +++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 src/MultiBuilder.php create mode 100644 tests/Builders/MultiBuilderTest.php diff --git a/src/Builder.php b/src/Builder.php index 9c74956..c9b450d 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -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; diff --git a/src/MultiBuilder.php b/src/MultiBuilder.php new file mode 100644 index 0000000..3eed20a --- /dev/null +++ b/src/MultiBuilder.php @@ -0,0 +1,50 @@ +builders[] = [ + 'index' => $indexName ?? $builder->getIndex(), + 'builder' => $builder, + ]; + + return $this; + } + + public function getPayload(): array + { + $payload = []; + foreach ($this->builders as $builderInstance) { + $index = $builderInstance['index']; + $builder = $builderInstance['builder']; + $payload[] = $index ? ['index' => $index] : []; + $payload[] = $builder->getPayload(); + } + return $payload; + } + + public function multiSearch(): Elasticsearch|Promise + { + $payload = $this->getPayload(); + + $params = [ + 'body' => $payload, + ]; + + return $this->client->msearch($params); + } +} diff --git a/tests/Builders/MultiBuilderTest.php b/tests/Builders/MultiBuilderTest.php new file mode 100644 index 0000000..a658f8b --- /dev/null +++ b/tests/Builders/MultiBuilderTest.php @@ -0,0 +1,95 @@ +client = ClientBuilder::create()->build(); + + $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); + } +} From 7a49405476e154bcfbb2002482d623d93cd63eaf Mon Sep 17 00:00:00 2001 From: Chris Pappas Date: Wed, 5 Feb 2025 12:11:17 -0600 Subject: [PATCH 2/7] Use `search` method name --- src/MultiBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MultiBuilder.php b/src/MultiBuilder.php index 3eed20a..f5f18c5 100644 --- a/src/MultiBuilder.php +++ b/src/MultiBuilder.php @@ -37,7 +37,7 @@ public function getPayload(): array return $payload; } - public function multiSearch(): Elasticsearch|Promise + public function search(): Elasticsearch|Promise { $payload = $this->getPayload(); From 32d678f7cec27c9145dda5f667f0dba1ecfdba28 Mon Sep 17 00:00:00 2001 From: Chris Pappas Date: Wed, 5 Feb 2025 12:11:29 -0600 Subject: [PATCH 3/7] Add docs in README --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index dea135e..ac0d885 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 From 24ecb35a9856ff3786a5cbf0f2bfaa4e5a233af8 Mon Sep 17 00:00:00 2001 From: Chris Pappas Date: Wed, 5 Feb 2025 13:43:07 -0600 Subject: [PATCH 4/7] Remove unused imports --- tests/Builders/MultiBuilderTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Builders/MultiBuilderTest.php b/tests/Builders/MultiBuilderTest.php index a658f8b..e4cfad7 100644 --- a/tests/Builders/MultiBuilderTest.php +++ b/tests/Builders/MultiBuilderTest.php @@ -4,11 +4,9 @@ use Elastic\Elasticsearch\Client; use Elastic\Elasticsearch\ClientBuilder; -use PhpParser\Node\Expr\AssignOp\Mul; use Spatie\ElasticsearchQueryBuilder\Builder; use Spatie\ElasticsearchQueryBuilder\MultiBuilder; use PHPUnit\Framework\TestCase; -use Spatie\ElasticsearchQueryBuilder\Queries\MatchQuery; use Spatie\ElasticsearchQueryBuilder\Queries\TermQuery; class MultiBuilderTest extends TestCase From 88f0f1ee486ed582c388d328898017ce3af281dc Mon Sep 17 00:00:00 2001 From: Chris Pappas Date: Wed, 5 Feb 2025 13:47:33 -0600 Subject: [PATCH 5/7] Use array destructure, less verbose --- src/MultiBuilder.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/MultiBuilder.php b/src/MultiBuilder.php index f5f18c5..85c1b68 100644 --- a/src/MultiBuilder.php +++ b/src/MultiBuilder.php @@ -29,8 +29,7 @@ public function getPayload(): array { $payload = []; foreach ($this->builders as $builderInstance) { - $index = $builderInstance['index']; - $builder = $builderInstance['builder']; + ['index' => $index, 'builder' => $builder] = $builderInstance; $payload[] = $index ? ['index' => $index] : []; $payload[] = $builder->getPayload(); } From 23b4c71ec5b640af0f2dc3ad9d8ce552746624f2 Mon Sep 17 00:00:00 2001 From: Chris Pappas Date: Thu, 6 Feb 2025 12:56:33 -0600 Subject: [PATCH 6/7] Formatting --- src/MultiBuilder.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/MultiBuilder.php b/src/MultiBuilder.php index 85c1b68..3c7beb1 100644 --- a/src/MultiBuilder.php +++ b/src/MultiBuilder.php @@ -16,7 +16,6 @@ public function __construct(protected Client $client) public function addBuilder(Builder $builder, ?string $indexName = null): static { - // if we have a name, use the key, else just let is use numeric indices $this->builders[] = [ 'index' => $indexName ?? $builder->getIndex(), 'builder' => $builder, @@ -28,11 +27,13 @@ public function addBuilder(Builder $builder, ?string $indexName = null): static public function getPayload(): array { $payload = []; + foreach ($this->builders as $builderInstance) { ['index' => $index, 'builder' => $builder] = $builderInstance; $payload[] = $index ? ['index' => $index] : []; $payload[] = $builder->getPayload(); } + return $payload; } From cf2ca1b29573fbf861cc6ddc6d779174f7e6e18d Mon Sep 17 00:00:00 2001 From: Chris Pappas Date: Thu, 6 Feb 2025 12:58:54 -0600 Subject: [PATCH 7/7] Create a mocked ES Client Follow the ClientTest in elasticsearch-php: https://github.com/elastic/elasticsearch-php/blob/main/tests/ClientTest.php - likely useful for testing the original Builder as well? - add `php-http/mock-client` to composer require-dev to provide mocking - give method calls some breathing room --- composer.json | 1 + tests/Builders/MultiBuilderTest.php | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index eec2988..7ed8cd6 100644 --- a/composer.json +++ b/composer.json @@ -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" diff --git a/tests/Builders/MultiBuilderTest.php b/tests/Builders/MultiBuilderTest.php index e4cfad7..a13fa14 100644 --- a/tests/Builders/MultiBuilderTest.php +++ b/tests/Builders/MultiBuilderTest.php @@ -3,10 +3,11 @@ namespace Spatie\ElasticsearchQueryBuilder\Tests\Builders; use Elastic\Elasticsearch\Client; -use Elastic\Elasticsearch\ClientBuilder; +use Elastic\Transport\TransportBuilder; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Spatie\ElasticsearchQueryBuilder\Builder; use Spatie\ElasticsearchQueryBuilder\MultiBuilder; -use PHPUnit\Framework\TestCase; use Spatie\ElasticsearchQueryBuilder\Queries\TermQuery; class MultiBuilderTest extends TestCase @@ -17,7 +18,13 @@ class MultiBuilderTest extends TestCase protected function setUp(): void { - $this->client = ClientBuilder::create()->build(); + $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); } @@ -30,13 +37,17 @@ public function testEmptyPayloadGeneratesCorrectly(): void public function testSingleBuilderPayloadGeneratesCorrectly(): void { $this->multiBuilder->addBuilder( - (new Builder($this->client)) - ->addQuery(TermQuery::create('test', 'value')) + (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' => [