Elasticsearch/OpenSearch Performance Tuning for Magento
AI generated
60fps
ms
Performance · Elasticsearch · OpenSearch · Magento 2
Elasticsearch/OpenSearch Performance Tuning for Magento
Getting index mapping, shards, and JVM heap right

A misconfigured Elasticsearch or OpenSearch installation noticeably slows down every category page and every faceted search in Magento. This article shows how index mapping, shard sizes, JVM heap, and query patterns determine actual response time, how to find bottlenecks with the Explain API and cluster health checks, and how to avoid expensive wildcard and fuzzy queries in layered navigation.

16 min. read Index Mapping · Shards · JVM Heap Explain API · Cluster Health · Slow Log

1. Why search performance directly determines conversion

Every category page, every faceted search, and every autosuggest request in a Magento store running Elasticsearch or OpenSearch goes through the same cluster, and that cluster becomes an invisible bottleneck as the product catalog grows. Unlike a slow MySQL query, which can usually be traced to a single missing index, search performance problems emerge from the interplay of several factors: incorrect index mapping, too many or too few shards, an undersized JVM heap, and expensive query types that only become noticeable under load.

The tricky part: on a development system with a few hundred products, practically any configuration runs fast enough. The weaknesses only surface at realistic catalog scale, under concurrent user load, and during regular reindexing, usually exactly when the store can least afford downtime under load, such as during a sale. Understanding the mechanics behind mapping, sharding, heap, and query execution lets you catch bottlenecks in advance instead of debugging them in production.

2. Index mapping and its impact on query speed

Index mapping determines how each field is stored, analyzed, and searched, and that decision directly affects query speed. The most common mistake in Magento catalogs: attributes used only for filters (facets) get mapped as text instead of keyword. text fields go through an analyzer with tokenizing, lowercasing, and stemming, which makes sense for full-text search but creates unnecessary overhead for exact filter values like color or size, and provokes wildcard-style workarounds.

Equally important: disable doc_values for fields that are never sorted or aggregated, and set index: false for fields that only serve display purposes and are never searched, such as internal SKUs or free-text notes. Every unnecessarily indexed field bloats the index, slows down refresh cycles, and increases the memory footprint of the filesystem cache. A clean mapping is therefore the most effective lever, even ahead of hardware upgrades, because it reduces the amount of data the cluster actually has to process per query.


{
  "mappings": {
    "properties": {
      "sku": { "type": "keyword" },
      "name": {
        "type": "text",
        "analyzer": "standard",
        "fields": {
          "keyword": { "type": "keyword", "ignore_above": 256 }
        }
      },
      "color": { "type": "keyword", "doc_values": true },
      "internal_note": { "type": "text", "index": false },
      "price": { "type": "scaled_float", "scaling_factor": 100 },
      "created_at": { "type": "date", "doc_values": true },
      "description": { "type": "text", "index_options": "freqs" }
    }
  }
}

3. Shard sizing for growing product catalogs

Shard sizing determines how an index is distributed across the cluster, and it directly affects parallelization and overhead. The common rule of thumb for Magento catalogs: a shard should hold between 10 and 50 GB of data, never less. Too many small shards, a classic mistake in "more is better" configurations, create unnecessary coordination overhead, since every query has to be executed against every shard individually and the results then merged.

For most Magento installations with a catalog under 5 million products, one to three primary shards per store-view index are entirely sufficient. The number of replicas should follow read frequency, not availability alone: each replica effectively doubles storage requirements, but also increases parallel query capacity. Changing the primary shard count after the fact always requires a full reindex into a new index followed by an alias swap, since the shard count cannot be changed once an index has been created.


# Create index with tuned shard/replica count for catalog scale
curl -X PUT "http://localhost:9200/catalogsearch_v2" -H 'Content-Type: application/json' -d '
{
  "settings": {
    "number_of_shards": 2,
    "number_of_replicas": 1,
    "refresh_interval": "30s"
  }
}'

# Verify shard distribution and index size at a glance
curl -s "http://localhost:9200/_cat/indices?v&h=index,pri,rep,docs.count,store.size"

# Reindex into the new index, then swap the alias atomically
curl -X POST "http://localhost:9200/_reindex" -H 'Content-Type: application/json' -d '
{
  "source": { "index": "catalogsearch_v1" },
  "dest": { "index": "catalogsearch_v2" }
}'

curl -X POST "http://localhost:9200/_aliases" -H 'Content-Type: application/json' -d '
{
  "actions": [
    { "remove": { "index": "catalogsearch_v1", "alias": "catalogsearch" } },
    { "add": { "index": "catalogsearch_v2", "alias": "catalogsearch" } }
  ]
}'

4. JVM heap tuning without garbage collection pauses

The JVM heap is the second most common cause of erratic response times. The basic rule: reserve at most 50% of available RAM for the heap, leaving the rest to the operating system for the filesystem cache that Lucene segments are read through. An oversized heap is paradoxically harmful, because it lengthens garbage collection pauses and leaves the operating system less memory for caching index files, and both effects cause noticeable latency spikes.

A hard ceiling sits around 30 to 32 GB of heap: above that threshold, the JVM loses "compressed ordinary object pointers" (compressed OOPs), which makes every object pointer take twice as much memory and shrinks the effectively usable heap despite more RAM being available. -Xms and -Xmx should always be set to the same value, so the JVM never resizes the heap at runtime, which causes even brief stop-the-world pauses. For garbage collection, the G1GC collector is recommended, the default since Java 9, which works with predictable pause times.


## config/jvm.options.d/heap.options
## Heap size must be identical for Xms and Xmx to avoid runtime resizing
-Xms8g
-Xmx8g

## Stay below ~30GB to keep compressed ordinary object pointers enabled
## Use G1GC for predictable, shorter garbage collection pauses
-XX:+UseG1GC
-XX:G1ReservePercent=25
-XX:InitiatingHeapOccupancyPercent=30

## Dump the heap on OutOfMemoryError for post-mortem analysis
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/elasticsearch/heapdump.hprof

5. Query profiling with the Explain API

The Explain API shows, for a single query and a single document, exactly how the relevance score is computed and which query clauses cost how much processing time. Instead of guessing why a faceted search is slow, GET /index/_explain/{doc_id} returns a detailed breakdown of every sub-calculation, indispensable when a custom score plugin or a complex bool query produces unexpected results.

For pure performance analysis, the Profile API ("profile": true in the request body) is often more informative than Explain, because it measures the actual execution time of every query component per shard, instead of just explaining the score calculation. This makes it possible to pinpoint precisely whether a slow layered navigation request is caused by an expensive aggregation, an inefficient filter, or network overhead between coordinator and data nodes. Neither API should ever run against live production traffic, since they generate noticeable overhead themselves; they belong in staging or targeted debugging sessions.

6. Avoiding expensive wildcard and fuzzy queries in layered navigation

Wildcard and fuzzy queries are the most common cause of sudden latency spikes in layered navigation. A wildcard query with a leading asterisk like *shoe* cannot be resolved through the inverted index by Lucene and instead forces a scan across every term in the field; with high cardinality, such as free-text attributes, this quickly turns into a linear scan across the entire dataset.

The solution almost always lies in the indexing phase rather than the query phase: an edge_ngram analyzer already generates all meaningful prefixes of a term at index time, so a simple match query is sufficient at search time instead of running an expensive wildcard query. The same applies to fuzzy queries: fuzziness: "AUTO" instead of a fixed edit-distance value automatically caps computation cost based on word length, and fuzzy search should run exclusively on the actual search field, never as a global fallback across every field at once.


<?php

declare(strict_types=1);

namespace Mironsoft\SearchPerformance\Plugin;

use Magento\Elasticsearch\SearchAdapter\QueryContainer;

/**
 * Replaces expensive wildcard filters with match queries against
 * pre-indexed edge-ngram fields for layered navigation search.
 */
class AvoidWildcardQueryPlugin
{
    /**
     * Rewrites a wildcard-style clause into a match query against
     * the "_prefix" sub-field populated by an edge_ngram analyzer.
     *
     * @param QueryContainer $subject
     * @param array $result
     * @return array
     */
    public function afterGetQuery(QueryContainer $subject, array $result): array
    {
        // Detect leading-wildcard patterns injected by legacy filter code
        if (isset($result['wildcard'])) {
            foreach ($result['wildcard'] as $field => $clause) {
                $value = str_replace('*', '', (string) $clause['value']);

                // Route to the pre-indexed prefix field instead of a full scan
                $result['match'][$field . '_prefix'] = ['query' => $value];
                unset($result['wildcard'][$field]);
            }
        }

        return $result;
    }
}

7. Magento-specific search engine configuration

Magento configures the search engine via Stores > Configuration > Catalog > Catalog Search and, for deeper changes, via di.xml preferences in the Magento_Elasticsearch7 module, or Magento_Elasticsearch8 for OpenSearch-compatible setups. The elasticsearch7_server_hostname, _port, and _index_prefix values live in env.php and should be maintained separately per environment, so staging reindex runs never accidentally hit the production index.

For custom query logic, Magento offers SearchAdapterInterface extension points as well as plugins on Magento\Elasticsearch\SearchAdapter\QueryContainer to add extra filters or boosting rules without replacing the core adapter. Important for performance: bin/magento indexer:reindex catalogsearch_fulltext should run in batch mode with an adjusted batch_size for large catalogs, since the default of 100 documents per bulk request becomes a bottleneck with millions of products; raising it to 1000-5000 drastically cuts the number of HTTP roundtrips.


<?xml version="1.0"?>
<!-- app/code/Mironsoft/SearchPerformance/etc/di.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <!-- Increase bulk batch size to reduce HTTP roundtrips on large catalogs -->
    <type name="Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductFieldsProvider">
        <arguments>
            <argument name="batchSize" xsi:type="number">2000</argument>
        </arguments>
    </type>

    <!-- Route wildcard cleanup through a dedicated plugin -->
    <type name="Magento\Elasticsearch\SearchAdapter\QueryContainer">
        <plugin name="mironsoft_avoid_wildcard_query"
                type="Mironsoft\SearchPerformance\Plugin\AvoidWildcardQueryPlugin"
                sortOrder="10"/>
    </type>

</config>

8. Monitoring cluster health: _cluster/health, _cat/indices, slow log

The fastest health check is GET _cluster/health?pretty: a status of green means every shard, including replicas, is assigned; yellow means primary shards are available but at least one replica is missing; and red means at least one primary shard is unreachable, a state in which search returns incomplete or no results at all. A persistently yellow cluster isn't an acute emergency, but it is a warning sign of missing capacity or poorly distributed nodes.

GET _cat/indices?v&h=index,docs.count,store.size,pri,rep shows document count, storage size, and shard distribution for every index at a glance, ideal for identifying orphaned old indices left behind by failed reindex runs. The slow log records queries that exceed a configurable threshold, split by query and fetch phase, and is the most reliable way to identify recurring expensive query patterns in production, instead of only noticing them through user complaints.

9. Query patterns compared side by side

The following query patterns show up in nearly every Magento search index, and their actual execution cost often differs by orders of magnitude. The table below shows which pattern stays performant in practice and which one becomes a problem as the catalog grows.

Area Recommended pattern (good) Expensive pattern (mistake) Recommended optimization
Prefix search match on edge_ngram field wildcard: "*shoe*" Create an edge-ngram field at index time
Imprecise input fuzziness: "AUTO" fuzzy with a fixed edit-distance value Limit fuzziness to the search field
Attribute filter term/terms on keyword field wildcard on an analyzed text field Map facet attributes as keyword
Sort by formula precomputed sort field script_score on every request Precompute the value at index time
Aggregation size size capped (e.g. 50) unbounded terms aggregation Tune size and shard_size deliberately

What stands out is that almost every expensive pattern has an equivalent, index-based alternative; the trick is usually to shift computational cost from search time to index time, where it's incurred once instead of on every request. Consistently avoiding these five patterns in a catalog often cuts average query latency by more than half.

Mironsoft

Performance tuning, Elasticsearch/OpenSearch, and search optimization for Magento stores

Ready to professionally tune your Elasticsearch or OpenSearch cluster?

We analyze your Magento search cluster's index mapping, shard distribution, JVM heap, and query patterns, identify the concrete bottlenecks, and implement targeted optimizations, from mapping corrections to slow-log-based query analysis.

Cluster health audit

Mapping, shard, and heap analysis with a prioritized action plan

Query profiling

Explain and Profile API analysis of expensive layered navigation queries

Monitoring setup

Slow log configuration and cluster health alerts in operational monitoring

10. Summary

Elasticsearch/OpenSearch performance tuning for Magento solves a recurring problem: slow faceted search and category pages despite adequately sized hardware. A clean index mapping with keyword fields for filter attributes reduces the amount of data processed per query from the start. Shard sizing between 10 and 50 GB per shard avoids unnecessary coordination overhead, while a JVM heap capped at 50% of RAM and below the 32 GB compressed-OOPs threshold keeps garbage collection pauses under control.

The Explain and Profile APIs provide the diagnostic data needed to replace expensive query patterns, such as leading wildcards or unbounded fuzzy searches, with index-based alternatives like edge-ngram analyzers. Continuous monitoring via _cluster/health, _cat/indices, and the slow log ensures that regressions after reindex runs or catalog growth surface early, instead of only becoming visible through noticeable timeouts in production.

Elasticsearch/OpenSearch Performance Tuning - The Essentials at a Glance

Mapping

keyword instead of text for facet attributes, enable doc_values and index only where needed.

Shards & heap

10-50 GB per shard, heap capped at 50% of RAM and below 32 GB due to compressed OOPs.

Query profiling

Explain and Profile API for targeted diagnosis, never running permanently in production.

Monitoring

Continuously watch _cluster/health, _cat/indices, and the slow log.

11. FAQ: Elasticsearch/OpenSearch Performance Tuning for Magento

1Why does keyword matter over text for filter attributes?
keyword stores the exact value without analyzer overhead and enables fast term filters and aggregations. text is meant for full-text search and creates unnecessary computation for filter values.
2How many shards does a Magento catalog need?
Usually 1 to 3 primary shards per index for catalogs under 5 million products, targeting 10 to 50 GB of data per shard. More shards only add coordination overhead.
3How large should the JVM heap be?
At most 50% of available RAM, never above roughly 30 to 32 GB due to the loss of compressed OOPs. Always set -Xms and -Xmx to the same value.
4What exactly does the Explain API show?
How a query's relevance score for a document is composed from the individual clauses, ideal for debugging unexpected sort order or boosting mistakes.
5Why are leading-wildcard queries so expensive?
They cannot be resolved through the inverted index and force a scan across every term in the field, leading to linear search time at high cardinality.
6What does a yellow cluster status mean?
All primary shards are available, but at least one replica is missing. Search still works, but fault tolerance and read capacity are reduced.
7What is the slow log useful for?
It logs queries above a threshold, split by query and fetch phase, and surfaces recurring expensive query patterns in production.
8What is the difference between Elasticsearch and OpenSearch for Magento?
Magento supports OpenSearch through the Elasticsearch-compatible adapter starting with 2.4.x. The API is largely identical, but licensing and feature development increasingly diverge.
9How often should reindexing happen and does it affect performance?
Live reindexing via the standard indexer runs incrementally. Run full reindex jobs outside peak hours with an increased batch_size to minimize cluster load.
10Is a single node enough for a production Magento store?
Technically yes for small catalogs, but there's no fault tolerance without replica shards. At medium scale, at least a two- to three-node cluster with replicas is recommended.