4.3 搜索与筛选:云数据库索引
场景
店开起来后,搜索的缺位很快就显形了。朋友有天跟我抱怨:有个老顾客来找“上次那个青瓷的、带杯托的”,在列表里翻了五分钟没找到,最后放弃走了。我嘴上说“下次上架多分类”,心里清楚——200 个 sku 的店,分不了几个类就到头,真正的解法是搜索。朋友还补了一刀:线下店顾客找不到东西,导购能立刻迎上去;线上没有搜索,顾客翻不到就是永远翻不到。这句话我一直记着,后来成了我们做搜索时反复回响的产品准则。
第一版搜索我图省事,全表扫:云函数里拿到关键词,使用正则匹配 name 字段,200 条数据一次查完,感觉够用了。直到有一天看云函数日志,一个搜索请求耗时两秒多——慢不是最绝望的,最绝望的是这种方案没有出路:商品涨到 500、1000 条的时候,它只会更慢,而云数据库的查询是有超时上限的。正则匹配压根用不上索引,等于每次都在翻整本书。
还有一个看似细节、实则要命的问题:分词。顾客搜“青瓷杯”,商品名是“景德镇青瓷手绘茶杯”,正则模糊匹配能命中;但顾客搜“手绘杯子”呢?名字里没有“杯子”两个字(只有“茶杯”),就搜不出来。光扩字典是填不满的窟窿。还有一种尴尬:搜“茶”出来所有茶杯,搜“杯”出来所有杯子,这种单字搜索的命中率最高、准确率最低,顾客反而经常这样搜——这一节的很多设计,都是被这种朴素又任性的搜索习惯教育出来的,读者读到后面自会有体会。这节讲我们怎么从“正则全表扫”一步步演进到可用的搜索:字段拆分、索引设计、复合筛选条件、以及搜索体验的最后一公里。
方案
搜索方案的演进路径,也是小型电商最务实的阶梯:
| 方案 | 原理 | 优势 | 局限 |
|---|---|---|---|
| 正则匹配 | 全表正则扫 | 零配置 | 慢、不分词 |
| 关键词数组 | 预埋 keywords 字段 + 索引 | 走索引、可控 | 依赖人工埋词 |
| 复合筛选 | 多字段 where + 复合索引 | 条件组合灵活 | 索引设计要细致 |
| 外部搜索服务 | 专用搜索引擎 | 效果最好 | 成本与维护 |
我们最终落在方案二加方案三的组合:结构上预埋关键词,交互上支持多维筛选。核心思路是给每个 sku 文档加一个 keywords 数组字段,上架时由商品名分词 + 人工补充同义词构成(上架同学敲两下的事),查询时 where({ keywords: keyword }) 命中多键索引。从“每次翻整本书”变成“翻到目录直接找页码”,这个跨越是质变。
关键词的预埋看着土,但它有三个好处:第一,索引友好,多键索引对这个结构是原生支持的;第二,人工埋词的精度其实不低——上架的人最懂顾客会搜什么,“青瓷杯”埋的同义词有“瓷杯、茶杯、杯子”;第三,可运营,发现新词(比如某个电视剧带火了“压手杯”)不用改代码,直接在后台加词。上架流程里朋友每天顺手补几个词,搜索命中率越来越高,这件事她乐此不疲。反面教材也要说:早期没规范的时候,埋词全凭心情,有的 sku 只埋了商品名原词,顾客换个说法立刻失灵——搜索质量成了薛定谔的猫。规范化(后文踩坑里细说)之后才稳定下来。
筛选维度我们定义了几组:价格区间、匠人、系列、是否有货。这些条件和搜索词可以组合,比如“青瓷 + 200-500 元 + 有货”。组合一多,索引设计就得跟上:复合索引按等值在前、范围在后的原则建,查询条件里字段的顺序和索引对齐。给个具体感受:同样一个“关键词 + 价格区间”的组合查询,没建复合索引时扫描了全部 800 个文档,建好后只扫命中的 20 来个,这里差的是近两个数量级的扫描量——云函数的耗时肉眼可见地从一秒多降到百毫秒内。
方案选择上多说一句:为什么不上独立搜索引擎?算过账:外部搜索服务要额外维护一套数据同步链路,小店多一个系统就多一份挂掉的可能。云数据库的关键词方案在 1000 个 sku 以内的规模完全够用,搜索体验的短板更多来自埋词质量而非引擎能力。等店做大了再上专门的搜索服务也不迟——技术选型最美的姿态是在合适的规模用合适的工具。
配置/代码
先看集合结构。sku 文档里负责可搜索性的字段:
// sku 文档(节选)
{
name: '景德镇青瓷手绘茶杯',
keywords: ['青瓷', '瓷杯', '茶杯', '杯子', '手绘', '景德镇'],
price: 28900, // 分,便于区间筛选
artisan: '陈师傅',
inStock: true,
series: '青瓷系列'
}
搜索云函数的核心查询,多个组合条件在这里攒齐:
// search 云函数
const q = [];
if (keyword) q.push({ keywords: keyword });
if (artisan) q.push({ artisan });
if (priceRange) {
q.push({ price: _.gte(priceRange[0]), price: _.lte(priceRange[1]) });
}
if (onlyInStock) q.push({ inStock: true });
const { data } = await db.collection('skus')
.where(_.and(q))
.limit(20)
.get();
索引清单(在云开发控制台为 skus 集合建立):
| 索引 | 字段 | 支持的查询 |
|---|---|---|
| idx_keywords | keywords(多键) | 关键词搜索 |
| idx_price | price | 价格区间 |
| idx_artisan_stock | artisan, inStock | 匠人 + 是否有货 |
| idx_composite | keywords, price | 搜索 + 价格区间组合 |
索引建好后用 explain 类手段验证查询走了索引(控制台可看扫描文档数),别凭感觉。
搜索建议(用户输入时实时提示)用一个轻量云函数,只查关键词集合的前缀匹配。输入“青”就提示“青瓷、青瓷杯、青瓷系列”,这个交互对搜索体验提升很大——顾客经常不知道完整的词是什么,建议帮他补全。这里要加节流:每次输入都请求会打爆接口,我们用 300 毫秒的输入停顿作为触发点,既灵敏又克制。
踩坑
第一个坑是正则匹配慢而不自知。小数据量时一切正常,数据一涨立刻雪崩。定期看慢查询日志,扫描文档数远大于结果数的查询就是嫌疑犯。
第二个坑是关键词录入没规范。早期上架同学埋词全凭手感,有的埋了同义词、有的只埋了名字拆分。后来出了一份两行字的规范:每个 sku 至少埋 5 个词、必须包含品类词和材质词。规范越简单越容易执行,长篇大论没人看。同时配套了一个后台小工具:上架时自动把商品名词拆成候选词,上架同学勾选补充、不手敲,既省事又保证下限。埋词的活儿枯燥但回报直接:第一批规范化埋词上线后两周,搜索带来的订单占比从一成都不到涨到了接近三成,朋友那两天心情明显好。
第三个坑是大小写和繁简体。顾客输入繁体“琺瑯”搜不到简体“珐琅”,早期漏掉了。后台加了同义词表统一管理大小写和繁简映射,一词关系全词生效,比一个个 sku 改强。
第四个坑是筛选条件数量超限。云数据库单次 where 的组合有上限,条件堆太多直接报错。约定筛选维度最多三组同时启用(产品上也克制:维度太多顾客自己也会迷路),把组合空间收敛到可控范围。
第七个坑是索引建好没验证。索引建了不代表查询会走它,条件顺序不对照样全表扫。验证手段很朴素:控制台的数据库监控里有每次查询的扫描文档数,用它对照结果数即可,发现问题就调整查询语句的字段顺序。
第五个坑是空结果页直接显示“无商品”。顾客搜不到东西啪一下空页面拍在脸上,流失就在这一刻。我们做了空结果的兜底:推荐相似品类的热销款,配一句“没找到这个词,要不要看看这些”——搜索失败也是一次推荐机会,挽救回来的转化率实打实。另外空结果页还会把这个词自动写进“高频未命中词表”,运营每周看一眼,补词的方向一目了然。
第六个坑是搜索词日志没存。上线一个月才意识到:顾客搜了什么、哪些词搜不到,是金子般的运营数据。每天把搜索词入库,按频次一排,下周上架照着补词,上架一件新品就直接奔着顾客的真实需求去。我们最早的定制款“压手杯”,就是看了一个月的搜索词榜单后决定开发的——需求不在问卷里,在搜索框里。
小结
现在回头看,搜索这件事的阶梯很清晰:先让顾客搜得到(关键词索引)、再让顾客筛得快(复合索引)、最后让顾客搜得爽(搜索建议、空结果兜底、词表运营)。技术上是云数据库索引的应用课,业务上是“上架即运营”的功夫——关键词埋得好,搜索就是免费的流量放大器。朋友有句话总结得到位:搜索框是她店里最勤快的店员,不请假、不抱怨,就是得经常教它新词——这话虽然朴素,却很到位。索引和性能的话题还没完,下一节系统讲性能优化:分包加载、预渲染、以及那块永远首屏慢的心病。关于索引和查询计划更深的内容,云开发 Python 实战里有服务端视角的整理,适合想继续深入的朋友。
延伸阅读
- 商品模型:SPU 与 SKU 集合设计:搜索依赖的商品字段结构
- 性能优化:分包加载与预渲染:下一节,系统讲性能
- 回到本书目录:全书五篇结构一览
