搜索引擎是互联网最核心的基础设施之一。本文从系统设计角度,拆解如何从零设计一个类 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 查询每分钟预热,直接返回
|
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 在每个环节都有数十年的工程积累,但核心架构思路与此相通。
参考资料