给外包客户做搜索框自动补全,被配额打回原形
上周接了个小单:客户是个卖本地特产的小店,想在自家静态页面上加一个搜索框,输入「红」就联想出「红糖糍粑」「红薯干」。页面是纯静态托管在 Cloudflare Pages 上的,后端只有我给他写的一个 Cloud Run 小接口,数据量说出来有点丢人——商品总共 400 多条,存在一张 BigQuery 小表里。
我的第一版做法很直接:前端每次输入变化就 debounce 300ms 打一次接口,接口去 BigQuery 里跑 WHERE name LIKE。跑通之后我挺得意,觉得这不就完事了吗。
SELECT name FROM `shop.products`
WHERE name LIKE CONCAT('%', @kw, '%')
LIMIT 8
结果第二天客户说「打字的时候有时候不动」。我打开日志一看,BigQuery 按查询量计费,虽然单次扫描量很小,但输入「红薯干」三个字就是三次查询,客户自己测试一下午,账单里多了几百次 job。更难受的是冷启动叠加每次查询 1 秒左右的延迟,体验像在拨号上网。
改法比较土,但管用:把 400 条商品名一次性拉出来,在接口内存里做前缀匹配,BigQuery 只在商品表变更时被读一次。
let cache = [];
let cachedAt = 0;
async function loadNames() {
if (Date.now() - cachedAt < 10 * 60 * 1000) return cache;
const [rows] = await bq.query('SELECT name FROM `shop.products`');
cache = rows.map(r => r.name);
cachedAt = Date.now();
return cache;
}
app.get('/suggest', async (req, res) => {
const kw = String(req.query.q || '').trim();
if (!kw) return res.json([]);
const names = await loadNames();
res.json(names.filter(n => n.startsWith(kw)).slice(0, 8));
});
为什么这样做:400 条字符串常驻内存也就几十 KB,前缀匹配是 O(n),根本不值得为它维护一个搜索服务。代价是缓存有最多 10 分钟的滞后,客户改完商品名要等一会儿才能搜到——这一点我写进了交付说明里,他能接受,因为他的商品上架频率是「一周几次」。
哪里会失效:如果商品量涨到几万条,全量加载就变成每次冷启动都拖慢;前缀匹配对「糍粑」这种中间词完全无用,客户后来就抱怨搜「糍粑」搜不到「红糖糍粑」。这是个真坑,我最后的补丁是匹配前把关键词两边加空格切一下,但本质上这就是在重新发明搜索,只是没预算上 Elasticsearch。
另外 Cloud Run 实例缩到 0 之后,第一个请求还是要扛冷启动,我在前端加了个「正在输入」的占位,算是遮羞布。
你们做这种小体量联想的时候,是直接硬扛内存匹配,还是会为了「中间词也能搜到」提前把数据丢进别的服务?我现在有点拿不准该在哪一步收手。
0 条评论
还没有评论,写下第一条。