Tutorial: Integrate any search engine into a project
Edit on GitHubIn a Spryker-based project, you can use any external search provider instead of the default Elasticsearch. This tutorial shows how to replace the default Elasticsearch with any other search engine.
Challenge description
Every search engine comes along with its own functionalities and search approaches. However, in most search platforms, the integration of search and search suggestions is similar.
This tutorial shows how to integrate the FACT-Finder (PHP) search platform. A system integrator development team can use this guide to integrate other platforms, taking into account the differences between the target search platform.
The integration is following the concept described in Search Migration Concept.
Challenge solving highlights
To use FACT-Finder as a search data provider, do the following:
-
Execute search and search suggestion requests, which implies the following actions:
- Handling the search request.
- Building a query object from the customer’s request. Usually, the request contains a query string, facets, and pagination. All project-specific parameters must be used.
- Making a request to FACT-Finder with the built query object.
- Mapping the response to the shop’s specific format.
Executing search and search suggestion requests
To execute the search and search suggestion requests, follow these steps:
1. Build and pass a query
- Define, for example, the
FfSearchQueryTransfer
object, which must contain at leastsearchString
(string, customer’s input) andrequestParams
(string, containing, for example, pagination and filters):
<transfer name="FfSearchQuery">
<property name="searchString" type="string"/>
<property name="requestParams" type="string[]" singular="requestParam"/>
</transfer>
- Create a query model—for example,
FactFinderQuery
. The basic version can look like this:
Code sample
class FactFinderQuery implements QueryInterface, SearchContextAwareQueryInterface
{
/**
* @var FfSearchQueryTransfer
*/
private $searchQueryTransfer;
/**
* @var SearchContextTransfer
*/
private $searchContextTransfer;
/**
* @param FfSearchQueryTransfer $queryTransfer
*/
public function __construct(FfSearchQueryTransfer $queryTransfer)
{
$this->searchQueryTransfer = $queryTransfer;
}
/**
* @inheritDoc
*/
public function getSearchQuery()
{
return $this->searchQueryTransfer;
}
/**
* @inheritDoc
*/
public function getSearchContext(): SearchContextTransfer
{
$this->searchContextTransfer = $this->searchContextTransfer ?? (new SearchContextTransfer())->setSourceIdentifier(FFSearchAdapterPlugin::FF);
return $this->searchContextTransfer;
}
/**
* @inheritDoc
*/
public function setSearchContext(SearchContextTransfer $searchContextTransfer): void
{
$this->searchContextTransfer = $searchContextTransfer;
}
}
- In particular
catalogSearch
andcatalogSuggestSearch
, extendSpryker\Client\Catalog\CatalogClient
:
public function catalogSuggestSearch($searchString, array $requestParameters = [])
{
$searchQuery = $this->buildFFSearchQuery($searchString, $requestParams);
return $this
->getFactory()
->getSearchClient()
->search($searchQuery);
}
private function buildFFSearchQuery($searchString, $requestParams): FactFinderQuery
{
$ffSearchQueryTransfer = new FfSearchQueryTransfer();
$ffSearchQueryTransfer->setSearchString($searchString)
->setRequestParams($requestParams);
$searchQuery = new FactFinderQuery($ffSearchQueryTransfer);
}
2. Execute the search request
To handle search requests through a different source, you need your own model implementing the SearchAdapterPluginInterface
interface.
The following is a template for this model:
Code sample:
class FFSearchAdapterPlugin implements SearchAdapterPluginInterface
{
const FACT_FINDER = 'FACT_FINDER';
/**
* @inheritDoc
*/
public function search(QueryInterface $searchQuery, array $resultFormatters = [], array $requestParameters = [])
{
return <MAPPED DATA>;
}
/**
* @inheritDoc
*/
public function readDocument(SearchDocumentTransfer $searchDocumentTransfer): SearchDocumentTransfer
{
// TODO: Implement readDocument() method.
}
/**
* @inheritDoc
*/
public function deleteDocument(SearchDocumentTransfer $searchDocumentTransfer): bool
{
// TODO: Implement deleteDocument() method.
}
/**
* @inheritDoc
*/
public function deleteDocuments(array $searchDocumentTransfers): bool
{
// TODO: Implement deleteDocuments() method.
}
/**
* @inheritDoc
*/
public function isApplicable(SearchContextTransfer $searchContextTransfer): bool
{
return $searchContextTransfer->getSourceIdentifier() === self::FACT_FINDER;
}
/**
* @inheritDoc
*/
public function writeDocument(SearchDocumentTransfer $searchDocumentTransfer): bool
{
// TODO: Implement writeDocument() method.
}
/**
* @inheritDoc
*/
public function writeDocuments(array $searchContextTransfers): bool
{
// TODO: Implement writeDocuments() method.
}
/**
* @inheritDoc
*/
public function getName(): string
{
return self::FACT_FINDER;
}
}
The isApplicable
method in the preceding template validates that the request is supposed to be processed in this adapter—in this example, by FACT-Finder.
Make sure that all events affecting FACT-Finder-related product data are triggered with this type. For this purpose, the following change is required in Pyz/Zed/ProductPageSearch/Persistence/Propel/Schema/spy_product_page_search.schema.xml
:
<?xml version="1.0"?>
<database xmlns="spryker:schema-01" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="zed" xsi:schemaLocation="spryker:schema-01 https://static.spryker.com/schema-01.xsd"
namespace="Orm\Zed\ProductPageSearch\Persistence"
package="src.Orm.Zed.ProductPageSearch.Persistence">
<table name="spy_product_abstract_page_search">
<behavior name="synchronization">
<parameter name="params" value='{"type":"FACT_FINDER"}'/>
</behavior>
</table>
<table name="spy_product_concrete_page_search">
<behavior name="synchronization">
<parameter name="params" value='{"type":"FACT_FINDER"}'/>
</behavior>
</table>
</database>
3. Request data from FACT-Finder
Implement method search in the adapter plugin.
Your search function receives FactFinderQuery
with FFSearchQueryTransfer
in it as the first argument.
Prepare proper request to a FACT-Finder based on these parameters.
If you need specific $resultFormatters
or $requestParameters
, use the arrays proposed in the adapter plugin.
4. Map response
The general idea behind the mapping of the response is to make sure you can display the received data.
The FACT-Finder module provides a response in FactFinderSdkSearchResponse
, but Spryker provides the complete rendering of the search results and search suggestions based on the response from the default search provider, which is Elasticsearch.
It means that in order to use the FACT-Finder response, you have to comply with the response structure produced there. This will be changed in the future, but for now, you have to implement mapping to the similar response Elasticsearch modules provides.
You have to respond with an object, supporting an array-based or get
-based index—for example, creating a JSON object or a transfer object.
Code sample of a response from the search provider:
{
"facets": {
"category": {},
"price-DEFAULT-EUR-GROSS_MODE": {},
"rating": {},
"label": {},
"color": {},
"storage_capacity": {},
"brand": {},
"touchscreen": {},
"weight": {},
"merchant_name": {}
},
"sort": {
"sortParamNames": [],
"sortParamLocalizedNames": [],
"currentSortParam": "",
"currentSortOrder": ""
},
"pagination": {},
"products": [
{
"images": [
{
"fk_product_image_set": 277,
"id_product_image": 277,
"product_image_key": "product_image_277",
"updated_at": "2020-08-20 10:03:03.710824",
"external_url_small": "https:\/\/images.icecat.biz\/img\/gallery_mediums\/29231675_7943.jpg",
"external_url_large": "https:\/\/images.icecat.biz\/img\/gallery\/29231675_7943.jpg",
"created_at": "2020-08-20 10:03:03.710824",
"id_product_image_set_to_product_image": 277,
"sort_order": 0,
"fk_product_image": 277
}
],
"id_product_labels": [],
"price": 19700,
"abstract_name": "Samsung Galaxy S4 Mini",
"id_product_abstract": 63,
"type": "product_abstract",
"prices": {
"DEFAULT": 19700,
"ORIGINAL": 20000
},
"abstract_sku": "063",
"url": "\/en\/samsung-galaxy-s4-mini-63"
}
],
"spellingSuggestion": null
}
Returning this JSON data as an object shows you an empty result page.
To see what is supported by Spryker’s template, refer to CatalogDependencyProvider::createCatalogSearchResultFormatterPlugins
.
The response structure for search suggestions must be investigated in a similar way.
Populate Fact Finder with product data
To handle search update events, use the instructions from the following sections.
1. Adjust the Adapter plugin
To handle search update events, you have to implement the following methods of the SearchAdapterPluginInterface
:
deleteDocument
—when a single document is supposed to be removed. You will receive an internal identifier as a keydeleteDocuments
—when documents are supposed to be removed in bulk. You will receive a list of internal identifierswriteDocument
writeDocuments
2. Handle the events
Since Spryker stores not only product data in Elasticsearch, but also CMS pages and categories, you have to make sure that only product data is handled by the FACT-Finder adapter.
To achieve this, in spy_product_page_search.schema.xml
, change the schema for the search documents. Make sure to use the same source identifier as used in the adapter class.
<?xml version="1.0"?>
<database xmlns="spryker:schema-01" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="zed" xsi:schemaLocation="spryker:schema-01 https://static.spryker.com/schema-01.xsd"
namespace="Orm\Zed\ProductPageSearch\Persistence"
package="src.Orm.Zed.ProductPageSearch.Persistence">
<table name="spy_product_abstract_page_search">
<behavior name="synchronization">
<parameter name="params" value='{"type":"FACT_FINDER"}'/>
</behavior>
</table>
<table name="spy_product_concrete_page_search">
<behavior name="synchronization">
<parameter name="params" value='{"type":"FACT_FINDER"}'/>
</behavior>
</table>
</database>
3. Map data
To load and map data properly, you might have to adjust data loaders, expanders, and mappers in ProductPageSearchDependencyProvider
, both for product abstract and product concrete.
After completing these steps, the search engine is integrated into your project.
Using search service provider in Glue API
The current version of the catalog search in Glue has more requirements for the response.
It expects that sort
value in the response supports toArray
function and contains sortParamNames
, sortParamLocalizedNames
, currentSortParam
, and currentSortOrder
fields. As a reference, use the RestCatalogSearchSortTransfer
transfer object.
Thank you!
For submitting the form