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, 17:14    Павел Гвоздь   
20    626 +21

Комментарии (10)

  1. Александр 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)
    Или я чего-то недопонял?
    1. Павел Гвоздь 14 августа 2017, 18:21 # +1
      Перечисляет, но оно срабатывает только тогда, когда в списке полей таблицы modx_ms2_product_options есть sku_id. Не стал ни тестировать, ни править это, т.к. у меня нет такого поля в этой таблице.
      1. Александр 14 августа 2017, 18:23 # 0
        Уже разобрался, но непонятно зачем это поле… Задаток на работу с остатками?
        1. Павел Гвоздь 14 августа 2017, 18:33 # 0
          К Василию, я не разработчик компонента.
          1. Василий Наумкин 14 августа 2017, 20:47 # +1
            Это от отменённой версии 2.3.

            Ни в miniShop2.2, ни в 2.4 этого поля нет.
        2. Павел Гвоздь 14 августа 2017, 18:35 # 0
          Простите, но мое воображение меня порой сводит с ума… Я представил, как цикл прогоняет 7000 элементов за 0.3 сек… «Тра-та-та-та-та-та-та-та-та-пиууу-та-та-та-та-та-та-пиу-та-та-та-та-та...» Скорость, ух…
          К слову сказать, цикл гораздо больше, ибо только в таблице modx_ms2_product_options 62k записей.
        3. Олег Максименко 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;
          }
          Не будет есть меньше памяти и быстрее отрабатывать?
          1. Павел Гвоздь 16 августа 2017, 12:49 # 0
            Разрешаю проверить и отписаться. :)
            1. Олег Максименко 16 августа 2017, 12:50 # 0
              Проверил бы, да не на чем :)
              1. Павел Гвоздь 16 августа 2017, 12:51 # 0
          Вы должны авторизоваться, чтобы оставлять комментарии.