3. 系统架构设计:整体架构图、模块划分与数据流设计
好,咱们进入第三个章节。说实话,很多初学者一上来就撸代码,结果写到一半发现各个模块耦合在一起,改一个地方崩一片。我早期做搜索项目时就吃过这个亏——爬虫和索引写在一个类里,后来想换一个爬虫策略,差点把整个索引重建了。
所以,在动手之前,先把架构想清楚。这就像盖房子,先画好图纸再砌砖。今天我就带你把这个「图纸」画出来。
3.1 整体架构图:四层结构
我个人习惯把搜索引擎拆成四个核心层。你想想看,用户输入一个关键词,到看到结果,中间经历了什么?
- 数据采集层:负责从互联网上抓取网页内容。说白了就是爬虫。
- 数据处理层:把抓回来的原始网页清洗、解析、建立索引。这是最脏最累的活。
- 检索服务层:接收用户的查询,去索引里找匹配的文档,然后排序返回。
- API 网关层:对外提供统一的接口,比如 RESTful API,顺便做限流、鉴权。
这四层之间是单向依赖的。数据从采集层往下流,最终流到 API 层。千万别搞成循环依赖,否则调试起来你会想哭的。
核心原则:每一层只依赖它的下一层,绝不跨层调用。爬虫不直接访问索引,API 不直接操作爬虫。
3.2 模块划分:五个核心模块
再往下拆,每个层里都有具体的模块。我按实际项目经验给你列一下:
3.2.1 爬虫模块 (Crawler)
- URL 管理器:维护待爬取队列和已爬取集合。我用 Redis 的 Set 来去重,效率很高。
- 下载器:发送 HTTP 请求,获取网页内容。记得设置 User-Agent 和超时时间,不然容易被封。
- 解析器:从 HTML 里提取正文、标题、链接。BeautifulSoup 够用,但追求性能可以用 lxml。
避坑指南:我曾经在解析器里直接用正则表达式提取正文,结果遇到嵌套标签就全乱了。后来改用基于 DOM 树的提取方式,稳定多了。
3.2.2 索引模块 (Indexer)
- 分词器:把中文句子切成一个个词。比如「搜索引擎实战」切分成「搜索」「引擎」「实战」。
- 倒排索引构建器:建立词到文档的映射。这是搜索引擎的核心数据结构。
- 索引存储器:把索引持久化到磁盘。我一般用倒排文件格式,按块存储,方便增量更新。
3.2.3 检索模块 (Searcher)
- 查询解析器:把用户输入的查询字符串解析成内部查询对象。比如处理布尔操作符、短语查询。
- 评分排序器:计算每个文档和查询的相关性分数。TF-IDF 是基础,BM25 是进阶。
- 结果缓存:对热门查询缓存结果,减少重复计算。我用 LRU 缓存,命中率大概在 30% 左右。
3.2.4 API 模块
- REST 接口:提供
/search?q=关键词这样的接口。 - 限流器:防止恶意调用。我用令牌桶算法,每秒限制 100 个请求。
- 结果格式化:把内部数据结构转成 JSON 返回给前端。
3.2.5 存储模块
- 原始网页库:存储抓取到的 HTML 原文。用 MongoDB 或者直接存文件。
- 元数据库:存储 URL、标题、抓取时间等结构化信息。MySQL 或者 SQLite 都行。
- 索引库:倒排索引文件。自己定义二进制格式,或者用 LevelDB 这种嵌入式数据库。
3.3 数据流设计:一条查询的完整旅程
光有模块还不够,你得知道数据是怎么在这些模块之间流动的。我画个流程图给你看:
用户输入 "搜索引擎实战"
↓
API 网关接收请求
↓
查询解析器分词 → ["搜索", "引擎", "实战"]
↓
检索模块去索引库查找每个词对应的文档ID
↓
合并结果集,计算 BM25 分数
↓
按分数排序,取 Top 10
↓
从原始网页库获取标题和摘要
↓
格式化 JSON 返回给用户
整个过程大概在 50 毫秒以内完成。如果超过 200 毫秒,用户就会觉得卡。嗯,这里要注意,缓存一定要加在查询解析器之后、检索模块之前,这样能省掉最耗时的磁盘 I/O。
警告:千万别把缓存放在 API 网关层。我见过有人这么干,结果不同用户的不同查询参数混在一起,返回了错误的结果。缓存一定要基于查询的哈希值来做。
3.4 模块间通信:接口约定
模块之间怎么通信?我建议用明确的接口定义,而不是共享内存或者全局变量。举个例子:
| 模块 | 接口 | 输入 | 输出 |
|---|---|---|---|
| 爬虫 → 索引 | index_document(doc_id, title, content) |
文档 ID、标题、正文 | 成功/失败状态 |
| 检索 → 索引 | search_index(term) |
查询词 | 文档 ID 列表及位置信息 |
| API → 检索 | search(query, page, size) |
查询字符串、分页参数 | 搜索结果列表 |
这样做的好处是,每个模块都可以独立测试、独立部署。我当年重构爬虫模块时,只要保证接口不变,索引模块完全不用动。
3.5 部署视角:单机 vs 分布式
我们这个课程的项目,初期跑在单机上就够了。但架构设计时要留好扩展点。比如:
- 爬虫模块可以拆成多个 worker,用消息队列分发任务。
- 索引模块可以分片存储,每台机器只存一部分索引。
- 检索模块可以做成无状态服务,前面加负载均衡。
说白了,先让它在单机上跑通,再考虑分布式。别一开始就上微服务,那是给自己找麻烦。
我的建议:先实现一个最小可行产品(MVP),只包含爬虫、索引、检索三个核心模块,API 用 Flask 简单封装。等跑通了再逐步优化。
3.6 小结
这一章我们画了整体架构图,拆了五个核心模块,理清了数据流动的方向。你可能会觉得有点抽象,没关系,后面每一章都会对应到具体的代码实现。
下一章,咱们就开始写爬虫了。到时候你会看到,今天画的这些模块,是怎么一行一行变成代码的。