
最近折腾 Magento 2 搜索自动补全功能,踩了几个性能上的大坑,这篇把问题说清楚。
搜索自动补全是 Magento 2 店铺里资源消耗最大的交互功能。用户在搜索框每敲一个字,前端就会发一个 AJAX(异步JavaScript和XML)请求到后端,后端查询搜索引擎、加载商品数据、应用权限检查、最后返回 HTML(超文本标记语言)。在大流量店铺里,几百个并发访客同时打字,自动补全产生的后端请求数量比任何其他页面都多——而且几乎没有任何缓存。
见过一些店铺的自动补全单个请求就要 800ms 到 1.5 秒。在一个分类页面上如果有 200 个并发用户,这点时间足够把 PHP-FPM(PHP FastCGI进程管理器)线程池打满,整站直接趴窝。最气人的是:这些开销大部分完全没必要。
这篇文章详细剖析 Magento 2 自动补全的底层实现、找出瓶颈在哪、以及如何在大型类目下把响应时间压到 200ms 以内。
当用户在搜索框输入时,Magento 的 JavaScript 会向 /search/ajax/suggest 发送 AJAX 请求。整个请求链路是这样的:
quickSearch.js 对输入进行防抖处理(默认 300ms 延迟)/search/ajax/suggest — 携带搜索词和店铺范围参数Magento\Search\Controller\Ajax\Suggest::execute()multi_match 查询关键问题:步骤 4-6 每次请求都完整执行,完全没有缓存。一次典型的自动补全请求要加载 5-10 个带 EAV(实体属性值)属性的完整商品对象,还要生成布局块、渲染模板——这跟分类页面的处理几乎一样,只不过完全没有缓存,而且每隔 300ms 就触发一次。
自动补全响应使用标准的 EAV 集合来加载商品。这意味着:
SELECT 查询SELECT 查询(名称、价格、图片、描述等)在一个有 5 万+ SKU 的类目上,这些查询会对缓冲池造成压力。即使有索引,EAV 的 SELECT 查询也要关联 catalog_product_entity_varchar、catalog_product_entity_decimal、catalog_product_entity_int 等多张表——每个属性每个商品都要单独查一次。
实测方法:
-- 临时开启通用查询日志
SET GLOBAL general_log = 'ON'; -- 触发一次自动补全请求,然后统计查询数量
SELECT COUNT(*) FROM mysql.general_log WHERE command_type = 'Query' AND event_time > DATE_SUB(NOW(), INTERVAL 5 SECOND);
单次自动补全请求通常产生 80-150 条 SQL(结构化查询语言)语句。
Magento 使用完整的布局 XML 管道来渲染自动补全 HTML。这意味着:
catalogsearch_ajax_suggest.xml 与默认 handletoHtml()仅布局合并就要 10-30ms。根据模板复杂度,块渲染再加 20-50ms。
默认的 Elasticsearch 自动补全查询使用 multi_match,类型是 best_fields,还带模糊匹配。在大型类目上,这完全没必要这么重:
{ "query": { "multi_match": { "query": "search term", "fields": ["name^3", "sku^2", "description"], "type": "best_fields", "fuzziness": "AUTO" } }, "size": 10
}
fuzziness: AUTO 参数是最大的性能杀手。它要对每个词在每个文档上做编辑距离计算。在一个 10 万 SKU 的类目上,每次按键都要做大量计算。
默认的自动补全加载所有可搜索属性。通过限制加载哪些属性,可以大幅减少查询:
<!-- 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>
或者直接在插件里覆盖商品集合:
// 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; }
}
效果: 单次自动补全请求的 SQL 查询从 80-150 条降到 30-50 条,响应时间缩短 40-60%。
默认的模糊查询对自动补全来说太重了。换成一个更高效的 match_phrase_prefix,或者使用专门为"边输入边搜索"设计的字段类型。
覆盖搜索请求,改用前缀匹配,这样可以直接利用倒排索引:
<!-- 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>
创建一个使用 Elasticsearch search_as_you_type 字段类型的自定义映射:
PUT /magento2_product/_mapping
{ "properties": { "name_suggest": { "type": "search_as_you_type", "max_shingle_size": 3 } }
}
然后用 bool_prefix 查询:
{ "query": { "multi_match": { "query": "search term", "fields": [ "name_suggest", "name_suggest._2gram", "name_suggest._3gram", "sku" ], "type": "bool_prefix" } }, "size": 5
}
效果: search_as_you_type 配合 bool_prefix 比 multi_match 配 fuzziness: AUTO 快 5-10 倍,因为它直接使用预计算好的 n-gram 索引,而不是运行时计算编辑距离。
最简单的高收益优化:缓存自动补全响应。大多数用户搜索的热门词就那么些。
// 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; }
}
注册这个插件:
<!-- 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>
商品更新时添加缓存失效逻辑:
// 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