diff --git a/public/greeting.js b/public/instant-search.js similarity index 100% rename from public/greeting.js rename to public/instant-search.js diff --git a/src/Builder/QueryBuilderInterface.php b/src/Builder/QueryBuilderInterface.php index 3d14f9c..66e7f1c 100644 --- a/src/Builder/QueryBuilderInterface.php +++ b/src/Builder/QueryBuilderInterface.php @@ -44,4 +44,8 @@ public function buildCompletionSuggestersQuery( string $searchTerm, ?string $source = 'suggest', ): array; + + public function buildTermSuggestersQuery( + string $searchTerm, + ): array; } diff --git a/src/Builder/TwigQueryBuilder.php b/src/Builder/TwigQueryBuilder.php index 29f450c..09f2a3d 100644 --- a/src/Builder/TwigQueryBuilder.php +++ b/src/Builder/TwigQueryBuilder.php @@ -211,6 +211,27 @@ public function buildCompletionSuggestersQuery( return $completionSuggestersQuery; } + public function buildTermSuggestersQuery( + string $searchTerm, + ): array { + $localeCode = $this->localeContext->getLocaleCode(); + $query = $this->twig->render('@WebgriffeSyliusElasticsearchPlugin/query/term-suggesters/query.json.twig', [ + 'searchTerm' => $searchTerm, + 'localeCode' => $localeCode, + ]); + $termSuggestersQuery = []; + /** @var array $queryNormalized */ + $queryNormalized = json_decode($query, true, 512, JSON_THROW_ON_ERROR); + $termSuggestersQuery['suggest'] = $queryNormalized; + + $this->logger->debug(sprintf( + 'Built term suggesters query: "%s".', + json_encode($termSuggestersQuery, JSON_THROW_ON_ERROR), + )); + + return $termSuggestersQuery; + } + /** * @return TaxonInterface[] */ diff --git a/src/Client/ClientInterface.php b/src/Client/ClientInterface.php index fc466ee..b582c20 100644 --- a/src/Client/ClientInterface.php +++ b/src/Client/ClientInterface.php @@ -21,6 +21,7 @@ * @psalm-type ESQueryResult = array{took: int, timed_out: bool, _shards: array, hits: array{total: array{value: int, relation: string}, max_score: ?int, hits: array}, aggregations?: array} * @psalm-type ESSuggestOption = array{text: string, _index: string, _type: string, _id: string, _score: float, _source: array{suggest: array}} * @psalm-type ESCompletionSuggesters = array}>> + * @psalm-type ESTermSuggesters = array}>> */ interface ClientInterface extends LoggerAwareInterface { @@ -77,4 +78,14 @@ public function completionSuggesters( array $query, array $indexes = [], ): array; + + /** + * @param string[] $indexes + * + * @return ESTermSuggesters + */ + public function termSuggesters( + array $query, + array $indexes = [], + ): array; } diff --git a/src/Client/ElasticsearchClient.php b/src/Client/ElasticsearchClient.php index 026d68e..8fad127 100644 --- a/src/Client/ElasticsearchClient.php +++ b/src/Client/ElasticsearchClient.php @@ -18,6 +18,7 @@ /** * @psalm-import-type ESQueryResult from ClientInterface * @psalm-import-type ESCompletionSuggesters from ClientInterface + * @psalm-import-type ESTermSuggesters from ClientInterface */ final class ElasticsearchClient implements ClientInterface { @@ -233,6 +234,19 @@ public function completionSuggesters( return $result['suggest']; } + public function termSuggesters( + array $query, + array $indexes = [], + ): array { + /** @var array{took: int, timed_out: bool, _shards: array, hits: array, suggest: ESTermSuggesters} $result */ + $result = $this->getClient()->search([ + 'index' => implode(',', $indexes), + 'body' => $query, + ]); + + return $result['suggest']; + } + private function getClient(): Client { if ($this->client === null) { diff --git a/src/Controller/SearchController.php b/src/Controller/SearchController.php index e55bc57..67da309 100644 --- a/src/Controller/SearchController.php +++ b/src/Controller/SearchController.php @@ -27,7 +27,7 @@ * @psalm-suppress PropertyNotSetInConstructor * * @psalm-import-type ESSuggestOption from ClientInterface - * @psalm-import-type ESCompletionSuggesters from ClientInterface + * @psalm-import-type ESTermSuggesters from ClientInterface */ final class SearchController extends AbstractController implements SearchControllerInterface { @@ -95,20 +95,53 @@ public function __invoke(Request $request, ?string $query = null): Response $page, $size, ); - // This prevents Pagerfanta from querying ES from a template - /** @var ResponseInterface[] $results */ + /** + * This prevents Pagerfanta from querying ES from a template + * + * @var ResponseInterface[] $results + */ $results = $paginator->getCurrentPageResults(); if (count($results) === 1) { $result = $results[0]; return $this->redirectToRoute($result->getRouteName(), $result->getRouteParams()); } + $termSuggesters = $this->client->termSuggesters( + $this->queryBuilder->buildTermSuggestersQuery($query), + $indexAliasNames, + ); return $this->render('@WebgriffeSyliusElasticsearchPlugin/Search/results.html.twig', [ 'query' => $query, 'paginator' => $paginator, 'filters' => $esSearchQueryAdapter->getQueryResult()->getFilters(), 'queryResult' => $esSearchQueryAdapter->getQueryResult(), + 'termSuggesters' => $this->buildTermSuggesters($query, $termSuggesters), ]); } + + /** + * @param ESTermSuggesters $termSuggesters + */ + private function buildTermSuggesters(string $query, array $termSuggesters): array + { + $suggestions = []; + foreach ($termSuggesters as $suggestion) { + foreach ($suggestion as $suggestionData) { + $options = $suggestionData['options']; + if (count($options) === 0) { + continue; + } + $textToReplace = $suggestionData['text']; + foreach ($options as $option) { + $replaceTerm = $option['text']; + $suggestionKey = str_replace($textToReplace, $replaceTerm, $query); + $suggestionHtml = str_replace($textToReplace, '' . $replaceTerm . '', $query); + $suggestions[$suggestionKey] = $suggestionHtml; + } + } + } + + return $suggestions; + } } diff --git a/templates/Common/Components/_attributes.html.twig b/templates/Common/Components/_attributes.html.twig index 34351ad..b69b888 100644 --- a/templates/Common/Components/_attributes.html.twig +++ b/templates/Common/Components/_attributes.html.twig @@ -7,7 +7,7 @@