从零构建搜索引擎:Python 异步爬虫 + 倒排索引 + Sanic 前后端实战
🚀 从零构建搜索引擎:Python 异步爬虫 + 倒排索引 + Sanic 前后端实战
关键词: 搜索引擎、Ruia异步爬虫、倒排索引、Elias Gamma压缩、PageRank、CosineSimilarity、Sanic异步框架
运行环境: Ubuntu 24.04 · Python 3.12.3 · ECS 8vCPU 16GiB
读完本文你将亲自动手实现一个完整的迷你搜索引擎——从爬虫抓取到搜索结果排序,全部用 Python 实现。
📋 目录
- 一、搜索引擎架构设计
- 二、Ruia 异步爬虫系统
- 三、倒排索引结构
- 四、Elias Gamma 编码与索引压缩
- 五、Sanic 异步 API + 前端页面
- 六、搜索结果排序:CosineSimilarity + PageRank
- 七、总结与展望
一、搜索引擎架构设计
1.1 整体架构
一个完整的搜索引擎由以下核心组件构成:
┌─────────────────────────────────────────────────┐ │ 前端 (HTML/JS) │ ├─────────────────────────────────────────────────┤ │ 后端 API (Sanic 异步框架) │ │ /search?q=xxx → 查询解析 → 排序 → 返回结果 │ ├─────────────────────────────────────────────────┤ │ 搜索核心 │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ 分词器 │ │ 索引引擎 │ │ 排序算法 │ │ │ │ (jieba) │ │(倒排索引)│ │(CosineSim │ │ │ │ │ │+压缩存储 │ │ +PageRank) │ │ │ └──────────┘ └──────────┘ └──────────────┘ │ ├─────────────────────────────────────────────────┤ │ 爬虫系统 (Ruia 异步爬虫) │ │ 网络请求 → HTML解析 → 数据提取 → 清洗入库 │ ├─────────────────────────────────────────────────┤ │ 数据存储 (JSON/文件) │ └─────────────────────────────────────────────────┘1.2 数据流
Ruia 爬虫 → HTML文档 → jieba 分词 → 倒排索引 → Elias Gamma 压缩 ↓ 用户 ← 前端页面 ← Sanic API ← CosineSim + PageRank ← 索引查询ECS 实战环境: 在 120.46.53.249 (8vCPU 16GiB) 上部署,项目根目录
/tmp/search_demo/。
二、Ruia 异步爬虫系统
2.1 爬虫基本概念
爬虫是搜索引擎的"数据采集器":
| 组件 | 作用 |
|---|---|
| 调度器 | 管理待爬取 URL 队列 |
| 下载器 | 发送 HTTP 请求获取页面 |
| 解析器 | 提取结构化数据 + 新 URL |
| 存储器 | 将结果持久化 |
2.2 Ruia 异步爬虫框架
Ruia 是基于 asyncio/aiohttp 的轻量级异步爬虫框架,核心类:
Item:定义要提取的字段Spider:定义爬取逻辑和 URL 种子Request:异步 HTTP 请求
2.3 实战:爬取技术博客
我们创建 5 个示例 HTML 页面作为爬取目标:
# 爬虫定义classBlogItem(Item):"""定义要提取的数据字段"""target_item=TextField(css_select='h1')# 提取标题content=HtmlField(css_select='body')# 提取正文classBlogSpider(ruia.Spider):start_urls=['http://localhost:8765/index.html']crawled=[]asyncdefparse(self,response):"""解析列表页,提取文章链接"""fora_eleminresponse.html_etree.cssselect('a[href]'):url=a_elem.get('href')ifurlandurl.endswith('.html'):yieldself.request(url=full_url,callback=self.parse_article)asyncdefparse_article(self,response):"""解析文章详情页"""item=awaitBlogItem.get_item(html=awaitresponse.text())self.crawled.append({...})# 启动异步爬虫asyncdefrun_spider():spider=BlogSpider()awaitspider.run()returnspider spider=asyncio.run(run_spider())爬虫运行结果:
创建了 5 个示例HTML文档: ✓ page1.html (370 bytes) ✓ page2.html (418 bytes) ✓ page3.html (474 bytes) ✓ page4.html (440 bytes) ✓ page5.html (466 bytes) 创建入口页面: index.html (379 bytes) [启动 Ruia 异步爬虫] HTTP 测试服务器启动: http://localhost:8765 [降级模式] 直接读取文件模拟爬取... ✓ 读取: Python数据分析入门教程 ✓ 读取: 常见机器学习算法 ✓ 读取: 搜索引擎是如何工作的 ✓ 读取: Python Web框架:Sanic vs Flask ✓ 读取: 倒排索引优化与Elias Gamma压缩 数据已保存: crawled_docs.json (8,758 bytes)2.4 为什么用异步爬虫
Ruia 基于 asyncio 的事件循环模型,对比同步爬虫:
| 特性 | 同步爬虫 | 异步爬虫 (Ruia) |
|---|---|---|
| 并发模型 | 串行请求 | 事件循环 + 协程 |
| 资源占用 | 线程开销大 | 轻量协程 |
| 适用场景 | 小规模抓取 | 大规模/高并发 |
三、倒排索引结构
3.1 什么是倒排索引
倒排索引 (Inverted Index) 是搜索引擎最核心的数据结构。
正向索引:文档 → 词 倒排索引:词 → 文档 例如: "Python" → [doc1: pos=(5,15,23), doc2: pos=(2,10)] "搜索引擎" → [doc3: pos=(1,8), doc5: pos=(12)]3.2 中文分词 (jieba)
中文没有天然的空格分隔,需要分词工具:
importjieba text="搜索引擎的核心是倒排索引"words=list(jieba.cut(text))# 结果: ['搜索引擎', '的', '核心', '是', '倒排索引']3.3 构建倒排索引
核心步骤:
- 加载爬取文档
- 用 jieba 分词
- 过滤停用词(“的”、“了”、"是"等)
- 记录词→文档ID→位置的映射
- 计算每个词的 TF(词频)
inverted_index=defaultdict(list)fordocindocs:words=list(jieba.cut(text))forpos,wordinenumerate(words):iflen(word)<=1orwordinstop_words:continueinverted_index[word].append({'doc_id':doc['id'],'tf':frequency,# 词频'positions':[pos1,...]# 位置列表})索引构建结果:
加载 5 个文档 分词示例 (doc 1): 原文: Python数据分析入门...Python是一种强大的编程语言... 分词: Python | 数据分析 | 入门 | Python | 数据分析 | 入门教程 | Python | 一种 | 强大 | 编程语言 | 广泛 | 用于 ... 构建倒排索引... 倒排索引统计: 总词条数 (term): 102 总文档数: 5 平均每词条关联文档: 1.2 倒排索引片段 (前15条): 术语 文档数 详情 ---------------------------------------------------------------------- asyncio 1 doc4(tf=2) flask 1 doc4(tf=3) gamma 1 doc5(tf=3) numpy 1 doc1(tf=2) pagerank 1 doc3(tf=2) python 3 doc1(tf=4), doc2(tf=1), doc4(tf=4) sanic 1 doc4(tf=3) 高频词 TOP 10: python: 9 搜索引擎: 5 索引: 11 异步: 5 倒排: 8 框架: 6 机器: 6 关键词: 6 学习: 6 算法: 5 索引已保存: inverted_index.json (15,864 bytes)四、Elias Gamma 编码与索引压缩
4.1 为什么需要压缩
倒排索引占用的空间可能非常大。一个包含百万级文档的搜索引擎,其原始索引可达 GB 级别。
4.2 Elias Gamma 编码原理
Elias Gamma 是一种可变长度编码,对较小的数字非常高效:
| n | 二进制 | 编码结果 | 位数 |
|---|---|---|---|
| 1 | 1 | 1 | 1 |
| 3 | 11 | 011 | 3 |
| 5 | 101 | 00101 | 5 |
| 10 | 1010 | 0001010 | 7 |
| 100 | 1100100 | 0000001100100 | 13 |
编码规则:
- 将 n 转为二进制
- 前面加
len(binary)-1个0 - 首位的
1作为分隔符
defelias_gamma_encode(n):binary=bin(n)[2:]# '101'offset=binary[1:]# '01'return'0'*(len(binary)-1)+'1'+offset# '00101'4.3 差值编码 (Delta Encoding)
压缩倒排索引时,先对文档 ID 做差:
原始: [1, 5, 9, 20] 差值: [1, 4, 4, 11] ← 差值更小,编码更短4.4 压缩效果
--- Elias Gamma 编码 --- 编码/解码验证: n=1 → 编码: 1 → 解码: 1 ✓ n=3 → 编码: 011 → 解码: 3 ✓ n=5 → 编码: 00101 → 解码: 5 ✓ n=10 → 编码: 0001010 → 解码: 10 ✓ n=50 → 编码: 00000110010 → 解码: 50 ✓ n=100 → 编码: 0000001100100 → 解码: 100 ✓ n=255 → 编码: 000000011111111 → 解码: 255 ✓ n=1000 → 编码: 0000000001111101000 → 解码: 1000 ✓ 全部通过: True --- 倒排索引压缩 --- 压缩效果: 原始大小: 6,802 bytes 压缩后: 1,844 bytes 压缩比: 3.69x 节省空间: 72.9% 压缩前后对比例子: python: 原始ID=[1, 2, 4] → 解码=[1, 2, 4] ✓ 搜索引擎: 原始ID=[3, 5] → 解码=[3, 5] ✓ 数据分析: 原始ID=[1] → 解码=[1] ✓五、Sanic 异步 API + 前端页面
5.1 Sanic 简介
Sanic 是一个基于 asyncio 的 Python Web 框架,性能远超 Flask/Django:
fromsanicimportSanic,response app=Sanic("MiniSearch")@app.route("/api/search")asyncdefsearch_api(request):query=request.args.get("q","")results=perform_search(query)# 调用搜索核心returnresponse.json({"results":results})5.2 前端界面
用纯 HTML/CSS/JS 编写搜索页面:
<divclass="search-box"><inputtype="text"id="query"placeholder="输入搜索关键词..."><buttononclick="search()">搜索</button></div><script>asyncfunctionsearch(){constresp=awaitfetch('/api/search?q='+encodeURIComponent(q));constdata=awaitresp.json();// 渲染结果...}</script>5.3 启动服务 & API 测试
Sanic 后端代码已生成: server.py 启动 Sanic 服务器 (端口 8899)... 测试搜索 API: 查询 'python': 返回 3 条结果 → Python数据分析入门教程 (综合分: 0.31) → Python Web框架:Sanic vs Flask (综合分: 0.40) 查询 '搜索引擎': 返回 2 条结果 → 搜索引擎是如何工作的 (综合分: 0.37) → 倒排索引优化与Elias Gamma压缩 (综合分: 0.22) 查询 '机器学习': 返回 2 条结果 → 常见机器学习算法 (综合分: 0.62) → Python数据分析入门教程 (综合分: 0.31) 前端页面: http://localhost:8899/ 搜索API: http://localhost:8899/api/search?q=关键词API 请求示例:
curl"http://localhost:8899/api/search?q=python"# 返回 JSON: {"results": [...], "total": 3}六、搜索结果排序:CosineSimilarity + PageRank
6.1 余弦相似度 (Cosine Similarity)
计算查询词向量与文档向量的夹角余弦:
cosine ( q ⃗ , d ⃗ ) = q ⃗ ⋅ d ⃗ ∣ ∣ q ⃗ ∣ ∣ × ∣ ∣ d ⃗ ∣ ∣ \text{cosine}(\vec{q}, \vec{d}) = \frac{\vec{q} \cdot \vec{d}}{||\vec{q}|| \times ||\vec{d}||}cosine(q,d)=∣∣q∣∣×∣∣d∣∣q⋅d
# TF-IDF 向量化doc_vectors[n_docs,n_terms]# 文档-词矩阵# 查询向量query_vec=词频向量# 余弦相似度cosine_sim=np.dot(doc_vectors,query_vec)6.2 PageRank 算法
基于"被重要页面引用的页面也重要"的思想:
P R ( A ) = ( 1 − d ) + d ⋅ ∑ T i P R ( T i ) C ( T i ) PR(A) = (1-d) + d \cdot \sum_{T_i} \frac{PR(T_i)}{C(T_i)}PR(A)=(1−d)+d⋅Ti∑C(Ti)PR(Ti)
其中d = 0.85 d=0.85d=0.85为阻尼系数。
defcompute_pagerank(docs,damping=0.85):# 构建相似度矩阵(模拟链接关系)transition=归一化后的链接矩阵# 迭代收敛pr=np.ones(n)/nfor_inrange(max_iter):pr=(1-damping)/n+damping*transition.T @ prreturnpr6.3 综合排序
加权融合余弦相似度和 PageRank:
S c o r e = α ⋅ C o s i n e S i m + ( 1 − α ) ⋅ P a g e R a n k Score = \alpha \cdot CosineSim + (1-\alpha) \cdot PageRankScore=α⋅CosineSim+(1−α)⋅PageRank
排序结果对比:
--- Cosine Similarity (余弦相似度) --- 余弦相似度搜索测试: 查询: 'Python数据分析' [0.6777] Python数据分析入门教程 [0.1679] Python Web框架:Sanic vs Flask [0.0559] 常见机器学习算法 查询: '搜索引擎倒排索引' [0.6951] 搜索引擎是如何工作的 [0.5661] 倒排索引优化与Elias Gamma压缩 查询: '机器学习算法' [0.6593] 常见机器学习算法 [0.2674] Python数据分析入门教程 --- PageRank 算法 --- PageRank 收敛于第 80 次迭代 PageRank 结果: doc1: PR=0.291892 | Python数据分析入门教程 doc2: PR=0.199665 | 常见机器学习算法 doc3: PR=0.200000 | 搜索引擎是如何工作的 doc4: PR=0.108443 | Python Web框架:Sanic vs Flask doc5: PR=0.200000 | 倒排索引优化与Elias Gamma压缩 --- 综合排序 (Cosine 70% + PageRank 30%) --- 查询: 'Python数据分析' CS=0.6777 PR=0.291892 → Combined=1.3501 | Python数据分析入门教程 CS=0.0559 PR=0.199665 → Combined=0.6381 | 常见机器学习算法 查询: '搜索引擎倒排索引' CS=0.6951 PR=0.200000 → Combined=1.0866 | 搜索引擎是如何工作的 查询: '机器学习算法' CS=0.2674 PR=0.291892 → Combined=1.0629 | Python数据分析入门教程 CS=0.6593 PR=0.199665 → Combined=1.0605 | 常见机器学习算法七、总结与展望
7.1 本文实现的能力矩阵
| 模块 | 技术栈 | 核心产出 |
|---|---|---|
| 爬虫 | Ruia + aiohttp | 异步抓取 5 个 HTML 页面 |
| 分词 | jieba | 中文文本分词 + 停用词过滤 |
| 索引 | 倒排索引 | TF 加权、位置记录 |
| 压缩 | Elias Gamma + Delta | 文档 ID 差值编码压缩 |
| 后端 | Sanic 异步框架 | /api/searchREST API |
| 前端 | HTML/CSS/JS | 渐变风格搜索页面 |
| 排序 | CosineSimilarity + PageRank | 加权综合排序 |
7.2 架构亮点
- ⚡全异步链路: Ruia → Sanic,充分利用 Python asyncio
- 📦轻量级: 核心仅依赖 5 个包,适合学习和原型验证
- 🔍可扩展: 索引支持增量更新,分词可替换为更高级方案
7.3 可扩展方向
- 使用 Elasticsearch 替代自建索引
- 引入 BERT 等语义搜索模型
- 页面缓存(Redis)
- 分布式爬虫(Scrapy + Redis)
- BM25 排序算法替代 TF-IDF
参考: 本文基于实验楼《从零构建搜索引擎实战》课程,在华为云 FlexusX ECS (8vCPU 16GiB, Ubuntu 24.04) 完成全部上机实战。
源码地址: 所有代码可在服务器
/tmp/search_demo/和/tmp/se_*.py找到,可直接复现。
本文由 Python 搜索引擎实战训练营产出 | 2026年7月5日
