系统设计:设计一个 Google Search 系统

搜索引擎是互联网最核心的基础设施之一。本文从系统设计角度,拆解如何从零设计一个类 Google 的大规模搜索系统,覆盖爬取、索引、检索、排序全链路。

1. 需求分析

功能需求

  • 用户输入关键词,返回相关网页列表
  • 支持分页(每页 10 条结果)
  • 结果按相关性排序
  • 支持基本的布尔查询(AND / OR / NOT)

非功能需求

指标 目标
规模 索引 200 亿+ 网页
QPS 峰值 10 万+ 查询/秒
延迟 P99 < 200ms
可用性 99.99%
索引新鲜度 热门页面分钟级更新

2. 整体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
用户请求


[负载均衡层]


[查询服务 Query Service]
├── 查询解析 / 分词
├── 拼写纠正 & 同义词扩展
├── 索引检索 (Inverted Index)
└── 结果排序 (Ranking)


[返回 Top-K 结果]

后台流水线:
[Web Crawler] → [内容存储] → [索引构建] → [倒排索引集群]

3. 核心组件详解

3.1 Web 爬虫(Crawler)

爬虫是整个系统的数据入口,负责持续抓取互联网页面。

设计要点:

1
2
3
4
5
6
7
8
URL 前沿队列 (URL Frontier)

▼ 取 URL
[DNS 解析] → [下载器] → [内容去重] → [HTML 解析]

提取新 URL ─→ 回到 URL Frontier

提取正文 ─→ 内容存储

关键考量:

  • 礼貌性爬取:遵守 robots.txt,限制同一域名爬取频率(如每域 1 req/s)
  • 去重:URL 去重用 Bloom Filter,内容去重用 SimHash(近似重复检测)
  • 优先级调度:按 PageRank / 更新频率动态调整爬取优先级
  • 分布式:数千台爬虫机器,按域名哈希分片,避免多台机器重复爬同一域

规模估算:

  • 200 亿页面,平均 100KB/页 → 原始存储 ≈ 2PB
  • 每天重新爬取 5% → 日爬取量约 10 亿页

3.2 内容存储与存储选型

爬取的原始页面需要持久化,供索引构建和调试使用。不同数据有截然不同的访问模式,需要针对性选型。

存储层分类与选型

数据类型 数据量 访问模式 选型方案 选型理由
原始 HTML 页面 ~500TB(压缩后) 批量写、顺序读(索引构建) 对象存储(GCS / S3 / HDFS) 吞吐优先,无需随机读,成本低
文档元数据 ~20TB 高频随机读写(按 URL 查) Bigtable / HBase 宽列存储,按 row key (URL hash) 快速点查
倒排索引(Posting List) ~100TB 随机读(按 term 查) 定制文件格式 + 本地 SSD 延迟敏感,Google SSTable / 自研格式
URL 去重集合 ~几百 GB 高频写、membership 查询 Bloom Filter(内存)+ RocksDB Bloom Filter 判重,RocksDB 持久化
PageRank 图数据 ~数 TB 批量迭代计算 分布式图存储 / GFS MapReduce/Pregel 批处理友好
查询结果缓存 ~数十 GB 热数据 高频读写,低延迟 Redis Cluster 内存 KV,支持 TTL,集群水平扩展
用户行为日志 ~PB 级 写多读少,离线分析 列式存储(BigQuery / Parquet) OLAP 查询优化,高压缩比

核心存储详解

① 原始页面存储 — 对象存储

1
2
3
4
5
6
7
8
9
写入路径:
Crawler → Kafka(缓冲)→ 批量写入 GCS/HDFS
└── 按 crawl_date/url_hash 分区
└── Snappy 压缩(兼顾速度和压缩比)
└── 保留最近 3 个版本(支持历史对比)

读取路径:
Indexer (Spark/MapReduce) → 顺序扫描分区
└── 批量读取效率高,无需随机寻道

② 文档元数据 — Bigtable

1
2
3
4
5
6
7
8
Row key 设计:reverse_url_hash
例:com.google.www → 按域名前缀聚集,便于域名级别扫描

列族:
crawl: last_crawl_time, http_status, content_hash
rank: pagerank_score, inlink_count, outlink_count
meta: language, charset, content_type, word_count
index: last_indexed_time, index_version

Row key 使用 URL 反转(com.google.www)而非正向 URL,原因是同一域名下的页面会聚集存储,有利于按域名扫描(如爬取调度、域名级 PageRank 分析)。

③ 倒排索引存储 — 定制 SSTable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
索引文件结构(类 Google SSTable):
┌─────────────────────────────────┐
│ Term Dictionary(词典区) │ 内存常驻,B-Tree 或 Hash Map
├─────────────────────────────────┤
│ Posting List Data(数据区) │ 磁盘/SSD,按 term 偏移量定位
│ [doc_id delta][tf][positions] │ Delta 编码 + VByte 压缩
├─────────────────────────────────┤
│ Skip List Index(跳表索引) │ 加速 AND 归并
├─────────────────────────────────┤
│ Bloom Filter(存在性判断) │ 快速判断 term 是否在此 shard
└─────────────────────────────────┘

部署:每个 Index Server 本地 SSD 存储
└── 避免网络 IO,P99 延迟 < 5ms
└── 内存缓存热门 term 的 Posting List

为什么不用通用数据库?

  • MySQL / PostgreSQL:不适合,倒排索引的 Posting List 是变长二进制数据,关系型数据库行锁和 MVCC 开销大,无法支撑 10 万 QPS 的点查场景。
  • MongoDB:文档型数据库适合半结构化数据,但对 Posting List 的顺序扫描和 Delta 解码支持差,且 100TB 级别成本高。
  • Elasticsearch:本质上就是搜索引擎,适合中小规模。Google 级别需要更精细控制存储格式、压缩算法、Shard 路由策略,故选择自研。

数据流与存储交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[爬虫]
│ 写原始 HTML

[对象存储 GCS/HDFS]
│ 批量读(Spark Job)

[索引构建(MapReduce)]
│ │
│ 写 Posting List │ 写元数据
▼ ▼
[Index SSTable] [Bigtable]
│ │
│ 查询时读 │ 排序时读 PageRank
▼ ▼
[Query Service ←←←←←←←←←←]


[Redis Cache] ← 热门结果缓存

3.2b 爬虫调度细节

URL Frontier 不是简单的队列,而是一个多优先级的调度系统:

1
2
3
4
5
6
7
8
9
10
11
12
13
URL Frontier 内部结构:

[前端队列 - 优先级维度] [后端队列 - 礼貌性维度]
┌──────────────────┐ ┌──────────────────────┐
│ Priority Queue 1 │ (高 PR) │ Queue_google.com │ ← 同域名串行
│ Priority Queue 2 │ (中 PR) │ Queue_wikipedia.org │ ← 每队列独立计时
│ Priority Queue 3 │ (低 PR) │ Queue_reddit.com │ ← 礼貌等待
└──────────────────┘ └──────────────────────┘
│ │
└── Bipartite Selector ──────────┘
│ 选取 ready 的 URL(距上次爬取已超冷却时间)

[下载器池]

优先级计算公式:

1
2
3
4
Priority(url) = α × PageRank(url)
+ β × UpdateFrequency(url) # 历史变化频率
+ γ × ImportanceSignal(url) # 新闻/热门站点加权
- δ × TimeSinceLastCrawl(url) # 老化惩罚

DNS 缓存: 爬虫维护本地 DNS 缓存(TTL 30min),避免每个 URL 都发起 DNS 解析,节省 50% 以上的 DNS 查询开销。

3.3 倒排索引(Inverted Index)

倒排索引是搜索引擎的核心数据结构。

结构:

1
2
3
4
5
6
词项(Term) → Posting List
└── [(doc_id, tf, positions), ...]

例:
"search" → [(doc3, tf=5, [12,45,89]), (doc7, tf=2, [3,67]), ...]
"engine" → [(doc3, tf=3, [15,91,102]), (doc9, tf=1, [8]), ...]

构建流程(MapReduce / Spark):

1
2
Map:   (doc_id, content) → (term, doc_id, tf, position)
Reduce: term → sorted posting list by doc_id

分片策略(Term vs Doc 分片):

策略 描述 优点 缺点
Term 分片 按词项哈希,同一词的所有文档在同一节点 单词查询快 多词 AND 查询需跨节点
Doc 分片 按文档哈希,每节点存所有词的部分文档 支持独立排序 需 scatter-gather 合并

Google 实践:采用 Doc 分片(Index Server)+ 多层级索引(Tier)。热门词有独立的小索引(Hit List),优先命中。

索引压缩:

  • doc_id 使用 Delta 编码(存差值)
  • 词频用 VByte 编码
  • 压缩比可达 10:1

3.4 查询理解(Query Understanding)

在检索倒排索引之前,原始查询需要经过深度理解,这是搜索质量的重要一环:

查询分类

1
2
3
4
5
6
7
查询类型识别(影响后续处理策略):
├── 导航型 (Navigational):"youtube"、"gmail login"
│ → 直接返回目标网站,排名不敏感
├── 信息型 (Informational):"how to cook pasta"
│ → 需要多样化结果,覆盖不同角度
└── 事务型 (Transactional):"buy iphone 16"
→ 优先电商页面,可能触发购物广告

查询改写流水线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
原始输入: "googl searh engin desing"

▼ 1. 拼写纠正(Spell Correction)
│ "google search engine design"
│ 方法:字符级 n-gram 相似度 + 语言模型联合打分
│ 候选词:Edit Distance ≤ 2,用 BK-Tree 高效检索

▼ 2. 查询分词(Tokenization)
│ ["google", "search", "engine", "design"]
│ 中文额外需要:分词(jieba/BERT-based)

▼ 3. 停用词处理
│ 搜索场景谨慎删除,"how to"、"what is" 保留语义
│ 只删除真正无意义的:如单独的标点

▼ 4. 同义词 & 实体扩展
│ "car" → ["car", "automobile", "vehicle"]
│ "NYC" → ["New York City", "New York"]
│ 知识图谱实体识别:Google Knowledge Graph

▼ 5. 查询意图理解(BERT/LLM)
│ 语义向量化,用于 ANN 向量检索(Neural Retrieval)

▼ 最终:结构化查询计划
must_match: ["google", "search", "engine"]
should_match: ["design", "architecture", "system"]
entity: {type: "topic", name: "Search Engine"}

向量检索(Neural Retrieval)— 现代补充

传统倒排索引基于词汇精确匹配,无法理解语义。现代搜索引擎引入双塔模型(Dual Encoder):

1
2
3
4
5
6
7
8
9
10
11
12
13
Query Encoder          Document Encoder
│ │
BERT/T5 BERT/T5
│ │
q_vector d_vector (离线预计算,存向量数据库)
│ │
└──── cosine_sim ────────┘

ANN 检索(FAISS / ScaNN)

语义候选集

最终结果 = 倒排索引结果 ∪ 向量检索结果(融合排序)

3.4b 查询处理(Query Processing)

用户查询到结果的完整链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
原始查询 "google search design"


[预处理]
├── 分词: ["google", "search", "design"]
├── 停用词过滤: 保留全部(搜索场景慎删)
├── 拼写纠正: "serach" → "search"(Edit Distance + 语言模型)
└── 同义词扩展: "design" → ["design", "architect"]


[索引查询]
├── 每个词查 Posting List
├── 多词 AND:归并排序(利用 doc_id 有序性)
└── 取 Top-K candidates(数千个)


[排序 Ranking]
└── 返回 Top-10 结果

AND 查询优化(跳表 Skip List):

1
2
3
4
Posting: [3, 8, 15, 23, 47, 89, 102, ...]
Skip: [3→15→47→102→...]

跳表让 AND 归并从 O(N) 加速到 O(√N)

3.5 排序系统(Ranking)

排序是搜索质量的核心,Google 综合数百个信号。

经典两阶段排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
[粗排 - L1 Ranking]
├── 数千候选 → 数百
├── TF-IDF / BM25 相关性得分
└── 轻量特征(文档长度、词频)


[精排 - L2 Ranking]
├── 数百 → Top 10
├── PageRank 链接权威性
├── 用户行为信号(CTR, 停留时长)
├── 新鲜度(内容更新时间)
├── 个性化(用户历史)
└── 机器学习模型(LambdaMART / Neural Ranking)

BM25 公式(现代 TF-IDF):

1
2
3
4
5
6
Score(D,Q) = Σ IDF(qi) × [tf(qi,D) × (k1+1)] / [tf(qi,D) + k1×(1-b+b×|D|/avgdl)]

其中:
- IDF = log((N-df+0.5)/(df+0.5)) 文档频率惩罚常见词
- k1=1.2, b=0.75 调节参数
- |D|/avgdl 文档长度归一化

PageRank(链接分析):

1
2
3
4
5
PR(A) = (1-d) + d × Σ [PR(T)/C(T)]

- d=0.85 阻尼系数
- T 为所有指向 A 的页面
- C(T) 为 T 的出链数

4. 系统关键设计

4.1 高可用与容错

1
2
3
4
5
6
7
8
9
[全球多 Region 部署]
├── 每 Region 独立服务,故障隔离
├── DNS GeoDNS 就近路由
└── 跨 Region 索引复制(最终一致)

[单 Region 内]
├── 每个 Index Shard 3 副本
├── 查询 scatter-gather:发送到所有 shard,任一副本响应即可
└── 超时熔断:单 shard 超时则返回部分结果,不阻塞整体响应

4.2 索引更新(新鲜度)

不同页面对新鲜度要求不同:

类型 更新策略 延迟
热门新闻页 实时索引 (Caffeine) 分钟级
普通网页 批量增量更新 小时级
低质量页面 定期全量重建 天/周级

增量索引设计:

1
2
3
4
5
6
7
8
9
新爬取文档


[实时 Indexing Pipeline]
├── 解析 → 提取 terms
├── 写入 Delta Index(小型,内存友好)
└── 定期 Merge → Base Index

查询时:Base Index + Delta Index 合并结果

4.3 缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
[查询结果缓存]
├── 热门查询("weather", "NBA")命中率 > 80%
├── Key: 规范化查询串
├── TTL: 热门词 1min,长尾词 1h
└── 分布式缓存(Redis Cluster)

[Posting List 缓存]
├── 高频词("the", "how")的 Posting List 常驻内存
└── 按访问频率 LRU 淘汰

[预计算热门查询]
└── Top 1000 查询每分钟预热,直接返回

4.4 PageRank 工程实现

PageRank 是迭代计算,工程实现需要处理超大规模图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
图规模:200 亿节点,千亿条边

实现方案:Pregel(图计算框架)/ Spark GraphX

迭代计算(直到收敛,通常 30-50 轮):
Round 0: 所有节点 PR = 1/N
Round 1: 每个节点将 PR/出链数 分发给邻居
Round 2: 汇总收到的 PR 值,更新自身
...
收敛条件: Σ|PR_new - PR_old| < ε

工程优化:
├── 图分区:按 URL 哈希分片,减少跨机器通信
├── 稀疏矩阵压缩:CSR 格式存储邻接矩阵
├── Combiner:本地聚合后再发送,减少网络传输
└── 增量更新:只重算受新链接影响的子图(TrustRank)

计算频率:全量重算每周一次,增量更新每天

Spam 防御(Link Farm 攻击):

1
2
3
4
5
攻击:创建大量互链垃圾站,人工抬高 PageRank
防御:
├── TrustRank:从可信种子站点(edu/gov)出发传播信任值
├── SpamRank:标记低质量站点,降权传播
└── 链接多样性检测:同 IP/同注册人的链接权重衰减

4.5 反爬与质量控制

内容质量过滤(索引前):

1
2
3
4
5
6
过滤规则(任一命中则不入索引):
├── 内容过短:< 100 词
├── 近重复内容:SimHash 距离 < 3
├── 垃圾关键词堆砌:词项分布异常(KL 散度检测)
├── 无效状态码:4xx / 5xx
└── robots.txt 禁止索引的页面

爬虫自我保护:

1
2
3
4
5
防封策略:
├── User-Agent 轮换(Googlebot 官方声明)
├── 爬取速率自适应:服务器响应慢则降速
├── 异常检测:5xx 率 > 10% 则暂停该域名
└── IP 池:避免单 IP 被封(数据中心 IP,非代理)

4.6 可观测性(Monitoring)

搜索系统的监控需要覆盖业务指标和技术指标两个维度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
业务指标(Search Quality):
├── CTR (Click-Through Rate):结果点击率
├── Zero-Result Rate:无结果查询比例(目标 < 1%)
├── MRR (Mean Reciprocal Rank):正确结果排名倒数均值
└── NDCG (Normalized DCG):综合相关性评估

技术指标(Infrastructure):
├── 查询延迟分布:P50/P95/P99(目标 P99 < 200ms)
├── Index Freshness:索引延迟(新页面从爬取到可检索的时间)
├── Crawler 健康:爬取成功率、每秒爬取页数
└── Shard 可用性:各 Index Shard 副本存活状态

告警策略:
├── P99 延迟 > 500ms → PagerDuty 告警
├── Zero-Result Rate 上升 > 2% → 索引问题排查
└── Shard 丢失 > 1/3 副本 → 自动触发副本重建

4.7 规模估算

存储:

1
2
3
原始网页:200 亿 × 100KB = 2PB(压缩后 ~500TB)
倒排索引:词典 + Posting List ≈ 100TB
文档元数据:200 亿 × 1KB = 20TB

计算:

1
2
3
QPS: 10 万查询/秒
每次查询:scatter 到 ~1000 shard,每 shard ~100ms
Index Server 数量 ≈ 10 万 / (1000 QPS/server) ≈ 100 台(每 shard 3 副本 → 约 300 台)

5. 架构图总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
                        ┌─────────────┐
│ 用户请求 │
└──────┬──────┘

┌──────▼──────┐
│ 负载均衡 │
└──────┬──────┘

┌──────────▼──────────┐
│ Query Processor │
│ 分词/纠错/同义词扩展 │
└──────────┬──────────┘
│ scatter
┌────────────────────┼────────────────────┐
▼ ▼ ▼
[Index Shard 1] [Index Shard 2] [Index Shard N]
(Inverted Index) (Inverted Index) (Inverted Index)
│ │ │
└────────────────────┼────────────────────┘
│ gather + merge
┌──────────▼──────────┐
│ Ranking Service │
│ BM25 + PageRank │
│ + ML Ranking Model │
└──────────┬──────────┘

┌──────▼──────┐
│ Top-10 结果 │
└─────────────┘

后台流水线:
[Crawler] → [Raw Storage] → [Indexer (MapReduce)] → [Index Shards]

[Link Graph] → [PageRank 计算]

6. 面试要点总结

维度 关键决策 核心理由
爬虫调度 双层队列(优先级 + 礼貌性)+ DNS 缓存 兼顾重要性和服务器友好
存储选型 对象存储(原始页面)/ Bigtable(元数据)/ 定制 SSTable(索引) 按访问模式分层,避免通用数据库
去重 Bloom Filter(URL)+ SimHash(内容近重复) 空间效率极高,适合 PB 级数据
索引结构 倒排索引 + Doc 分片 + 跳表 + Delta 编码压缩 随机读延迟 < 5ms,压缩比 10:1
查询理解 拼写纠正 + 同义词 + 实体识别 + 向量检索补充 覆盖词汇匹配盲区
排序 两阶段(BM25 粗排 → PageRank+ML 精排) 平衡延迟与质量
新鲜度 Delta Index + 分级爬取(热门页分钟级) 实时性与成本的平衡
PageRank Pregel 迭代 + TrustRank 防 Spam 权威性评估 + 反作弊
高可用 多 Region + 每 Shard 3 副本 + 超时熔断 单点故障不影响整体
可观测性 业务 QoS(CTR/NDCG)+ 技术指标(P99/新鲜度) 发现质量问题和性能问题

搜索系统的设计精髓在于:

  • 用空间换时间:倒排索引、多级缓存、预计算 PageRank
  • 用并行换延迟:scatter-gather 并发查询所有 shard
  • 用分层换精度:粗排快速筛选候选,精排保证质量
  • 按访问模式选存储:不同数据特征对应不同存储引擎,不盲目用通用数据库

真实的 Google 在每个环节都有数十年的工程积累,但核心架构思路与此相通。

参考资料