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:
-
JavaScript intercepts keystroke —
quickSearch.jsdebounces input (default 300ms delay) -
AJAX GET to
/search/ajax/suggest— includes the search term and store scope -
Search controller executes —
Magento\Search\Controller\Ajax\Suggest::execute() -
Search engine query fires — Elasticsearch/OpenSearch gets a
multi_matchquery with fuzzy matching - Product collection loads — Full product objects are loaded for each search result (5-10 products)
- HTML renders server-side — Each product goes through layout XML, block rendering, and template output
- 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
SELECTqueries for product entities - 50-100+
SELECTqueries 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);
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.xmlwith 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
}
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>
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;
}
}
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>
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
}
}
}
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
}
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;
}
}
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>
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]);
}
}
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
}
}
}
};
// 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
}
});
};
});
Additional client-side optimizations:
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.
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.
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>
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"
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_prefixinstead. - [ ] 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:
- Cache autocomplete responses (60-80% of requests become instant)
- Reduce product attribute loading (40-60% reduction in query time)
- Tune Elasticsearch queries (5-10x faster search execution)
- Simplify templates (eliminate unnecessary product data queries)
- 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)