Configure a search query
Edit on GitHubOnce you have all the necessary data in Elasticsearch, you can display it on Yves.
To achieve this, query Elasticsearch, which returns raw data needed for processing the query result to display it in the templates.
In SearchClient
, you can find the search()
method (\Spryker\Client\Search\SearchClientInterface::search())
. Call this method to execute any search query. It expects to receive an instance of \Spryker\Client\SearchExtension\Dependency\Plugin\QueryInterface
as the first parameter, which represents the query itself, and a collection of \Spryker\Client\SearchExtension\Dependency\Plugin\ResultFormatterPluginInterface
instances, which are applied to the response data to format it.
Query Elasticsearch
The first step is implementing the QueryInterface
. To communicate with Elasticsearch, Spryker uses the Elastica library as a Data Query Language.
Inside QueryInterface
, create an instance of \Elastica\Query
, configure it to fit your needs, and then return it with getSearchQuery()
.
This is the point where configuring the query is completely up to you. Use Elastica to alter the query to your needs, add filters, aggregations, boosts, sorting, pagination, or anything else you like and Elasticsearch allows you.
The QueryInterface
instance is a stateful class; sometimes, the getSearchQuery()
method is called multiple times and alters the original query (see Expanding queries), so make sure that it returns the same instance. This can be achieved by creating the \Elastica\Query
instance at construction time and returning it in the getSearchQuery()
method.
Besides, this new QueryInterface
instance has to implement Spryker\Client\SearchExtension\Dependency\Plugin\SearchContextAwareQueryInterface
. To be compliant with this interface, implementations for the ::setSearchContext()
and ::getSearchContext()
methods must be provided. This is needed for setting and maintaining a search context that is later used during the search process, particularly for resolving the correct Elasticsearch index for search. For more information, see Search migration concept.
Query
<?php
namespace Pyz\Client\Catalog\Plugin\Query;
use Elastica\Query;
use Elastica\Query\MatchAll;
use Generated\Shared\Search\PageIndexMap;
use Spryker\Client\Kernel\AbstractPlugin;
use Spryker\Client\SearchExtension\Dependency\Plugin\QueryInterface;
use Spryker\Client\SearchExtension\Dependency\Plugin\SearchContextAwareQueryInterface;
class MatchAllQueryPlugin extends AbstractPlugin implements QueryInterface, SearchContextAwareQueryInterface
{
protected const SOURCE_IDENTIFIER = 'page';
/**
* @var \Elastica\Query
*/
protected $query;
/**
* @var \Generated\Shared\Transfer\SearchContextTransfer
*/
protected $searchContextTransfer;
/**
* @param string $searchString
*/
public function __construct()
{
$this-->query = $this->createSearchQuery();
}
/**
* @return \Elastica\Query
*/
public function getSearchQuery()
{
return $this-->query;
}
/**
* @return \Elastica\Query
*/
protected function createSearchQuery()
{
$query = new Query();
$query = $this->addMatchAllQuery($query);
$query->setSource([PageIndexMap::SEARCH_RESULT_DATA]);
return $query;
}
/**
* @param \Elastica\Query $baseQuery
*
* @return \Elastica\Query
*/
protected function addMatchAllQuery(Query $baseQuery)
{
$baseQuery->setQuery(new MatchAll());
return $baseQuery;
}
/**
* @return \Generated\Shared\Transfer\SearchContextTransfer
*/
public function getSearchContext(): SearchContextTransfer
{
if (!$this->hasSearchContext()) {
$this->setupDefaultSearchContext();
}
return $this->searchContextTransfer;
}
/**
* @param \Generated\Shared\Transfer\SearchContextTransfer $searchContextTransfer
*
* @return void
*/
public function setSearchContext(SearchContextTransfer $searchContextTransfer): void
{
$this->searchContextTransfer = $searchContextTransfer;
}
/**
* @return void
*/
protected function setupDefaultSearchContext(): void
{
$searchContextTransfer = new SearchContextTransfer();
$searchContextTransfer->setSourceIdentifier(static::SOURCE_IDENTIFIER);
$this->searchContextTransfer = $searchContextTransfer;
}
/**
* @return bool
*/
protected function hasSearchContext(): bool
{
return (bool)$this->searchContextTransfer;
}
}
In the preceding example, a simple query is created, which returns all the documents from your mapping type.
To execute this query, you need to call the search()
method of the SearchClient
.
Expand queries
Query expanders are a way to reuse partial queries to build more complex ones.
The suggested way to create queries is to create the simplest possible query as a base query for your use case. Then, use query expanders to expand it with other reusable behaviors, such as pagination or sorting.
You can create a new expander by implementing \Spryker\Client\SearchExtension\Dependency\Plugin\QueryExpanderPluginInterface
.
Again, if you use query expanders, make sure that your base query is expandable so it provides the same instance by calling getSearchQuery()
multiple times.
To expand a base query with a collection of expanders, use expandQuery()
method from the SearchClient
:
<?php
// ...
/**
* @var \Spryker\Client\Search\SearchClientInterface
*/
protected $searchClient;
// ...
/**
* @param \Spryker\Client\SearchExtension\Dependency\Plugin\QueryInterface $baseQuery
* @param \Spryker\Client\SearchExtension\Dependency\Plugin\QueryExpanderPluginInterface[] $queryExpanders
* @param array $requestParameters
*
* @return \Spryker\Client\SearchExtension\Dependency\Plugin\QueryInterface
*/
protected function expandBaseQuery(QueryInterface $baseQuery, array $queryExpanders, array $requestParameters)
{
$searchQuery = $this
->searchClient
->expandQuery($baseQuery, $queryExpanders, $requestParameters);
return $searchQuery;
}
// ...
Query expander plugins
Spryker provides the following query expander plugins.
Filter by store
The Filter by store feature is a background capability that enables filtering content according to the request’s store.
To filter content according to the request’s store, use \Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\StoreQueryExpanderPlugin
.
Filter by locale
The Filter by locale feature is a background capability that enables filtering content according to the request’s locale.
To filter content according to the request’s store, use \Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\LocalizedQueryExpanderPlugin
.
Filter by the “is active” flag
To display only active records in search results, use \Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\IsActiveQueryExpanderPlugin
. Add this to expander plugin stack, for example \Pyz\Client\Catalog\CatalogDependencyProvider::createSuggestionQueryExpanderPlugins
. You also must export the is-active
field by your search collector. The value for it is a boolean.
Filter by “is active” within a given date range
To display only records which are active within a given date range, use \Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\IsActiveInDateRangeQueryExpanderPlugin
. Add this plugin to expander plugin stack—for example, \Pyz\Client\Catalog\CatalogDependencyProvider::createSuggestionQueryExpanderPlugins
.
You also must export active-from
and active-to
by your search collector. The value is any valid Elasticsearch Date datatype value. For more information, see Elasticsearch reference.
Faceted navigation and filters
The Faceted navigation and filtering feature lets you refilter search results by specific criteria. The filters are commonly displayed on the left side of the catalog page.
\Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\FacetQueryExpanderPlugin
is responsible for adding necessary aggregations to your query based on a predefined configuration (see Configure search features. Use this plugin to get the necessary data for the faceted navigation of your search results.
If you use this plugin, add \Spryker\Client\SearchElasticsearch\Plugin\ResultFormatter\FacetResultFormatterPlugin
to your result formatter collection, which processes the returned raw aggregation data.
To optimize facet aggregations, the Search
module combines all fields in groups of simple faceted aggregations—for example, string-facet
. However, in some cases, you need more control over facet generation.
To manage each facet filter separately, check the aggregationParams
field in FacetConfigTransfer
. If no custom parameters are set to a facet config, it is grouped by default.
However, if your project requires more, replace the default behavior in the provided extension points. FacetQueryExpanderPlugin
, FacetResultFormatterPlugin
are good points to start.
Paginate the results
It provides information about paginating the catalog pages and their current state.
\Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\PaginatedQueryExpanderPlugin
takes care of paginating your results based on the predefined configuration.
If you use this plugin, add \Spryker\Client\SearchElasticsearch\Plugin\ResultFormatter\PaginatedResultFormatterPlugin
to your result formatter collection.
Sort the results
It provides information and functionality necessary for sorting the results.
\Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\SortedQueryExpanderPlugin
takes care of sorting your results based on the predefined configuration. The necessary result formatter for this plugin is \Spryker\Client\SearchElasticsearch\Plugin\ResultFormatter\SortedResultFormatterPlugin
.
Spell suggestion
It adds a spelling correction suggestion to search results.
Use \Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\SpellingSuggestionQueryExpanderPlugin
to allow Elasticsearch to provide the “did you mean” suggestions for full-text search typos. The suggestions are collected from the suggestion_terms
field of the page
index map. Therefore, inside this field, store only the information that you want to use for this purpose. The necessary result formatter for this plugin is \Spryker\Client\SearchElasticsearch\Plugin\ResultFormatter\SpellingSuggestionResultFormatterPlugin
Fuzzy search (query)
Fuzzy search is valid for Master Suite only and has not been integrated into B2B/B2C Suites yet.
It looks up for products even if a customer makes typos and spelling mistakes in a search query. Use \Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\FuzzyQueryExpanderPlugin
to allow Elasticsearch to add the "fuzziness": "AUTO" parameter
to any matching query that is created as the suggested search.
Before enabling this plugin for the primary search (not a suggestions search), make sure that you are not using the cross_fields
search type, which is not allowed in conjunction with the fuzzy search in Elasticsearch.
You can change this behavior by overriding \Spryker\Client\Catalog\Plugin\Elasticsearch\Query\CatalogSearchQueryPlugin
on the project level and adjusting the createMultiMatchQuery
method.
For example, you can change the type to the best_fields
:
/**
* @param array<string> $fields
* @param string $searchString
*
* @return \Elastica\Query\MultiMatch
*/
protected function createMultiMatchQuery(array $fields, string $searchString): MultiMatch
{
return (new MultiMatch())
->setFields($fields)
->setType(MultiMatch::TYPE_BEST_FIELDS)
->setFuzziness(MultiMatch::FUZZINESS_AUTO)
->setQuery($searchString)
->setType(MultiMatch::TYPE_BEST_FIELDS);
}
Please check [official Elastic Search documentation]{https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#multi-match-types} in order to pick most preferable type for the multi-match search query.
Suggestions by page type
Suggestions by page type result by page types such as a category, products, and CMS pages. To return sets of documents matching a full-text search query grouped by type, use \Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\SuggestionByTypeQueryExpanderPlugin
—for example, “product”, “category”, or “cms page”. Typical usage for this plugin is suggesting the top results by type when the user is typing in the search field. The necessary result formatter for this plugin is \Spryker\Client\SearchElasticsearch\Plugin\ResultFormatter\SuggestionByTypeResultFormatterPlugin
.
Autocompletion
Autocompletion adds the functionality to predict the rest of the word or search string.
\Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\CompletionQueryExpanderPlugin
provides top completion terms for full-text search queries. Typical usage for this plugin is autocompleting the input of users with the top result when they type something in the full-text search field and providing more suggestions as they type. The suggestions are collected from the completion_terms
field of the page
index map. Hence, make sure to store only the information inside the field that you’d like to use for this purpose. The necessary result formatter for this plugin is \Spryker\Client\SearchElasticsearch\Plugin\ResultFormatter\CompletionResultFormatterPlugin
.
To enable autocompletion when the user types, add some analyzers to the full-text search fields. Without this, the standard analyzer of Elasticsearch only provides suggestions after each completed word. The solution to providing mid-word suggestions is to add an edge ngram filter to the fields in which you are searching. To add this behavior to the page
index, add the following settings to your src/Pyz/Shared/Search/Schema/page.json
file.
Keep in mind that for existing indexes, changing the analyzers is not possible, so you need to set it up from the ground.
src/Pyz/Shared/Search/Schema/page.json
{
"settings": {
"analysis": {
"analyzer": {
"fulltext_index_analyzer": {
"tokenizer": "standard",
"filter": [
"lowercase",
"fulltext_index_ngram_filter"
]
},
"fulltext_search_analyzer": {
"tokenizer": "standard",
"filter": [
"lowercase"
]
}
},
"filter": {
"fulltext_index_ngram_filter": {
"type": "edge_ngram",
"min_gram": 2,
"max_gram": 20
}
}
}
},
"mappings": {
"page": {
"properties": {
"full-text": {
"analyzer": "fulltext_index_analyzer",
"search_analyzer": "fulltext_search_analyzer"
},
"full-text-boosted": {
"analyzer": "fulltext_index_analyzer",
"search_analyzer": "fulltext_search_analyzer"
}
}
}
}
}
Process query result
After creating your query, process the raw response from Elasticsearch. This is done by providing a collection of \Spryker\Client\SearchExtension\Dependency\Plugin\ResultFormatterPluginInterface
.
To create one, extend \Spryker\Client\SearchElasticsearch\Plugin\ResultFormatter\AbstractElasticsearchResultFormatterPlugin
.
It’s also possible to not provide any result formatters; in this case, the raw response is returned at the end.
Pyz\Client\Catalog\Plugin\ResultFormatter
<?php
namespace Pyz\Client\Catalog\Plugin\ResultFormatter;
use Elastica\Result;
use Elastica\ResultSet;
use Generated\Shared\Search\PageIndexMap;
use Spryker\Client\SearchElasticsearch\Plugin\ResultFormatter\AbstractElasticsearchResultFormatterPlugin;
class DummyResultFormatterPlugin extends AbstractElasticsearchResultFormatterPlugin
{
const NAME = 'test';
/**
* @return string
*/
public function getName()
{
return static::NAME;
}
/**
* @param \Elastica\ResultSet $searchResult
* @param array $requestParameters
*
* @return array
*/
protected function formatSearchResult(ResultSet $searchResult, array $requestParameters)
{
$results = [];
foreach ($searchResult->getResults() as $result) {
$results[] = $this->formatResult($result);
}
return $results;
}
/**
* @param \Elastica\Result $result
*
* @return mixed
*/
protected function formatResult(Result $result)
{
// do something with the result ...
return $result;
}
}
To execute the previously created query along with this result formatter plugin, you need to call the search()
method of SearchClient
and provide this formatter to its second parameter.
When you use the result formatter plugins, the result of the SearchClient::search()
method is an associative array, where the keys are the name of each result formatters (provided by the getName()
method) and the values are the response for each result formatter.
This way, in your controller, where you get the response at the end, you can simply provide everything you get right to the template to care of.
Thank you!
For submitting the form