diff --git a/src/Contracts/SearchQuery.php b/src/Contracts/SearchQuery.php new file mode 100644 index 00000000..09f4e22d --- /dev/null +++ b/src/Contracts/SearchQuery.php @@ -0,0 +1,240 @@ +className = $className; + } + + /** + * @return class-string + */ + public function getClassName(): string + { + return $this->className; + } + + public function setQuery(string $q): SearchQuery + { + $this->q = $q; + + return $this; + } + + public function getQuery(): ?string + { + return $this->q; + } + + public function setFilter(array $filter): SearchQuery + { + $this->filter = $filter; + + return $this; + } + + public function getFilter(): ?array + { + return $this->filter; + } + + public function setAttributesToRetrieve(array $attributesToRetrieve): SearchQuery + { + $this->attributesToRetrieve = $attributesToRetrieve; + + return $this; + } + + public function setAttributesToCrop(array $attributesToCrop): SearchQuery + { + $this->attributesToCrop = $attributesToCrop; + + return $this; + } + + public function setCropLength(?int $cropLength): SearchQuery + { + $this->cropLength = $cropLength; + + return $this; + } + + public function setAttributesToHighlight(array $attributesToHighlight): SearchQuery + { + $this->attributesToHighlight = $attributesToHighlight; + + return $this; + } + + public function setCropMarker(string $cropMarker): SearchQuery + { + $this->cropMarker = $cropMarker; + + return $this; + } + + public function setHighlightPreTag(string $highlightPreTag): SearchQuery + { + $this->highlightPreTag = $highlightPreTag; + + return $this; + } + + public function setHighlightPostTag(string $highlightPostTag): SearchQuery + { + $this->highlightPostTag = $highlightPostTag; + + return $this; + } + + public function setFacets(array $facets): SearchQuery + { + $this->facets = $facets; + + return $this; + } + + public function setShowMatchesPosition(?bool $showMatchesPosition): SearchQuery + { + $this->showMatchesPosition = $showMatchesPosition; + + return $this; + } + + public function setShowRankingScore(?bool $showRankingScore): SearchQuery + { + $this->showRankingScore = $showRankingScore; + + return $this; + } + + /** + * @param bool $showRankingScoreDetails whether the feature is enabled or not + */ + public function setShowRankingScoreDetails(?bool $showRankingScoreDetails): SearchQuery + { + $this->showRankingScoreDetails = $showRankingScoreDetails; + + return $this; + } + + public function setSort(array $sort): SearchQuery + { + $this->sort = $sort; + + return $this; + } + + public function setMatchingStrategy(string $matchingStrategy): SearchQuery + { + $this->matchingStrategy = $matchingStrategy; + + return $this; + } + + public function setOffset(?int $offset): SearchQuery + { + $this->offset = $offset; + + return $this; + } + + public function setLimit(?int $limit): SearchQuery + { + $this->limit = $limit; + + return $this; + } + + public function setHitsPerPage(?int $hitsPerPage): SearchQuery + { + $this->hitsPerPage = $hitsPerPage; + + return $this; + } + + public function setPage(?int $page): SearchQuery + { + $this->page = $page; + + return $this; + } + + /** + * @param list> $vector a multi-level array floats + */ + public function setVector(array $vector): SearchQuery + { + $this->vector = $vector; + + return $this; + } + + /** + * @param list $attributesToSearchOn + */ + public function setAttributesToSearchOn(array $attributesToSearchOn): SearchQuery + { + $this->attributesToSearchOn = $attributesToSearchOn; + + return $this; + } + + /** + * @internal + */ + public function toEngineQuery(string $prefix, array $indices): EngineQuery + { + $query = new EngineQuery(); + foreach ($indices as $indice) { + if ($indice['class'] === $this->className) { + $query->setIndexUid("$prefix{$indice['name']}"); + + break; + } + } + if (null !== $this->q) { + $query->setQuery($this->q); + } + + // @todo: set all data + + return $query; + } +} diff --git a/src/Engine.php b/src/Engine.php index 1bd47662..e78093eb 100644 --- a/src/Engine.php +++ b/src/Engine.php @@ -5,6 +5,7 @@ namespace Meilisearch\Bundle; use Meilisearch\Client; +use Meilisearch\Contracts\SearchQuery; use Meilisearch\Exceptions\ApiException; final class Engine @@ -133,6 +134,14 @@ public function search(string $query, string $indexUid, array $searchParams): ar return $this->client->index($indexUid)->rawSearch($query, $searchParams); } + /** + * @param list $queries + */ + public function multiSearch(array $queries): array + { + return $this->client->multiSearch($queries); + } + /** * Search the index and returns the number of results. */ diff --git a/src/SearchService.php b/src/SearchService.php index 9fbdcfef..47dab0e1 100644 --- a/src/SearchService.php +++ b/src/SearchService.php @@ -5,6 +5,7 @@ namespace Meilisearch\Bundle; use Doctrine\Persistence\ObjectManager; +use Meilisearch\Bundle\Contracts\SearchQuery; interface SearchService { @@ -60,6 +61,13 @@ public function search( array $searchParams = [] ): array; + /** + * @param list $queries + * + * @return array> + */ + public function multiSearch(ObjectManager $objectManager, array $queries): array; + /** * Get the raw search result. * diff --git a/src/Services/MeilisearchService.php b/src/Services/MeilisearchService.php index 095e8db4..ce2f55c8 100644 --- a/src/Services/MeilisearchService.php +++ b/src/Services/MeilisearchService.php @@ -7,6 +7,7 @@ use Doctrine\Common\Util\ClassUtils; use Doctrine\Persistence\ObjectManager; use Meilisearch\Bundle\Collection; +use Meilisearch\Bundle\Contracts\SearchQuery; use Meilisearch\Bundle\Engine; use Meilisearch\Bundle\Entity\Aggregator; use Meilisearch\Bundle\Exception\ObjectIdNotFoundException; @@ -188,6 +189,42 @@ public function search( return $results; } + public function multiSearch(ObjectManager $objectManager, array $queries): array + { + $prefix = $this->configuration->get('prefix'); + $indices = $this->configuration->get('indices'); + + // $response = $this->engine->multiSearch(array_map(function (SearchQuery $query) use ($prefix, $indices) { + // $this->assertIsSearchable($query->getClassName()); + // + // return $query->toEngineQuery($prefix, $indices); + // }, $queries)); + $response = $this->engine->multiSearch(array_map(static fn (SearchQuery $query) => $query->toEngineQuery($prefix, $indices), $queries)); + $results = []; + + foreach ($response['results'] as $indexResponse) { + $indexResults = []; + $className = $this->getClassNameFromIndex($indexResponse['indexUid']); + + foreach ($indexResponse['hits'] as $hit) { + if (!isset($hit[self::RESULT_KEY_OBJECTID])) { + throw new ObjectIdNotFoundException(sprintf('There is no "%s" key in the multi search "%s" result.', self::RESULT_KEY_OBJECTID, $indexResponse['indexUid'])); + } + + $repo = $objectManager->getRepository($className); + $entity = $repo->find($hit['objectID']); + + if (null !== $entity) { + $indexResults[] = $entity; + } + } + + $results[$className] = $indexResults; + } + + return $results; + } + public function rawSearch( string $className, string $query = '', @@ -346,4 +383,20 @@ private function assertIsSearchable(string $className): void throw new Exception('Class '.$className.' is not searchable.'); } } + + /** + * @return class-string + */ + private function getClassNameFromIndex(string $index): string + { + $prefix = $this->configuration->get('prefix'); + + foreach ($this->configuration->get('indices') as $indice) { + if ("$prefix{$indice['name']}" === $index) { + return $indice['class']; + } + } + + throw new Exception(sprintf('Cannot find searchable class for "%s" index.', $index)); + } } diff --git a/tests/Integration/SearchTest.php b/tests/Integration/SearchTest.php index f5a8ddeb..d33fd26a 100644 --- a/tests/Integration/SearchTest.php +++ b/tests/Integration/SearchTest.php @@ -6,7 +6,9 @@ use Doctrine\DBAL\Connection; use Doctrine\Persistence\ObjectManager; +use Meilisearch\Bundle\Contracts\SearchQuery; use Meilisearch\Bundle\Tests\BaseKernelTestCase; +use Meilisearch\Bundle\Tests\Entity\Comment; use Meilisearch\Bundle\Tests\Entity\Post; use Meilisearch\Bundle\Tests\Entity\Tag; use Meilisearch\Endpoints\Indexes; @@ -14,9 +16,6 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; -/** - * Class SearchTest. - */ class SearchTest extends BaseKernelTestCase { private static string $indexName = 'aggregated'; @@ -115,8 +114,44 @@ public function testSearchPagination(): void $this->assertEqualsCanonicalizing(array_slice($testDataTitles, 2, 2), $resultTitles); } - protected function tearDown(): void + public function testMultiSearch(): void { - parent::tearDown(); + $posts = []; + $comments = []; + + for ($i = 0; $i < 5; ++$i) { + $post = new Post(['title' => $i < 2 ? "Test post $i" : "Good post $i"]); + if ($i < 2) { + $posts[] = $post; + } + + $this->entityManager->persist($post); + + $comment = new Comment(); + $comment->setPost($post); + $comment->setContent($i < 2 ? "Test comment $i" : "Good comment $i"); + if ($i < 2) { + $comments[] = $comment; + } + + $this->entityManager->persist($comment); + } + + $this->entityManager->flush(); + + $firstTask = $this->client->getTasks()->getResults()[0]; + $this->client->waitForTask($firstTask['uid']); + + $result = $this->searchService->multiSearch($this->entityManager, [ + (new SearchQuery(Post::class)) + ->setQuery('test'), + (new SearchQuery(Comment::class)) + ->setQuery('test'), + ]); + + self::assertEquals([ + Post::class => $posts, + Comment::class => $comments, + ], $result); } }