Использование поиска Sphinx в PrestaShop

В PrestaShop уже имеется встроенный поисковик, который обладает неплохой функциональностью, но иногда его возможностей не хватает. К тому же он не очень быстрый и на крупных сайтах сам поиск и индексация могут работать очень медленно.
Если вас это не устраивает (как и меня), то можно использовать отличную замену — Sphinx.

Цитата из Википедии:

Sphinx (англ. SQL Phrase Index) — система полнотекстового поиска, разработанная Андреем Аксеновым и распространяемая по лицензии GNU GPL. Отличительной особенностью является высокая скорость индексации и поиска, а также интеграция с существующими СУБД (MySQL, PostgreSQL) и API для распространённых языков веб-программирования (официально поддерживаются PHP, Python, Java; существуют реализованные сообществом API для Perl, Ruby,.NET[1] и C++).

Официальный сайт Sphinx — http://sphinxsearch.com/

Установка Sphinx

Я использовал Debian 8.1, в других дистрибутивах (а тем более в Windows) процесс может немного отличаться.
Если Sphinx имеется в репозиториях, то устанавливаем его:

1
2
3
su
apt-get update
apt-get install sphinxsearch

Если в репозиториях его нет, то можно скачать с официального сайта установщик, попутно установив необходимые пакеты:

1
2
3
apt-get install mysql-client unixodbc libpq5
wget http://sphinxsearch.com/files/sphinxsearch_2.2.9-release-1~wheezy_i386.deb
dpkg -i sphinxsearch_2.2.9-release-1~wheezy_i386.deb

Имя и адрес файла установщика нужно, конечно, заменить на актуальные.

Настройка Sphinx

Открываем конфигурационный файл Sphinx и изменяем его для своих нужд:

1
nano /etc/sphinxsearch/sphinx.conf

Редактируем имеющийся по умолчанию блок source src1 (или добавляем свой блок), должно получиться примерно следующее:

1
2
3
4
5
6
7
8
9
10
11
12
source PrestaSite
{
type = mysql
sql_host = localhost
#имя пользователя базы данных PrestaShop
sql_user = DBUSER
#пароль пользователя базы данных PrestaShop
sql_pass = DBPASSWORD
#имя базы данных PrestaShop
sql_db = DBNAME
sql_port = 3306 # optional, default is 3306
sql_query_pre = SET NAMES utf8

#MySQL запрос со списком полей для индексации
sql_query = \
SELECT id_product, name, description, description_short \
FROM ps_product_lang
}

Сначала идут настройки, отвечающие за подключение к базе данных (не забываем поменять значения этих параметров на свои). В параметре sql_query указан MySQL запрос, в котором описано, какая информация будет проиндексирована.
Это все незакомментированные настройки из моего конфига. В принципе, конфигурационный файл хорошо документирован и все параметры там описаны, так что вы можете без проблем настроить свою конфигурацию.

Далее, в блоке index definition настраиваем индекс:

1
2
3
4
index PrestaSite
{
# Источник данных для индексирования
source = PrestaSite

# Адрес, где будут хранится данные индекса
path = /var/lib/sphinxsearch/data/prestasite

# Индекс с учетом морфологии
morphology = stem_ru

# Минимальная длина слова для индексации
min_word_len = 1
}

Далее идут блоки indexer settings и searchd settings, их можно не трогать.

Таким образом, мы настроили источник информации (source PrestaSite) и индекс (index PrestaSite).

Индексация

Чтобы проиндексировать нашу базу, запускаем indexer:

1
indexer --all

Запуск Sphinx

Не забываем запустить/перезапустить searchd:

1
searchd

И добавляем indexer в crontab:

1
15 * * * * root indexer --all

Индексация будет запускаться каждый час.

Настройка PrestaShop

Ниже приведен код для PrestaShop 1.6. Для других версий он может немного отличаться.

Создаем (или редактируем) класс Search:
/override/classes/Search.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<?php
class Search extends SearchCore {
    public static function find($id_lang, $expr, $page_number = 1, $page_size = 1, $order_by = 'position',
        $order_way = 'desc', $ajax = false, $use_cookie = true, Context $context = null)
    {
        if (!$context) {
            $context = Context::getContext();
        }
        $db = Db::getInstance(_PS_USE_SQL_SLAVE_);

        // TODO : smart page management
        if ($page_number < 1) $page_number = 1;
        if ($page_size < 1) $page_size = 1;

        if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way))
            return false;

        if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) {
            return false;
        }
       
        $start = ($page_number - 1) * $page_size;
       
        // Sphinx search, get ids of found products
        $sphinx_results = self::getSphinxResults($expr, $start, $page_size);
       
        $result = null;
        $total = 0;
        // get products by id if something found
        if (is_array($sphinx_results) AND sizeof($sphinx_results) AND isset($sphinx_results['total']) AND $sphinx_results['total'] > 0) {
            $sql = 'SELECT p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity,
                    pl.`description_short`, pl.`available_now`, pl.`available_later`, pl.`link_rewrite`, pl.`name`,
                    MAX(image_shop.`id_image`) id_image, il.`legend`, m.`name` manufacturer_name, MAX(product_attribute_shop.`id_product_attribute`) id_product_attribute,
                    DATEDIFF(
                        p.`date_add`,
                        DATE_SUB(
                            NOW(),
                            INTERVAL '
.(Validate::isUnsignedInt(Configuration::get('PS_NB_DAYS_NEW_PRODUCT')) ? Configuration::get('PS_NB_DAYS_NEW_PRODUCT') : 20).' DAY
                        )
                    ) > 0 new
                    FROM '
._DB_PREFIX_.'product p
                    '
.Shop::addSqlAssociation('product', 'p').'
                    INNER JOIN `'
._DB_PREFIX_.'product_lang` pl ON (
                        p.`id_product` = pl.`id_product`
                        AND pl.`id_lang` = '
.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl').'
                    )
                    LEFT JOIN `'
._DB_PREFIX_.'product_attribute` pa ON (p.`id_product` = pa.`id_product`)
                    '
.Shop::addSqlAssociation('product_attribute', 'pa', false, 'product_attribute_shop.`default_on` = 1').'
                    '
.Product::sqlStock('p', 'product_attribute_shop', false, $context->shop).'
                    LEFT JOIN `'
._DB_PREFIX_.'manufacturer` m ON m.`id_manufacturer` = p.`id_manufacturer`
                    LEFT JOIN `'
._DB_PREFIX_.'image` i ON (i.`id_product` = p.`id_product`)'.
                            Shop::addSqlAssociation('image', 'i', false, 'image_shop.cover=1').'
                    LEFT JOIN `'
._DB_PREFIX_.'image_lang` il ON (i.`id_image` = il.`id_image` AND il.`id_lang` = '.(int)$id_lang.')
                    WHERE p.`id_product` IN('
.implode(',', $sphinx_results['results']).')
                    GROUP BY product_shop.id_product'
;
            $result = $db->executeS($sql);
       
            // results count
            $total = $sphinx_results['total'];
        }
       
        if (!$result) {
            $result_properties = array();
        } else {
            $result_properties = Product::getProductsProperties((int)$id_lang, $result);
        }
       
        return array('total' => $total, 'result' => $result_properties);
    }

    protected static function getSphinxResults($search_query, $page_number, $page_size)
    {
        $results = array();
        $total = 0;

        if(!$search_query)
            return null;

        // connect to Sphinx database
        $link = mysqli_connect('127.0.0.1', '', '', '', '9306');
        if($link)
        {
            $query = 'SELECT * FROM `PrestaSite` WHERE MATCH(\''.$search_query.'\') limit '.$page_number.', '.$page_size.';';
            if ($result = $link->query($query))
            {
                while($query_results = $result->fetch_array())
                {
                    $results = array_merge($results, $query_results);
                }

                /* clear result */
                $result->close();
            }

            // get count of results
            $query_total = 'SELECT count(*) FROM `PrestaSite` WHERE MATCH(\''.$search_query.'\');';
            if ($result = $link->query($query_total))
            {
                $total = (int)$result->fetch_array()[0];
                if($total > 1000)
                    $total = 1000;
            }

            mysqli_close($link);
        }

        return array('results' => $results, 'total' => $total);
    }
}

В принципе, код довольно простой. Мы переопределяем функцию find, в которой заменяем механизм поиска PrestaShop на поиск Sphinx. Sphinx возвращает id товаров, по этим id делаем запрос на получение полных данных товаров. Вторая функция (getSphinxResults) непосредственно осуществляет поиск.

При необходимости очищаем кэш и удаляем кэш классов (cache/class_index.php). Все готово.

Использование поиска Sphinx в PrestaShop: 6 комментариев

  1. Спасибо тебе за этот пост.
    Он был очень полезным .
    Вопрос :
    Какой порт я должен выбрать в search.php и должен я создать таблицу в моей базе данных для`PrestaSite`
    «‘SELECT * FROM `PrestaSite` WHERE…»?

    1. `PrestaSite` это таблица Sphinx, она создается при его настройке.
      `PrestaSite` is a Sphinx table, it is created when we configure Sphinx.

  2. Thank you, I got it. Had some problems with your Search override class.
    1. Got error for mysqli_connect, I changed to PDO object.

    1
    2
    3
    4
    $dns = 'mysql:host='.$server.';dbname='. $dbname;    
    try {
        $link = new PDO($dns,$db_user ,$db_pass);
    }

    2. Problem with count(*) query. I have to run two queries change it:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $query_total = 'SELECT * FROM `PrestaSite` WHERE MATCH(\''.$search_query.'\') LIMIT 0;';
    $link->query($query_total);
    $query_total = 'SHOW META;';
    if ($result2 = $link->query($query_total))
    {
        $total = (int)$result2->fetchAll()[0][1];
        if($total > 1000)
           $total = 1000;
    }
    $link = null;
      1. New Question:
        Current search will not take to consideration product ‘out of stock’. How can I Order By stock.quantity>0 DESC
        if I don’t want to include quantity to indexation?
        When I add Order By in Search Class it only works per page, I want to be able to sort products in sphinx before hand.
        Any idea?

        1. I think it’s not possible without including quantity to indexation since the sorting must be applied in the Sphinx query. How can you sort by quantity without using quantity?

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *