DEV Community

Magevanta
Magevanta

Posted on • Originally published at magevanta.com

Magento 2 Search Autocomplete Performance: Keeping Sub-200ms at Scale

Search autocomplete is the single most resource-intensive interactive feature on a Magento 2 storefront. Every keystroke in the search bar triggers an AJAX request to the backend, which queries the search engine, loads product data, applies permissions, and returns HTML. On a busy store with hundreds of concurrent visitors, autocomplete generates more backend requests than any other page — and almost none of it is cached.

I've seen Magento 2 stores where autocomplete took 800ms to 1.5 seconds per request. On a category page with 200 concurrent users, that's enough to saturate your PHP-FPM pool and bring the entire site to its knees. The frustrating part: most of this overhead is completely unnecessary.

In this post, I'll walk through exactly how Magento 2 autocomplete works under the hood, where the bottlenecks hide, and how to bring response times under 200ms — even on large catalogs.

How Magento 2 Autocomplete Works

When a customer types into the search field, Magento's JavaScript sends an AJAX request to /search/ajax/suggest. Here's the request flow:

  1. JavaScript intercepts keystrokequickSearch.js debounces input (default 300ms delay)
  2. AJAX GET to /search/ajax/suggest — includes the search term and store scope
  3. Search controller executesMagento\Search\Controller\Ajax\Suggest::execute()
  4. Search engine query fires — Elasticsearch/OpenSearch gets a multi_match query with fuzzy matching
  5. Product collection loads — Full product objects are loaded for each search result (5-10 products)
  6. HTML renders server-side — Each product goes through layout XML, block rendering, and template output
  7. Response returns — The HTML fragment is sent back as JSON

The critical issue: steps 4-6 run on every request with zero caching. A typical autocomplete request loads 5-10 full product objects with EAV attributes, generates layout blocks, and renders templates — identical work to what happens on a category page, except it's uncached and fires every 300ms.

Where the Bottlenecks Hide

1. Product Collection Loading (60-70% of response time)

The autocomplete response loads products using a standard collection with EAV. This means:

  • 5-10 SELECT queries for product entities
  • 50-100+ SELECT queries for EAV attribute values (name, price, image, description, etc.)
  • Additional queries for tier prices, stock status, category associations

On a catalog with 50,000+ SKUs, these queries hit buffer pool pressure. Even with indexes, the EAV SELECT queries require joining catalog_product_entity_varchar, catalog_product_entity_decimal, catalog_product_entity_int, and more — one per attribute per product.

Measurement:

-- Enable the general query log temporarily
SET GLOBAL general_log = 'ON';

-- Trigger an autocomplete request, then count queries
SELECT COUNT(*) FROM mysql.general_log 
WHERE command_type = 'Query' 
AND event_time > DATE_SUB(NOW(), INTERVAL 5 SECOND);
Enter fullscreen mode Exit fullscreen mode

A single autocomplete request typically generates 80-150 SQL queries.

2. Layout XML and Block Rendering

Magento renders the autocomplete HTML using a full layout XML pipeline. This means:

  • Loading layout XML handles
  • Merging catalogsearch_ajax_suggest.xml with default handles
  • Instantiating block classes
  • Running toHtml() on each block

The layout merge alone takes 10-30ms. Block rendering adds another 20-50ms depending on template complexity.

3. Search Engine Query Design

The default Elasticsearch query for autocomplete uses multi_match with best_fields type and fuzzy matching. On large catalogs, this is unnecessarily expensive:

{
  "query": {
    "multi_match": {
      "query": "search term",
      "fields": ["name^3", "sku^2", "description"],
      "type": "best_fields",
      "fuzziness": "AUTO"
    }
  },
  "size": 10
}
Enter fullscreen mode Exit fullscreen mode

The fuzziness: AUTO parameter is the biggest performance killer. It generates edit-distance calculations for every term against every document. On a 100,000 SKU catalog, that's a lot of computation per keystroke.

Fix 1: Reduce EAV Attribute Loading

The default autocomplete loads all searchable attributes. You can drastically reduce this by limiting which attributes are loaded:

<!-- app/code/Vendor/Module/view/frontend/layout/catalogsearch_ajax_suggest.xml -->
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceBlock name="search_result_list">
            <arguments>
                <argument name="search_result_list" xsi:type="array">
                    <item name="attributes_to_select" xsi:type="array">
                        <item name="name" xsi:type="string">name</item>
                        <item name="sku" xsi:type="string">sku</item>
                        <item name="price" xsi:type="string">price</item>
                        <item name="thumbnail" xsi:type="string">thumbnail</item>
                        <item name="url_key" xsi:type="string">url_key</item>
                    </item>
                </argument>
            </arguments>
        </referenceBlock>
    </body>
</page>
Enter fullscreen mode Exit fullscreen mode

Or override the product collection directly in a plugin:

// app/code/Vendor/Module/Plugin/SearchProductProviderPlugin.php
namespace Vendor\Module\Plugin;

use Magento\CatalogSearch\Model\Search\ProductProvider;

class SearchProductProviderPlugin
{
    /**
     * Strip heavy attributes from autocomplete product loading
     */
    public function afterGetList(ProductProvider $subject, $result)
    {
        foreach ($result as $product) {
            $product->setData('description', null);
            $product->setData('short_description', null);
            $product->setData('meta_title', null);
            $product->setData('meta_description', null);
            $product->setData('meta_keyword', null);
        }
        return $result;
    }
}
Enter fullscreen mode Exit fullscreen mode

Impact: Reduces SQL queries from 80-150 down to 30-50 per autocomplete request. Response time drops by 40-60%.

Fix 2: Tune the Elasticsearch Query

The default fuzzy query is overkill for autocomplete. Replace it with a more efficient match_phrase_prefix or use a dedicated search-as-you-type field:

Option A: Use match_phrase_prefix Instead of Fuzzy

Override the search request to use prefix matching, which leverages the inverted index directly:

<!-- app/etc/elasticsearch/custom_search_request.xml -->
<requests>
    <request name="quick_search_container" label="Quick Search">
        <queries>
            <query name="quick_search_container" xsi:type="boolQuery">
                <queryReference clause="should" ref="search_query"/>
                <queryReference clause="should" ref="sku_query"/>
            </query>
            <query name="search_query" xsi:type="matchQuery">
                <match field="name" condition="match_phrase_prefix"/>
            </query>
            <query name="sku_query" xsi:type="matchQuery">
                <match field="sku" condition="match"/>
            </query>
        </queries>
    </request>
</requests>
Enter fullscreen mode Exit fullscreen mode

Option B: Use search_as_you_type Field Mapping

Create a custom field mapping that uses Elasticsearch's search_as_you_type field type:

PUT /magento2_product/_mapping
{
  "properties": {
    "name_suggest": {
      "type": "search_as_you_type",
      "max_shingle_size": 3
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Then query it with bool_prefix:

{
  "query": {
    "multi_match": {
      "query": "search term",
      "fields": [
        "name_suggest",
        "name_suggest._2gram",
        "name_suggest._3gram",
        "sku"
      ],
      "type": "bool_prefix"
    }
  },
  "size": 5
}
Enter fullscreen mode Exit fullscreen mode

Impact: search_as_you_type with bool_prefix is 5-10x faster than multi_match with fuzziness: AUTO because it uses pre-computed n-gram indexes instead of runtime edit-distance calculations.

Fix 3: Cache Autocomplete Results

The simplest high-impact optimization: cache autocomplete responses. Most users search for the same popular terms.

// app/code/Vendor/Module/Plugin/AjaxSuggestCachePlugin.php
namespace Vendor\Module\Plugin;

use Magento\Search\Controller\Ajax\Suggest;
use Magento\Framework\App\Response\Http;
use Magento\Framework\App\CacheInterface;

class AjaxSuggestCachePlugin
{
    private const CACHE_TAG = 'search_autocomplete';
    private const CACHE_LIFETIME = 3600; // 1 hour

    public function __construct(
        private CacheInterface $cache
    ) {}

    public function aroundExecute(Suggest $subject, callable $proceed): Http
    {
        $query = $subject->getRequest()->getParam('q');
        $storeId = $subject->getRequest()->getParam('store_id', 0);

        if (empty($query) || strlen($query) < 2) {
            return $proceed();
        }

        $cacheKey = sprintf(
            'autocomplete_%d_%s',
            $storeId,
            md5(strtolower(trim($query)))
        );

        $cached = $this->cache->load($cacheKey);
        if ($cached !== false) {
            $response = $subject->getResponse();
            $response->setHeader('Content-Type', 'application/json');
            $response->setBody($cached);
            $response->setHeader('Cache-Control', 'public, max-age=60');
            $response->setHeader('X-Cache', 'HIT');
            return $response;
        }

        $response = $proceed();

        $this->cache->save(
            $response->getBody(),
            $cacheKey,
            [self::CACHE_TAG],
            self::CACHE_LIFETIME
        );

        $response->setHeader('X-Cache', 'MISS');
        return $response;
    }
}
Enter fullscreen mode Exit fullscreen mode

Register the plugin:

<!-- app/code/Vendor/Module/etc/frontend/di.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Search\Controller\Ajax\Suggest">
        <plugin name="autocomplete_cache" type="Vendor\Module\Plugin\AjaxSuggestCachePlugin" sortOrder="10"/>
    </type>
</config>
Enter fullscreen mode Exit fullscreen mode

Add cache invalidation when products are updated:

// app/code/Vendor/Module/Observer/InvalidateAutocompleteCache.php
namespace Vendor\Module\Observer;

use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
use Magento\Framework\App\CacheInterface;

class InvalidateAutocompleteCache implements ObserverInterface
{
    public function __construct(
        private CacheInterface $cache
    ) {}

    public function execute(Observer $observer)
    {
        $this->cache->clean([\Vendor\Module\Plugin\AjaxSuggestCachePlugin::CACHE_TAG]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Impact: For popular search terms (which cover 80%+ of queries), this eliminates the backend entirely. Response time goes from 200-500ms to under 10ms on cache hit.

Fix 4: Debounce and Client-Side Optimization

The default Magento debounce is 300ms. On fast typists, this still generates a lot of requests. Increase it and add minimum character requirements:

// app/code/Vendor/Module/view/frontend/requirejs-config.js
var config = {
    config: {
        mixins: {
            'Magento_CatalogSearch/js/form-mini': {
                'Vendor_Module/js/search-mixin': true
            }
        }
    }
};
Enter fullscreen mode Exit fullscreen mode
// app/code/Vendor/Module/view/frontend/web/js/search-mixin.js
define([], function () {
    'use strict';

    return function (target) {
        return target.extend({
            defaults: {
                searchDelay: 500,       // Increased from 300ms
                minSearchLength: 3      // Require at least 3 characters
            }
        });
    };
});
Enter fullscreen mode Exit fullscreen mode

Additional client-side optimizations:

  1. Abort previous requests — When a new keystroke fires, abort the in-flight AJAX request. Magento's default implementation does this, but verify it's working.

  2. Cache results in localStorage — For repeat visitors, store recent autocomplete results client-side with a 5-minute TTL. This eliminates server roundtrips entirely for repeated searches.

  3. Show results from cache instantly — Display cached results immediately, then update in the background if the API returns new data.

Fix 5: Limit Result Count and Simplify Templates

The default autocomplete shows 5-10 products. Reducing this to 3-5 cuts rendering time proportionally:

<!-- app/etc/view.xml -->
<vars module="Magento_CatalogSearch">
    <var name="max_query_length">10</var>
</vars>
Enter fullscreen mode Exit fullscreen mode

Strip heavy elements from the autocomplete template. Skip rating summary, tier prices, and swatches — they all trigger additional queries. A simplified template with just image, name, and price eliminates 3-7 SQL queries per product. With 5 products shown, that's 15-35 fewer queries per request.

Measuring the Impact

Use a load testing tool to compare before and after:

# Benchmark with 100 concurrent connections, 1000 requests
ab -n 1000 -c 100 -k -H "Accept: application/json" \
   "https://your-store.com/search/ajax/suggest?q=shirt"
Enter fullscreen mode Exit fullscreen mode

Expected results:

Metric Before After
Avg response time 350-800ms 50-150ms
P95 response time 1.2-2.5s 200-400ms
SQL queries per request 80-150 15-30
Cache hit rate 0% 60-80%

Production Checklist

Before deploying these changes:

  • [ ] Test autocomplete accuracy — Ensure results are still relevant after disabling fuzzy matching. If relevance drops too much, use match_phrase_prefix instead.
  • [ ] Monitor Elasticsearch cluster health — Changing query patterns can shift load. Watch CPU, heap, and query latency.
  • [ ] Set up cache warming — Pre-warm autocomplete cache with top 100-200 search terms from your analytics.
  • [ ] Add cache invalidation — Product updates, stock changes, and price changes should trigger selective cache clearing.
  • [ ] Test with slow connections — Autocomplete UX degrades badly on 3G. Consider showing a loading indicator.
  • [ ] Audit third-party modules — Some search extensions (ElasticSuite, Amasty, Mirasvit) override the default autocomplete. Check their configurations first.

Conclusion

Search autocomplete is one of those features that "just works" in development but falls apart at scale. The combination of uncached EAV loading, expensive fuzzy search queries, and full layout rendering makes every keystroke a mini page load.

The highest-impact fixes, in order:

  1. Cache autocomplete responses (60-80% of requests become instant)
  2. Reduce product attribute loading (40-60% reduction in query time)
  3. Tune Elasticsearch queries (5-10x faster search execution)
  4. Simplify templates (eliminate unnecessary product data queries)
  5. Increase debounce and add client-side caching (reduce request volume by 40-50%)

Combined, these optimizations bring autocomplete from 500-1500ms down to 50-150ms. Your customers get instant suggestions, your server handles 3-5x more concurrent searchers, and your Core Web Vitals scores stop bleeding.

The search bar is your store's front door. Make sure it opens fast.

Top comments (0)