Adding custom data sources and keeping performance in check during fast typing
Autocomplete suggestions in the search box look like a pure frontend feature, but they are assembled entirely server side from a set of data providers. This article explains how those providers work together, how to add a custom source such as CMS pages, and which levers actually determine how smooth suggestions feel while a customer is typing quickly.
Table of Contents
- 1. Why autocomplete is not purely a frontend concern
- 2. The data provider architecture at a glance
- 3. Implementing a custom provider for CMS pages
- 4. The GraphQL path for storefronts without a classic autocomplete block
- 5. Server side limits and caching for suggestions
- 6. Weighing server side against client side debounce
- 7. Multiple providers inside a shared performance budget
- 8. Measuring and monitoring autocomplete response time
- 9. Edge cases and the boundary with full text search
- 10. Summary
- 11. FAQ
1. Why autocomplete is not purely a frontend concern
Anyone who thinks of autocomplete primarily in terms of debounce timers and JavaScript is missing the actual work: Magento already assembles and sorts the suggestion list on the server, and the frontend simply renders whatever it receives. The interesting levers, which data sources feed in, how many results per source, and in what order, all live on the backend.
That clearly separates this topic from layered navigation or general full text relevance tuning, which are covered elsewhere. Autocomplete runs through its own, leaner request path that deliberately skips the full search index with all its aggregations and is instead optimized purely for response speed.
That speed optimization is exactly what makes the topic relevant for backend developers: every additional data source, every additional field in the result list costs milliseconds that add up again on every single keystroke.
2. The data provider architecture at a glance
The central entry point is Magento\Search\Model\Autocomplete, which iterates over a configured list of data providers and merges their results into one shared suggestion list. Each provider implements DataProviderInterface with a single method, getItems, returning a list of ItemInterface objects, typically carrying a title and a target URL.
Out of the box, two providers are active: one for product suggestions based on the search index, and one for popular or recently used search terms based on the search_query table. Categories and CMS pages are deliberately not part of the default set, so anyone wanting that content in the suggestions has to add it through a custom provider.
The order of providers in the configuration directly determines the order in the suggestion list, a frequently overlooked lever for, say, showing product suggestions ahead of popular search terms on purpose.
<type name="Magento\Search\Model\Autocomplete">
<arguments>
<argument name="dataProviders" xsi:type="array">
<item name="popularQueries" xsi:type="object">Magento\Search\Model\Autocomplete\DataProvider</item>
<item name="products" xsi:type="object">Magento\CatalogSearch\Model\Autocomplete\DataProvider</item>
<item name="cmsPages" xsi:type="object">Mironsoft\SearchAutocomplete\Model\Autocomplete\CmsPageDataProvider</item>
</argument>
</arguments>
</type>
3. Implementing a custom provider for CMS pages
A custom provider for CMS pages follows the same pattern as the built in ones: getItems reads the current search term, filters a CMS page collection by title and returns a bounded number of matches as ItemInterface objects. It is important to explicitly restrict the collection to active pages visible on the current store, since unlike products, CMS pages have no per customer group visibility attribute of their own.
The result count should stay small, three to five matches are entirely sufficient for a suggestion list. An unfiltered LIKE query across the full CMS page title index can become noticeably slower than the already index backed product search as page count grows, so a simple title index on its own column pays off once more than a few hundred pages are maintained.
<?php
declare(strict_types=1);
namespace Mironsoft\SearchAutocomplete\Model\Autocomplete;
use Magento\Cms\Model\ResourceModel\Page\CollectionFactory;
use Magento\Search\Model\Autocomplete\DataProviderInterface;
use Magento\Search\Model\Autocomplete\ItemFactory;
use Magento\Search\Model\QueryFactory;
/**
* Provides CMS pages as an additional data source for the autocomplete suggestion list.
*/
class CmsPageDataProvider implements DataProviderInterface
{
private const RESULT_LIMIT = 4;
/**
* @param CollectionFactory $collectionFactory
* @param ItemFactory $itemFactory
* @param QueryFactory $queryFactory
*/
public function __construct(
private readonly CollectionFactory $collectionFactory,
private readonly ItemFactory $itemFactory,
private readonly QueryFactory $queryFactory,
) {
}
/**
* Looks up active CMS pages matching the current search term and returns them as suggestion items.
*
* @return \Magento\Search\Model\Autocomplete\ItemInterface[]
*/
public function getItems(): array
{
$queryText = $this->queryFactory->get()->getQueryText();
$collection = $this->collectionFactory->create();
$collection->addFieldToFilter('is_active', ['eq' => 1])
->addFieldToFilter('title', ['like' => '%' . $queryText . '%'])
->setPageSize(self::RESULT_LIMIT);
$items = [];
foreach ($collection as $page) {
$items[] = $this->itemFactory->create(['title' => $page->getTitle()]);
}
return $items;
}
}
4. The GraphQL path for storefronts without a classic autocomplete block
Storefronts built consistently on GraphQL do not fetch suggestions through the REST leaning autocomplete controller, but through a products query with a reduced field set, complemented by a dedicated, lean query for CMS suggestions. That approach makes it possible to bundle product and CMS suggestions into a single request, instead of firing two separate requests on every keystroke.
It is important to only request the fields the suggestion list actually needs in the GraphQL query, meaning name, url_key and a small image, rather than the full product data with every attribute. Every additional field increases resolver time on the server, which is immediately noticeable for a feature that reacts to every single keystroke.
query AutocompleteSuggestions($search: String!) {
products(search: $search, pageSize: 5) {
items {
name
url_key
small_image {
url
}
}
}
}
5. Server side limits and caching for suggestions
The number of suggestions per request is controlled through the configuration path catalog/search/autocomplete_limit and directly affects response time, since every additional match means an additional database or index operation. A limit of five to eight results is sufficient for most stores in practice and noticeably faster than twenty or more matches.
Since the customer account runs through private content but autocomplete requests mostly return the same data regardless of login state, a brief, few seconds long HTTP cache for the response at the full page cache layer or an upstream reverse proxy is worth it, especially for the most common search terms, which stay nearly identical anyway.
6. Weighing server side against client side debounce
Client side debounce, meaning delaying the request by typically two hundred to three hundred milliseconds after the last keystroke, reduces how many requests even reach the server. That is the single most effective lever, because every avoided request saves its server time entirely, while server side optimizations only make the remaining requests faster, not fewer.
On the server side, a simple per session request throttle helps as a complement, for example a short cache key built from search term and store that catches identical, closely spaced requests before they hit the data sources again. That guards against edge cases such as duplicated events or users typing the same term twice in quick succession, for instance through copy paste.
In practice, a combination of moderate client side debounce and a tight server side limit is far more effective than trying to make every single request faster purely through backend optimization, since the fastest request is always the one that never gets made in the first place.
7. Multiple providers inside a shared performance budget
Once several data providers run in parallel, say products, popular search terms and a custom CMS source, their individual times add up inside Autocomplete::getItems, since providers are queried sequentially by default. A single slow provider therefore delays the entire response, even if the remaining providers would have been done long ago.
For custom providers, a deliberate time budget is worth building in, for example a hard cap on the collection query via setPageSize and an index instead of a LIKE query on the search field. Anyone adding several extra providers should regularly measure their combined runtime against the original response time with only the default providers active, to catch gradual slowdowns early.
8. Measuring and monitoring autocomplete response time
Without measurement, any claim about perceived speed stays speculative. A simple approach is a timing log built directly into Autocomplete that records each provider's runtime separately, instead of only capturing the total request time. That makes it immediately obvious which provider is actually responsible for a slowdown.
For production, it is worth forwarding these values to existing monitoring such as New Relic or a self hosted APM tool, with a warning threshold for the total autocomplete response time. Experience suggests that threshold should sit well under two hundred milliseconds, since anything above that gets perceived as noticeable lag during fast typing.
9. Edge cases and the boundary with full text search
Very short search terms of one or two characters should, in most cases, not trigger a request to the providers at all, since the result set is too unspecific to yield useful suggestions anyway while still generating unnecessary server load. A minimum length of three characters, configurable client side, has proven effective in practice.
The boundary with the actual results page after submitting a search remains important: autocomplete deliberately delivers a reduced, fast preview, while the full results page with filters, facets and complete relevance scoring runs through the regular search index. That separation should be preserved through every extension of the autocomplete backend, so the suggestion field's speed does not get burdened with functionality that really belongs on the results page.
| Data source | Active by default? | Typical latency | Recommended limit |
|---|---|---|---|
| Products (catalogsearch) | Yes | low, index backed | 5 to 8 matches |
| Popular search terms | Yes | very low | 3 to 5 matches |
| CMS pages (custom provider) | No | medium without index | 3 to 4 matches |
| Categories (custom provider) | No | low, index backed | 3 matches |
| Customer specific suggestions | No | high without caching | not recommended without caching |
Mironsoft
Magento development, module consulting, and system architecture
A Magento project that needs a second opinion or experienced execution?
We build custom Magento modules, advise on architecture decisions, and take on complex implementations, from service contract planning to production-ready deployment.
Architecture Consulting
Have module and system architecture thought through properly before you build.
Custom Module Development
Build custom Magento modules cleanly, following best practices.
Code Review & Audit
Have existing modules reviewed for performance, security, and maintainability.
10. Summary
Autocomplete backend
Architecture
DataProviderInterface implementations return items that Autocomplete::getItems merges into one shared list.
Extension
Custom providers such as CMS pages get added through di.xml and follow the same interface as the default providers.
Performance
Client side debounce eliminates requests entirely, server side limits and indexing speed up the ones that remain.
Monitoring
Per provider timing logs surface slow data sources before they turn into noticeable lag.