Klevu with Magento 2 Template

We have talked about Klevu in one of our previous articles – what it is, how to install and use it, and most important, how it can improve your store. In this article we are going to focus on Search Results Page. Make sure you have at least Klevu Premium pricing plan to have the ability to chose template at all. 🙂

If you go to

System > Configuration > Klevu > Search configuration > Search Result Page Settings

you will see this dropdown:

Klevu Search Result Page Settings

What it comes to eye is that Preserves Your Theme Layout option is chosen, although it clearly says that Based on Klevu Template is recommended. Why is it so? We’ll see how search works with both templates and which compromises are included with the selected template.

Klevu Template

Search Results Page based on Klevu template really works like charm. Fast ajaxed results and attributes multiselect make you wonder why would you even want to deal with Magento Search. We’ll take a look to browser’s Developer Tools Network Tab to see what’s going on. URL remains http://magento.loc/search/result/?q=tees, but for every new feature selected (sorting, price range, page, etc.) a XHR request is made to the Klevu cloud host hosting our search, i.d.

http://eucs6.ksearchnet.com/cloud-search/n-search/search

with parameters

ticket=klevu-54597553548116773
term=tees // searched term
sortPrice=false
ipAddress=undefined
analyticsApiKey=klevu-54597553548116773
klevuShowOutOfStockProducts=true
klevuFetchPopularTerms=false
klevu_priceInterval=500
fetchMinMaxPrice=true
klevu_multiSelectFilters=true
noOfResults=9 // items per page, defaults are 9, 15 and 30
paginationStartsFrom=18 // get us items for page 3 since 0-8 are showed on page 1, 9-17 on page 2
klevuSort=lth // sort by price: low to high
enableFilters=true
filterResults=category%3Atees%3B%3Bklevu_price%3A22%20-%2044 // selected category "tees", price range 22-44
category=KLEVU_PRODUCT
sv=2129
lsqt=WILDCARD_AND
responseType=json

which are explained at Klevu Developer APIs Documentation. Whole response can be seen here. In this case Magento is something like guest in Klevu restaurant – Magento orders items with specified criteria and Klevu delivers specifically what is asked for. As you can see from response, if there are 24 results for query and we are on last page with specified 9 items per page, Klevu will return only that 6 last items (first 18 are on pages one and two).

Magento Template

Yes, we can style Klevu template to fit rest of the store, but it won’t fit completely. For example, with third party extension for layered navigation and custom additional data (like available sizes) attached to products, Category page will have specific functionalities. Best way to “copy and paste” them to Search Results Page is to use existing Magento template. Now the look is here, but Klevu restaurant is more like self service in this case. When you search for “tees” everything will look like regular Magento Catalog Search. Even the URL will be http://magento.loc/catalogsearch/result/?q=tees. Klevu interferes in search flow with its Cleaner class which rewrites default MagentoFrameworkSearchRequestCleaner class.

public function klevuQueryCleanup($requestData){
 
// check if we are in search page
if(!isset($requestData['queries']['quick_search_container'])) return $requestData;
//check if klevu is supposed to be on
if ($this->klevuConfig->isLandingEnabled()!=1 || !$this->klevuConfig->isExtensionConfigured()) return $requestData;
//save data in session so we do not request via api for filters
$queryTerm = $requestData['queries']['search']['value'];
$queryScope = $requestData['dimensions']['scope']['value'];
$idList = $this->sessionManager->getData('ids_'.$queryScope.'_'.$queryTerm);
if(!$idList){
$idList = $this->klevuRequest->_getKlevuProductIds($queryTerm);
if(empty($idList)) $idList = array(0);
$this->sessionManager->setData('ids_'.$queryScope.'_'.$queryTerm,$idList );
}
//register the id list so it will be used when ordering
$this->magentoRegistry->unregister('search_ids');
$this->magentoRegistry->register('search_ids', $idList); //pulled out in ScoreBuilder class!
 
// .... //
return $requestData;
}

All searched terms are saved in current session along with corresponding result ids. If query can be found in session, result ids will be pulled out for another use to save up request to Klevu cloud host. Otherwise, a request to cloud host will be made, with endpoint

http://eucs6.ksearchnet.com/cloud-search/n-search/idsearch
 
//notice that is different from URL used for search with Klevu template!

and parameters

ticket = klevu-54597553548116773
noOfResults = 2000
term = tees
paginationStartsFrom = 0
enableFilters = false
klevuShowOutOfStockProducts = true
category => KLEVU_PRODUCT

Response will look like this:

{
"meta":{
"totalResultsFound":"27",
"typeOfQuery":"WILDCARD_AND",
"paginationStartFrom":"0",
"noOfResults":"2000"
},
"result":[
{
"id":"1529-1517",
"itemGroupId":"1529"
},
{
"id":"1449-1441",
"itemGroupId":"1449"
},
....
,
{
"id":"1831-1826",
"itemGroupId":"1831"
}
]
}

Result items don’t look like Magento products ids, but thanks to the _getKlevuProductIds function, variable $idList contains real ids. Notice that amonut of information in this response is much smaller compared to response given when using Klevu template. The reason is that after list of given ids is taken from Magento Registry in KlevuSearchAdapterMysqlScoreBuilder class and put into MySQL FIELD function, Magento works like it’s performing default Catalog Search – creates temporary table, inserts this “FIELD(…)” part in its queries and so on. Ids are all it takes.

// KlevuSearchAdapterMysqlScoreBuilder
 
public function build()
{
$scoreAlias = parent::getScoreAlias();
$sessionOrder = $this->magentoRegistry->registry('search_ids');
if(is_array($sessionOrder)) return "FIELD(search_index.entity_id,".implode(",",array_reverse($sessionOrder)).") AS {$scoreAlias}";
return parent::build();
}

So, basically, Klevu delivered list of ids that fit search query and left Magento to deal with them in terms of sorting, ordering, pagination and other. To continue with with Klevu restaurant – now it’s clear where the self service comparison comes from. It’s like Klevu waiter took our order (which, indeed, couldn’t be complicated because of lack of filters on Search Results Page) and brought us ingredients to cook ordered meal ourself. But we have Search Results Page just as we wanted!

Benefits of using Magento template

Possible problems

For stores with large number of products Klevu cloud host can return more than 1000 items in response when searching for some general term. There is no “page” parameter in request which would regulate on which page are we and how many items are shown per page. If you search for “tees” and there are 1000 tees in your store Klevu with get you all of them! Not just 9, 15, 30 or X number you have chosen to be shown on Search Results Page. Instead of 1000 tees you could end up with this exception

Exception #0 (Zend_Db_Statement_Exception): SQLSTATE[42S22]: Column not found: 1054 Unknown column 'FIELD(search_index.entity_id,163182,163119,140982,141045,140919,140667,140730,140856,140415,140352,140541,140289,140478,135573,135384,129087,129045,129465,70080,70017,69765,69954,69639,69702,5' in 'field list'

Fix – rewrite KlevuSearchAdapterMysqlScoreBuilder class:

namespace InchooFixesModelKlevuSearchAdapterMysql;
 
use MagentoFrameworkRegistry as MagentoRegistry;
 
class ScoreBuilder extends KlevuSearchAdapterMysqlScoreBuilder
{
private $magentoRegistry;
 
public function __construct(
MagentoRegistry $magentoRegistry
) {
$this->magentoRegistry = $magentoRegistry;
parent::__construct($magentoRegistry);
}
 
public function build()
{
$scoreAlias = parent::getScoreAlias();
$sessionOrder = $this->magentoRegistry->registry('search_ids');
if(is_array($sessionOrder)) {
foreach ($sessionOrder as $search_id) {
if (!is_numeric($search_id)) {
return false;
}
}
return new Zend_Db_Expr("FIELD(search_index.entity_id,".implode(",",array_reverse($sessionOrder)).") AS {$scoreAlias}");
}
return parent::build();
}
}

Original build method returns string which contains ids of products that suit search criteria. Too long id list creates too long string which then generates PREG_JIT_STACKLIMIT_ERROR in preg_match with pattern REGEX_COLUMN_EXPR, in Zend_Db_Select->_tableCols. In that case MySQL FIELD function is wrongly interpreted as column instead as function which results in MySQL Unknown Column Exception. Correct output of that preg_match results in creating Zend_Db_Expr so it is returned here instead of string to bypass problematic preg_match. Just in case, we added check if $sessionOrder contains only numbers.

To conclude

We hope that with explained workflows of each template option it will be easier to decide which one to use. And deal with problems if they occur. Happy coding (and debugging)! 🙂