
从零构建搜索引擎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?qxxx → 查询解析 → 排序 → 返回结果 │ ├─────────────────────────────────────────────────┤ │ 搜索核心 │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ 分词器 │ │ 索引引擎 │ │ 排序算法 │ │ │ │ (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_itemTextField(css_selecth1)# 提取标题contentHtmlField(css_selectbody)# 提取正文classBlogSpider(ruia.Spider):start_urls[http://localhost:8765/index.html]crawled[]asyncdefparse(self,response):解析列表页提取文章链接fora_eleminresponse.html_etree.cssselect(a[href]):urla_elem.get(href)ifurlandurl.endswith(.html):yieldself.request(urlfull_url,callbackself.parse_article)asyncdefparse_article(self,response):解析文章详情页itemawaitBlogItem.get_item(htmlawaitresponse.text())self.crawled.append({...})# 启动异步爬虫asyncdefrun_spider():spiderBlogSpider()awaitspider.run()returnspider spiderasyncio.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搜索引擎的核心是倒排索引wordslist(jieba.cut(text))# 结果: [搜索引擎, 的, 核心, 是, 倒排索引]3.3 构建倒排索引核心步骤加载爬取文档用 jieba 分词过滤停用词“的”、“了”、是等记录词→文档ID→位置的映射计算每个词的 TF词频inverted_indexdefaultdict(list)fordocindocs:wordslist(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(tf2) flask 1 doc4(tf3) gamma 1 doc5(tf3) numpy 1 doc1(tf2) pagerank 1 doc3(tf2) python 3 doc1(tf4), doc2(tf1), doc4(tf4) sanic 1 doc4(tf3) 高频词 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二进制编码结果位数111131101135101001015101010000101071001100100000000110010013编码规则将 n 转为二进制前面加len(binary)-1个0首位的1作为分隔符defelias_gamma_encode(n):binarybin(n)[2:]# 101offsetbinary[1:]# 01return0*(len(binary)-1)1offset# 001014.3 差值编码 (Delta Encoding)压缩倒排索引时先对文档 ID 做差原始: [1, 5, 9, 20] 差值: [1, 4, 4, 11] ← 差值更小编码更短4.4 压缩效果--- Elias Gamma 编码 --- 编码/解码验证: n1 → 编码: 1 → 解码: 1 ✓ n3 → 编码: 011 → 解码: 3 ✓ n5 → 编码: 00101 → 解码: 5 ✓ n10 → 编码: 0001010 → 解码: 10 ✓ n50 → 编码: 00000110010 → 解码: 50 ✓ n100 → 编码: 0000001100100 → 解码: 100 ✓ n255 → 编码: 000000011111111 → 解码: 255 ✓ n1000 → 编码: 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/DjangofromsanicimportSanic,response appSanic(MiniSearch)app.route(/api/search)asyncdefsearch_api(request):queryrequest.args.get(q,)resultsperform_search(query)# 调用搜索核心returnresponse.json({results:results})5.2 前端界面用纯 HTML/CSS/JS 编写搜索页面divclasssearch-boxinputtypetextidqueryplaceholder输入搜索关键词...buttononclicksearch()搜索/button/divscriptasyncfunctionsearch(){constrespawaitfetch(/api/search?qencodeURIComponent(q));constdataawaitresp.json();// 渲染结果...}/script5.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 请求示例curlhttp://localhost:8899/api/search?qpython# 返回 JSON: {results: [...], total: 3}六、搜索结果排序CosineSimilarity PageRank6.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_simnp.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 d0.85d0.85为阻尼系数。defcompute_pagerank(docs,damping0.85):# 构建相似度矩阵模拟链接关系transition归一化后的链接矩阵# 迭代收敛prnp.ones(n)/nfor_inrange(max_iter):pr(1-damping)/ndamping*transition.T prreturnpr6.3 综合排序加权融合余弦相似度和 PageRankS 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: PR0.291892 | Python数据分析入门教程 doc2: PR0.199665 | 常见机器学习算法 doc3: PR0.200000 | 搜索引擎是如何工作的 doc4: PR0.108443 | Python Web框架Sanic vs Flask doc5: PR0.200000 | 倒排索引优化与Elias Gamma压缩 --- 综合排序 (Cosine 70% PageRank 30%) --- 查询: Python数据分析 CS0.6777 PR0.291892 → Combined1.3501 | Python数据分析入门教程 CS0.0559 PR0.199665 → Combined0.6381 | 常见机器学习算法 查询: 搜索引擎倒排索引 CS0.6951 PR0.200000 → Combined1.0866 | 搜索引擎是如何工作的 查询: 机器学习算法 CS0.2674 PR0.291892 → Combined1.0629 | Python数据分析入门教程 CS0.6593 PR0.199665 → Combined1.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 RedisBM25 排序算法替代 TF-IDF参考: 本文基于实验楼《从零构建搜索引擎实战》课程在华为云 FlexusX ECS (8vCPU 16GiB, Ubuntu 24.04) 完成全部上机实战。源码地址: 所有代码可在服务器/tmp/search_demo/和/tmp/se_*.py找到可直接复现。本文由 Python 搜索引擎实战训练营产出 | 2026年7月5日