Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Succed a search even with spelling errors #970

Closed
wants to merge 14 commits into from
60 changes: 60 additions & 0 deletions controllers/front/SearchController.php
Expand Up @@ -83,6 +83,10 @@ public function initContent()
$query = Tools::replaceAccentedChars(urldecode($originalQuery));
if ($this->ajax_search) {
$searchResults = Search::find((int) (Tools::getValue('id_lang')), $query, 1, 10, 'position', 'desc', true);

if(empty($searchResults))
$searchResults = Search::find((int) (Tools::getValue('id_lang')), self::findFirstClosestWords($query), 1, 10, 'position', 'desc', true);

if (is_array($searchResults)) {
foreach ($searchResults as &$product) {
$product['product_link'] = $this->context->link->getProductLink($product['id_product'], $product['prewrite'], $product['crewrite']);
Expand Down Expand Up @@ -227,4 +231,60 @@ public function setMedia()
$this->addCSS(_THEME_CSS_DIR_.'product_list.css');
}
}

/**
* findFirstClosestWord
*
* @param $searchString
*
* @return string
*/
static function findFirstClosestWords($searchString)
{
$cache = array();
$closestWords = "";
$lenghtWordCoefMin = 0.7;
$lenghtWordCoefMax = 1.5;
$MINWORDLEN = (int)Configuration::get('PS_SEARCH_MINWORDLEN');

$queries = explode(' ', Search::sanitize($searchString, (int)Context::getContext()->language->id, false, Context::getContext()->language->iso_code));

foreach ($queries as $query)
{
if(strlen($query) < $MINWORDLEN)
continue;

$targetLenghtMin = (int)(strlen($query) * $lenghtWordCoefMin);
$targetLenghtMax = (int)(strlen($query) * $lenghtWordCoefMax);

if($targetLenghtMin < $MINWORDLEN)
$targetLenghtMin = $MINWORDLEN;

$sql = 'SELECT `word` FROM `'._DB_PREFIX_.'search_word`
WHERE `id_lang` = '.(int)Context::getContext()->language->id.'
AND `id_shop` = '.(int)Context::getContext()->shop->id.'
AND LENGTH(`word`) > '.$targetLenghtMin.'
AND LENGTH(`word`) < '.$targetLenghtMax.';';

$selectedWords = Db::getInstance()->executeS($sql);

$closestWords .= array_reduce($selectedWords, function($a, $b) use ($query, &$cache){

if (!isset($cache[$a['word']]))
$cache[$a['word']] = levenshtein($a['word'], $query);
if (!isset($cache[$b['word']]))
$cache[$b['word']] = levenshtein($b['word'], $query);

return $cache[$a['word']] < $cache[$b['word']] ? $a : $b;

}, array ("word" => 'initial'))['word'];

if(next($queries)) {
unset($cache);
$closestWords .= ' ';
}
}

return $closestWords;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method could be optimized. The two iterations over $words array are unnecessary. We could do this in one iteration simply by remembering word with lowest 'level'. This would also reduce memory complexity from O(n) to O(1).

Another suggestion is to query only words that have appropriate length (not necessarily same length, but it should be close enough). It doesn't make much sense to compare words 'thirtybees' with 'the', as the difference will always be huge.

}