· 王思腾 · 系统架构 · 8 分钟阅读

系统设计:如何设计搜索自动补全(搜索建议)系统

在搜索框敲下几个字,下拉框立刻给出最热门的补全建议——这个功能要求在几十毫秒内、从海量词条里返回“以当前输入为前缀、且最热门”的 Top-K。本文讲清前缀匹配的数据结构(Trie/字典树)、热度排序 Top-K、查询日志离线构建词典、在线低延迟服务与缓存,带你设计一个类 Google/淘宝的搜索建议系统。

在搜索框敲下几个字,下拉框立刻给出最热门的补全建议——这个功能要求在几十毫秒内、从海量词条里返回“以当前输入为前缀、且最热门”的 Top-K。本文讲清前缀匹配的数据结构(Trie/字典树)、热度排序 Top-K、查询日志离线构建词典、在线低延迟服务与缓存,带你设计一个类 Google/淘宝的搜索建议系统。

在搜索框输入“跑步”,下拉立刻提示“跑步鞋男”“跑步机家用”“跑步袜”——这就是自动补全(Autocomplete / Typeahead)。它的核心挑战是:每次击键都要在几十毫秒内,从海量词条中找出“以当前输入为前缀、且最热门”的前 K 条。

一、需求与约束

  • 输入前缀 p,返回以 p 开头的查询建议 Top-K(K 通常 5~10);
  • 建议按热度排序(搜索量、点击率、转化率),而非简单字典序;
  • 极低延迟:用户每敲一个字符就触发一次请求,要求 P99 < 50~100ms;
  • 高 QPS(搜索是最高频入口之一);
  • 支持中文(需分词/拼音)、个性化(结合用户历史)、容错(错别字)。

二、核心数据结构:Trie(字典树/前缀树)

前缀匹配的天然结构是 Trie:从根到某节点的路径代表一个前缀,共享前缀的词共用同一条路径。查“跑步”的所有候选,就是走到“跑步”节点,再遍历它的子树。

flowchart TB
    Root(("根")) --> P["跑"]
    P --> B["步"]
    P --> D["道"]
    B --> X["鞋"]
    B --> J["机"]
    B --> W["袜"]
    X --> X1["鞋男 (热度 9800)"]
    X --> X2["鞋女 (热度 7200)"]
    J --> J1["机家用 (热度 5100)"]
    W --> W1["袜 (热度 3300)"]

但朴素 Trie 有两个问题:

  1. 遍历整棵子树再排序太慢:“跑”下面可能挂着几十万个词,每次都遍历 + 排序无法满足几十毫秒;
  2. 内存占用大(每个字符一个节点)。

解法:在每个节点预存 Top-K 热门候选。

构建 Trie 时,为每个节点维护一个容量为 K 的小顶堆,只保留经过该前缀的、最热的 K 个词。这样查询时走到对应节点,直接拿到已排好序的 Top-K,无需遍历子树,复杂度约等于前缀长度 O(len(p))。

  • 词典通常百万级词条,Trie 常驻内存(或用压缩 Trie / Radix Tree 省空间),单机即可扛住大部分查询;
  • 节点存的是候选词的 ID/引用 + 热度,而非完整字符串。

三、热度从哪来:离线构建

建议的热度不是拍脑袋的,而是从历史搜索日志统计出来的。整套系统分离线和在线两部分:

flowchart TB
    subgraph Offline["离线(定期构建)"]
        Log["搜索/点击日志"] --> Agg["统计词频与热度<br/>(搜索量 + 点击率加权)"]
        Agg --> Dict["生成热门词典<br/>过滤低频/违规词"]
        Dict --> Trie["构建 Trie<br/>每节点预计算 Top-K"]
        Trie --> Snap["生成数据快照"]
    end
    Snap --> Online
    subgraph Online["在线(低延迟查询)"]
        Req["用户输入前缀"] --> Cache{"缓存命中?"}
        Cache -- 是 --> Ret["返回建议"]
        Cache -- 否 --> T2["查内存 Trie 取 Top-K"]
        T2 --> Rank["个性化/业务规则重排"]
        Rank --> Ret
    end
  • 离线用 Spark 等聚合日志,统计每个查询词的热度(可加入时间衰减,让新词、突发热点上位);
  • 过滤低频词、违禁词,只保留 Top 百万级热门词构建 Trie;
  • 生成快照后推送到在线服务内存,定时(如每天/每小时)更新,热加载不中断服务。

四、在线服务:极致低延迟

  • 内存查询:Trie 常驻进程内存,查询不走磁盘、不查数据库,微秒级;
  • 多级缓存:热门前缀(如“跑”“手机”)的结果放 Redis 甚至本地缓存,命中率极高——因为用户输入高度集中在少数热词;
  • 前端防抖 + 边缘加速:输入停顿才发请求(debounce),结果可经 CDN/边缘节点缓存;
  • 服务无状态水平扩展,Trie 快照在每个实例各持一份(词典不大,全量复制比分片更简单)。

五、中文、个性化与容错

  • 中文/拼音:中文没有空格,需对词条做分词,或同时建“汉字 Trie + 拼音 Trie”,输入“paobu”也能出“跑步”;
  • 个性化重排:在通用热度基础上,结合用户画像(性别、地域、历史搜索)对 Top-K 做轻量重排——例如同样是“跑步鞋”,给男性用户优先“男款”;
  • 容错/模糊:对错别字可在 Trie 之外加编辑距离匹配,或借助搜索引擎的模糊召回兜底;
  • 纯前缀覆盖不到的长尾,可回退到 Elasticsearch 的 edge-ngram 前缀索引做补充。

六、方案对比

方案优点缺点适用
内存 Trie + 节点 Top-K极快、可控需构建、占内存热门词前缀建议(主流)
Redis ZSET实现简单、可分片网络开销、词多时慢中小规模快速上线
ES edge-ngram功能强、支持模糊/分词延迟相对高、成本高长尾兜底、复杂搜索

实践常组合:Trie 扛热门前缀(90%+ 流量),ES 兜长尾和容错。

七、小结

  1. 自动补全的本质是“前缀匹配 + 热度 Top-K”,且要求几十毫秒内返回;
  2. 数据结构用 Trie,关键优化是在每个节点预存 Top-K 热门候选,查询复杂度降到接近前缀长度;
  3. 热度来自搜索日志离线统计,离线构建 Trie 快照、推送到在线服务内存定期热更新;
  4. 在线靠内存查询 + 多级缓存 + 前端防抖 + 无状态扩容做到极低延迟;
  5. 中文靠分词/拼音 Trie,个性化靠重排,长尾与容错靠 ES 兜底。

搜索建议看起来是个小下拉框,背后却是“离线数据工程 + 内存数据结构 + 低延迟服务”的组合拳——体验的差距,往往就在那几十毫秒里。

本文由 AI 辅助创作,所有内容仅供交流与参考。
分享:
返回博客
系统设计:如何设计一个云盘(网盘)系统

系统设计:如何设计一个云盘(网盘)系统

云盘的核心不是“把文件存到服务器上”,而是支撑海量小文件与超大文件的上传下载、断点续传、秒传去重、分块存储与弹性扩容。本文从文件元数据与内容分离讲起,覆盖分块上传、秒传(内容哈希去重)、对象存储、目录树模型与分享权限,带你设计一个类百度网盘/Google Drive 的系统。

系统设计:如何设计一个聊天系统(即时通讯 IM)

系统设计:如何设计一个聊天系统(即时通讯 IM)

聊天系统看似简单(发消息、收消息),背后却要解决实时推送、消息有序、已读回执、离线消息、多端同步、海量连接等一系列难题。本文从收发消息的核心链路入手,讲清长连接网关、消息投递与确认、存储模型、群聊扇出,以及可靠性(不丢、不重、有序)的设计取舍。

个人与小团队的全栈技术栈:前端、后端到持续部署,怎么做到低成本、快交付

个人与小团队的全栈技术栈:前端、后端到持续部署,怎么做到低成本、快交付

一个人或两三个人做产品,最大的约束不是技术,是时间和钱。这套栈的目标很明确:不养服务器、不写运维、Git push 就上线、流量小的时候几乎零成本。本文从前端静态优先、后端 Serverless、托管数据库,到 CI/CD 自动部署,给出一套能今天动手、明天上线、跑起来月账单趋近于零的务实组合,顺便拿这个网站本身当例子。