Всем привет! Давайте знакомиться ;) Я Аня, и я php разработчик. Основной стек - Magento. Очень люблю в свободное время писать всякие интересные штуки, и сегодня я хочу поделиться своей наработкой для реализации поиска по изображению в Magento 2. На мой взгляд - это полезная фича, и довольно удобная для пользователей.

Для нетерпеливых, вот прямая ссылка на github

ВАЖНО! ПРОЧИТАТЬ ПЕРЕД ДАЛЬНЕЙШИМ ЧТЕНИЕМ СТАТЬИ!!!!

1) Мой код не претендует на идеальный.

2) Данный модуль писался мной с целью just for fun, и было мне интересно поработать с питоном в связке с пхп, а не в коммерческих целях.

3) Данный модуль абсолютно не подходит для big commerce и явно не оптимизирован под большой объем - и я это понимаю (см пункт 2)

4) Повторюсь, это просто proof of concept, с которым мне интересно поделиться. Возможно, это поможет в будущем кому-то в работе.

5) Я буду рада почитать ваши конструктивные идеи/рекомендации/предложения в комментариях. И помним про пункт 1,2,3 написанный выше =)

Содержание статьи

  1. Введение (обещаю, будет коротко и без воды =) )

  2. Как это работает: FE часть

  3. Как это работает: Admin часть

  4. Как это работает: BE часть

  5. Заключение

1. Введение

Я очень люблю онлайн магазины, и у меня есть разный опыт работы с проектами на Magento - от мелких до крупных проектов с 500К+ продуктов. Как пользователь, мне бы хотелось в эпоху ИИ иметь возможность поиска по картинке - это действительно очень удобно, особенно, когда каталог большой. Так же для стороны бизнеса я вижу тут много плюшек, но сейчас не о них.

Мне стало интересно посмотреть уже готовые существующие именно модули, а не интеграции со сторонними приложениями, которые предложили бы out of the box возможность поиска по изображению. И я нашла. Однако, эти решения сводили все к текстовму поиску - т.е. распознавалось что именно изображено на картинке, далее ИИ генерировал запрос, например "платье", и уже в результатах поиска были продукты, релевантые слову "платье".

Мне же хотелось искать именно по изображению, а не по тексту, тк считаю такой метод более релевантный запросу пользователя. И по этому, я решила использовать CNN модели, используя image vectors + Tensorfow.

А теперь, подробнее ниже.

Как это работает: FE часть

1) С точки зрения пользователя:

Для пользователя все устроено довольно просто. Рядом с поиском, есть дополнительная кнопка, при нажатии которой, открывается форма поиска по картинке. Сама форма довольно простая, она позволяет:

1) Искать по всему каталогу, либо искать в определенных категориях.

2) Можно задать область изображения, по которому хотим найти товар.

Magento 2 Visual Search button
Magento 2 Visual Search button
Magento 2 Visual search form
Magento 2 Visual search form

После сабмита формы, пользователю предлагаются варианты, схожие по данному изображению.

Magento 2 Visual search result page
Magento 2 Visual search result page

Над релевантостью нужно поработать, но об этом ниже. Сейчас мы рассматриваем FE side.

2) С точки зрения программиста:

  • В зависимости от настроек модуля, можно скрыть, либо показать category selector. Для подгрузки категорий, доступных для visual search, я использую аякс запрос на следующий контроллер - BelSmol/VisualSearch/Controller/Ajax/SearchCategoryList.php , который использует внутреннюю реализацию для BelSmol\VisualSearch\API\SearchManagerInterface

  • Т.к. я использую стороннюю библиотеку, которая делает crop изображения, а не просто передаю картинку на бэк, то здесь идет работа с blob. Оперировать blob - не самый лучший вариант, по этому, я использую для сабмита формы "вспомогательный" контроллер, который подготовит запрос для бэкэнда BelSmol/VisualSearch/Controller/Ajax/PrepareRequest.php. Здесь сохраняется во временную папку blob изображение и, используя внутреннюю реализацию SearchManagerInterface, сохраняется search image реквест в таблицу в базе данных, после чего возвращаем searchRequestParamValue - по сути, это идентификатор поискового запроса, по которому будут выдаваться результаты. В случае success, пользователя редиректит на страницу с результатом поиска. BelSmol/VisualSearch/Controller/Search/Result.php - об этом подробнее будет ниже.

  • ВАЖНО! Для тех, кто подумает, что сервер будет "захламляться" мусором из картинок, и таблица БД будет "жрать ресурсы" из-за пользовательских запросов, могу сказать следующее - спокойно, у нас есть фича, которая чистит это дело =) Детали будут ниже.

Основной js код можно посмотреть здесь:

BelSmol/VisualSearch/view/frontend/web/js/components/visual-search/popup-form.js - логика формы

BelSmol/VisualSearch/view/frontend/web/js/components/visual-search/popup-widget.js - виджет для pop-up формы

Я написала довольно подробную документацию, так что все будет понятно из кода.

Так же хочу отметить, что в модуле используются сторонние библиотеки js, и они лежат здесь, включая документацию этих библиотек. Мне важно, что бы код читался как книга, по этому стараюсь хорошо писать код + документацию + документацию для библиотек.

3-d party js libraries location
3-d party js libraries location

Что касается страницы с результатом поиска, то ее можно посмотреть в данном лаяуте:

BelSmol/VisualSearch/view/frontend/layout/visual_search_search_result.xml
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column"
      xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<!--
/**
* Copyright (c) 2023 by https://github.com/annysmolyan
*
* This module provides a visual search functionality for an e-commerce store.
* For license details, please view the GNU General Public License v3 (GPL 3.0)
* https://www.gnu.org/licenses/gpl-3.0.en.html
*/
-->
    <body>
        <referenceContainer name="columns.top">
            <!-- Init page title -->
            <block class="Magento\Theme\Block\Html\Title"
                name="page.main.title"
                template="html/title.phtml"
            />

            <!-- Init messages from BE-->
            <container name="page.messages" htmlTag="div" htmlClass="page messages">
                <block name="ajax.message.placeholder"
                       template="Magento_Theme::html/messages.phtml"/>
                <block name="messages" as="messages"
                       template="Magento_Theme::messages.phtml"
                />
            </container>
        </referenceContainer>

        <referenceContainer name="content">
            <!-- Search results main content-->
            <block class="BelSmol\VisualSearch\Block\VisualSearchResult"
                name="visual_search_result"
                template="BelSmol_VisualSearch::visual_search_result.phtml"
            >
                <arguments>
                    <argument name="view_model" xsi:type="object">BelSmol\VisualSearch\ViewModel\SearchResultViewModel</argument>
                    <argument name="options_data_view_model" xsi:type="object">Magento\Catalog\ViewModel\Product\OptionsData</argument>
                </arguments>

                <!-- "add to" block here, e.g. wishlist buttons, add to compare -->
                <block class="Magento\Catalog\Block\Product\ProductList\Item\Container"
                       name="category.product.addto" as="addto"
                >
                    <!-- Add to compare -->
                    <block class="Magento\Catalog\Block\Product\ProductList\Item\AddTo\Compare"
                           name="category.product.addto.compare" as="compare"
                           template="Magento_Catalog::product/list/addto/compare.phtml"/>
                    <!-- Add to wishlist -->
                    <block class="Magento\Wishlist\Block\Catalog\Product\ProductList\Item\AddTo\Wishlist"
                           name="category.product.addto.wishlist" as="wishlist" before="compare"
                           template="Magento_Wishlist::catalog/product/list/addto/wishlist.phtml"/>
                </block>

                <!-- Product toolbar with pager-->
                <block class="Magento\Catalog\Block\Product\ProductList\Toolbar"
                       name="product_list_toolbar"
                       template="BelSmol_VisualSearch::search_result/toolbar.phtml">
                    <block class="Magento\Theme\Block\Html\Pager"
                           name="product_list_toolbar_pager"
                    />
                </block>

                <action method="setToolbarBlockName">
                    <argument name="name" xsi:type="string">product_list_toolbar</argument>
                </action>

            </block>
        </referenceContainer>
    </body>
</page>

Основное "сердце" страницы лежит в блоке - BelSmol\VisualSearch\Block\VisualSearchResult . Блок работает схоже с LayeredNavigation. Здесь основной метод, на который следует обратить внимание:

    public function getSearchRequest(): ?SearchRequestInterface
    {
        if (null == $this->searchRequest) {
            Profiler::start('BelSmol_VisualSearch:' . __METHOD__);
            $searchParam = $this->request->getParam(self::SEARCH_PARAM_NAME, "");
            $this->searchRequest = $this->searchRequestManager->getBySearchTermValue($searchParam);
            Profiler::stop('BelSmol_VisualSearch:' . __METHOD__);
        }

        return $this->searchRequest;
    }

Я так же предусмотрела профайлер, чтобы можно было продебажить/улучшить перфоманс, если кому-то это будет интересным.

ВАЖНО!!!!! Модуль имеет сервис котракт, используйте именно его для вызова "из вне" - BelSmol/VisualSearch/API/SearchServiceInterface.php

Думаю, на этом можно закончить обзор FE части, и давайте перейдем к админ панеле.

Как это работает: Admin часть

Stores -> Settings -> Configuration -> Tab "BelSmol" -> "Visual Search"

Magento 2 Visual Search - admin settings
Magento 2 Visual Search - admin settings

Настройки поделены на 5 групп. С General, думаю, что все понятно. Давайте рассмотрим остальные:

Search Settings:

Magento 2 Visual search: Search Settings
Magento 2 Visual search: Search Settings
  • Allow Customers Category Selection - скрыть / показать search selector на форме (писала выше)

  • Include All Categories In Search (Yes/No allowed) - здесь я предусмотрела гибкую настройку для выбора категорий. ВАЖНО! не забываем про реиндекс каталога, если изменяются требующие этого настройки.

  • Exclude from Search - будет показываться тогда, когда "Include All Categories In Search" = YES. Если значение будет NO, то будет доступен другой конфиг, позволяющий выбрать категории, в которых можно делать поиск.

  • Max Total Items Search Result Count - максимальное количество продуктов в результате поиска

  • Min Relevance Scope - показатель "схожести", используется в эластик серч. Об этом ниже

  • REST API Product Page Size - количество продуктов, когда будет вызываться REST API.

AI Settings:

  • CNN model - сейчас доступна только одна (можете добавить свои модели, если захотите)

  • AI Server Domain - необходимо указать домен сервера. ВАЖНО! Как развернуть сервер и что это - будет в конце статьи

Image Vector Settings:

Я использую Tensorflow и CNN модели. Немного теории будет в разделе про BE часть, но расписывать здесь подробнее я не буду. Если кратко, для реализации поиска по картинке нам нужно сделать экстракт image features, они же потом преобразуются в vectors, это можно сделать при помощи CNN моделей. И уже по этим векторам идет сравнение изображений.

  • Update Product Vector - данная настройка отвечает за то, в каком именно моменте мы будем обновлять данный вектор изображения. Это можно делать по крону, или же делать при сохранении продукта. Я рекомендую в целях производительности выбирать крон.

  • Cron Schedule for Image Vector Update - здесь установите свое расписание для крона

  • Batch Size - укажите сколько за один раз можно процессить изображений для экстракта векторов. В зависимости от мощностей вашего сервера, здесь можно поиграть с настройкой.

Cleaning Settings:

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

Admin Menu

Модуль использует RabbitMq, я сделала отдельное меню, что бы было удобно смотреть статусы и информацию об очередях.

Обращаю внимание, что очередь используется когда Image Vector Settings -> Update Product Vector установлен как on Save.

За это отвечает BelSmol/VisualSearch/Observer/UpdateProductImageVectorObserver.php
<?php
/**
 * Copyright (c) 2023 by https://github.com/annysmolyan
 *
 * This module provides a visual search functionality for an e-commerce store.
 * For license details, please view the GNU General Public License v3 (GPL 3.0)
 * https://www.gnu.org/licenses/gpl-3.0.en.html
 */

declare(strict_types=1);

namespace BelSmol\VisualSearch\Observer;

use BelSmol\VisualSearch\API\ConfigStorageInterface;
use BelSmol\VisualSearch\API\Data\QueueTaskInterface;
use BelSmol\VisualSearch\API\QueueTaskManagerInterface;
use BelSmol\VisualSearch\Model\Config\Source\VectorUpdateMode;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Message\ManagerInterface;

/**
 * Class UpdateProductImageVectorObserver
 * @package BelSmol\VisualSearch\Observer
 */
class UpdateProductImageVectorObserver implements ObserverInterface
{
    /**
     * @param ConfigStorageInterface $configStorage
     * @param ManagerInterface $messageManager
     * @param QueueTaskManagerInterface $queueTaskManager
     */
    public function __construct(
        private ConfigStorageInterface $configStorage,
        private ManagerInterface $messageManager,
        private QueueTaskManagerInterface $queueTaskManager
    ) {}

    /**
     * Every time when a product is getting updated, create a queue task to check small_image changes,
     * if the image was changed then build a new vector and save it
     *
     * @param Observer $observer
     * @return void
     */
    public function execute(Observer $observer): void
    {
        if (!$this->isObserverAllowed()) {
            return;
        }

        $product = $observer->getProduct();

        $task = $this->queueTaskManager->initEmptyTask();
        $task->setSkus([$product->getSku()]);
        $task->setStatus(QueueTaskInterface::STATUS_PENDING);
        $task->setStartedBy(QueueTaskInterface::STARTER_SAVE_ACTION);

        $this->queueTaskManager->pushToQueue($task);

        $this->messageManager->addSuccessMessage(
            __("Created a new queue for the small_image vector update")
        );
    }

    /**
     * @return bool
     */
    private function isObserverAllowed(): bool
    {
        return $this->configStorage->isModuleEnabled()
            && $this->configStorage->getVectorUpdateMode() == VectorUpdateMode::VALUE_UPD_ON_SAVE;
    }
}

Я знаю, что по magento best practice нельзя создавать отдельный пункт меню, дабы не захламлять админку. Пока оставлю как есть, это будет пунктом для рефакторинга в будущем:

Просто измените конфиг, обновите small image для любого продукта, сохраните его, и вы увидете следующую информацию:

Queue example 1
Queue example 1
Queue example 2
Queue example 2

Убедитесь, что у вас работают queue:consumers!

С обзором админской части у меня все, теперь перейдем к самому интересному.

Как это работает: BE часть

Как я уже писала выше, я делаю не просто поиск по слову, мне важно, что именно изображено на картинке, и искать именно похожий такой товар. Для того, чтобы это сделать, необходимо определить image vectors, которые определяются при помощи CNN моделей. На хабре уже есть теория про сверточные модели (CNN) и vectors (features) - Здесь можно почитать

Все это работает при помощи следующих технологий: Tensorflow + keras + inception_v3 CNN model. Вы можете использовать свою CNN модель, только необходимо будет сделать пару адаптаций (для этого см код модуля). Я же использовала inception_v3 как пример.

И так, для того, чтобы сделать поиск, не достаточно только иметь векторы изображений, нужен так же какой-то алгоритм/движок, который бы сравнивал векторы и выдавал схожие результаты. Для этих вещей отлично подойдет elastcisearch и его новая фича - KNN Search (прочитать об этом на официальном сайте).

ВАЖНО! Вам нужно будет обновить для Magento Elasticsearch до версии 8.4.3 (можно использовать этот поиск начинаяя с 8.1 версии, но я не проверяла эту версию). Также, нужно будет установить через композер модуль для работы с 8 версией эластик серча:

composer require magento/module-elasticsearch-8

Далее, в админ панеле, не забудьте установить 8 версию эластика:

Set Elasticsearch 8 for Magento 2
Set Elasticsearch 8 for Magento 2

Шаг 1: Развернуть наше ИИ приложение

В папке модуля вы найдете код приложения, написанного на Python: BelSmol/VisualSearch/_tensorflow_app. Перед началом работы, требуется поднять энв с этим приложением. Все довольно просто, см README file в BelSmol/VisualSearch/_tensorflow_app. Не забудьте настроить маунтинг папок. Это все прописано в readme.

Если коротко - там используется Flask, и есть 2 реста:

1) POST ВАШ_ДОМЕН/feature-extract/csv-source - используется для bulk определения векторов изображений. Сюда в качестве параметра передаем название CNN модели и csv файла, который имеет следующие колонки:

image_pub_media_path - путь картинки в magento pub/media директории (заполняется на стороне php)

vector - заполняется python приложением, именно этот вектор будет далее использоваться в elasticsearch.

Пример запроса:

POST http://localhost:5000/feature-extract/csv-source


{
    "modelName": "InceptionV3",
    "csvFileName": "image.csv"
}

Пример ответа:

Status 200 без body

Я решила использовать csv файл для bulk action, тк передавать непосредственно сами изображения - это довольно производительная операция. При настроенном маунтинге между ИИ приложением и magento, python скрипт будет иметь доступы к файлам на сервере напрямую, что экономит ресурсы. Если у вас есть какие-то иные идеи - буду рада увидеть в комментариях.

2) POST ВАШ_ДОМЕН/feature-extract/path-source - для определения вектора для конкретного изображения (single mode). Здесь 2 параметра - путь до файла и CNN модель

Пример запроса:

POST http://localhost:5000/feature-extract/path-source


{
    "modelName": "InceptionV3",
    "path": "catalog/product/m/b/mb01-blue-0.jpg"
}

Пример ответа:

{
    "vector": [array]
}

Шаг 2: Генерация image vectors и добавление в эластик серч

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

bin/magento visual-search:vector-data:build

bin/magento visual-search:vector-data:build OUTPUT RESULT
bin/magento visual-search:vector-data:build OUTPUT RESULT

ВАЖНО! эта команда может быть "тяжелой", запускайте ее в бэкграунде.

source code лежит здесь - BelSmol/VisualSearch/Console/Command/BuildVectorDataCommand.php

И так, как это работает (MAGENTO SIDE):

1) Вначале создается временная таблица, что бы не испортить существующий дата индекс. Здесь мы подготавливаем информацию, и при успешной итерации - копируем в основную таблицу. Все происходит здесь - BelSmol/VisualSearch/Model/Manager/VSDataManager.php

Детальнее ниже:

2) Скрипт собирает для каждого store_view product small_image (учитывая категории). Здесь используется batchSize, который мы уже установили из конфига. И здесь самое интересное. Во время процессинга информации скрипт отправляет массив путей изображений в класс генератора векторов - BelSmol/VisualSearch/Model/VectorGenerator.php . Данный генератор создает csv файл здесь: YOUR_MAGENTO_PROJECT_DIR/var/visual_search/GENRATED_FILE_NAME.csv

Файл состоит из двух колонок, как писалось для Шага 1:

image_pub_media_path - путь картинки в pub/media директории (заполняется на стороне php)

vector - заполняется приложением, использующий tensorflow. Именно этот вектор будет далее использоваться в elasticsearch.

Таким образом, в данном файле у нас лежит информация о путях к картинкам. Далее, мы посылаем файл в наше tensorflow приложение. Оно парсит файл, проходится по каждой картинке, генерирует вектор для изображения, обновляет csv файл, и возвращает респонс на сторону php об успешном выполнении

3) Полученная информация сохраняется во временной таблице. Так происходит до тех пор, пока все батчи не будут перегенерированны для store_view. После чего, временная таблица копируется в постоянную и удаляется.

4) Происходит реиндексация таблиц с векторами.

И так, у нас все готово для визуального поиска - все картинки проиндексированы с соответсвующими векторами и лежат в elasticsearch.


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

bin/magento visual-search:vector-data:update --skus XXX,YYY,ZZZ

source code лежит здесь - BelSmol/VisualSearch/Console/Command/UpdateVectorDataCommand.php


Шаг 3: Осуществление самого поиска

И так, после того, когда пользователь добавил изображение и нажал на "Search", модуль сгенерировал запрос и сохранил изображение во временной папке. Далее, вызывается контроллер BelSmol/VisualSearch/Controller/Search/Result.php и, как писалось выше, вся магия происходит в этом блоке: BelSmol/VisualSearch/Block/VisualSearchResult.php

Скрипт достает из реквсета идентификатор поискового запроса, подгружается модель с информацией о запросе BelSmol/VisualSearch/Model/SearchRequest.php.

Далее, происходит инициализация коллекции продуктов, основываясь на ту самую SearchRequest модель:

    /**
     * Either load data from visual search request
     * or just return empty collection
     * @return ProductCollection
     */
    private function initializeProductCollection(): ProductCollection
    {
        if (null === $this->_productCollection) {
            $searchRequest = $this->getSearchRequest();
            $this->_productCollection = (null === $searchRequest)
                ? $this->initEmptyCollection()
                : $this->searchManager->getSimilarProductsByImage(
                    trim($searchRequest->getImagePath()),
                    $searchRequest->getCategories()
                );
        }

        return $this->_productCollection;
    }

И, наконец, сердце этого поиска - BelSmol/VisualSearch/Model/Manager/SearchManager.php

Здесь происходит процессинг изображения пользователя: генерируется вектор изображения, затем этот вектор используется при кастомном KNN search запросе: BelSmol/VisualSearch/Model/Elasticsearch/Request/KnnRequest.php

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

Таким образом, наш Magento 2 Visual Search готов =)

Заключение:

Если вы дочитали до этого момента - большое спасибо за внимание! Надеюсь, что моя реализация поиска по картинке покажется вам интересной. Если вы знаете, как можно улучшить код, или же подправить питон приложение - буду рада!

В данном модуле есть еще пару to-do моментов, над которыми можно поработать:

1 - это улучшить релевантность поиска - для каждого Magento shop сделать CNN model pre-training, и использовать labels для более точного поиска

2 - улучшить скорость работы - сейчас это занимает около 5 секунд

3 - улучшить саму фичу - чтобы распознавались все вещи на изображении, и предлагались релевантные результаты для каждой из вещей.

4 - Сделать GraphQL

но над этим, может быть, я подумаю позже.

Полные исходники здесь - прямая ссылка на гитхаб

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