题干与适用场景
设计一个根据已输入前缀返回 Top 10 查询建议的后端服务。假设日活用户 5000 万,每位用户每天搜索 10 次,每次搜索触发 5 次建议请求,峰值为每秒 15 万次请求,p99 延迟目标为 50 毫秒,热度更新目标为 15 分钟,策略下架目标为 1 分钟。
这些数字都是面试假设,不是生产实测。基础方案提供匿名、按语言地区区分的热门查询建议;浏览器组件、拼写纠错、语义补全和个人排序暂不纳入。日志里出现过的罕见或违规查询不能直接成为建议。
这道题的核心跨越事件采集、排序、不可变索引构建、在线服务、缓存与分片、内容安全和版本发布,因此归为 system-design。前端自动补全组件只是该 API 的消费者,Trie 也只是本地索引的一种实现。
面试官考察点
第一项信号是能否把写多的学习链路与读多的服务链路拆开。每次按键都扫描日志或现场排序无法稳定满足尾延迟。聚合、准入、审核和主要排序应提前完成,在线链路只做有界的前缀查询。
第二项是容量推导。每天请求量为 5000 万乘 10 再乘 5,即 25 亿;平均约每秒 2.89 万次,题设峰值约为平均值的 5 倍。若响应按 1 KB 估算,峰值响应载荷约为 150 MB/s,尚未计入协议和副本开销。这个推导用于确定副本、缓存和压测目标;索引内存必须用真实序列化样本测量,不能凭空假设 Trie 节点大小。
第三项是发布正确性。半成品索引或路由到不同版本,会造成缺项和排序漂移。成熟方案构建带版本的不可变产物,验证后与旧版并存加载,原子切换路由,并保留上一个健康版本回滚。
最后,热门不等于可发布。搜索日志可能包含隐私、刷量和有害文本。不同用户数门槛、保留规则、反作弊、发布前审核和更快的紧急禁用通道都属于正确性。
回答前需要澄清的问题
- 建议代表什么? 查询句、商品实体和导航入口的数据源与排序特征不同;基础方案返回完整查询句。
- 需要哪种匹配? 纯前缀可用紧凑索引;中缀、模糊或语义匹配需要额外召回器,并增加在线预算。
- 不同变化要求多新? 热度允许 15 分钟,策略下架要求 1 分钟,因此采用版本化基础索引与独立禁用层。
- 全局还是个性化? 全局结果便于共享缓存;个性化会引入授权、删除和特征延迟,基础方案先不做。
- 语言与归一化如何定义? 大小写、文字体系和重音规则因语言而异,构建端和查询端必须使用同一版策略。
- 空前缀和单字符怎么办? 它们极热且候选宽泛;空前缀返回编辑选定列表,单字符返回单独预计算列表。
- 安全与隐私要求是什么? 答案会改变日志保留、聚合阈值、审核流程和区域存储方式。
30 秒回答框架
“我会把系统拆成离线构建与有界在线查询。搜索事件进入流,按语言和时间窗口归一化、聚合,再经过频次、反作弊、隐私和内容审核。排序任务把候选写成带版本的前缀索引,验证后由服务副本并行加载,再原子切换。在线请求先归一化,查热门前缀缓存,路由到语言和前缀分片,应用快速禁用层后返回十条。我会按每秒 15 万峰值规划副本,按需拆热前缀,保留旧索引回滚,并观察 p99、覆盖率、安全召回、旧版本比例和排序质量。”
分步骤深入解答
第一步:推导预算与接口契约
使用小型幂等读接口:
GET /v1/suggestions?prefix=iph&locale=zh-CN&limit=10
200 {
"suggestions": [
{ "text": "iphone 充电器", "id": "q_7f2" }
],
"indexVersion": "2026-07-19T17:30Z"
}服务限制 limit 和归一化前缀长度,拒绝未知语言地区,也不允许客户端传排序权重。稳定的不透明 ID 便于分析,且不会把展示文字当主键;indexVersion 让旧版或混版响应可观测。
25 亿日请求对应平均约 2.89 万 QPS,容量按 15 万峰值设计。响应若约 1 KB,峰值约 150 MB/s,需要区域副本和压缩传输。缓存与索引 RAM 则应把代表性数据真正序列化,测量每个前缀和候选的字节数,再加副本与余量。
第二步:从日志生成可发布候选
客户端在搜索完成后发送查询 ID、语言地区、粗粒度上下文、时间、结果信号,以及仅用于有界聚合的短期隐私保护主体键。采集服务校验结构、过滤明显机器人,再写入追加式事件流。窗口聚合计算不同主体数和质量信号,用户原始标识绝不进入建议主键。
候选必须经过不同用户数下限、限速与反作弊、隐私和保留规则、内容策略分类,再按热度、时间衰减、结果质量和编辑规则组合排序。精确权重需实验确定,不是通用常量。拒绝原因进入受限审计存储,不进入服务索引。
点击信号会受当前展示位置影响,单看点击率容易强化既有排序。离线相关性判断和带护栏实验应与互动指标结合,同时观察覆盖率、无结果率、投诉和曝光集中度。自动补全来自日志和文档表示时可能重现偏见或有害内容,因此审核发生在构建前。
第三步:物化有界前缀索引
每条合格建议按照在线端相同的语言归一化生成前缀。每个前缀只保存有界候选,例如 API 返回 10 条时保存前 20 条,给去重和紧急过滤留出余量,查询仍保持有界。
Trie 或有限状态转换器能共享前缀;按“语言地区加前缀”键控的有序键值表更容易运维,也可能压缩得很好。用产物体积、构建耗时、查询 p99 和更新流程的基准测试做选择。Elasticsearch completion suggester 体现了相同权衡:快速前缀查询依赖构建成本较高的内存结构,并通过权重和上下文影响排序与过滤。
基础范围只做精确前缀。模糊补全是独立召回器,因为编辑距离扩展会改变召回、CPU 和安全分析,不能默认共享同一延迟承诺。
第四步:安全发布不可变版本
每个构建记录输入水位、归一化版本、排序器版本、策略版本、校验和及创建时间。验证包括结构、候选数量、禁止测试词、语言隔离、稳定并列规则、抽样查询、产物大小和加载后的延迟。
副本在当前版本旁加载新版,报告校验和和代表性查询健康度;控制面再按分片组原子切换。一个请求固定使用一个版本,混版比例单独监控。新版通过灰度窗口前保留旧版,若延迟、安全或覆盖退化,直接回切路由。
构建失败或延迟时继续服务上一个健康版。新鲜度是 SLO,不能成为发布无效产物的理由。
第五步:缩短在线链路
请求依次经过限流、归一化、语言解析和精确热门前缀缓存,再由路由器选择前缀分片与副本。副本做一次索引查询,删除快速禁用集合中的条目,按稳定 ID 去重并取前 10。在线链路不访问原始日志,不做分布式聚合,也不做全量排序。
缓存键包含语言地区、归一化前缀、数量上限、策略版本和当前索引版本,避免切换后继续返回旧排序或已禁用内容。空前缀与单字符单独预计算。不存在前缀可做短期负缓存,但 TTL 不能让新候选超过 15 分钟目标仍不可见。
第六步:按局部性与热点分片
先按语言地区,再按前几个归一化字符的范围或哈希分片。范围分片有局部性但易热;纯哈希均衡,却需要更多路由元数据。实用方案维护带版本的“前缀范围到分片”映射,能单独拆分一个热门首字符而不重建无关范围。
每个分片跨故障域复制,请求走本地健康副本。持续记录每前缀 QPS、缓存命中率、分片 CPU、查询 p99 和索引字节。加副本解决读负载,拆分或隔离热范围解决倾斜。分片不可用时可返回带新鲜度指标的缓存或空列表,不能借用另一语言的建议。
第七步:满足两套新鲜度时钟
基础索引每 15 分钟重建。小型趋势叠加层可用更短窗口聚合,并与基础候选做有界合并,但必须经过相同隐私和安全门槛。叠加层故障时继续服务健康基础索引。
策略下架由独立分发的禁用集合满足一分钟目标。副本在查询后按 ID 过滤,缓存键包含该集合版本;下一次基础构建永久移除条目。这样无需为紧急下架等待大产物重建。
第八步:验证排序、安全和运维
压测按真实前缀长度与语言分布回放 15 万峰值 QPS,覆盖热单字符、冷缓存启动、副本丢失和版本切换。在服务边界断言 p99 低于 50 毫秒、错误率有界、单响应不混校验和,且恢复不产生惊群。
离线评估包括 Top-K 相关性、覆盖率、重复率、语言正确性、违规内容召回和版本稳定性。在线实验观察搜索完成与下游结果质量,并用无结果率、延迟、投诉和曝光集中度作护栏。点击率受展示位置影响,不能单独作为结论。
演练污染事件批次、构建失败、超大产物、热分片、过期禁用层、部分副本上线和排序回归。每类告警都对应安全动作:暂停激活、回到旧版、隔离叠加层、拆范围或启用紧急禁用。
高质量示范回答
“我先把范围定为匿名、按语言地区区分的前缀补全,每次十条。5000 万用户乘每天 10 次搜索、每次 5 个请求,是每天 25 亿次,平均约 2.89 万 QPS;容量按题设 15 万峰值规划,索引内存用序列化样本实测,不猜 Trie 节点大小。
数据链路和服务链路分开。搜索完成事件进入流,聚合出不同用户数、时效和结果质量。候选经过频次、反作弊、隐私与审核,再由排序任务给每个归一化前缀写入有界列表。产物记录数据水位、归一化、排序和策略版本。
服务副本持有不可变版本。新版在旧版旁加载验证,路由原子切换并可回滚。请求归一化前缀与语言,查询带版本缓存,路由到前缀分片,做一次索引查询,再应用快速禁用集合并返回十条。热单字符有专用缓存,也能单独拆分。
基础构建满足 15 分钟目标,小型趋势层可提升时效;紧急下架走一分钟禁用层,两者失败都不破坏最后健康基础版。我会按 15 万 QPS 和实际前缀分布压测,并监控 p99、新鲜度、命中率、分片倾斜、相关性、语言串漏、安全召回和回滚时间。”
常见错误
- 每次按键都查询并排序原始日志 → 工作量随历史增长,尾延迟不可控 → 预计算有界 Top-K,在线只做定长查询。
- 说“用 Trie”就结束 → 缺少排序、发布、分片、安全和恢复 → 同时说明构建与服务生命周期,并基准比较表示方式。
- 用臆测节点大小估内存 → 编码方式和前缀共享决定真实字节 → 序列化代表性数据后实测。
- 缓存键只有前缀 → 语言、策略或版本结果会串漏 → 把全部影响结果的版本写入键。
- 原地更新索引 → 读者可能看到半成品或混版 → 构建不可变产物、并存加载并原子激活。
- 只以热度判断准入 → 隐私、刷量或有害文本可能出现 → 加入不同用户门槛、反作弊、隐私和审核。
- 紧急下架也重建全量 → 安全期限受大批任务约束 → 分发快速禁用层,下次构建永久移除。
- 把点击率当无偏相关性 → 展示位置本身影响点击 → 结合离线判断、护栏实验和安全指标。
追问及应对
追问一:如何加入模糊匹配?
先执行便宜的精确前缀召回,只在达到最小长度或精确覆盖不足时触发模糊召回。限制编辑距离和候选数,经过同一排序与审核后合并。Unicode 距离和对抗输入必须压测,因为扩展会增加 CPU,也可能召回敏感变体。
追问二:如何加入个性化?
先取全局候选,再混入小型、获授权的个人候选。共享缓存止于混排之前,最终响应按用户隔离且不能进入公共缓存。上线前明确同意、保留、删除、敏感查询排除、特征超时和仅全局回退。
追问三:某种语言的索引放不进内存怎么办?
按实测字节与 QPS 拆前缀范围,更新版本化路由映射。顶层热前缀放专用副本;冷范围可在 p99 仍达标时使用内存映射或远端索引。迁移随产物版本完成,避免读路径依赖原地搬键。
追问四:突发新闻要求数秒内出现怎么办?
增加严格有界的流式叠加层,限定可信来源、高准入门槛、即时审核、TTL 和熔断开关,在固定候选预算内与基础版合并。若其时效或策略水位过期,就丢弃叠加层,继续用旧的健康基础版。
追问五:隐私删除请求涉及某条查询怎么办?
按数据模型删除或标记原始事件和聚合,把建议 ID 立即加入禁用层,通过策略版本失效相关缓存,再从修正输入重建基础索引。跨区域审计传播时长,但不能再次记录敏感明文。