Shopware 6 Hidden Gems #2: Bulk imports done right — taming the indexers
Fabian Blechschmidt
Every Shopware developer has been here: you push 50,000 products through the Sync API and the import crawls. The writes themselves are fast. What kills you is everything that happens after each write — inheritance updates, cheapest price calculation, search keyword generation, category denormalization. Shopware calls all of this „indexing“, and by default it runs synchronously, per request, whether you need it right now or not.
The funny thing: Shopware has a complete toolbox to control exactly this. Headers, context states, command options — all wired up, all used internally. The indexing-behavior header gets a passing mention in the docs. The rest? Nothing. Time to fix that.
Two headers on the Sync API
POST /api/_action/sync reads two HTTP headers before it does anything else (Framework/Api/Controller/SyncController.php:48-53):
indexing-behavior accepts two values (they are constants in EntityIndexerRegistry):
Value
Effect
use-queue-indexing
Indexing is pushed to the message queue instead of running synchronously. Your API call returns as soon as the data is written.
disable-indexing
Indexing is skipped entirely. You rebuild it later yourself.
indexing-skip is even more granular: a comma-separated list of indexer names to leave out — while everything else still runs normally.
The skip list goes deeper than you think
Here is the part I did not expect. You can obviously skip whole indexers — every indexer has a name like product.indexer or category.indexer (its getName()). But the skip list also works on sub-updaters inside an indexer. The ProductIndexer alone checks eleven of them (ProductIndexer.php:34-44):
Each one is wrapped in if ($message->allow(...)) inside the indexer. So if your import updates stock levels only, you can skip the expensive search keyword and cheapest price recalculation and keep the stock updater — per request, via one header.
The same thing in PHP: two context states
Not going through the Sync API? Writing via repositories in a command or subscriber? Same mechanics, different door. SyncService translates the header into a context state (SyncService.php:52-55) — and you can set that state yourself:
use Shopware\Core\Framework\DataAbstractionLayer\Indexing\EntityIndexerRegistry;
// push indexing to the queue
$context->addState(EntityIndexerRegistry::USE_INDEXING_QUEUE);
// or: skip indexing entirely
$context->addState(EntityIndexerRegistry::DISABLE_INDEXING);
$this->productRepository->upsert($payload, $context);
The core itself does this in places you’d expect once you know it exists: the thumbnail service disables indexing during bulk thumbnail generation, the customer registration route queues it to keep the response fast.
One warning from experience: if you use DISABLE_INDEXING, you own the rebuild. Which brings us to the third tool.
dal:refresh:index — with --skip and --only
The rebuild command everyone knows. The options almost nobody knows:
$ bin/console dal:refresh:index --help
--use-queue Ignore cache and force generation
--skip=SKIP Comma separated list of indexer names to be skipped
--only=ONLY Comma separated list of indexer names to be generated
So after a disable-indexing import you don’t have to reindex the whole shop. You rebuild exactly what you touched:
--use-queue chops the work into messages for your workers instead of blocking the CLI for an hour. And --skip works here too, including sub-updaters — the lists get passed straight into the same EntityIndexerRegistry::index() we saw above (EntityIndexerRegistry.php:79).
The recipe
Putting it together, a large import looks like this:
Send your sync operations with indexing-behavior: disable-indexing (fastest) or use-queue-indexing (safest).
If you only need parts of the indexing, use indexing-skip with specific updater names instead.
Watch your workers do the heavy lifting while your import is already done.
On a real project this turned a multi-hour nightly import into minutes of writing plus background indexing. Same data, same hardware — we just stopped recalculating cheapest prices 50,000 times for no reason.
TL;DR
The Sync API reads two undocumented headers: indexing-behavior (use-queue-indexing / disable-indexing) and indexing-skip (comma-separated indexer and sub-updater names like product.cheapest-price). In PHP, $context->addState(EntityIndexerRegistry::DISABLE_INDEXING) does the same. Rebuild selectively with dal:refresh:index --only=... --use-queue. Your imports will thank you.
Next up: sw-expect-packages — the header that lets your API integration fail fast (and loudly) when the shop doesn’t have the plugins it expects.
If you have questions or war stories about slow imports, drop me an email!
Found in Shopware 6.7.0.0: header handling in Framework/Api/Controller/SyncController.php:48-53, state translation in Framework/Api/Sync/SyncService.php:47-55, states and skip logic in Framework/DataAbstractionLayer/Indexing/EntityIndexerRegistry.php:25-29,79-90, sub-updater constants in Content/Product/DataAbstractionLayer/ProductIndexer.php:34-44, command options in Framework/DataAbstractionLayer/Command/RefreshIndexCommand.php:45-48.