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. На этом пока все. Спасибо за внимание.