Configuring Elasticsearch/OpenSearch in Shopware 6.7 correctly, including the .env variable that prevents a silent MySQL fallback, shard planning you only get to do once, and where relevance tuning actually happens.
Past roughly 10,000 SKUs, MySQL-backed search and filtering stop keeping up: listing pages time out, facets get slow under load, and relevance is a coin flip. OpenSearch fixes all three, but Shopware ships three ways for the setup to fail quietly. Skip SHOPWARE_ES_THROW_EXCEPTION and search silently falls back to MySQL with no error on the storefront. Pick a shard count without thinking and you're stuck with it until a full reindex. Tune relevance in code when the admin already has a settings screen for it, and you've built something nobody on your team can maintain.
Everything below is checked against a real Shopware 6.7.1.2 install, not paraphrased documentation: the actual environment variables, the actual fallback logic in ElasticsearchHelper, the actual CLI commands Shopware ships, and the actual database table that drives relevance.
The Composer package is still called shopware/elasticsearch, and most of the admin UI and documentation still says "Elasticsearch." But the PHP client behind it is opensearch-project/opensearch-php, and the connection variable is OPENSEARCH_URL, not ELASTICSEARCH_URL. If you're provisioning infrastructure, you're standing up an OpenSearch cluster. The naming in Shopware's own codebase just hasn't caught up.
This matters for two practical reasons: don't pay for a licensed Elastic Stack deployment you don't need, and when you search for troubleshooting help, search both terms, because Shopware's error messages, log lines, and community threads use them interchangeably.
Set these in .env.local. Every value here is a real default pulled from Shopware's own elasticsearch.yaml, not an approximation:
# .env.local
SHOPWARE_ES_ENABLED=1
SHOPWARE_ES_INDEXING_ENABLED=1
SHOPWARE_ES_INDEXING_BATCH_SIZE=100
OPENSEARCH_URL=http://opensearch:9200
SHOPWARE_ES_INDEX_PREFIX=sw
SHOPWARE_ES_THROW_EXCEPTION=1
SHOPWARE_ES_INDEXING_BATCH_SIZE defaults to 100 documents per bulk request. Raise it on a large catalog with headroom on the cluster, lower it if bulk indexing requests are timing out.
Shopware's default index_settings for the product index ships with number_of_shards and number_of_replicas set to null, meaning OpenSearch's own defaults apply unless you override them. Do that in config/packages/elasticsearch.yaml before the first index build (see the shard planning section below for why this can't wait):
# config/packages/elasticsearch.yaml
shopware:
elasticsearch:
index_settings:
number_of_shards: 3
number_of_replicas: 1
bin/console es:create:alias
bin/console es:index
bin/console es:status
es:status is a real, shipped Shopware command. It reports cluster health and whether indexing is current. Don't write a custom health check for this, it already exists.
Here's the exact mechanism, from ElasticsearchHelper::logAndThrowException() in the shipped source:
public function logAndThrowException(\Throwable $exception): bool
{
$this->logger->critical($exception->getMessage());
if ($this->environment === 'test' || $this->throwException) {
throw $exception;
}
return false;
}
Unless throwException is true, a failed OpenSearch connection or query logs a single critical line and returns false. Search then quietly serves MySQL results instead. No error page, no 500, nothing a customer would notice or a synthetic uptime check would catch. Facets get slower, relevance gets worse, and the only trace is a log entry that most teams aren't alerting on.
Why this bites real shops: the packaged default for SHOPWARE_ES_THROW_EXCEPTION is "1", but deploy templates carried over from a 6.4 or 6.5 install, or an infrastructure team's environment config, routinely drop or blank out env vars they don't recognize. Set it explicitly in every environment's .env. Don't rely on the packaged default surviving your deployment pipeline.
Catch it two ways: run bin/console es:status as part of your deployment or monitoring checks, and alert on critical-level log entries from the elasticsearch channel. Both are cheaper than finding out during a traffic spike that search has been degraded for a week.
Shard count is fixed the moment an index is created. Changing it means building a new index with the new setting and swapping the alias, which is a full reindex, not a config reload. Shopware's own admin search index ships with number_of_shards: 3 and number_of_replicas: 3 hardcoded, a useful reference point since that index handles a much smaller, more predictable dataset than a large product catalog.
Rough sizing guidance, not a substitute for load testing your own catalog:
Over-sharding a small catalog is a common overcorrection. Each shard is its own Lucene index with its own overhead. Ten shards on 20,000 products slows queries down instead of speeding them up.
Product search relevance in 6.7 is driven by data, not code. The product_search_config_field table stores a field, tokenize, searchable, and ranking value for every searchable field, and it's the same configuration the query builder reads at search time.
Edit it through Settings → Search in the administration, under the Searchable Content tab. Raise the ranking on name or a custom SKU field if product names or codes aren't surfacing first, or adjust tokenize if partial matches feel too loose or too strict. This is where relevance tuning belongs for the vast majority of shops, no deployment required, and any admin user with permission can adjust it.
You also probably don't need custom analyzers. Shopware ships sw_english_analyzer, sw_german_analyzer, sw_ngram_analyzer, and a sw_lowercase_normalizer, wired up automatically per storefront language via language_analyzer_mapping. Writing your own analyzer from scratch is solving a problem Shopware already solved for standard multi-language storefronts.
The escape hatch, for cases the ranking table genuinely can't express, is decorating AbstractProductSearchQueryBuilder and adjusting the query directly. Reach for this only after the admin settings have been tried and found insufficient, custom scoring logic, boosting by a field that changes per sales channel, that kind of thing:
class CustomProductSearchQueryBuilder extends AbstractProductSearchQueryBuilder
{
public function __construct(
private readonly AbstractProductSearchQueryBuilder $decorated
) {}
public function getDecorated(): AbstractProductSearchQueryBuilder
{
return $this->decorated;
}
public function build(Criteria $criteria, Context $context): BuilderInterface
{
$query = $this->decorated->build($criteria, $context);
// Custom scoring logic on top of the default query
return $query;
}
}
A shard change, a mapping change, or a corrupted index all need a rebuild. Shopware's alias-based indexing means the storefront keeps serving the old index while the new one builds:
# Build a fresh index behind a new alias
bin/console es:create:alias
# Populate it, --no-queue for a synchronous run, --only to limit entities
bin/console es:index --no-queue --only=product
# Update field mappings if the schema changed
bin/console es:mapping:update
# Once the new alias is verified, drop the old indices
bin/console es:index:cleanup --force
Run es:status between the indexing and cleanup steps. Don't run es:index:cleanup until you've confirmed the new index is actually complete and serving correctly, it deletes the old one.
The problem: A shop crossing 60,000 SKUs is running MySQL-backed search. Category listing pages with active filters take several seconds to render under normal traffic, and time out entirely during promotions.
The fix: Enable OpenSearch, size shards for the catalog up front (3 shards at this scale), and let Shopware's built-in analyzers handle the two storefront languages. Listing and filtering move off MySQL entirely, and the database goes back to handling orders and carts.
The problem: A hosting migration reset environment variables, and SHOPWARE_ES_THROW_EXCEPTION silently dropped out. Search kept working, just against MySQL, slower and with worse relevance. Nobody caught it until conversion on search-driven sessions started sliding during the traffic ramp into Black Friday.
The fix: Add es:status to the post-deploy checklist and alert on critical log entries from the elasticsearch channel. A silent fallback should show up in monitoring the same hour it happens, not three weeks later in a conversion report.
The problem: A DE storefront's product names carry more search weight in customer behavior than the SKU field, but the default ranking treats them close to equally, so exact SKU searches got buried under loosely related name matches.
The fix: No code change. Raise the ranking value on the SKU field in Settings → Search, Searchable Content, and confirm sw_german_analyzer is mapped for the DE storefront. Reindex, verify, done.
OpenSearch in Shopware 6.7 fails quietly by default. Set SHOPWARE_ES_THROW_EXCEPTION explicitly, decide your shard count before the first index build, and tune relevance in the admin before you reach for a decorator. Get those three right and search stops being the thing that breaks without telling you.
Written by Huzaifa Mustafa
Explore more Shopware development topics:
Whether you're setting up OpenSearch for the first time or diagnosing a fallback nobody caught, get help from a Certified Shopware Developer.
Talk to an Engineer