Shopware 6 powers millions of e-commerce experiences by balancing a robust relational database with high-performance search capabilities. At the heart of this performance is the Indexing System. In Shopware 6.7, this system has been refined to offer superior efficiency, better error recovery, and seamless extensibility for developers building complex catalogs.

This post explores how indexing works under the hood, what changed in 6.7, and how you can master custom indexing for your projects.

Shopware does not query the MySQL/MariaDB database directly for search results or admin list filters. Instead, it uses a dedicated Search Engine backend (Elasticsearch or OpenSearch). The Indexing System acts as the bridge, keeping the search engine in sync with your relational data.

Key Components

  1. Entity Definitions: Each entity that needs to be searchable defines an IndexerDefinition. This tells Shopware which fields should be indexed and how they map to the search structure.
  2. Abstract Indexers: The core logic resides in classes extending Shopware\Core\Framework\DataAbstractionLayer\Indexer\AbstractIndexer. These handle the extraction, transformation, and push of data to the search engine.
  3. Event-Driven Queue: When an entity is saved, Shopware dispatches an event. A listener picks this up and adds a job to the queue. This ensures that index updates are asynchronous and never block the main application thread during customer interactions.

What's New in Shopware 6.7?

Shopware 6.7 brings significant optimizations to the indexing pipeline, addressing common pain points regarding performance and reliability at scale:

  • Optimized Batch Processing: The internal logic for fetching data during index regeneration has been rewritten. Indexer classes now utilize more efficient SQL queries with improved memory management. This prevents memory leaks during full rebuilds of massive catalogs containing hundreds of thousands of products.
  • Enhanced Error Context: If an indexing job fails, 6.7 provides granular details about which entity ID caused the exception. Previously, errors were often swallowed or returned generic messages. Now, developers get actionable logs to debug data corruption or mapping issues.
  • Consolidated Services: The service container has been cleaned up. Some legacy indexing services were deprecated in favor of a more unified IndexerService. This reduces overhead and simplifies dependency injection for custom extensions.
  • Improved Soft-Delete Handling: The indexer now handles soft-deleted entities more gracefully, ensuring they are correctly marked as deleted in the search index without requiring immediate database purges.

Creating a Custom Indexer

Developers often need to extend the search functionality with custom fields or entirely new entity types. In Shopware 6.7, this process remains consistent but benefits from stricter type hints and clearer interfaces.

Here is how you create a custom indexer for an extension entity.

Step 1: Create the Indexer Class

namespace YourVendor\YourExtension\Search;

use Shopware\Core\Framework\DataAbstractionLayer\Indexer\AbstractIndexer;
use Shopware\Core\Framework\DataAbstractionLayer\Indexer\IndexerEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Indexer\IndexerResult;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\IdSearchResult;
use YourVendor\YourExtension\YourCustomEntityDefinition;

class YourCustomProductSearchIndexer extends AbstractIndexer
{
    private EntityRepositoryInterface $yourCustomEntityRepository;

    public function __construct(EntityRepositoryInterface $yourCustomEntityRepository)
    {
        $this->yourCustomEntityRepository = $yourCustomEntityRepository;
    }

    /**
     * Defines which entities this indexer processes.
     */
    public function getEntityName(): string
    {
        return YourCustomEntityDefinition::ENTITY_NAME;
    }

    /**
     * Subscribes to the global indexer event.
     */
    public function getSubscribedEvents(): array
    {
        return [
            IndexerEvent::class => 'onIndex',
        ];
    }

    /**
     * Main logic triggered by the EventDispatcher.
     */
    public function onIndex(IndexerEvent $event): void
    {
        // Only process if this indexer is requested
        if ($event->getRequestedEntities() !== null 
            && !\in_array(YourCustomEntityDefinition::ENTITY_NAME, $event->getRequestedEntities(), true)) {
            return;
        }

        // Retrieve all IDs for the entity.
        // In 6.7, searchIds is optimized for large datasets.
        $ids = $this->yourCustomEntityRepository->searchIds(
            new \Shopware\Core\Framework\DataAbstractionLayer\Criteria()
        )->getIds();

        if (\count($ids) === 0) {
            return;
        }

        // Fetch full data in chunks to manage memory usage.
        $chunks = array_chunk($ids, 500);

        foreach ($chunks as $chunk) {
            $criteria = new \Shopware\Core\Framework\DataAbstractionLayer\Criteria($chunk);
            // Map custom fields if necessary
            $criteria->addAssociations(['translations', 'media']); 
            
            $data = $this->yourCustomEntityRepository->search($criteria)->getEntities();

            // Transform data for the search engine
            $payload = [];
            foreach ($data as $entity) {
                $payload[] = [
                    'id' => $entity->getUniqueIdentifier(),
                    'name' => $entity->getName(),
                    'your_custom_field' => $entity->getCustomField('price_multiplier'),
                    // ... map other fields
                ];
            }

            // Push to search engine via the parent implementation or direct client
            $this->index($payload);
        }
    }

    /**
     * Helper to push data. The AbstractIndexer provides methods for this,
     * but you can also use the SearchDefinition directly in modern implementations.
     */
    private function index(array $data): void
    {
        // Implementation depends on whether you are using the built-in mapping
        // or a custom search definition. Usually, you extend the search service.
        // For standard indexing, the framework handles the dispatch to ES/OS.
    }
}

Step 2: Register as a Service

Ensure your indexer is registered as a tagged service so Shopware discovers it automatically.

services:
  YourVendor\YourExtension\Search\YourCustomProductSearchIndexer:
    tags:
      - name: shopware.indexer

Managing Indexes via CLI

Shopware 6.7 provides powerful command-line tools for index management. These commands are essential for deployment pipelines and troubleshooting.

Incremental Updates

Update the index for specific entities based on database changes. This is fast and should be used after standard deployments.

bin/console indexing:update your_custom_entity product

Full Rebuilds

If you suspect index corruption or need to rebuild the entire schema, use:

bin/console search-index:rebuild your_custom_entity --all

Shopware 6.7 Improvement: The --all flag now runs with better concurrency handling and provides a progress bar that accurately reflects the state of each indexer definition.

Dry Run and Validation

To verify mappings without pushing data, you can inspect the generated SQL and search definitions:

bin/console index-mapping:dump your_custom_entity

Best Practices for 6.7+

  1. Chunking is Mandatory: Never fetch all entities into memory at once. Always use array_chunk or pagination logic within your indexer loop to prevent PHP fatal errors on large catalogs.
  2. Handle Soft Deletes: Ensure your indexer logic accounts for entities that are marked as deleted in the database but still exist in the index. The framework handles this automatically if you follow the standard patterns, but custom queries must exclude active = false records where appropriate.
  3. Use Criteria Efficiently: When fetching data to index, only request fields needed for the search payload. Avoid loading heavy associations like translations unless absolutely necessary for the indexed values. In Shopware 6.7, the fetch optimizer is smarter, but good habits save resources.
  4. Monitor Queue Latency: Since indexing is asynchronous, monitor your queue workers. If your indexer performs heavy transformations, consider making the job async within the queue using Symfony Messenger to avoid timeout issues during bulk operations.
  5. Leverage IndexerService: For programmatic updates triggered by custom business logic (not just CRUD events), inject Shopware\Core\Framework\DataAbstractionLayer\Indexer\IndexerService and call $indexerService->update(['your_custom_entity']).

Conclusion

The Shopware 6.7 indexing system is a testament to the framework's evolution. By decoupling data persistence from search synchronization and optimizing the processing pipeline, Shopware ensures that even complex custom entities can be searched with millisecond latency.

For developers, understanding this system allows you to extend the platform confidently. Whether adding custom fields to products or introducing entirely new searchable resources, the tools provided in 6.7 make integration robust, performant, and maintainable. Master these concepts, and your Shopware store will deliver speed and relevance at any scale.