Как устранять неисправности Elasticsearch в Magento 2

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Всем привет! Меня зовут Надя Глонягина, я Back-end Tech Lead в компании Magecom.

Это статья основана на моём докладе, с которым я выступала на Magento Meetup Online #10. Фидбэк после доклада был положительным, и кейсы, которые я собрала, для многих оказались полезными, поэтому я решила конвертировать его в статью.

Как вы все знаете, Magento с версии 2.4 оставила единственным поисковым движком Elasticsearch, MySQL поиска больше нет. Поэтому периодически возникающие проблемы с Elasticsearch сейчас актуальны для многих.

При подготовке этой статьи я использовала версию Magento 2.4.2 и для того, чтобы наглядно продемонстрировать различные проблемы и пути их решения, я немного сломала Magento, добавила несколько модулей с багами, которые нам сегодня предстоит найти.

В материале я решила рассказать о самых распространенных кейсах работы с Elasticsearch в Magento 2 и о способах их устранения.

Содержание

— Кейс #1: Продукт не отображается при поиске или на странице категории
— Исправление ошибок при добавлении данных в Elasticsearch
— Исправление ошибок при формировании search request к Elasticsearch
— Кейс #2: Сортировка не работает
— Кейс #3: Агрегация в Elasticsearch

Кейс #1: Продукт не отображается при поиске или на странице категории

Существуют две основных причины, почему так происходит:

  • Продукт не попадает в индекс Elasticsearch, и тогда он очевидно не будет попадать в нашу поисковую выборку;
  • Продукт есть в индексе Elasticsearch, но по каким-то причинам он не попал в выборку, и мы его не видим на странице категории или поиска.

Например, мы видим пустую страницу категории, здесь нет продуктов:

Есть два условия необходимые для того, чтобы продукт попал в индекс Elasticsearch:

Атрибут visibility продукта должен иметь значение:

visibility in 
(Visibility::VISIBILITY_IN_SEARCH,
Visibility::VISIBILITY_IN_CATALOG,
Visibility::VISIBILITY_BOTH)

Статус должен быть:

status == Status::STATUS_ENABLED

Этого достаточно, чтобы продукт попал в индекс.

Зайдем в админку и убедимся, что продукты здесь есть. На странице категории мы можем посмотреть, что здесь есть продукт с visibility и status, который удовлетворяет нашим условиям, то есть он должен быть в индексе:

Как точно проверить, что продукт есть в индексе, а также посмотреть, какие поля там хранятся?

Для этого нам нужно отправить Get запрос к Elasticsearch, который формируется следующим образом:

GET elasticsearch:9200/{{index_name}}/_doc/{{product_id}}?pretty 
GET elasticsearch:9200/{{index_name}}/document/{{product_id}}?pretty

Имя индекса также можно узнать, если отправить следующий Get-запрос к Elasticsearch:

GET elasticsearch:9200/_cat/indices?v

В результате вы увидите список всех индексов, которые хранятся в Elasticsearch.

health status index                 uuid                   pri rep docs.count docs.deleted store.size pri.store.size
yellow open   meetup_product_1_v3   EoFzV0A6SyCoX9wiPVJqIg   1   1        187            0      616kb          616kb
yellow open   magento2_product_1_v1 OvMWlHsqRm6HvMDgOoFqVA   1   1          0            0      6.3kb          6.3kb

Имя индекса формируется следующим образом: префикс, указанный в админке, имя entity, номер стора и номер версии.

Давайте теперь проверим, попал ли продукт с ID=1402 в индекс.

Продукт действительно есть в индексе. Осталось выяснить, почему же он не попадает на страницу категории.

Есть две причины, почему так могло произойти:

  • Ошибки при добавлении данных в Elasticsearch;
  • Ошибки при формировании search request к Elasticsearch.

Исправление ошибок при добавлении данных в Elasticsearch

Если мы внимательно посмотрим на данные, которые хранятся в Elasticsearch, то можем заметить кое-что странное. Status и Visibility имеют значение 0. В Magento вообще нет таких значений для этих атрибутов. При формировании запроса к Elasticsearch, Magento всегда добавляет фильтр visibility. Это обеспечивается FulltextSearch коллекцией. Поэтому продукт и не попадает в результат поиска Elasticsearch.

Давайте посмотрим, как формируются данные для записи в индекс.

Здесь уже нам пригодится дебагер. Мы можем поставить breakpoint в DataProvider:

\Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::getSearchableProducts()

А затем запустить дебагер.

В указанном DataProvider есть функция getSearchableProducts(). Здесь Magento получает из базы данных продукты, которые попадут в индекс, и мы ее можем использовать, чтобы ограничить наш дебаг одним продуктом. Поэтому самое время немного нахардкодить :) Укажем product id, который нас интересует, для того, чтобы сейчас не дебажить добавление всех продуктов в индекс.

public function getSearchableProducts(
   $storeId,
   array $staticFields,
   $productIds = null,
   $lastProductId = 0,
   $batch = 100
) {
   //start hardcode
   if (empty($productIds)) {
       $productIds = 1402;
   }
   //end hardcode
   $select = $this->getSelectForSearchableProducts($storeId, $staticFields, $productIds, $lastProductId, $batch);
   if ($productIds === null) {
       $select->where(
           'e.entity_id < ?',
           $lastProductId ? $this->antiGapMultiplier * $batch + $lastProductId + 1 : $batch + 1
       );
   }
   $products = $this->connection->fetchAll($select);
   if ($productIds === null && !$products) {
       // try to search without limit entity_id by batch size for cover case with a big gap between entity ids
       $products = $this->connection->fetchAll(
           $this->getSelectForSearchableProducts($storeId, $staticFields, $productIds, $lastProductId, $batch)
       );
   }

   return $products;
}

Ставим breakpoint, запускаем дебагер и reindex:

bin/magento indexer:reindex catalogsearch_fulltext

Дебагер у нас остановился на нашем breakpoint. Давайте посмотрим, что происходит дальше, в функции:

\Magento\CatalogSearch\Model\Indexer\Fulltext\Action\Full::rebuildStoreIndex()

$products = $this->dataProvider
   ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId, $this->batchSize);
while (count($products) > 0) {
   $productsIds = array_column($products, 'entity_id');
   $relatedProducts = $this->getRelatedProducts($products);
   $productsIds = array_merge($productsIds, array_values($relatedProducts));

   $productsAttributes = $this->dataProvider->getProductAttributes($storeId, $productsIds, $dynamicFields);

   foreach ($products as $productData) {
       $lastProductId = $productData['entity_id'];

       $productIndex = [$productData['entity_id'] => $productsAttributes[$productData['entity_id']]];
       if (isset($relatedProducts[$productData['entity_id']])) {
           $childProductsIndex = $this->getChildProductsIndex(
               $productData['entity_id'],
               $relatedProducts,
               $productsAttributes
           );
           if (empty($childProductsIndex)) {
               continue;
           }
           $productIndex = $productIndex + $childProductsIndex;
       }

       $index = $this->dataProvider->prepareProductIndex($productIndex, $productData, $storeId);
       yield $productData['entity_id'] => $index;
   }
   $products = $this->dataProvider
       ->getSearchableProducts($storeId, $staticFields, $productIds, $lastProductId, $this->batchSize);
}

Дойдя до метода prepareProductIndex(), мы можем увидеть в дебагере, что у нашего продукта статус visibility имеет правильное значение.

Давайте посмотрим функцию prepareProductIndex(), для нее есть два плагина:

Нас интересует второй плагин из модуля Magento_InventoryElasticsearch, в который мы зайдем и посмотрим, что он делает.

/**
* Filter out stock options for configurable product.
*
* @param DataProvider $dataProvider
* @param array $indexData
* @param array $productData
* @param int $storeId
* @return array
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function beforePrepareProductIndex(
   DataProvider $dataProvider,
   $indexData,
   $productData,
   $storeId
) {
   if (!$this->stockConfiguration->isShowOutOfStock($storeId)) {
       $productIds = array_keys($indexData);
       $store = $this->storeRepository->getById($storeId);
       $stock = $this->stockByWebsiteIdResolver->execute((int)$store->getWebsiteId());
       $stockId = $stock->getStockId();

       if ($this->defaultStockProvider->getId() === $stockId) {
           $stockStatuses = $this->getStockStatusesFromDefaultStock($productIds);
       } else {
           $stockStatuses = $this->getStockStatusesFromCustomStock($productIds, $stockId);
       }

       $indexData = array_intersect_key($indexData, $stockStatuses);
   }

   return [
       $indexData,
       $productData,
       $storeId,
   ];
}

/**
* Get product stock statuses on default stock.
*
* @param array $productIds
* @return array
*/
private function getStockStatusesFromDefaultStock(array $productIds): array
{
   $stockStatusCriteria = $this->stockStatusCriteriaFactory->create();
   $stockStatusCriteria->setProductsFilter($productIds);
   $stockStatusCollection = $this->stockStatusRepository->getList($stockStatusCriteria);
   $stockStatuses = $stockStatusCollection->getItems();

   return array_filter(
       $stockStatuses,
       function (StockStatusInterface $stockStatus) {
           return StockStatusInterface::STATUS_IN_STOCK === (int)$stockStatus->getStockStatus();
       }
   );
}

Этот плагин проверяет stock статусы для всех продуктов и фильтрует их таким образом, чтобы остались только продукты которые имеют статус in stock. (Если выключена настройка показывать out of stock продукты в админке.)

Дебагер привел нас в метод getStockStatusesFromDefaultStock(), в которой используется метод getStockStatus(), у которого также есть плагин:

public function afterGetStockStatus(StockStatusInterface $subject, int $result): int
{
   if ($subject->getQty() <= 0) {
       $result = Stock::STOCK_OUT_OF_STOCK;
   }
   return $result;
}

Это плагин, который я добавила. Видно, что он довольно странно выглядит.

В Magento у всех configurable продуктов атрибут qty равен нулю, и, похоже, мы нашли первый баг. Если закомментировать этот код и запустить реиндекс (убрав также product_id), то мы увидим привычную страницу категории с продуктами.

Исправление ошибок при формировании search request к Elasticsearch

Также могут быть ошибки при формировании запроса к Elasticsearch. Magento формирует запрос к Elasticsearch, используя коллекцию с её search criteria и search request.

\Magento\CatalogSearch\Model\ResourceModel\Fulltext\Collection

В Magento версии 2.4.2 описаны 5 search request, которые используются в зависимости от места, где мы используем поиск:

  • quick_search (используется при запросе из поля поиска в хедере);
  • advanced_search (используется при запросах со страницы advanced search);
  • catalog_view (используется на странице категории);
  • graphql_product_search (search request для GraphQL, без агрегации);
  • graphql_product_search_with_aggregation (search request для GraphQL, с агрегацией).

Они описаны в:

Рассмотрим quick_search, advanced_search и catalog_view search requests. Они все немного отличаются друг от друга.

Например, в контейнере quick_search_container описан query с именем partial_search, у которого естьmatchCondition="match_phrase_prefix" для атрибутов name и sku.

<query xsi:type="matchQuery" value="$search_term$" name="partial_search">
   <match field="*"/>
   <match field="name" matchCondition="match_phrase_prefix"/>
   <match field="sku" matchCondition="match_phrase_prefix"/>
</query>

Это означает, что Elasticsearch будет искать продукты, атрибуты name и sku которых начинаются на $search_term$. Это ключевое слово или фраза, которую мы вводим в поле поиска.

Если посмотрим в advanced_search_container, то здесь такого query не найдем, хотя есть wildcardFilter по полю sku. Он осуществляет частичный поиск по какому-то ключевому слову.

<filters>
   <filter xsi:type="wildcardFilter" name="sku_query_filter" field="sku" value="$sku$"/>
   <filter xsi:type="rangeFilter" name="price_query_filter" field="price" from="$price.from$" to="$price.to$"/>
   <filter xsi:type="termFilter" name="category_filter" field="category_ids" value="$category_ids$"/>
   <filter xsi:type="termFilter" name="visibility_filter" field="visibility" value="$visibility$"/>
</filters>

И catalog_view_container, где нет ничего особенного. Есть фильтр по category, price и visibility.

Давайте убедимся в разнице между этими search requests.

Например, если мы будем сейчас использовать quick_search_container для отправки search request к Elasticsearch, можем сразу это и протестировать.

Возьмем начало sku WJ0 и введем его в поле поиска в хедере. Elasticsearch нам вернет продукты, sku которых начинается с WJ0.

Но если мы сейчас попробуем поискать по «J01», например, то Elasticsearch нам ничего не находит, потому что в нашем quick_search_container нет wildcardFilter.

А если мы перейдем на страницу Advanced Search и попробуем здесь поискать по «J0», то Elasticsearch выдает нам продукты, sku которых содержит «J0»: «WJ0», «MJ0» и так далее.

Что это означает? Когда клиент к вам обращается с каким-то багом поиска, нужно обязательно посмотреть, какой search request используется в этом месте, и на разницу. Возможно, в вашем проекте есть кастомизации в конфигурации, и, может, это вовсе не баг, а какая-то фича тристороннего модуля.

Формирование запроса к Elasticsearch

Формирование запроса, добавление агрегации, запрос к Elasticsearch и получение ответа происходит в одной функции:

\Magento\Elasticsearch7\SearchAdapter\Adapter::query()

Давайте посмотрим, как это работает.

Нам нужен модуль Elasticsearch 7. Тут есть функция query(), в которой у меня всегда стоит breakpoint. Она удобна для дебага, здесь происходит формирование самого query к Elasticsearch, добавление агрегации, отправка запроса к Elasticsearch, получение ответа и формирование дальше $queryResponse для коллекции в Magento.

public function query(RequestInterface $request) : QueryResponse
{
   $client = $this->connectionManager->getConnection();
   $aggregationBuilder = $this->aggregationBuilder;
   $query = $this->mapper->buildQuery($request);
   $aggregationBuilder->setQuery($this->queryContainerFactory->create(['query' => $query]));

   try {
       $rawResponse = $client->query($query);
   } catch (\Exception $e) {
       $this->logger->critical($e);
       // return empty search result in case an exception is thrown from Elasticsearch
       $rawResponse = self::$emptyRawResponse;
   }

   $rawDocuments = $rawResponse['hits']['hits'] ?? [];
   $queryResponse = $this->responseFactory->create(
       [
           'documents' => $rawDocuments,
           'aggregations' => $aggregationBuilder->build($request, $rawResponse),
           'total' => $rawResponse['hits']['total']['value'] ?? 0
       ]
   );
   return $queryResponse;
}

Иногда удобно отправить search-запрос к Elasticsearch напрямую, не используя Magento, что гораздо быстрее.

Например, если у вас будут какие-то задачи по кастомизации и вы захотите немного поэкспериментировать с запросом, то вы можете отправить POST-запрос к Elasticsearch. Для этого нужно указать host и port, имя индекса и ключевое слово «search»:

POST elasticsearch:9200/{{index_name}}/_search

Пример body:

{"from":0,"size":"1","query":{"bool":{"must":[{"match":{"sku":{"query":"ges20","boost":2}}}]}}}

Пример body вы можете получить прямо в SearchAdapter. Для этого просто подождите, пока дебагер остановится.

Этот body можно использовать для POST-запроса, меняя его так, чтобы получить то, что вам нужно.

Кейс #2: Сортировка не работает

Дальше мы перейдем к проблемам сортировки.

Допустим, к вам обращается клиент с багом — сортировка не работает. Он сортирует продукты по product name, но ничего не получается.

Давайте зайдем на нашу категорию и проверим. Попробуем отсортировать по product name и увидим, что продукт c именем на «A» стоит после продукта с именем на «S» и «J», то есть, очевидно, сортировка не работает.

Давайте попробуем выяснить, в чем дело.

Для этого нам поможет breakpoint из прошлого раздела, включаем дебагер и перезагружаем.

Итак, Magento формирует query. Мы можем в него сразу зайти посмотреть на body.

Здесь мы видим, что поле sort пустое. Почему так может произойти? Иногда это происходит, когда sort order не засетился в коллекцию на момент отправки запроса к Elasticsearch.

Давайте посмотрим в Stacktrace, где мы видим Observer.

В этом Observer высчитывается последняя страница коллекции. Для этого Magento считает, сколько продуктов в коллекции. Соответственно, коллекция загружается, а для этого, в свою очередь, отправляет запрос к Elasticsearch.

Этот observer вычисляет URL для предыдущих и последующих страниц. В это время как раз происходит запрос к Elasticsearch, и оказывается, что здесь order еще не засетился. Как мы все знаем, order у нас сетится в toolbar:

\Magento\Catalog\Block\Product\ProductList\Toolbar

public function setCollection($collection)
{
   $this->_collection = $collection;

   $this->_collection->setCurPage($this->getCurrentPage());

   // we need to set pagination only if passed value integer and more that 0
   $limit = (int)$this->getLimit();
   if ($limit) {
       $this->_collection->setPageSize($limit);
   }
   if ($this->getCurrentOrder()) {
       if (($this->getCurrentOrder()) == 'position') {
           $this->_collection->addAttributeToSort(
               $this->getCurrentOrder(),
               $this->getCurrentDirection()
           );
       } else {
           $this->_collection->setOrder($this->getCurrentOrder(), $this->getCurrentDirection());
       }
   }
   return $this;
}

В методе setСollection происходит setOrder.

Observer:

public function execute(Observer $observer): LayoutGenerateBlocksAfterObserver
{
   $productListBlock = $this->getCategoryProductListBlock($observer);
   if ($productListBlock) {
       $toolbarBlock = $productListBlock->getToolbarBlock();
       if ($toolbarBlock) {
           /** @var Pager $pagerBlock */
           $pagerBlock = $toolbarBlock->getChildBlock('product_list_toolbar_pager');
           if ($pagerBlock) {
               $layer = $productListBlock->getLayer();
               $productListBlock->prepareSortableFieldsByCategory($layer->getCurrentCategory());
               $toolbarBlock->setAvailableOrders($productListBlock->getAvailableOrders());
               $toolbarBlock->setDefaultOrder($productListBlock->getSortBy());
               $toolbarBlock->setDefaultDirection($productListBlock->getDefaultDirection());

               if ($toolbarBlock->getCurrentOrder() == 'relevance') {
                   $layer->getProductCollection()->setOrder(
                       $toolbarBlock->getCurrentOrder(),
                       $toolbarBlock->getCurrentDirection()
                   );
               }
               $collection = $layer->getProductCollection();
               $pagerBlock
                   ->setAvailableLimit($toolbarBlock->getAvailableLimit())
                   ->setCollection($collection);
               $lastPage = $pagerBlock->getLastPageNum();
               $currentPage = $pagerBlock->getCurrentPage();

               if ($currentPage > 1) {
                   $url = $this->getPageUrl($pagerBlock->getPageVarName(), $currentPage - 1);
                   $this->pageConfig->addRemotePageAsset($url, 'link_rel', ['attributes' => ['rel' => 'prev']]);
               }

               if ($currentPage < $lastPage) {
                   $url = $this->getPageUrl($pagerBlock->getPageVarName(), $currentPage + 1);
                   $this->pageConfig->addRemotePageAsset($url, 'link_rel', ['attributes' => ['rel' => 'next']]);
               }
           }
       }
   }

   return $this;
}

Похоже, что в нашем Observer попытались как-то сымитировать работу toolbar. Здесь засетили AvailableOrders, DefaultOrder, DefaultDirection. Но забыли засетить саму коллекцию, в которой происходит setOrder. Поэтому мы сейчас cделаем setCollection и проверим, нашли ли баг.

$toolbarBlock->setCollection($layer->getProductCollection());

Выключаем наш дебагер и смотрим.

Мы видим, что коллекция отсортирована по атрибуту product name, что значит, что мы нашли баг! Вуаля.

Кейс #3: Агрегация в Elasticsearch

Агрегация в Elasticsearch имеет метрики — count, sum, min, max — и она в основном используется в Magento для того, чтобы посчитать количество продуктов по разным фильтрам для Layered Navigation.

Вот так выглядит агрегация в search request к Elasticsearch:

"aggregations": {
    "price_bucket": {
      "extended_stats": {
        "field": "price_0_1"
      }
    },
    "category_bucket": {
      "terms": {
        "field": "category_ids",
        "size": 500
      }
    },
    "in_stock_bucket": {
      "terms": {
        "field": "in_stock_1_168",
        "size": 500
      }
    },
    "activity_bucket": {
      "terms": {
        "field": "activity",
        "size": 500
      }
    },
    "climate_bucket": {
      "terms": {
        "field": "climate",
        "size": 500
      }
    },
    "color_bucket": {
      "terms": {
        "field": "color",
        "size": 500
      }
    },

…

}

То есть для подсчета count метрики используется term bucket по атрибутам, которые будут использоваться в Layered Navigation. И вот так выглядит ответ Elasticsearch, в нем подсчитанное количество документов по всем уникальным значениям конкретного атрибута. В этом случае для атрибута climate.

"climate_bucket": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
                {
                    "key": "207",
                    "doc_count": 12
                },
                {
                    "key": "203",
                    "doc_count": 8
                },
                {
                    "key": "205",
                    "doc_count": 8
                },
                {
                    "key": "209",
                    "doc_count": 7
                },
                {
                    "key": "210",
                    "doc_count": 4
                },
                {
                    "key": "201",
                    "doc_count": 3
                },
                {
                    "key": "204",
                    "doc_count": 3
                },
                {
                    "key": "202",
                    "doc_count": 2
                },
                {
                    "key": "206",
                    "doc_count": 2
                },
                {
                    "key": "208",
                    "doc_count": 1
                }
            ]
        },

И вот как это выглядит в Magento:

Обычно с агрегацией никаких проблем не возникает, тут все достаточно просто. Всё, что нужно сделать — это описать свои buckets в search request. Если вы делаете агрегацию по product атрибуту, он динамически попадет в search request. Для это стоит лишь задать свойство атрибута is_filterable. Но могут возникнуть проблемы при различных кастомизациях.

Расскажу пример из своей практики. Обратился клиент и попросил добавить новый фильтр «in stock». Дело в том, что в магазине включена опция backorder для продуктов. Это означает, что покупатель может купить продукт, у которого quantity равна нулю или меньше нуля, и он будет ему доставлен, когда этот продукт появится на складе.

Клиент хочет, чтобы был фильтр «in stock», который показывает реальное положение дел. То есть фильтр будет иметь значение Yes для продуктов, которые действительно есть на складе, а значит их quantity больше 0. И значение No для продуктов, у которых quantity меньше 0, то есть которые backordered.

Мы добавили этот фильтр, для этого кастомизировали search_request.xml, описав там агрегацию. Добавили termBucket c метрикой count для своего фильтра in_stock, для двух контейнеров — quick_search_container и catalog_view_container.

Пример реквеста для quick_search_container:

<request query="quick_search_container" index="catalogsearch_fulltext">
   <queries>
       <query xsi:type="boolQuery" name="quick_search_container" boost="1">
           <queryReference clause="must" ref="in_stock_query"/>
       </query>
       <query xsi:type="filteredQuery" name="in_stock_query">
           <filterReference clause="must" ref="in_stock_filter"/>
       </query>
   </queries>
   <filters>
       <filter xsi:type="termFilter" name="in_stock_filter" field="in_stock" value="$in_stock$"/>
   </filters>
   <aggregations>
       <bucket name="in_stock_bucket" field="in_stock" xsi:type="termBucket">
           <metrics>
               <metric type="count"/>
           </metrics>
       </bucket>
   </aggregations>
</request>

Но позже оказалось, что клиенту очень важен атрибут size, и он бы хотел, чтобы при выборе size фильтр «in stock» показывал in stock продукты для этого конкретного size. В каталоге все продукты были configurable по атрибутам color и size. То есть хотя бы один дочерний color этого size должен быть in stock, чтобы фильтр «in stock» имел значение Yes.

Здесь возникла проблема, потому что в Magento это сделать сложно. Elasticsearch поддерживает вложенную агрегацию, но дело в том, что в Magento это никак не реализовано. Кастомизировать Magento для того, чтобы использовать вложенную агрегацию, трудозатратно. Нужно будет менять не просто search request для того, чтобы агрегация была вложена, там ещё и ответ в совершенно другом виде приходит.

Поэтому было выбрано другое решение — использовать составное имя для фильтра «in stock», которое будет содержать в себе size_value. То есть такого формата in_stock_{store_id}_{size_value}. И значение формата in_stock_{store_id}_0 для случая, когда size не выбран в Layered Navigation.

Пример, как эти поля хранятся в индексе:

"in_stock_1_0" : 1,
"in_stock_1_167" : 1,
"in_stock_1_168" : 0,
"in_stock_1_169" : 1

Для этого всего лишь нужно написать FieldName Resolver. В нем мы проверяем активные фильтры. И если у нас есть активный фильтр — «size», мы этот size_value подставляем в имя фильтра.

public function getFieldName(AttributeAdapter $attribute, $context = []): ?string
{
   if ($attribute->getAttributeCode() === InStockFilter::CODE) {
       $sizeValue = 0;
       $activeFilterItems = $this->state->getActiveFilters();
       /** @var Item $activeFilterItem */
       foreach ($activeFilterItems as $activeFilterItem) {
           try {
               if ($activeFilterItem->getFilter()->getRequestVar() == self::SIZE_ATTRIBUTE) {
                   $sizeValue = $activeFilterItem->getDataByPath('value');
                   break;
               }
           } catch (LocalizedException $e) {
               continue;
           }
       }
       $storeId = $this->state->getLayer()->getCurrentStore()->getId();
       return InStockFilter::CODE . '_' . $storeId . '_' . $sizeValue;
   }

   return null;
}

Это всё! В итоге нам не пришлось сильно кастомизировать Magento, чтобы реализовать пожелание для клиента.

У нас всё работает, поле «in stock» у нас хранится в таком виде для всех продуктов. Отлично работает и фильтрация, и агрегация.

Заключение

Elasticsearch — это очень быстрый, удобный и гибкий инструмент для поиска.

Чтобы хорошенько с ним разобраться, нужно будет немного почитать документацию Elasticsearch, понять, как работает search request и search критерии. В результате вам точно понравится с ним работать!

👍НравитсяПонравилось12
В избранноеВ избранном4
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Подписаться на комментарии