
Привет! Меня зовут Павел и я занимаюсь бэкенд разработкой. Как уже писал AndreyHabr, многие из наших проектов основаны на стеке Adobe Magento 2 (для краткости далее я буду называть ее M2) в качестве бэкенда и Vue Storefront (VS) в качестве фронтенда.
Я не буду подробно останавливаться на архитектуре стека VS/M2 — мы уже писали об этом ранее. Предлагаю ознакомиться с данной статьей для более полного понимания изложенного ниже.
Сегодня я расскажу о взаимодействии с VS изнутри M2: посмотрим на реализацию индексеров, обсудим особенности их работы, после чего я кратко расскажу как создать свой индексер для кастомной сущности.
Погнали!
Модули интеграции
Официальные модули интеграции M2 и VS разрабатывает компания Divante. Модули представлены в их репозитории на Github и распространяются под лицензией MIT. Пакет интеграционных модулей включает в себя:
- Divante_ReviewApi;
- Divante_VsbridgeIndexerCore;
- Divante_VsbridgeIndexerCatalog;
- Divante_VsbridgeIndexerCms;
- Divante_VsbridgeIndexerTax;
- Divante_VsbridgeIndexerReview;
- Divante_VsbridgeIndexerAgreement;
- Divante_VsbridgeDownloadable.
Несложно догадаться, что все модули кроме первого занимаются индексацией различных сущностей с целью размещения их в Elasticsearch, используемый VS в качестве хранилища сущностей. Модуль Divante_ReviewApi реализует API для работы с отзывами (создание, удаление, получение коллекций).
Из коробки модули обеспечивают индексацию следующих сущностей:
- Категории и продукты каталога;
- CMS страницы и блоки;
- Отзывы;
- Налоги;
- Соглашения;
- Ссылки на скачиваемые продукты.
Из этого следует, что связка M2 и VS прямо после установки способна выполнять все базовые задачи интернет-магазина и требует минимальной настройки для начала применения.
Индексируем!
Посмотрим вблизи на реализацию индексеров. Для примера возьмем индексер CMS Page из стандартной поставки Divante. В целом, индексеры VSBridge работают на тех же принципах, что и другие индексеры в M2.
Объявляется новый индексер:
<indexer id="vsbridge_cms_page_indexer" view_id="vsbridge_cms_page_indexer" class="Divante\VsbridgeIndexerCms\Model\Indexer\CmsPage"> <title translate="true">Vsbridge Cms Page Indexer</title> <description translate="true">Update Cms Pages in Elastic</description> </indexer>
etc/indexer.xml
Где объявляется класс-индексер. Данный класс реализует общий для всех индексеров интерфейс \Magento\Framework\Indexer\ActionInterface, поэтому функционально схож с остальными индексерами M2. Его задача — запуск индексации в разных ситуациях. Здесь нас интересует свойство $cmsPageAction, содержащее Action класс, извлекающий сущности для индексации:
public function execute($ids) { $stores = $this->storeManager->getStores(); foreach ($stores as $store) { $this->indexHandler->saveIndex($this->cmsPageAction->rebuild($store->getId(), $ids), $store); $this->indexHandler->cleanUpByTransactionKey($store, $ids); $this->cacheProcessor->cleanCacheByTags($store->getId(), ['cmsPage']); } }
Model/Indexer/CmsPage.php
Action класс содержит единственный метод rebuild, который получает коллекцию и готовит ее для индексации:
public function rebuild($storeId = 1, array $pageIds = []) { $lastPageId = 0; do { $cmsPages = $this->resourceModel->loadPages($storeId, $pageIds, $lastPageId); foreach ($cmsPages as $pageData) { $lastPageId = (int)$pageData['page_id']; $pageData['id'] = $lastPageId; $pageData['content'] = $pageData['content']; $pageData['active'] = (bool)$pageData['is_active']; if (isset($pageData['sort_order'])) { $pageData['sort_order'] = (int)$pageData['sort_order']; } unset($pageData['creation_time'], $pageData['update_time'], $pageData['page_id']); unset($pageData['created_in']); unset($pageData['is_active'], $pageData['custom_theme'], $pageData['website_root']); yield $lastPageId => $pageData; } } while (!empty($cmsPages)); }
Model/Indexer/Action/CmsPage.php
Обратите внимание, что коллекция элементов извлекается методом loadPages ресурсной модели следующим образом:
public function loadPages($storeId = 1, array $pageIds = [], $fromId = 0, $limit = 1000) { $metaData = $this->getCmsPageMetaData(); $linkFieldId = $metaData->getLinkField(); $select = $this->getConnection()->select()->from(['cms_page' => $metaData->getEntityTable()]); $select->join( ['store_table' => $this->resource->getTableName('cms_page_store')], "cms_page.$linkFieldId = store_table.$linkFieldId", [] )->group("cms_page.$linkFieldId"); $select->where( 'store_table.store_id IN (?)', [ Store::DEFAULT_STORE_ID, $storeId, ] ); if (!empty($pageIds)) { $select->where('cms_page.page_id IN (?)', $pageIds); } $select->where('is_active = ?', 1); $select->where('cms_page.page_id > ?', $fromId) ->limit($limit) ->order('cms_page.page_id'); return $this->getConnection()->fetchAll($select); }
Model/ResourceModel/CmsPage.php
Данный метод может извлекать данные итеративно, коллекциями по 1000 элементов, либо только определенный набор элементов с IDs, указанными в переменной $pageIds.
Полученную коллекцию IndexHandler класса Indexer сохраняет в базу elasticsearch:
public function saveIndex(Traversable $documents, StoreInterface $store) { try { $index = $this->getIndex($store); $storeId = (int)$store->getId(); $batchSize = $this->indexOperations->getBatchIndexingSize(); foreach ($this->batch->getItems($documents, $batchSize) as $docs) { foreach ($index->getDataProviders() as $dataProvider) { if (!empty($docs)) { $docs = $dataProvider->addData($docs, $storeId); } } if (!empty($docs)) { $bulkRequest = $this->indexOperations->createBulk()->addDocuments( $index->getName(), $index->getType(), $docs ); $this->indexOperations->optimizeEsIndexing($storeId, $index->getName()); $response = $this->indexOperations->executeBulk($storeId, $bulkRequest); $this->indexOperations->cleanAfterOptimizeEsIndexing($storeId, $index->getName()); $this->bulkLogger->log($response); } $docs = null; } if ($index->isNew()) { $this->indexOperations->switchIndexer($store->getId(), $index->getName(), $index->getIdentifier()); } $this->indexOperations->refreshIndex($store->getId(), $index); } catch (ConnectionDisabledException $exception) { // do nothing, ES indexer disabled in configuration } catch (ConnectionUnhealthyException $exception) { $this->indexerLogger->error($exception->getMessage()); $this->indexOperations->cleanAfterOptimizeEsIndexing($storeId, $index->getName()); throw $exception; } }
Модуль Divante_VsbridgeIndexerCore: Indexer/GenericIndexerHandler.php
На этом этапе важно упомянуть класс Mapper, который задает типы полей объекта для elasticsearch.
public function getMappingProperties() { $properties = [ 'id' => ['type' => FieldInterface::TYPE_LONG], 'active' => ['type' => FieldInterface::TYPE_BOOLEAN], 'sort_order' => ['type' => FieldInterface::TYPE_LONG], //compatible with product/category attribute mapping 'page_layout' => ['type' => FieldInterface::TYPE_KEYWORD], 'identifier' => ['type' => FieldInterface::TYPE_KEYWORD], ]; foreach ($this->textFields as $field) { $properties[$field] = ['type' => FieldInterface::TYPE_TEXT]; } $mappingObject = new \Magento\Framework\DataObject(); $mappingObject->setData('properties', $properties); $this->eventManager->dispatch( 'elasticsearch_cms_page_mapping_properties', ['mapping' => $mappingObject] ); return $mappingObject->getData(); }
Index/Mapping/CmsPage.php
Это может быть необходимо, в том числе, если поле должно участвовать в полнотекстовом поиске — в этом случае полю надо задать тип keyword.
Данный класс используется в файле vsbridge_indices.xml, который задает идентификатор индекса и класс Mapper для полей объектов в индексе:
<index identifier="cms_page" mapping="Divante\VsbridgeIndexerCms\Index\Mapping\CmsPage"> <data_providers> <data_provider name="content">Divante\VsbridgeIndexerCms\Model\Indexer\DataProvider\Page\ContentData</data_provider> </data_providers> </index>
etc/vsbridge_indices.xml
Полная индексация элементов производится путем выполнения команды bin/magento indexer:reindex, которая последовательно запускает все индексеры М2. Также можно запустить конкретный индексер, указав его идентификатор в параметрах команды: bin/magento indexer:reindex <indexer_identifier>.
Из описанной процедуры индексации следуют несколько важных выводов:
- Объект в elasticsearch не обязан полностью отражать объект в БД M2. Его можно сократить, убрав некоторые поля, или наоборот дополнить — к примеру, на этапе выборки приджоинить таблицу с дополнительными данными. Также данные можно модифицировать после выборки. Это дает свободу и удобство при размещении объектов в elasticsearch, например если нужно разрешать поля с foreign ключами в их значения;
- Объект для индексации не обязательно доставать из базы — он может быть взят откуда угодно, к примеру, получен через запрос к внешнему источнику;
- Выборку можно производить посредством стандартных инструментов М2, например через репозитории или коллекции, а не сырым запросом в БД, как в примере выше.
Актуализация данных в elasticsearch
Конечно же, запускать полную реиндексацию каждый раз, когда какой либо объект меняется в базе, было бы расточительно. Для отслеживания изменения объектов в базе и их актуализацией в elasticsearch следит механизм M2, который называется mview. Данный механизм состоит из триггеров в таблице, за которой производится наблюдение, индексной таблицы в которой фиксируются ID измененных сущностей, а также cron-задачи, которая считывает ID измененных сущностей из индексной таблицы и запускает индексер с указанием этих ID (вспомним, что метод loadPages ресурсной модели может принимать в качестве аргумента массив ID). Рассмотрим каждый из элементов механизма подробнее.
Создание триггеров инициируется созданием файла mview.xml:
<view id="vsbridge_cms_page_indexer" class="Divante\VsbridgeIndexerCms\Model\Indexer\CmsPage" group="indexer"> <subscriptions> <table name="cms_page" entity_column="page_id"/> </subscriptions> </view>
etc/mview.xml
После присвоения индексеру статуса Scheduled указанная в файле таблица приобретает триггеры на добавление, изменение и удаление объектов в таблице. ID модифицированного объекта добавляется индексную таблицу с указанием версии изменений:

Крон-задача один раз в минуту (по умолчанию) делает выборку из этих таблиц, составляет массив ID измененных объектов, после чего запускает соответствующий индексер. Таким образом, данные в elasticsearch постоянно сохраняют актуальность максимально экономичным способом.
Добавляем индексер для своей сущности
Главным достоинством этого механизма является способность к расширению. Представим, что у нас есть сущность, которую необходимо разместить в эластике. Назовем ее whiteRabbit. Создадим для ее индексации стандартный M2 модуль, после чего создадим свой индексер:
<?xml version="1.0" encoding="UTF-8"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Indexer/etc/indexer.xsd"> <indexer id="vsbridge_white_rabbit_indexer" view_id="vsbridge_white_rabbit_indexer" class="RSHB\WhiteRabbit\Model\Indexer\WhiteRabbit"> <title translate="true">White Rabbits Indexer</title> <description translate="true">Update White Rabbits in Elastic</description> </indexer> </config>
etc/indexer.xml
А также сразу зададим таблицу за которой будем следить и идентификатор индекса, в котором будут храниться наши объекты:
<?xml version="1.0" encoding="UTF-8"?> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Mview/etc/mview.xsd"> <view id="vsbridge_white_rabbit_indexer" class="RSHB\WhiteRabbit\Model\Indexer\WhiteRabbit" group="indexer"> <subscriptions> <table name="rshb_white_rabbit” entity_column="id" /> </subscriptions> </view> </config>
etc/mview.xml
<?xml version="1.0" encoding="UTF-8"?> <indices xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Divante_VsbridgeIndexerCore:etc/vsbridge_indices.xsd"> <index identifier="white_rabbit" mapping="RSHB\WhiteRabbit\Index\Mapping\WhiteRabbit" /> </indices>
etc/vsbridge_indices.xml
Создадим индексер, маппер и экшен:
<?php namespace RSHB\WhiteRabbit\Model\Indexer; use Divante\VsbridgeIndexerCore\Indexer\StoreManager; use Exception; use Magento\Framework\Indexer\ActionInterface as IndexerActionInterface; use Magento\Framework\Mview\ActionInterface as MviewActionInterface; use Divante\VsbridgeIndexerCore\Indexer\GenericIndexerHandler as IndexerHandler; use RSHB\WhiteRabbit\Model\Indexer\Action\WhiteRabbit as WhiteRabbitAction; /** * Class WhiteRabbit * @package RSHB\WhiteRabbit\Model\Indexer */ class WhiteRabbit implements IndexerActionInterface, MviewActionInterface { /** * @var IndexerHandler */ private $indexHandler; /** * @var WhiteRabbitAction */ private $WhiteRabbitAction; /** * @var StoreManager */ private $storeManager; /** * WhiteRabbit constructor. * @param IndexerHandler $indexerHandler * @param WhiteRabbitAction $action * @param StoreManager $storeManager */ public function __construct( IndexerHandler $indexerHandler, WhiteRabbitAction $action, StoreManager $storeManager ) { $this->indexHandler = $indexerHandler; $this->whiteRabbitAction = $action; $this->storeManager = $storeManager; } /** * @inheritdoc * @throws Exception */ public function execute($ids) { $stores = $this->storeManager->getStores(); foreach ($stores as $store) { $this->indexHandler->saveIndex($this->whiteRabbitAction->rebuild($ids), $store); $this->indexHandler->cleanUpByTransactionKey($store, $ids); } } /** * @inheritdoc * @throws Exception */ public function executeFull() { $stores = $this->storeManager->getStores(); foreach ($stores as $store) { $this->indexHandler->saveIndex($this->whiteRabbitAction->rebuild(), $store); $this->indexHandler->cleanUpByTransactionKey($store); } } /** * @inheritdoc * @throws Exception */ public function executeList(array $ids) { $this->execute($ids); } /** * @inheritdoc * @throws Exception */ public function executeRow($id) { $this->execute([$id]); } }
Model/Indexer/WhiteRabbit.php
<?php namespace RSHB\WhiteRabbit\Index\Mapping; use Divante\VsbridgeIndexerCore\Api\Mapping\FieldInterface; use Divante\VsbridgeIndexerCore\Api\MappingInterface; use Magento\Framework\DataObject; use Magento\Framework\Event\ManagerInterface as EventManager; /** * Class WhiteRabbit * @package RSHB\WhiteRabbit\Index\Mapping */ class WhiteRabbit implements MappingInterface { /** * @var EventManager */ private $eventManager; /** * WhiteRabbit constructor. * @param EventManager $eventManager */ public function __construct( EventManager $eventManager ) { $this->eventManager = $eventManager; } /** * @inheritdoc */ public function getMappingProperties() { $properties = [ 'id' => ['type' => FieldInterface::TYPE_LONG] ]; $mappingObject = new DataObject(); $mappingObject->setData('properties', $properties); $this->eventManager->dispatch( 'elasticsearch_white_rabbit_mapping_properties', ['mapping' => $mappingObject] ); return $mappingObject->getData(); } }
Index/Mapping/WhiteRabbit.php
<?php namespace RSHB\WhiteRabbit\Model\Indexer\Action; use RSHB\WhiteRabbit\Model\ResourceModel\WhiteRabbit as WhiteRabbitResource; use Traversable; /** * Class WhiteRabbit * @package RSHB\WhiteRabbit\Model\Indexer\Action */ class WhiteRabbit { /** * @var WhiteRabbitResource */ private $resourceModel; public function __construct( WhiteRabbitResource $resourceModel ) { $this->resourceModel = $resourceModel; } /** * Rebuild * * @param array $ids * @return Traversable */ public function rebuild(array $ids = []) { $lastId = 0; do { $rabbits = $this->resourceModel->load($ids, $lastId); foreach ($rabbits as $rabbit) { $lastId = (int)$rabbit['id']; $rabbitData['id'] = $lastId; $rabbitData = $rabbit; yield $lastId => $rabbitData; } } while (!empty($rabbits)); } }
Model/Indexer/Action/WhiteRabbit.php
А также ресурсную модель:
<?php namespace RSHB\WhiteRabbit\Model\ResourceModel; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; /** * Class WhiteRabbit * @package RSHB\WhiteRabbit\Model\ResourceModel */ class WhiteRabbit { /** * @var ResourceConnection */ private $resource; /** * Organization constructor. * @param ResourceConnection $resourceConnection */ public function __construct( ResourceConnection $resourceConnection ) { $this->resource = $resourceConnection; } /** * @param array $ids * @param int $fromId * @param int $limit * @return array */ public function load($ids, $fromId, $limit = 1000) { $select = $this->getConnection()->select()->from('rshb_white_rabbit'); if (!empty($ids)) { $select->where('id IN (?)', $ids); } $select->where('id > ?', $fromId) ->order('id') ->limit($limit); return $this->getConnection()->fetchAll($select); } /** * @return AdapterInterface */ private function getConnection() { return $this->resource->getConnection(); } }
Model/ResourceModel/WhiteRabbit.php
Не забудем указать идентификатор индекса для класса IndexerHandler, для чего воспользуемся виртуальным типом:
... <virtualType name="RSHB\WhiteRabbit\Indexer\WhiteRabbitIndexOperationsVirtual" type="Divante\VsbridgeIndexerCore\Indexer\GenericIndexerHandler"> <arguments> <argument name="indexIdentifier" xsi:type="string">white_rabbit</argument> <argument name="typeName" xsi:type="string">white_rabbit</argument> <argument name="alias" xsi:type="string">vue_storefront_white_rabbit</argument> </arguments> </virtualType> <type name="RSHB\WhiteRabbit\Model\Indexer\WhiteRabbit"> <arguments> <argument name="indexerHandler" xsi:type="object"> RSHB\WhiteRabbit\Indexer\WhiteRabbitIndexOperationsVirtual </argument> </arguments> </type> ...
etc/di.xml
И, в качестве последнего штриха, создадим дата патч, который включит для нашего вновь созданного индексера режим индексации по крону:
<?php namespace RSHB\WhiteRabbit\Setup\Patch\Data; use Exception; use Magento\Framework\Indexer\IndexerRegistry; use Magento\Framework\Setup\Patch\DataPatchInterface; use Psr\Log\LoggerInterface; /** * Class SetWhiteRabbitIndexerScheduleMode * @package RSHB\WhiteRabbit\Setup\Patch\Data */ class SetWhiteRabbitIndexerScheduleMode implements DataPatchInterface { /** * @var LoggerInterface */ private $logger; /** * @var IndexerRegistry */ private $indexerRegistry; /** * SetWhiteRabbitIndexerScheduleMode constructor. * @param LoggerInterface $logger * @param IndexerRegistry $indexerRegistry */ public function __construct( LoggerInterface $logger, IndexerRegistry $indexerRegistry ) { $this->logger = $logger; $this->indexerRegistry = $indexerRegistry; } /** * @return DataPatchInterface|void */ public function apply() { try { $indexer = $this->indexerRegistry->get('vsbridge_white_rabbit_indexer'); $indexer->setScheduled(true); } catch (Exception $e) { $this->logger->critical($e); } } /** * @inheritDoc */ public static function getDependencies() { return []; } /** * @inheritDoc */ public function getAliases() { return []; } }
Setup/Patch/Data/SetWhiteRabbitIndexerScheduleMode.php
На этом наш индексер готов.
Подведение итогов
Механизм индексации данных позволяет действовать M2 и VS автономно, обеспечивая слабую связанность и, как следствие, повышение надежности и быстродействия. Гибкий механизм масштабирования позволяет использовать elasticsearch для хранения разнообразной информации, а скорость выборки и поиска позволяет фонтенду работать быстро.
Напоследок хотелось бы заострить внимание на некоторых нюансах работы индексеров, с которыми мы сталкиваись в процессе работы:
- Если у объекта меняется набор полей или их типы, необходимо удалить индекс из ealasticsearch и запустить полную индексацию. Это происходит по причине несоответствия типов полей новых объектов с теми, что уже есть в индексе;
- Индексация не запустится, если данные в таблицу попадают при помощи дампа. В этом случае также нужно запустить индексацию вручную;
- Если в процессе индексации в объект elasticsearch включаются данные из другого объекта (к примеру, разрешается foreign ключ в его значение при помощи join`а таблицы) то необходимо отслеживать изменения этой таблицы и обеспечивать внесение в индексную таблицу ID элементов, для которых изменились данные, связанные foreign ключами.
На этом все. Пишите код с удовольствием, используйте правильные решения и да пребудет с вами сила.
ссылка на оригинал статьи https://habr.com/ru/company/rshb/blog/521692/
Добавить комментарий