18 июн. 2018 г.
В нашем проекте DIY price мы используем Magento 2 в качестве ecommerce движка. Сначала пытались делать проект полностью на magento но в итоге пришли к headless режиму. Frontend написан на VueJS, отделен от magento и взаимодействует с ней по Rest API. Такой подход приносит определенные сложности - многое нужно продумывать самому, нехватает некоторых методов api и т.д. В то же время упрощает работу frontend разработчиков, дает больше гибкости. Настраивать тему magento под свои нужды иногда может быть непростирой задачей. В конечном итоге headless архитектура оправдывает себя.
В качестве поискового движка планировали использовать Elastic Search. Причем в нашем случае мы можем даже обращаться к нему напрямую, тем самым разгружая magento. К тому же мы используем Magento Enterprice и его поддержка заявлена из коробки. Правда начинали мы разработку на Comunity Edition и искали возможности интеграции уже в этот момент.
Для начала рассмотрим интеграцию, которую Mаgento EE поддерживает из коробки. Elastic Search в Magento 2 EE из коробки работает. Нужен Elastic Search версии 2.4.6 Последняя версия Elastic на данный момент - 6.1. С ней интеграция не работает. С 5.4.6 не завелось. C Elastic Search 2.4 работает нормально. Подробнее настройку интеграции стандартными средствами планирую написать в отдельную статью.
Загрузка и обновление данных в API Elastic Search работает без нареканий. Поиск по каталогу в стандартной теме Luma не заработал(Ошибки). Но наверное можно разобраться. В целом решение рабочее но нам не очень подходило т.к. у нас кастомный фронтенд. И очень кастомные структуры данных. Не подойдет стандартная реализация и тем, у кого нет денег на Magento EE.
В принципе ничто не мешает написать кастомный модуль, который будет делать все, что угодно. В том числе и загружать данные в Elastic Search. Модуль должен уметь загружать данные из Magento в Elastic Search
Также было условие, что продуктов много(~400 000) и требование, чтобы загрузка шла асинхронно либо в фоновом режиме.
Elastic Search развернут в docker, как и большинство наших сервисов.
В итоге есть линк такого типа: http://172.18.0.1:9200
Нам нужно чтобы плагин знал, куда обращаться.
Редактируем конфиг /app/etc/env.php
Добавляем секцию elastic_search
, в нее добавляем api host и порт.
Возможно в будушем захотим добавить другие параметры
'elastic_search' => array ( 'connection' => array ( 'host' => '172.18.0.1', 'port' => '9200', ), ),
Управлять плагином будем с помощью консольных команд. Потом можно запускать их по cron c заданной периодичностью.
Регистрируем команды в файле /app/code/DIY/ElasticSuite/etc/di.xml
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\Framework\Console\CommandList"> <arguments> <argument name="commands" xsi:type="array"> <item name="elastic-test" xsi:type="object">DIY\ElasticSuite\Console\Test</item> <item name="elastic-info" xsi:type="object">DIY\ElasticSuite\Console\Info</item> <item name="elastic-push-categories" xsi:type="object">DIY\ElasticSuite\Console\PushCategories</item> <item name="elastic-push-products" xsi:type="object">DIY\ElasticSuite\Console\PushProducts</item> <item name="elastic-update-categories" xsi:type="object">DIY\ElasticSuite\Console\UpdateCategories</item> <item name="elastic-update-products" xsi:type="object">DIY\ElasticSuite\Console\UpdateProducts</item> </argument> </arguments> </type> </config>
Сами консольные команды хранятся в папке console и очень напоминают консольные команды Symfony.
Т.к. основаны на них). Название команды указывается в самой команде. Пример консольной команды /app/code/DIY/ElasticSuite/Console/Test.php
<?php namespace DIY\ElasticSuite\Console; use DIY\ElasticSuite\Integration\ElasticClient; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Magento\Framework\App\Config\Storage\WriterInterface; /** * Class Test * @package DIY\ElasticSuite\Console */ class Test extends Command { private $configWriter; private $elasticService; private $elasticClient; /** * Test constructor. * @param WriterInterface $configWriter * @param \Magento\Framework\App\State $state * @param \DIY\ElasticSuite\Integration\ElasticService $elasticService */ public function __construct( WriterInterface $configWriter, \Magento\Framework\App\State $state, \DIY\ElasticSuite\Integration\ElasticService $elasticService, \DIY\ElasticSuite\Integration\ElasticClient $elasticClient ) { parent::__construct(); $this->configWriter = $configWriter; $this->elasticService = $elasticService; $this->elasticClient = $elasticClient; } protected function configure() { $this->setName('elastic-suite:test'); $this->setDescription('Elastic Suite Test'); parent::configure(); } /** * @param InputInterface $input * @param OutputInterface $output */ protected function execute(InputInterface $input, OutputInterface $output) { $data = $this->elasticClient->testConnection(); print_r($data); } }
Создадим 5 консольных команд.
Test
- проверка соединенияPushCategories
- Загрузка всех категорийPushProducts
- Загрузка всех продуктовUpdateCategories
- Загрузка обновленных категорийUpdateProducts
- Загрузка обновленных продуктовОбновление будем делать с помощью Observer, встроеных в Magento.
Создаем 4 observer на обновление продуктов и категорий.
Настроим конфиг /app/code/DIY/ElasticSuite/etc/events.xml
<?xml version="1.0"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> <event name="catalog_category_save_after"> <observer name="elastic_category_save" instance="DIY\ElasticSuite\Observer\CategorySaveObserver" /> </event> <event name="catalog_category_delete_after"> <observer name="elastic_category_delete" instance="DIY\ElasticSuite\Observer\CategoryDeleteObserver" /> </event> <event name="catalog_product_save_after"> <observer name="elastic_product_save" instance="DIY\ElasticSuite\Observer\ProductSaveObserver" /> </event> <event name="catalog_product_delete_after"> <observer name="elastic_product_delete" instance="DIY\ElasticSuite\Observer\ProductDeleteObserver" /> </event> </config>
Пример класса observer - /app/code/DIY/ElasticSuite/Observer/ProductSaveObserver.php
<?php // app/code/DIY/ElasticSuite/Observer/ProductSaveObserver.php namespace DIY\ElasticSuite\Observer; use DIY\ElasticSuite\Queue\CategoryQueueItem; use DIY\ElasticSuite\Queue\ProductQueueItem; use DIY\OrdersWebhooks\Helpers\WebhookHelper; use Magento\Framework\Event\ConfigInterface; use Magento\Framework\Event\InvokerInterface; /** * Class ProductSaveObserver * @package DIY\ElasticSuite\Observer */ class ProductSaveObserver implements \Magento\Framework\Event\ObserverInterface { protected $scopeConfig; protected $queueManager; const EVENT_NAME = 'catalog_product_save_after'; /** * ProductSaveObserver constructor. * @param \DIY\ElasticSuite\Queue\QueueManager $queueManager */ public function __construct( \DIY\ElasticSuite\Queue\QueueManager $queueManager ) { $this->queueManager = $queueManager; } /** * @param \Magento\Framework\Event\Observer $observer * @return $this */ public function execute(\Magento\Framework\Event\Observer $observer) { $product = $observer->getEvent()->getProduct(); $sku = $product->getData('sku'); $item = new ProductQueueItem(ProductQueueItem::EVENT_SAVE, $sku); $this->queueManager->addItem($item); return $this; } }
Для отзывов в magento нет стандартных observer, поэтому будем использовать interceptor. Работа interceptor подробно описана в документации и статье на habrahabr.
В итоге работает примерно так же, как и observer. Пример:
<?php // app/code/DIY/ElasticSuite/Plugin/ReviewObserver.php namespace DIY\ElasticSuite\Plugin; use DIY\ElasticSuite\Queue\ProductQueueItem; use Magento\Review\Model\Review; /** * Class ReviewObserver * @package DIY\ElasticSuite\Plugin */ class ReviewObserver { const ENTITY_PRODUCT = 1; private $queueManager; /** * ReviewObserver constructor. * @param \DIY\ElasticSuite\Queue\QueueManager $queueManager */ public function __construct( \DIY\ElasticSuite\Queue\QueueManager $queueManager ) { $this->queueManager = $queueManager; } /** * @param Review $subject * @param $intercepter * @throws \Exception */ public function afterSave(Review $review, $intercepter) { $entity = $review->getData('entity_id'); $productId = $review->getData('entity_pk_value'); $reviewId = $review->getData('review_id'); if($entity == self::ENTITY_PRODUCT){ $item = new ProductQueueItem(ProductQueueItem::EVENT_SAVE, $productId); $this->queueManager->addItem($item); } return; } }
Можно было бы сразу слать запрос к Elastic Search API, Но... Было требование сделать это асинхронно. Ок. Можно было бь класть запросы в mysql, потом выбирать оттуда и отправлять.
Но мы используем Redis для этих целей. В модуле реализованы классы для работы с очередью
/app/code/DIY/ElasticSuite/Queue/QueueManager.php
/app/code/DIY/ElasticSuite/Queue/ProductQueueItem.php
/app/code/DIY/ElasticSuite/Queue/CategoryQueueItem.php
А также классы обработчики
/app/code/DIY/ElasticSuite/Queue/Worker/CategoryDeleteWorker.php
/app/code/DIY/ElasticSuite/Queue/Worker/CategorySaveWorker.php
/app/code/DIY/ElasticSuite/Queue/Worker/ProductDeleteWorker.php
/app/code/DIY/ElasticSuite/Queue/Worker/ProductSaveWorker.php
Обработчики отвечают за отправку запроса к Elastic Search API.
Возможно, что для поиска нам захочется настроить синонимы. Например Отвертка - Шуруповерт, Стол - Верстак, Углошлифовальная машина - болгарка, и т.д.
Логичнее всего редактировать и хранить синонимы в базе данных magento. И периодически загружать обновления в Elastic Search. Для этого реализуем отдельный модуль и grid для управления синонимами.
В Elastic Service реализуем метод updateSynonyms, который будет забирать из БД синонимы, закрывать индекс Elastic Search, Загружать синонимы и снова открывать индекс.
/app/code/DIY/ElasticSuite/Integration/ElasticService.php
Рекомендуется запускать по cron не очень часто. Скажем, раз в сутки. Т.к. в результате индекс на некоторое время закрывается и, фактически, получаем простой сервиса. Пусть и ненадолго. Вероятно, это можно рещить с помощью кластера из нескольких нод.
В результате получаем гибкий настраиваемый плагин, реализующий интеграцию с Elastic Search. Поддерживающий последние версии. Работающий асинхронно либо в фоновом режиме. Доступный для использования в Magento 2 CE.
Код (будет) доступен на github. На этом пока все. Спасибо за внимание.