mFilter2 и оптимизация скорости на 7k товарах


Всем, кто хоть раз пытался завести mFilter2 на большом кол-ве товаров и хотя-бы 10 опциях в фильтре, известно, что тормозов не избежать. Вот и мне попался на днях сайт, в котором, казалось бы, всего-то 7000 товаров и 10 опций на странице каталога. Однако сниппет фильтра отрабатывал за 3-4 секунды. Переписал на Fenom — стал отрабатывать за 2-3 секунды.
Не годится! Тем более, что у заказчика было требование: «Чтобы сайтом было комфортно пользоваться». Что-ж, взял задачу — надо решать!

Дебаг показал, что проблема в методе mse2FiltersHandler::getMsOptionValues, а именно в запросе (7k товаров!), который выглядит как-то так:
SELECT `product_id`, `key`, `value` FROM `modx_ms2_product_options` AS `msProductOption` 
WHERE (`msProductOption`.`product_id` IN (3394,3408,еще 7 тысяч ID)
AND `msProductOption`.`key` IN ('atr_n1','atr_n6','atr_n5','atr_n12','atr_n43','atr_n46','atr_n29','atr_n11','atr_n44','atr_n2'))

Оговорочка


  1. В данной статье мы улучшаем только метод для работы с опциями miniShop2, которые хранятся в таблице modx_ms2_product_options. Фильтрацию по ТВ и прочее мы здесь не затрагиваем.
  2. Пока не исследовано, как данные правки поведут себя на небольшом каталоге, поэтому тестируйте — отписывайтесь!
 

Поиск решения


Честно скажу, пытался я по-всякому, и разделял запрос на несколько, по количеству элементов в массиве $keys, и даже думал создавать дополнительную таблицу, которая бы обладала удобным форматом хранения значений, но так и не придумал, что это за формат такой. :) В итоге, несколько часов раздумий и тестов привели меня к сумасшедшей идее, которую я, собственно, и предлагаю рассмотреть в данной статье. Реализуется она в пару шагов:
 

Шаг 1

Системной настройке mse2_filters_handler_class присваиваем значение:
customFiltersHandler
 

Шаг 2

В директории core/components/msearch2/custom/filters/ создаём файл custom.class.php следующего содержания:
<?php
class customFiltersHandler extends mse2FiltersHandler
{
    /**
     * @param array $keys
     * @param array $ids
     *
     * @return array
     */
    public function getMsOptionValues(array $keys, array $ids)
    {
        $filters = array();
        $fields = $this->modx->getFields('msProductData');
        $q = $this->modx->newQuery('msProductOption');
        if (array_key_exists('sku_id', $fields)) {
            $q->innerJoin('msProductData', 'Data', 'Data.sku_id = msProductOption.product_id');
            $q->where(array('Data.id:IN' => $ids, 'key:IN' => $keys));
            $q->select('Data.id as product_id, key, value');
        } else {
            $q->where(array('key:IN' => $keys));
            $q->select('product_id, key, value');
        }
        $tstart = microtime(true);
        if ($q->prepare() && $q->stmt->execute()) {
            $this->modx->queryTime += microtime(true) - $tstart;
            $this->modx->executedQueries++;
            if ($rows = $q->stmt->fetchAll(PDO::FETCH_ASSOC)) {
                $ids_flip = array_flip($ids);
                foreach ($rows as $row) {
                    if (!isset($ids_flip[$row['product_id']])) {
                        continue;
                    }
                    $value = str_replace('"', '"', trim($row['value']));
                    $key = $row['key'];
                    
                    // Get ready for the special options in "key==value" format
                    if (strpos($value, '==')) {
                        list($key, $value) = explode('==', $value);
                        $key = preg_replace('/\s+/', '_', $key);
                    }
                    if (isset($filters[$key][$value])) {
                        $filters[$key][$value][$row['product_id']] = $row['product_id'];
                    } else {
                        $filters[$key][$value] = array($row['product_id'] => $row['product_id']);
                    }
                }
            }
        } else {
            $this->modx->log(modX::LOG_LEVEL_ERROR, "[mSearch2] Error on get filter params.\nQuery: " . $q->toSQL() . "\nResponse: " . print_r($q->stmt->errorInfo(), 1));
        }
        
        return $filters;
    }
}
 

Если вкратце, то


Мы исключаем из выборки параметр со списком ID товаров (так как это делает запрос слишком большим) и производим сравнение непосредственно в переборке результатов (на стороне PHP).

В нашем случае, данное решение оказалось довольно действенным, о чем говорят результаты:
Сниппет mFilter2 со сброшенным кешем отрабатывал на старом методе за 2-3 секунды, а на новом — за ~0.3-0.5. На мой взгляд, это существенно сказалось на восприятии удобства пользования сайтом. Заказчик, к слову, остался доволен.
 

Оптимизирую


Предоставляю услуги по оптимизации скорости работы сайтов на MODX Revolution.
Телеграм: t.me/pavelgvozdb
Скайп: pavelgvozdb
Павел Гвоздь
14 августа 2017, 14:14
modx.pro
41
4 883
+21
Поблагодарить автора Отправить деньги

Комментарии: 11

Александр
14 августа 2017, 18:10
0
Простите, но мое воображение меня порой сводит с ума… Я представил, как цикл прогоняет 7000 элементов за 0.3 сек… «Тра-та-та-та-та-та-та-та-та-пиууу-та-та-та-та-та-та-пиу-та-та-та-та-та...» Скорость, ух…
А разе
$q->where(array('Data.id:IN' => $ids, 'key:IN' => $keys));
не перечисляет все эти 7000 id так же, как и
`msProductOption`.`product_id` IN (3394,3408,еще 7 тысяч ID,9775,9776)
Или я чего-то недопонял?
    Павел Гвоздь
    14 августа 2017, 18:21
    +1
    Перечисляет, но оно срабатывает только тогда, когда в списке полей таблицы modx_ms2_product_options есть sku_id. Не стал ни тестировать, ни править это, т.к. у меня нет такого поля в этой таблице.
      Александр
      14 августа 2017, 18:23
      0
      Уже разобрался, но непонятно зачем это поле… Задаток на работу с остатками?
        Павел Гвоздь
        14 августа 2017, 18:33
        0
        К Василию, я не разработчик компонента.
          Василий Наумкин
          14 августа 2017, 20:47
          +1
          Это от отменённой версии 2.3.

          Ни в miniShop2.2, ни в 2.4 этого поля нет.
        Павел Гвоздь
        14 августа 2017, 18:35
        0
        Простите, но мое воображение меня порой сводит с ума… Я представил, как цикл прогоняет 7000 элементов за 0.3 сек… «Тра-та-та-та-та-та-та-та-та-пиууу-та-та-та-та-та-та-пиу-та-та-та-та-та...» Скорость, ух…
        К слову сказать, цикл гораздо больше, ибо только в таблице modx_ms2_product_options 62k записей.
        Олег Максименко
        16 августа 2017, 12:31
        0
        А если вместо
        $ids_flip = array_flip($ids);
        и
        if (!isset($ids_flip[$row['product_id']])) {
        заюзать бинарный поиск? реализация, например такая:

        function binarySearch($arr, $num)
        {
            $first = 0;
        
            $last = sizeof($arr) - 1;
        
            while ($first <= $last) {
                $i = floor(($first + $last) / 2);
        
                if ($arr[$i] == $num) {
                    return true;
                } elseif ($arr[$i] > $num) {
                    $last = $i - 1;
                } else {
                    $first = $i + 1;
                }
            }
        
            return false;
        }
        Не будет есть меньше памяти и быстрее отрабатывать?
        Sergey (Sentinel)
        28 ноября 2019, 19:41
        0
        А как оптимизировать для tv параметров?
          Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
          11