系统设计 Deep Dive:设计搜索引擎
本文基于真实候选人面经整理系统设计面试全流程。还原面试题目、解题思路与技术考察重点,覆盖Elasticsearch、Go、算法、系统设计,附详细准备策略助你高效备战。
面试官真实提问
“请设计一个搜索引擎,类似 Google 或 Elasticsearch。支持全文搜索、分页、排序和高亮。”
“如何从十亿篇文档中快速找到相关结果?搜索延迟怎么控制在 100ms 以内?”
这道题考察倒排索引、分词、排序算法和分布式搜索的综合设计能力,是系统设计面试中的高频经典题目。
需求澄清清单
功能需求
Must-have:
- ✓ 全文搜索(关键词匹配)
- ✓ 相关性排序(最相关的排在前面)
- ✓ 分页(结果太多时分页展示)
- ✓ 高亮(匹配关键词高亮显示)
Nice-to-have:
- 搜索建议(Autocomplete)
- 拼写纠错(Did you mean…)
- 过滤与聚合(按类别/时间过滤)
- 同义词扩展
规模估算
| 指标 | 估算值 |
|---|---|
| 文档数 | 10 亿篇 |
| 索引大小 | 10TB |
| QPS | 10 万次/秒 |
| 搜索延迟 | P99 < 100ms |
| 索引更新 | 近实时(秒级) |
非功能需求
- 可用性: 99.99%
- 延迟: < 100ms
- 准确性: 高召回率 + 高精度排序
第 1 步:高层设计
┌─────────┐
│ 客户端 │
└────┬────┘
│
┌────▼──────┐
│ API GW │
└────┬──────┘
│
┌────▼────────────┐
│ Query │ 查询协调器
│ Coordinator │
└────┬────────────┘
│ 并行查询
┌─┴──────────────────┐
┌──▼──┐ ┌───▼──┐ ┌───▼──┐
│Shard│ │ Shard│ │ Shard│
│ 1 │ │ 2 │ │ 3 │ 节点 A / B / C
└─────┘ └──────┘ └──────┘
│ │ │
┌──▼──┐ ┌───▼──┐ ┌───▼──┐
│Shard│ │ Shard│ │ Shard│
│ 4 │ │ 5 │ │ 6 │ 节点 A / B / C
└─────┘ └──────┘ └──────┘
索引管道:
文档 → 分词 → 过滤 → 倒排索引构建 → 存储
核心组件:
- Query Coordinator:接收查询、分片路由、结果合并
- Shard:索引的分片,每个分片包含倒排索引
- 索引管道:文档 → 分词 → 过滤 → 倒排索引
第 2 步:核心组件设计
★ 倒排索引(核心考点)
正排索引 vs 倒排索引:
正排索引(Document → Terms):
Doc1: "the cat sat on the mat"
Doc2: "the dog sat on the log"
Doc3: "the cat chased the dog"
倒排索引(Term → Documents):
"the" → [Doc1, Doc2, Doc3]
"cat" → [Doc1, Doc3]
"sat" → [Doc1, Doc2]
"dog" → [Doc2, Doc3]
"chased"→ [Doc3]
倒排索引结构:
Term
│
▼
Posting List
├── DocID (文档 ID)
├── TF (词频)
├── Position (位置,短语搜索)
└── Payloads (额外信息)
示例:
"cat" → [
{doc_id: 1, tf: 1, positions: [2], payload: {}},
{doc_id: 3, tf: 1, positions: [1], payload: {}}
]
倒排索引优化:
1. 压缩 Posting List(Varint、Skip List)
2. 字典树(Trie)存储词表
3. 内存缓存热点词
4. 磁盘存储使用 LSM-Tree 或 B-Tree
分词(Tokenization)
英文分词:
"Hello, world!" → ["hello", "world"]
"can't" → ["can", "t"] 或 ["cannot"]
中文分词:
"我喜欢机器学习" → ["我", "喜欢", "机器", "学习"]
或 → ["我", "喜欢", "机器学习"]
分词工具:
- jieba(中文)
- IK Analyzer
- HanLP
- Elastic Chinese Analysis Plugin
分词策略:
- Standard:按单词边界分词
- Whitespace:按空格分词
- Keyword:整个字符串作为一个 term
- N-gram:生成连续字符子串
排序算法
1. TF-IDF(Term Frequency - Inverse Document Frequency)
TF(t, d) = 词 t 在文档 d 中出现的次数 / 文档 d 的总词数
IDF(t) = log(总文档数 / 包含词 t 的文档数)
TF-IDF(t, d) = TF(t, d) × IDF(t)
示例:
文档总数 = 1000
词 "cat" 出现在 10 篇文档中
IDF("cat") = log(1000/10) = log(100) ≈ 4.6
文档 d 中 "cat" 出现 2 次,文档总词数 100
TF("cat", d) = 2/100 = 0.02
TF-IDF("cat", d) = 0.02 × 4.6 = 0.092
2. BM25(Best Matching 25)
BM25 是 TF-IDF 的改进版,考虑文档长度和词频饱和
BM25(d, q) = Σ IDF(q_i) × (TF(q_i, d) × (k1 + 1)) / (TF(q_i, d) + k1 × (1 - b + b × |d| / avgdl))
参数:
k1 = 1.2 - 2.0(词频饱和参数)
b = 0.75(文档长度归一化参数)
|d| = 文档 d 的长度
avgdl = 平均文档长度
优点:
- 词频饱和(词出现多次,分数增长递减)
- 文档长度归一化(短文档更容易获得高分)
3. 向量排序(Semantic Search)
使用 Embedding 模型将文档和查询转换为向量
计算余弦相似度:
Sim(query, doc) = (q · d) / (||q|| × ||d||)
优点:
- 语义匹配("猫" 匹配 "猫咪")
- 支持多语言
缺点:
- 计算成本高
- 需要 ANN 索引(Faiss、HNSW)
查询处理流程
1. 用户输入查询 → "机器学习教程"
2. 分词 → ["机器学习", "教程"]
3. 查询倒排索引 → 找到包含这些词的文档
4. 计算相关性分数(BM25)
5. 排序 → 返回 Top 10 结果
6. 高亮 → 标记匹配的关键词
查询扩展:
同义词:
"机器学习" → ["机器学习", "ML", "machine learning"]
拼写纠错:
"masine learning" → "machine learning"
查询建议:
"机器" → ["机器学习", "机器人", "机器学习教程"]
第 3 步:扩展性与优化
[重点] 分布式搜索
分片策略:
文档
│
▼ 按文档 ID 哈希
哈希函数
│
├── Doc1 → Shard 1
├── Doc2 → Shard 2
└── Doc3 → Shard 3
好处:负载均衡
问题:查询需要扫描所有分片
查询路由:
┌── Query Coordinator ──────────────────┐
│ │
│ 1. 接收查询 │
│ │ │
│ ▼ │
│ 2. 并行查询所有分片 │
│ │ │
│ ▼ │
│ 3. 合并结果 (Top-K merge) │
│ │ │
│ ▼ │
│ 4. 排序、分页 │
│ │ │
│ ▼ │
│ 5. 返回最终结果 │
│ │
└───────────────────────────────────────┘
副本策略:
┌── 副本策略(每个分片 3 个副本)─────┐
│ │
│ Shard 1 → Node A, Node B, Node C │
│ Shard 2 → Node B, Node C, Node A │
│ Shard 3 → Node C, Node A, Node B │
│ │
└─────────────────────────────────────┘
好处:高可用、负载均衡
问题:索引延迟(主分片 → 副本)
索引更新
近实时索引:
新文档 → 内存缓冲 → 每秒刷新到磁盘 → 搜索可见
好处:近实时(秒级)
问题:频繁刷新影响性能
批量索引:
新文档 → Kafka → 批量处理 → 合并到索引
好处:高性能
问题:延迟高(分钟级)
容量估算
| 指标 | 计算 | 结果 |
|---|---|---|
| 索引大小 | 10 亿文档 × 10KB | 10TB |
| 分片数 | 10TB / 50GB per shard | 200 个分片 |
| 节点数 | 200 分片 / 10 per node | 20 个节点 |
| QPS | 10 万次/秒 | 每节点 5000 QPS |
[问答] 面试官常问 Trade-offs 与实战问答
[问] Q1:你选择 BM25 还是 TF-IDF?
候选人回答:
“我选择 BM25。它是 TF-IDF 的改进版,考虑了文档长度归一化和词频饱和。TF-IDF 有一个问题:词出现多次,分数线性增长,但实际中词出现 2 次和 20 次的区别不大。BM25 解决了这个问题,而且短文档更容易获得高分,更符合用户预期。”
面试官追问:
“如果我想支持语义搜索(如’猫’匹配’猫咪’)怎么办?”
候选人回答:
“使用 Embedding + 向量搜索。将文档和查询转换为向量,计算余弦相似度。可以结合 BM25 和向量搜索,用混合排序:BM25 负责关键词匹配,向量搜索负责语义匹配。“
[问] Q2:搜索延迟怎么控制在 100ms 以内?
候选人回答:
“多级优化。1)倒排索引压缩(Varint、Skip List),减少磁盘 IO;2)热点词内存缓存;3)查询并行扫描所有分片;4)结果Top-K merge,避免全量合并;5)查询缓存(相同查询直接返回缓存结果)。”
面试官追问:
“如果查询很复杂(如多条件过滤 + 聚合)怎么办?”
候选人回答:
“复杂查询会超时。我会设置查询超时时间(如 500ms),超时返回部分结果或降级。同时会预计算聚合结果,或者将复杂查询拆分为多个简单查询。“
[问] Q3:索引更新怎么实现近实时?
候选人回答:
“内存缓冲 + 定期刷新。新文档先写入内存缓冲,每秒刷新到磁盘,构建倒排索引。这样搜索延迟在秒级。对于批量索引,使用 Kafka 异步处理,延迟在分钟级。”
面试官追问:
“如果索引更新频繁(如每秒 10 万次)怎么办?”
候选人回答:
“批量合并。内存缓冲积累到一定大小(如 1000 条)再刷新到磁盘。同时使用 LSM-Tree 存储结构,写入是追加操作,定期合并。Elasticsearch 的 Translog 就是这样设计的。“
[问] Q4:如何处理中文分词?
候选人回答:
“使用 jieba 或 IK Analyzer。jieba 基于词典和统计,适合通用场景。IK Analyzer 是 Elasticsearch 的中文分词插件,支持自定义词典。对于专业领域(如医疗、法律),需要自定义词典和领域分词模型。”
面试官追问:
“如果分词错误(如’机器学习’分成’机器’+‘学习’)怎么办?”
候选人回答:
“自定义词典将’机器学习’作为一个词。同时使用 N-gram 分词,生成’机器学习’、‘机器学’、‘器学习’等子串,提高召回率。对于重要领域,训练领域分词模型。“
[问] Q5:搜索建议(Autocomplete)怎么实现?
候选人回答:
“使用 Trie 树 或 N-gram 索引。Trie 树支持前缀匹配,查询速度快。N-gram 索引支持模糊匹配。同时会缓存热门建议,减少计算量。对于实时建议,使用流处理(如 Flink)实时更新建议索引。”
面试官追问:
“如果建议太多(如 100 万个可能)怎么办?”
候选人回答:
“限制返回数量(如 Top 10)。使用热门度排序(搜索次数、点击率)。对于实时建议,使用近似算法(如 MinHash、SimHash)快速找到相似建议。“
进阶扩展方向
- 语义搜索: Embedding + 向量搜索(Faiss、HNSW)
- 个性化排序: 用户画像 + 点击率预估
- 实时索引: Kafka + Flink 流处理
- 多模态搜索: 文本 + 图片 + 视频
[注意] 常见踩坑点
| # | 踩坑点 | 解决方案 |
|---|---|---|
| 1 | 分词错误:中文分词不准确 | 自定义词典 + N-gram |
| 2 | 索引膨胀:删除文档后索引不缩小 | 定期段合并(Segment Merge) |
| 3 | 查询超时:复杂查询处理慢 | 查询超时 + 降级策略 |
| 4 | 分片不均:某些分片负载高 | 自定义分片路由 + 重平衡 |
总结
搜索引擎设计考察:
| 能力 | 考察点 |
|---|---|
| 倒排索引 | 核心数据结构,必须深入理解 |
| 排序算法 | TF-IDF、BM25、向量排序 |
| 分词 | 中文分词、自定义词典 |
| 分布式搜索 | 分片、副本、查询路由 |
[重点] 面试提示: 先讲清楚倒排索引的原理,然后讨论分词、排序、查询处理。面试官通常会追问”中文分词怎么处理”、“搜索延迟怎么优化”——准备好具体的技术方案。
推荐阅读
- 系统设计面试完全指南 — 掌握万能回答框架
- 设计推荐系统 — 机器学习系统的设计思路
💡 需要面试辅导?
如果你对准备技术面试感到迷茫,或者想要个性化的面试指导和简历优化,欢迎联系 Interview Coach Pro 获取一对一辅导服务。
👉 联系我们 获取专属面试准备方案