Configuring the search query

Edit on GitHub

Once you have all the necessary data in Elasticsearch, it’s time to 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.

Querying 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](#expanding)), 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 would later be 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 example above, 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.

Expanding 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:

Filtering 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.

Filtering 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.

Filtering by “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 have to export the is-active field by your search collector. The value for it is boolean.

Filtering 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 have to 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 allows you to re-filter search results by the 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 Configuring the search features. Use this plugin to get the necessary data for the faceted navigation of your search results.

Note

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 (e.g., 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.

Paginating 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.

Note

If you use this plugin, add \Spryker\Client\SearchElasticsearch\Plugin\ResultFormatter\PaginatedResultFormatterPlugin to your result formatter collection.

Sorting 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.

Spelling 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)

Note

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.

Suggestions by page type

Suggestions by page type result by page types such as a category, products, and CMS pages. Use \Spryker\Client\SearchElasticsearch\Plugin\QueryExpander\SuggestionByTypeQueryExpanderPlugin to return sets of documents matching a full-text search query grouped by type, i.e., “product”, “category”, “cms page”, etc. 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.

Autocompletion preparations

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 ($resultSet->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.