PHP / Magento Dev Blog

  • Publikacje
  • O autorze
  • Kontakt

Własny indekser – mview.xml, flat table, full/partial reindeksacja, triggerowane przez cron

by Henryk Tews / wtorek, 21 stycznia 2025 / Opublikowano w Magento 2

System indeksowania Magento 2 to jeden z bardziej niedocenianych mechanizmów platformy. Gdy masz własne dane które muszą być szybko dostępne – ceny z zewnętrznego ERP, stany magazynowe z WMS, własne atrybuty wyszukiwania – napisanie własnego indeksera jest często lepszym rozwiązaniem niż zapisywanie wszystkiego do EAV przy każdym zapytaniu. Pokazuję jak zbudować kompletny indekser z obsługą pełnej i częściowej reindeksacji.

Kiedy własny indekser ma sens?

Magento 2 ma wbudowane indeksery dla cen, kategorii, stanów magazynowych i wyszukiwania. Własny indekser jest uzasadniony gdy:

  • Masz dane z zewnętrznego systemu (ERP, WMS) które zmieniają się niezależnie od Magento
  • Obliczenia są kosztowne (złożone reguły cen B2B, agregacje danych sprzedaży) i robisz je za często
  • Potrzebujesz własnej flat tabeli zoptymalizowanej pod konkretne zapytania
  • Chcesz pre-obliczać dane które są wyświetlane na każdej stronie produktu

Architektura indeksera

Vendor/Module/
  Model/
    Indexer/
      ProductCustomData.php        - główna klasa indeksera
      Action/
        Full.php                   - pełna reindeksacja
        Row.php                    - reindeksacja jednego wiersza
        Rows.php                   - reindeksacja wielu wierszy
    ResourceModel/
      Indexer/
        ProductCustomData.php      - operacje na flat table
  etc/
    indexer.xml                    - rejestracja indeksera
    mview.xml                      - konfiguracja triggerów change tracking
  Setup/
    InstallSchema.php              - tworzenie flat table

Rejestracja indeksera – indexer.xml

<!-- etc/indexer.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Indexer/etc/indexer.xsd">

    <indexer id="vendor_module_product_custom"
             view_id="vendor_module_product_custom"
             class="Vendor\Module\Model\Indexer\ProductCustomData"
             primary="catalog_product">

        <title translate="true">Vendor Product Custom Data</title>
        <description translate="true">Indexes custom product data from external sources</description>

        <!-- Zależności - nasz indekser wymaga aktualnego indeksu produktów -->
        <fieldset name="catalogrule_product">
            <field name="entity_id" xsi:type="filterable" handler="Magento\CatalogRule\Model\Indexer\DeltaIndexer">
                <depends>
                    <indexer id="catalog_product_flat"/>
                </depends>
            </field>
        </fieldset>

    </indexer>

</config>

Konfiguracja change tracking – mview.xml

<!-- etc/mview.xml - obserwuj zmiany w tabelach i triggeruj reindeksację -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Mview/etc/mview.xsd">

    <view id="vendor_module_product_custom"
          class="Vendor\Module\Model\Indexer\ProductCustomData"
          group="indexer">

        <subscriptions>
            <!-- Obserwuj zmiany w tabeli produktów -->
            <table name="catalog_product_entity" entity_column="entity_id"/>

            <!-- Obserwuj zmiany w naszej własnej tabeli z danymi z ERP -->
            <table name="vendor_module_erp_data" entity_column="product_id"/>

            <!-- Obserwuj zmiany cen -->
            <table name="catalog_product_entity_decimal"
                   entity_column="entity_id"
                   column="attribute_id"/>
        </subscriptions>

    </view>

</config>

Flat table – setup schema

<?php

declare(strict_types=1);

namespace Vendor\Module\Setup;

use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\Setup\ModuleContextInterface;

class InstallSchema implements InstallSchemaInterface
{
    public function install(SchemaSetupInterface $setup, ModuleContextInterface $context): void
    {
        $setup->startSetup();

        $table = $setup->getConnection()->newTable(
            $setup->getTable('vendor_module_product_index')
        )
        ->addColumn('entity_id', Table::TYPE_INTEGER, null, [
            'unsigned' => true,
            'nullable' => false,
            'primary'  => true,
        ], 'Product ID')
        ->addColumn('erp_stock_qty', Table::TYPE_DECIMAL, '10,4', [
            'nullable' => false,
            'default'  => '0.0000',
        ], 'ERP Stock Quantity')
        ->addColumn('erp_price', Table::TYPE_DECIMAL, '12,4', [
            'nullable' => true,
        ], 'ERP Price Override')
        ->addColumn('lead_time_days', Table::TYPE_SMALLINT, null, [
            'unsigned' => true,
            'nullable' => false,
            'default'  => 0,
        ], 'Lead Time in Days')
        ->addColumn('is_available_erp', Table::TYPE_SMALLINT, null, [
            'nullable' => false,
            'default'  => 1,
        ], 'Available in ERP')
        ->addColumn('indexed_at', Table::TYPE_TIMESTAMP, null, [
            'nullable' => false,
            'default'  => Table::TIMESTAMP_INIT_UPDATE,
        ], 'Last Indexed At')
        ->addIndex(
            $setup->getIdxName('vendor_module_product_index', ['is_available_erp']),
            ['is_available_erp']
        )
        ->addForeignKey(
            $setup->getFkName('vendor_module_product_index', 'entity_id', 'catalog_product_entity', 'entity_id'),
            'entity_id',
            $setup->getTable('catalog_product_entity'),
            'entity_id',
            Table::ACTION_CASCADE
        )
        ->setComment('Vendor Module Product Custom Index');

        $setup->getConnection()->createTable($table);
        $setup->endSetup();
    }
}

Główna klasa indeksera

<?php

declare(strict_types=1);

namespace Vendor\Module\Model\Indexer;

use Magento\Framework\Indexer\ActionInterface;
use Magento\Framework\Mview\ActionInterface as MviewActionInterface;

class ProductCustomData implements ActionInterface, MviewActionInterface
{
    public function __construct(
        private Action\Full $fullAction,
        private Action\Row $rowAction,
        private Action\Rows $rowsAction
    ) {}

    // Wywoływane przez bin/magento indexer:reindex vendor_module_product_custom
    public function executeFull(): void
    {
        $this->fullAction->execute();
    }

    // Wywoływane dla jednego produktu (np. po zapisaniu w adminie)
    public function executeRow($id): void
    {
        $this->rowsAction->execute([$id]);
    }

    // Wywoływane dla wielu produktów naraz
    public function executeList(array $ids): void
    {
        $this->rowsAction->execute($ids);
    }

    // MviewActionInterface - wywoływane przez cron z listą zmienionych ID
    public function execute($ids): void
    {
        $this->rowsAction->execute($ids);
    }
}

Full reindeksacja

<?php

declare(strict_types=1);

namespace Vendor\Module\Model\Indexer\Action;

use Vendor\Module\Model\ResourceModel\Indexer\ProductCustomData as IndexerResource;

class Full
{
    private const BATCH_SIZE = 1000;

    public function __construct(
        private IndexerResource $indexerResource,
        private \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $productCollectionFactory,
        private \Vendor\Module\Model\ErpDataProvider $erpDataProvider,
        private \Psr\Log\LoggerInterface $logger
    ) {}

    public function execute(): void
    {
        $this->logger->info('Full reindex started');
        $start = microtime(true);

        // Wyczyść starą flat table
        $this->indexerResource->clearTable();

        // Pobierz wszystkie ID produktów w batchach
        $collection = $this->productCollectionFactory->create();
        $collection->addFieldToSelect('entity_id');

        $ids = $collection->getAllIds();
        $batches = array_chunk($ids, self::BATCH_SIZE);

        $total = count($ids);
        $processed = 0;

        foreach ($batches as $batchIds) {
            $this->processBatch($batchIds);
            $processed += count($batchIds);

            if ($processed % 5000 === 0) {
                $this->logger->info("Reindex progress: {$processed}/{$total}");
            }
        }

        $elapsed = round(microtime(true) - $start, 2);
        $this->logger->info("Full reindex completed: {$total} products in {$elapsed}s");
    }

    private function processBatch(array $productIds): void
    {
        // Pobierz dane z ERP dla całego batcha jednym zapytaniem
        $erpData = $this->erpDataProvider->getDataForProducts($productIds);

        $rows = [];
        foreach ($productIds as $productId) {
            $erp = $erpData[$productId] ?? [];

            $rows[] = [
                'entity_id'        => $productId,
                'erp_stock_qty'    => $erp['qty'] ?? 0.0,
                'erp_price'        => $erp['price'] ?? null,
                'lead_time_days'   => $erp['lead_time'] ?? 0,
                'is_available_erp' => !empty($erp) ? 1 : 0,
            ];
        }

        // Zbiorczy insert - znacznie szybszy niż pojedyncze row-by-row
        $this->indexerResource->insertBatch($rows);
    }
}

Resource Model indeksera

<?php

declare(strict_types=1);

namespace Vendor\Module\Model\ResourceModel\Indexer;

use Magento\Framework\App\ResourceConnection;

class ProductCustomData
{
    private const TABLE = 'vendor_module_product_index';

    public function __construct(
        private ResourceConnection $resourceConnection
    ) {}

    public function clearTable(): void
    {
        $this->getConnection()->truncateTable($this->getTableName());
    }

    public function insertBatch(array $rows): void
    {
        if (empty($rows)) {
            return;
        }

        $this->getConnection()->insertOnDuplicate(
            $this->getTableName(),
            $rows,
            ['erp_stock_qty', 'erp_price', 'lead_time_days', 'is_available_erp']
        );
    }

    public function deleteByProductIds(array $ids): void
    {
        $this->getConnection()->delete(
            $this->getTableName(),
            ['entity_id IN (?)' => $ids]
        );
    }

    public function getDataByProductId(int $productId): ?array
    {
        $select = $this->getConnection()->select()
            ->from($this->getTableName())
            ->where('entity_id = ?', $productId);

        $result = $this->getConnection()->fetchRow($select);
        return $result ?: null;
    }

    private function getConnection(): \Magento\Framework\DB\Adapter\AdapterInterface
    {
        return $this->resourceConnection->getConnection();
    }

    private function getTableName(): string
    {
        return $this->resourceConnection->getTableName(self::TABLE);
    }
}

Używanie danych z indeksu w modułach

<?php

declare(strict_types=1);

// Plugin który dodaje dane z flat index do produktu w GraphQL/API
namespace Vendor\Module\Plugin;

class AddIndexedDataToProductPlugin
{
    public function __construct(
        private \Vendor\Module\Model\ResourceModel\Indexer\ProductCustomData $indexResource
    ) {}

    public function afterGetPrice(
        \Magento\Catalog\Model\Product $subject,
        float $result
    ): float {
        // Użyj dane z flat index zamiast zapytania do EAV
        $indexData = $this->indexResource->getDataByProductId((int) $subject->getId());

        if ($indexData && $indexData['erp_price'] !== null) {
            return (float) $indexData['erp_price'];
        }

        return $result;
    }
}
# Uruchamianie indeksera
bin/magento indexer:reindex vendor_module_product_custom

# Status
bin/magento indexer:status vendor_module_product_custom

# Ustaw tryb reindeksacji
bin/magento indexer:set-mode schedule vendor_module_product_custom

# Ręczne uruchomienie mview dla trybu schedule
bin/magento indexer:reindex vendor_module_product_custom --mode=changelog

Podsumowanie

Własny indekser w Magento 2 to inwestycja która zwraca się gdy masz dane które zmieniają się niezależnie od cyklu życia produktu w Magento lub gdy obliczenia w locie przy każdym requeście są zbyt kosztowne. Flat table z własną strukturą jest zawsze szybsza do odczytu niż EAV join przez 5 tabel. Konfiguracja mview.xml sprawia że reindeksacja odbywa się automatycznie w tle – bez manualnego uruchamiania przy każdej zmianie.

About Henryk Tews

Co możesz przeczytać następne

OpenSearch 3.x vector search – embeddingi przez Ollama, k-NN, hybrid search
GraphQL – własny resolver, schemat, autoryzacja, testowanie w DDEV
AI-assisted optymalizacja SQL – LLM + EXPLAIN + Blackfire, 5x przyspieszenie
  • Publikacje
  • O autorze
  • Kontakt

© 2026 Created by

GÓRA
Zarządzaj zgodą
Aby zapewnić jak najlepsze wrażenia, korzystamy z technologii, takich jak pliki cookie, do przechowywania i/lub uzyskiwania dostępu do informacji o urządzeniu. Zgoda na te technologie pozwoli nam przetwarzać dane, takie jak zachowanie podczas przeglądania lub unikalne identyfikatory na tej stronie. Brak wyrażenia zgody lub wycofanie zgody może niekorzystnie wpłynąć na niektóre cechy i funkcje.
Funkcjonalne Zawsze aktywne
Przechowywanie lub dostęp do danych technicznych jest ściśle konieczny do uzasadnionego celu umożliwienia korzystania z konkretnej usługi wyraźnie żądanej przez subskrybenta lub użytkownika, lub wyłącznie w celu przeprowadzenia transmisji komunikatu przez sieć łączności elektronicznej.
Preferencje
Przechowywanie lub dostęp techniczny jest niezbędny do uzasadnionego celu przechowywania preferencji, o które nie prosi subskrybent lub użytkownik.
Statystyka
Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do celów statystycznych. Przechowywanie techniczne lub dostęp, który jest używany wyłącznie do anonimowych celów statystycznych. Bez wezwania do sądu, dobrowolnego podporządkowania się dostawcy usług internetowych lub dodatkowych zapisów od strony trzeciej, informacje przechowywane lub pobierane wyłącznie w tym celu zwykle nie mogą być wykorzystywane do identyfikacji użytkownika.
Marketing
Przechowywanie lub dostęp techniczny jest wymagany do tworzenia profili użytkowników w celu wysyłania reklam lub śledzenia użytkownika na stronie internetowej lub na kilku stronach internetowych w podobnych celach marketingowych.
  • Zarządzaj opcjami
  • Zarządzaj serwisami
  • Zarządzaj {vendor_count} dostawcami
  • Przeczytaj więcej o tych celach
Zobacz preferencje
  • {title}
  • {title}
  • {title}