从零构建搜索引擎:Python 异步爬虫 + 倒排索引 + Sanic 前后端实战

🚀 从零构建搜索引擎:Python 异步爬虫 + 倒排索引 + Sanic 前后端实战

关键词: 搜索引擎、Ruia异步爬虫、倒排索引、Elias Gamma压缩、PageRank、CosineSimilarity、Sanic异步框架

运行环境: Ubuntu 24.04 · Python 3.12.3 · ECS 8vCPU 16GiB

读完本文你将亲自动手实现一个完整的迷你搜索引擎------从爬虫抓取到搜索结果排序,全部用 Python 实现。


📋 目录

  • 一、搜索引擎架构设计
  • [二、Ruia 异步爬虫系统](#二、Ruia 异步爬虫系统)
  • 三、倒排索引结构
  • [四、Elias Gamma 编码与索引压缩](#四、Elias Gamma 编码与索引压缩)
  • [五、Sanic 异步 API + 前端页面](#五、Sanic 异步 API + 前端页面)
  • [六、搜索结果排序:CosineSimilarity + PageRank](#六、搜索结果排序: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 页面作为爬取目标:

python 复制代码
# 爬虫定义
class BlogItem(Item):
    """定义要提取的数据字段"""
    target_item = TextField(css_select='h1')   # 提取标题
    content = HtmlField(css_select='body')      # 提取正文

class BlogSpider(ruia.Spider):
    start_urls = ['http://localhost:8765/index.html']
    crawled = []
    
    async def parse(self, response):
        """解析列表页,提取文章链接"""
        for a_elem in response.html_etree.cssselect('a[href]'):
            url = a_elem.get('href')
            if url and url.endswith('.html'):
                yield self.request(url=full_url, callback=self.parse_article)
    
    async def parse_article(self, response):
        """解析文章详情页"""
        item = await BlogItem.get_item(html=await response.text())
        self.crawled.append({...})

# 启动异步爬虫
async def run_spider():
    spider = BlogSpider()
    await spider.run()
    return spider

spider = asyncio.run(run_spider())

爬虫运行结果

text 复制代码
创建了 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)

中文没有天然的空格分隔,需要分词工具:

python 复制代码
import jieba

text = "搜索引擎的核心是倒排索引"
words = list(jieba.cut(text))
# 结果: ['搜索引擎', '的', '核心', '是', '倒排索引']

3.3 构建倒排索引

核心步骤:

  1. 加载爬取文档
  2. 用 jieba 分词
  3. 过滤停用词("的"、"了"、"是"等)
  4. 记录词→文档ID→位置的映射
  5. 计算每个词的 TF(词频)
python 复制代码
inverted_index = defaultdict(list)

for doc in docs:
    words = list(jieba.cut(text))
    for pos, word in enumerate(words):
        if len(word) <= 1 or word in stop_words:
            continue
        inverted_index[word].append({
            'doc_id': doc['id'],
            'tf': frequency,      # 词频
            'positions': [pos1, ...]  # 位置列表
        })

索引构建结果

text 复制代码
加载 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

编码规则

  1. 将 n 转为二进制
  2. 前面加 len(binary)-10
  3. 首位的 1 作为分隔符
python 复制代码
def elias_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 压缩效果

text 复制代码
--- 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:

python 复制代码
from sanic import Sanic, response

app = Sanic("MiniSearch")

@app.route("/api/search")
async def search_api(request):
    query = request.args.get("q", "")
    results = perform_search(query)  # 调用搜索核心
    return response.json({"results": results})

5.2 前端界面

用纯 HTML/CSS/JS 编写搜索页面:

html 复制代码
<div class="search-box">
    <input type="text" id="query" placeholder="输入搜索关键词...">
    <button onclick="search()">搜索</button>
</div>
<script>
    async function search() {
        const resp = await fetch('/api/search?q=' + encodeURIComponent(q));
        const data = await resp.json();
        // 渲染结果...
    }
</script>

5.3 启动服务 & API 测试

text 复制代码
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 请求示例

bash 复制代码
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

python 复制代码
# 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.85 d=0.85 为阻尼系数。

python 复制代码
def compute_pagerank(docs, damping=0.85):
    # 构建相似度矩阵(模拟链接关系)
    transition = 归一化后的链接矩阵
    
    # 迭代收敛
    pr = np.ones(n) / n
    for _ in range(max_iter):
        pr = (1-damping)/n + damping * transition.T @ pr
    return pr

6.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 PageRank Score=α⋅CosineSim+(1−α)⋅PageRank

排序结果对比

text 复制代码
--- 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/search REST 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日

相关推荐
2401_868534781 小时前
网络规划设计师学习笔记
python·计算机网络
安然无虞1 小时前
Python自动化测试·Selenium操控元素的方法
python·selenium·测试工具
浩瀚之水_csdn2 小时前
Python 3 网络编程详解:从原理到实战
开发语言·网络·python
满怀冰雪2 小时前
24_中间件系统源码分析_Middleware链的洋葱模型与异常处理
人工智能·python·中间件·langchain
江华森2 小时前
03-python-包与模块异常处理
python
AI行业学习2 小时前
2026 版 Notepad++ 完整图文安装指南|官方渠道无捆绑,一键切换中文界面
开发语言·人工智能·python·html·notepad++
炘东5922 小时前
求助:在另一个 Reasonix 窗口或后台运行
python
apcipot_rain2 小时前
计科八股2026705——正态分布、独立与不相关、范数、列表元组集合、链表队列、sort函数、URL执行
python·计算机网络·概率论
江华森2 小时前
Python 编程快速上手:让繁琐工作自动化 —— 19章全实战笔记
笔记·python·自动化