diff --git a/psalm.xml b/psalm.xml index 421ff10..cbdac5e 100644 --- a/psalm.xml +++ b/psalm.xml @@ -6,6 +6,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" + ignoreInternalFunctionFalseReturn="true" > diff --git a/src/Elasticsearch/Adapter/ClientInterface.php b/src/Elasticsearch/Adapter/ClientInterface.php index 6f655a6..0ed0a2f 100644 --- a/src/Elasticsearch/Adapter/ClientInterface.php +++ b/src/Elasticsearch/Adapter/ClientInterface.php @@ -187,6 +187,22 @@ public function updateByQuery(string $index, array $query, array $options = []): */ public function search(string $index, array $query): SearchResults; + /** + * Perform a search query on an index + * + * @param string $index Index to search on + * @param array $queries Queries to perform. Each items use same format as $query parameter of {@see ClientInterface::search()}. + * + * @return list + * + * @throws NotFoundException When the index does not exist + * @throws InternalServerException When http 500 error occurs + * @throws InvalidRequestException When request is malformed + * @throws NoNodeAvailableException If elasticsearch server is down + * @throws ElasticsearchExceptionInterface When requested cannot be performed + */ + public function multiSearch(string $index, array $queries): array; + /** * Perform a search query on an index and return the matching documents count * diff --git a/src/Elasticsearch/Adapter/ES7Client.php b/src/Elasticsearch/Adapter/ES7Client.php index 76a9e7b..a7d0eee 100644 --- a/src/Elasticsearch/Adapter/ES7Client.php +++ b/src/Elasticsearch/Adapter/ES7Client.php @@ -249,6 +249,58 @@ public function search(string $index, array $query): SearchResults ); } + /** + * {@inheritdoc} + */ + public function multiSearch(string $index, array $queries): array + { + $body = []; + + foreach ($queries as $query) { + $body[] = []; + $body[] = $query; + } + + try { + $response = $this->client->msearch(['index' => $index, 'body' => $body]); + } catch (ElasticsearchException $e) { + $this->handleException($e); + } + + $results = []; + + foreach ($response['responses'] as $key => $result) { + if (isset($result['error'])) { + $status = (int) $result['status']; + + switch (intdiv($status, 100)) { + case 4: + throw $status === 404 ? new NotFoundException($result['error']['reason']) : new InvalidRequestException($result['error']['reason']); + + case 5: + throw new InternalServerException($result['error']['reason']); + + default: + throw new RuntimeException($result['error']['reason']); + } + } + + $results[$key] = new SearchResults( + $result['_scroll_id'] ?? null, + $result['took'], + $result['timed_out'], + $result['_shards'], + $result['hits']['total']['value'], + $result['hits']['total']['relation'] === 'eq', + $result['hits']['max_score'] ?? null, + $result['hits']['hits'], + $result + ); + } + + return $results; + } + /** * {@inheritdoc} */ diff --git a/src/Elasticsearch/Adapter/ES8Client.php b/src/Elasticsearch/Adapter/ES8Client.php index ba08450..77a0a8f 100644 --- a/src/Elasticsearch/Adapter/ES8Client.php +++ b/src/Elasticsearch/Adapter/ES8Client.php @@ -16,6 +16,8 @@ use Elastic\Elasticsearch\Exception\ServerResponseException; use Elastic\Transport\Exception\NoNodeAvailableException as DriverNoNodeAvailableException; +use function intdiv; + /** * Client adapter for PHP elasticsearch client v8 */ @@ -241,6 +243,58 @@ public function search(string $index, array $query): SearchResults ); } + /** + * {@inheritdoc} + */ + public function multiSearch(string $index, array $queries): array + { + $body = []; + + foreach ($queries as $query) { + $body[] = []; + $body[] = $query; + } + + try { + $response = $this->client->msearch(['index' => $index, 'body' => $body])->asArray(); + } catch (ElasticsearchException $e) { + $this->handleException($e); + } + + $results = []; + + foreach ($response['responses'] as $key => $result) { + if (isset($result['error'])) { + $status = (int) $result['status']; + + switch (intdiv($status, 100)) { + case 4: + throw $status === 404 ? new NotFoundException($result['error']['reason']) : new InvalidRequestException($result['error']['reason']); + + case 5: + throw new InternalServerException($result['error']['reason']); + + default: + throw new RuntimeException($result['error']['reason']); + } + } + + $results[$key] = new SearchResults( + $result['_scroll_id'] ?? null, + $result['took'], + $result['timed_out'], + $result['_shards'], + $result['hits']['total']['value'], + $result['hits']['total']['relation'] === 'eq', + $result['hits']['max_score'] ?? null, + $result['hits']['hits'], + $result + ); + } + + return $results; + } + /** * {@inheritdoc} */ diff --git a/src/Elasticsearch/Adapter/Response/SearchResults.php b/src/Elasticsearch/Adapter/Response/SearchResults.php index bb0651f..d747ee6 100644 --- a/src/Elasticsearch/Adapter/Response/SearchResults.php +++ b/src/Elasticsearch/Adapter/Response/SearchResults.php @@ -29,7 +29,7 @@ final class SearchResults implements ArrayAccess * @param int $total * @param bool $isAccurateCount * @param float|null $maxScore - * @param array $hits + * @param list $hits * @param array $raw */ public function __construct(?string $scrollId, int $took, bool $timedOut, array $shards, int $total, bool $isAccurateCount, ?float $maxScore, array $hits, array $raw) @@ -130,13 +130,15 @@ public function maxScore(): ?float /** * Array of returned document objects * - * @return array{ + * @return list + * @psalm-suppress MoreSpecificReturnType + * @psalm-suppress LessSpecificReturnStatement */ public function hits(): array { diff --git a/src/Elasticsearch/ElasticsearchIndex.php b/src/Elasticsearch/ElasticsearchIndex.php index 84fa79b..c1072a0 100644 --- a/src/Elasticsearch/ElasticsearchIndex.php +++ b/src/Elasticsearch/ElasticsearchIndex.php @@ -10,6 +10,7 @@ use Bdf\Prime\Indexer\Elasticsearch\Query\Bulk\ElasticsearchBulkQuery; use Bdf\Prime\Indexer\Elasticsearch\Mapper\Property\PropertyInterface; use Bdf\Prime\Indexer\Elasticsearch\Query\ElasticsearchCreateQuery; +use Bdf\Prime\Indexer\Elasticsearch\Query\ElasticsearchMultiSearchQuery; use Bdf\Prime\Indexer\Elasticsearch\Query\ElasticsearchQuery; use Bdf\Prime\Indexer\Elasticsearch\Query\ElasticsearchUpdateQuery; use Bdf\Prime\Indexer\Elasticsearch\Query\Result\BulkResultSet; @@ -251,6 +252,16 @@ public function bulk(): ElasticsearchBulkQuery ; } + /** + * Get a query object for performing multi search + * + * @return ElasticsearchMultiSearchQuery + */ + public function multi(): ElasticsearchMultiSearchQuery + { + return new ElasticsearchMultiSearchQuery($this->client, $this->mapper->configuration()->index(), $this->mapper); + } + /** * Refresh the current index * Make all operations performed since the last refresh available for search diff --git a/src/Elasticsearch/Query/ElasticsearchMultiSearchQuery.php b/src/Elasticsearch/Query/ElasticsearchMultiSearchQuery.php new file mode 100644 index 0000000..49eaac7 --- /dev/null +++ b/src/Elasticsearch/Query/ElasticsearchMultiSearchQuery.php @@ -0,0 +1,261 @@ + + */ + private array $queries = []; + + /** + * @param ClientInterface $client + * @param string $index + * @param ElasticsearchMapperInterface|null $mapper + */ + public function __construct(ClientInterface $client, string $index, ?ElasticsearchMapperInterface $mapper = null) + { + $this->client = $client; + $this->index = $index; + $this->mapper = $mapper; + + if ($this->mapper) { + $this->transformer = Closure::fromCallable([$this->mapper, 'fromIndex']); + } + } + + /** + * Set document transformer for each query result + * Takes as parameter the "hit" document, and returns the model value + * + * + * $query + * ->map(fn ($doc) => new City($doc['_source'])) + * ->all() + * ; + * + * + * @param Closure(array):mixed $transformer + * + * @return $this + * + * @see ElasticsearchQuery::map() + */ + public function map(Closure $transformer): self + { + $this->transformer = $transformer; + + return $this; + } + + /** + * Add a new query to the multi search + * + * @param ElasticsearchQuery $query The query to add + * @param int|string|null $key The key to use for the query. This key will be used to identify the query result in the response. If null, an incremental key will be used. + * + * @return $this + */ + public function push(ElasticsearchQuery $query, $key = null): self + { + $key ??= count($this->queries); + $this->queries[$key] = $query; + + return $this; + } + + /** + * Create a new query used by the multi search + * + * @param int|string|null $key The key to use for the query. This key will be used to identify the query result in the response. If null, an incremental key will be used. + * @param bool $withDefaultScope If true, the default scope will be applied to the query, if any. This parameter only has effect if the mapper is set. + * + * @return ElasticsearchQuery The new query + */ + public function query($key = null, bool $withDefaultScope = true): ElasticsearchQuery + { + $query = (new ElasticsearchQuery($this->client))->from($this->index); + + if ($withDefaultScope && $this->mapper) { + $scope = $this->mapper->scopes()['default'] ?? null; + + if ($scope) { + $scope($query); + } + } + + $this->push($query, $key); + + return $query; + } + + /** + * Execute all queries, and return the results, indexed by the query key + * + * @param array $parameters The parameters to apply on all queries + * + * @return array + * + * @throws QueryExecutionException When query execution failed + * @throws InvalidQueryException When the query is invalid and cannot be compiled or executed + */ + public function execute(array $parameters = []): array + { + $queries = []; + + foreach ($this->queries as $query) { + $queries[] = $parameters + $query->compile(); + } + + try { + $response = $this->client->multiSearch($this->index, $queries); + } catch (ElasticsearchExceptionInterface $e) { + throw new QueryExecutionException($e->getMessage(), 0, $e); + } + + return array_combine( + array_keys($this->queries), + $response + ); + } + + /** + * Get the count of each query, indexed by the query key + * + * @return array + * + * @throws QueryExecutionException When query execution failed + * @throws InvalidQueryException When the query is invalid and cannot be compiled or executed + */ + public function count(): array + { + $queries = []; + + foreach ($this->queries as $query) { + $queryBody = $query->compile(); + $queryBody['track_total_hits'] = true; // Ensure that the actual count is returned + $queryBody['size'] = 0; // Set size to 0 to avoid fetching documents + + $queries[] = $queryBody; + } + + try { + $response = $this->client->multiSearch($this->index, $queries); + } catch (ElasticsearchExceptionInterface $e) { + throw new QueryExecutionException($e->getMessage(), 0, $e); + } + + $counts = []; + + foreach ($response as $result) { + $counts[] = $result->total(); + } + + return array_combine( + array_keys($this->queries), + $counts + ); + } + + /** + * Get the first result of all queries, indexed by the query key + * If one of the query has no result, it will be skipped, so its key will not be present in the result + * + * Usage: + * ```php + * $multiSearch = new ElasticsearchMultiSearchQuery($client, 'index'); + * $multiSearch->query('foo')->match('field', 'foo'); + * $multiSearch->query('bar')->match('field', 'bar'); + * $results = $multiSearch->first(); // ['foo' => ['field' => 'foo', ...], 'bar' => ['field' => 'bar', ...]] + * ``` + * + * @return array + * + * @throws QueryExecutionException When query execution failed + * @throws InvalidQueryException When the query is invalid and cannot be compiled or executed + */ + public function first(): array + { + $values = []; + + foreach ($this->execute(['size' => 1]) as $key => $result) { + $hits = $result->hits(); + + if (!$hits) { + continue; + } + + $result = $hits[0]; + + if ($this->transformer) { + $result = ($this->transformer)($result); + } + + $values[$key] = $result; + } + + return $values; + } + + /** + * Get all results of all queries, indexed by the query key + * If one of the query has no result, an empty array will be returned for this key + * + * Usage: + * ```php + * $multiSearch = new ElasticsearchMultiSearchQuery($client, 'index'); + * $multiSearch->query('foo')->match('field', 'foo'); + * $multiSearch->query('bar')->match('field', 'bar'); + * $results = $multiSearch->all(); // ['foo' => [...], 'bar' => [...]] + * ``` + * + * @return array + * + * @throws QueryExecutionException When query execution failed + * @throws InvalidQueryException When the query is invalid and cannot be compiled or executed + */ + public function all(): array + { + $values = []; + + foreach ($this->execute() as $key => $result) { + $result = $result->hits(); + + if ($this->transformer) { + $result = array_map($this->transformer, $result); + } + + $values[$key] = $result; + } + + return $values; + } +} diff --git a/tests/Elasticsearch/ElasticsearchIndexTest.php b/tests/Elasticsearch/ElasticsearchIndexTest.php index f50f7d9..2fea332 100644 --- a/tests/Elasticsearch/ElasticsearchIndexTest.php +++ b/tests/Elasticsearch/ElasticsearchIndexTest.php @@ -10,6 +10,7 @@ use Bdf\Prime\Indexer\Elasticsearch\Query\Compound\BooleanQuery; use Bdf\Prime\Indexer\Elasticsearch\Query\Compound\Nested; use Bdf\Prime\Indexer\Elasticsearch\Query\ElasticsearchCreateQuery; +use Bdf\Prime\Indexer\Elasticsearch\Query\ElasticsearchMultiSearchQuery; use Bdf\Prime\Indexer\Elasticsearch\Query\ElasticsearchQuery; use Bdf\Prime\Indexer\Elasticsearch\Query\Expression\Script; use Bdf\Prime\Indexer\Elasticsearch\Query\Filter\MatchBoolean; @@ -26,6 +27,8 @@ use ElasticsearchTestFiles\WithDate; use ElasticsearchTestFiles\WithDateIndex; +use function array_map; + /** * Class ElasticsearchIndexTest */ @@ -260,6 +263,25 @@ public function test_query() ); } + /** + * + */ + public function test_multi() + { + $this->addCities(); + + $query = $this->index->multi(); + + $query->query('p')->where('population', '>', 1000000)->order('population', 'asc'); + $query->query('c')->where('population', '<', 1000000)->order('population', 'asc'); + + $this->assertInstanceOf(ElasticsearchMultiSearchQuery::class, $query); + $this->assertEquals( + ['p' => 'Paris', 'c' => 'Parthenay'], + array_map(fn (City $city) => $city->name(), $query->first()) + ); + } + /** * */ diff --git a/tests/Elasticsearch/Query/ElasticsearchMultiSearchQueryTest.php b/tests/Elasticsearch/Query/ElasticsearchMultiSearchQueryTest.php new file mode 100644 index 0000000..0ba119e --- /dev/null +++ b/tests/Elasticsearch/Query/ElasticsearchMultiSearchQueryTest.php @@ -0,0 +1,529 @@ +query = new ElasticsearchMultiSearchQuery(self::getClient(), 'test_cities'); + } + + protected function tearDown(): void + { + if (self::getClient()->hasIndex('test_cities')) { + self::getClient()->deleteIndex('test_cities'); + } + } + + public function test_execute() + { + $create = new ElasticsearchCreateQuery(self::getClient()); + $create + ->into('test_cities') + ->values([ + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ]) + ->values([ + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US' + ]) + ->refresh() + ->execute() + ; + + $frQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'FR')); + $usQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'US')); + + $results = $this->query + ->push($frQuery, 'fr') + ->push($usQuery, 'us') + ->execute() + ; + + $this->assertCount(2, $results); + $this->assertArrayHasKey('fr', $results); + $this->assertArrayHasKey('us', $results); + + $this->assertSame( + [ + [ + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ], + [ + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ] + ], + array_map(fn ($hit) => $hit['_source'], $results['fr']->hits()) + ); + + $this->assertSame( + [ + [ + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ], + [ + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US' + ] + ], + array_map(fn ($hit) => $hit['_source'], $results['us']->hits()) + ); + } + + public function test_execute_error() + { + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('no such index [test_cities]'); + + $this->query->query('fr')->filter(new MatchBoolean('#####', 'FR')); + $this->query->query('use')->filter(new MatchBoolean('#####', 'US')); + + $this->query->execute(); + } + + public function test_count() + { + $create = new ElasticsearchCreateQuery(self::getClient()); + $create + ->into('test_cities') + ->values([ + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ]) + ->values([ + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US' + ]) + ->refresh() + ->execute() + ; + + $frQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'FR')); + $usQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'US')); + + $results = $this->query + ->push($frQuery, 'fr') + ->push($usQuery, 'us') + ->count() + ; + + $this->assertSame([ + 'fr' => 2, + 'us' => 2, + ], $results); + } + + public function test_count_error() + { + $this->expectException(QueryExecutionException::class); + $this->expectExceptionMessage('no such index [test_cities]'); + + $frQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'FR')); + $usQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'US')); + + $this->query + ->push($frQuery, 'fr') + ->push($usQuery, 'us') + ->count() + ; + } + + public function test_query_and_count() + { + $create = new ElasticsearchCreateQuery(self::getClient()); + $create + ->into('test_cities') + ->values([ + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ]) + ->values([ + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US' + ]) + ->refresh() + ->execute() + ; + + $this->query->query('fr')->filter(new MatchBoolean('country', 'FR')); + $this->query->query('us')->filter(new MatchBoolean('country', 'US')); + $results = $this->query->count(); + + $this->assertSame(['fr' => 2, 'us' => 2], $results); + } + + public function test_first() + { + $create = new ElasticsearchCreateQuery(self::getClient()); + $create + ->into('test_cities') + ->values([ + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ]) + ->values([ + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US' + ]) + ->refresh() + ->execute() + ; + + $frQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'FR'))->order('population', 'desc'); + $usQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'US'))->order('population', 'desc'); + + $results = $this->query + ->push($frQuery, 'fr') + ->push($usQuery, 'us') + ->map(fn (array $doc) => $doc['_source']) + ->first() + ; + + $this->assertSame( + [ + 'fr' => [ + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ], + 'us' => [ + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ], + ], + $results + ); + } + + public function test_first_with_missing_queries() + { + $create = new ElasticsearchCreateQuery(self::getClient()); + $create + ->into('test_cities') + ->values([ + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ]) + ->values([ + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US' + ]) + ->refresh() + ->execute() + ; + + $frQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'FR'))->filter('population', '<', 1000000); + $usQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'US'))->filter('population', '<', 1000000); + + $results = $this->query + ->push($frQuery, 'fr') + ->push($usQuery, 'us') + ->map(fn (array $doc) => $doc['_source']) + ->first() + ; + + $this->assertSame( + [ + 'fr' => [ + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ], + ], + $results + ); + } + + public function test_all() + { + $create = new ElasticsearchCreateQuery(self::getClient()); + $create + ->into('test_cities') + ->values([ + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ]) + ->values([ + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ]) + ->values([ + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US' + ]) + ->refresh() + ->execute() + ; + + $frQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'FR'))->order('population', 'desc'); + $usQuery = (new ElasticsearchQuery(self::getClient()))->from('test_cities')->filter(new MatchBoolean('country', 'US'))->order('population', 'desc'); + + $results = $this->query + ->push($frQuery, 'fr') + ->push($usQuery, 'us') + ->map(fn (array $doc) => $doc['_source']) + ->all() + ; + + $this->assertSame( + [ + 'fr' => [ + [ + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ], + [ + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ], + ], + 'us' => [ + [ + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ], + [ + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US' + ], + ], + ], + $results + ); + } + + public function test_all_with_mapper() + { + $query = new ElasticsearchMultiSearchQuery(self::getClient(), 'test_cities', new ElasticsearchMapper(new CityIndex())); + $create = new ElasticsearchCreateQuery(self::getClient()); + $create + ->into('test_cities') + ->values([ + '_id' => '1', + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ]) + ->values([ + '_id' => '2', + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ]) + ->values([ + '_id' => '3', + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ]) + ->values([ + '_id' => '4', + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US' + ]) + ->refresh() + ->execute() + ; + + $query->query('fr', false)->filter(new MatchBoolean('country', 'FR'))->order('population', 'desc'); + $query->query('us', false)->filter(new MatchBoolean('country', 'US'))->order('population', 'desc'); + + $results = $query->all(); + + $this->assertEquals( + [ + 'fr' => [ + new City([ + 'id' => '1', + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ]), + new City([ + 'id' => '2', + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ]), + ], + 'us' => [ + new City([ + 'id' => '3', + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ]), + new City([ + 'id' => '4', + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US' + ]), + ], + ], + $results + ); + } + + public function test_all_with_mapper_default_scope() + { + $query = new ElasticsearchMultiSearchQuery(self::getClient(), 'test_cities', new ElasticsearchMapper(new CityIndex())); + $create = new ElasticsearchCreateQuery(self::getClient()); + $create + ->into('test_cities') + ->values([ + '_id' => '1', + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR', + 'enabled' => true, + ]) + ->values([ + '_id' => '2', + 'name' => 'Cavaillon', + 'population' => 26689, + 'country' => 'FR' + ]) + ->values([ + '_id' => '3', + 'name' => 'New York', + 'population' => 8175133, + 'country' => 'US' + ]) + ->values([ + '_id' => '4', + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US', + 'enabled' => true, + ]) + ->refresh() + ->execute() + ; + + $query->query('fr')->filter(new MatchBoolean('country', 'FR'))->order('population', 'desc'); + $query->query('us')->filter(new MatchBoolean('country', 'US'))->order('population', 'desc'); + + $results = $query->all(); + + $this->assertEquals( + [ + 'fr' => [ + new City([ + 'id' => '1', + 'name' => 'Paris', + 'population' => 2201578, + 'country' => 'FR' + ]), + ], + 'us' => [ + new City([ + 'id' => '4', + 'name' => 'Los Angeles', + 'population' => 3792621, + 'country' => 'US' + ]), + ], + ], + $results + ); + } +}