Skip to content

Commit

Permalink
Add term suggesters to improve search results
Browse files Browse the repository at this point in the history
  • Loading branch information
lruozzi9 committed Apr 16, 2024
1 parent 5833bb7 commit f149556
Show file tree
Hide file tree
Showing 13 changed files with 115 additions and 14 deletions.
File renamed without changes.
4 changes: 4 additions & 0 deletions src/Builder/QueryBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,8 @@ public function buildCompletionSuggestersQuery(
string $searchTerm,
?string $source = 'suggest',
): array;

public function buildTermSuggestersQuery(
string $searchTerm,
): array;
}
21 changes: 21 additions & 0 deletions src/Builder/TwigQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
*/
Expand Down
11 changes: 11 additions & 0 deletions src/Client/ClientInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<array-key, ESHit>}, aggregations?: array<string, array|ESAggregation|ESDefaultOptionAggregation>}
* @psalm-type ESSuggestOption = array{text: string, _index: string, _type: string, _id: string, _score: float, _source: array{suggest: array<array-key, string>}}
* @psalm-type ESCompletionSuggesters = array<string, array<array-key, array{text: string, offset: int, length: int, options: array<array-key, ESSuggestOption>}>>
* @psalm-type ESTermSuggesters = array<string, array<array-key, array{text: string, offset: int, length: int, options: array<array-key, ESSuggestOption>}>>
*/
interface ClientInterface extends LoggerAwareInterface
{
Expand Down Expand Up @@ -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;
}
14 changes: 14 additions & 0 deletions src/Client/ElasticsearchClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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) {
Expand Down
39 changes: 36 additions & 3 deletions src/Controller/SearchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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, '<strong>' . $replaceTerm . '</strong>', $query);
$suggestions[$suggestionKey] = $suggestionHtml;
}
}
}

return $suggestions;
}
}
2 changes: 1 addition & 1 deletion templates/Common/Components/_attributes.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<div class="ui fluid vertical menu">
<a href="{{ path(app.request.attributes.get('_route'), app.request.attributes.all('_route_params')) }}"
class="ui button">
Reset filters
{{ 'webgriffe_sylius_elasticsearch_plugin.ui.reset_filters'|trans }}
</a>

{% for filter in queryResult.filters %}
Expand Down
2 changes: 1 addition & 1 deletion templates/Layout/_scripts.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
window.instantSearchPath = '{{ path('sylius_shop_instant_search', {query: 'placeholder'}) }}';
</script>

<script src="{{ asset('bundles/webgriffesyliuselasticsearchplugin/greeting.js') }}" defer></script>
<script src="{{ asset('bundles/webgriffesyliuselasticsearchplugin/instant-search.js') }}" defer></script>
2 changes: 1 addition & 1 deletion templates/Search/Result/_breadcrumb.html.twig
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="ui breadcrumb">
<a href="{{ path('sylius_shop_homepage') }}" class="section">{{ 'sylius.ui.home'|trans }}</a>
<div class="divider"> / </div>
<div class="active section">{{ 'sylius.ui.search'|trans }}</div>
<div class="active section">{{ 'webgriffe_sylius_elasticsearch_plugin.ui.search'|trans }}</div>
</div>
18 changes: 12 additions & 6 deletions templates/Search/Result/_content.html.twig
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
<h1 class="ui monster section dividing header"
style="position: relative; max-width: 800px; margin: 50px auto 120px auto; text-align: center; border: 0;">
Search
<div class="sub header">Search results for "{{ query }}"</div>
{{ 'webgriffe_sylius_elasticsearch_plugin.ui.search'|trans }}
<div class="sub header">
{{ 'webgriffe_sylius_elasticsearch_plugin.ui.results_for_query'|trans({'{query}': query}) }}
</div>
</h1>

{% if suggesters|length > 0 %}
{% if termSuggesters|length > 0 %}
<div class="ui section header">
Maybe you were looking for:
{{ 'webgriffe_sylius_elasticsearch_plugin.ui.maybe_you_were_looking_for'|trans }}
<ul>
{% for completionSuggest in completionSuggesters %}
<li><a href="{{ path('sylius_shop_search', {'query': completionSuggest}) }}">{{ completionSuggest }}</a></li>
{% for termSuggestKey, termSuggestHtml in termSuggesters %}
<li>
<a href="{{ path('sylius_shop_search', {'query': termSuggestKey}) }}">
{{ termSuggestHtml|raw }}
</a>
</li>
{% endfor %}
</ul>
</div>
Expand Down
2 changes: 1 addition & 1 deletion templates/query/completion-suggesters/query.json.twig
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": {
"suggest": {
"prefix": "{{ searchTerm }}",
"completion": {
"field": "suggest",
Expand Down
9 changes: 9 additions & 0 deletions templates/query/term-suggesters/query.json.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"suggest": {
"text": "{{ searchTerm }}",
"term": {
"field": "name.{{ localeCode }}",
"suggest_mode" : "missing"
}
}
}
5 changes: 4 additions & 1 deletion translations/messages.it.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ webgriffe_sylius_elasticsearch_plugin:
filterable:
label: 'Usa come filtro in pagina categoria e ricerca'
ui:
results_for_query: 'Risultati per {query}:'
maybe_you_were_looking_for: 'Forse stavi cercando:'
results_for_query: 'Risultati per "{query}":'
search: 'Ricerca'
reset_filters: 'Resetta filtri'

0 comments on commit f149556

Please sign in to comment.