零依赖构建搜索引擎:只用 Python 3.14 标准库
几乎每个现代 Web 应用在某个时刻都会需要搜索功能。而通常的做法都是一样的:pip install elasticsearch whoosh fastapi uvicorn nltk click,几分钟后就有了一个功能完整但依赖庞大的搜索方案。
一位开发者决定挑战这个常规做法,只用 Python 3.14 的标准库构建了一个零依赖的搜索引擎。
为什么要零依赖
依赖管理是现代软件开发中最令人头疼的问题之一。每添加一个依赖,就意味着:
- 更多的安全攻击面
- 更复杂的版本兼容性问题
- 更大的部署体积
- 更慢的安装速度
- 更多的维护负担
对于很多中小型应用来说,Elasticsearch 这样的重型解决方案可能是过度设计。一个只需要搜索几千篇文章的博客,真的需要一个分布式搜索引擎吗?
零依赖方案的优势在于:简单、轻量、可预测、易于部署和维护。
Python 标准库能做什么
Python 标准库虽然常被认为"电池已包含",但很多开发者可能没有意识到它包含了多少功能。对于构建搜索引擎来说,标准库提供了以下关键组件:
1. 数据结构
dict:哈希表,用于倒排索引set:集合,用于布尔查询collections.defaultdict:自动初始化的字典collections.Counter:计数器,用于词频统计
2. 文本处理
re:正则表达式,用于分词和文本清洗string:字符串常量和工具函数unicodedata:Unicode 数据处理,用于规范化html:HTML 实体解码
3. 数学和统计
math:数学函数,包括对数(用于 TF-IDF 计算)statistics:统计函数
4. 文件和序列化
json:JSON 序列化,用于持久化索引sqlite3:SQLite 数据库,可选的存储后端pickle:Python 对象序列化gzip:压缩,用于减小索引体积
5. 网络和 Web
http.server:HTTP 服务器,用于提供搜索 APIurllib:URL 处理html.parser:HTML 解析,用于抓取网页内容
核心组件实现
分词器
分词是搜索引擎的第一步。一个简单的分词器可以用正则表达式实现:
import re
import string
def tokenize(text):
# 转小写
text = text.lower()
# 用非字母数字字符分割
tokens = re.findall(r'[a-z0-9]+', text)
# 移除停用词
stop_words = {'the', 'a', 'an', 'is', 'are', 'was', 'were', 'in', 'on', 'at', 'to', 'for', 'of', 'and', 'or', 'but'}
return [t for t in tokens if t not in stop_words and len(t) > 1]
倒排索引
倒排索引是搜索引擎的核心数据结构。它将每个词映射到包含该词的文档列表:
from collections import defaultdict
class InvertedIndex:
def __init__(self):
self.index = defaultdict(list) # term -> [(doc_id, position, ...)]
self.doc_lengths = {} # doc_id -> length
self.doc_count = 0
def add_document(self, doc_id, text):
tokens = tokenize(text)
self.doc_lengths[doc_id] = len(tokens)
self.doc_count += 1
for pos, term in enumerate(tokens):
self.index[term].append((doc_id, pos))
TF-IDF 评分
TF-IDF(词频-逆文档频率)是最经典的搜索排序算法:
import math
def tf_idf_score(self, term, doc_id):
# 词频:该词在该文档中出现的次数
tf = sum(1 for d, _ in self.index[term] if d == doc_id)
# 逆文档频率:包含该词的文档数的倒数
df = len(set(d for d, _ in self.index[term]))
idf = math.log(self.doc_count / (df + 1))
return tf * idf
搜索接口
将以上组件组合起来,就得到了一个基本的搜索接口:
def search(self, query, top_k=10):
query_tokens = tokenize(query)
scores = defaultdict(float)
for term in query_tokens:
if term not in self.index:
continue
for doc_id, _ in self.index[term]:
scores[doc_id] += self.tf_idf_score(term, doc_id)
# 按分数排序,返回 top_k
results = sorted(scores.items(), key=lambda x: -x[1])
return results[:top_k]
Web API
用 http.server 提供一个简单的 HTTP 搜索接口:
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
class SearchHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path.startswith('/search'):
query = self.path.split('q=')[1] if 'q=' in self.path else ''
results = index.search(query)
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(results).encode())
性能和局限
零依赖方案在性能上当然无法与 Elasticsearch 等专业搜索引擎相比,但对于很多场景已经足够:
- 索引速度:每秒可以处理数千篇文档
- 搜索速度:在百万级文档上,单次搜索通常在几十毫秒内
- 内存占用:索引可以按需加载,内存占用可控
主要局限包括:
- 不支持分布式
- 不支持实时索引(需要全量重建)
- 分词功能简单(不支持中文分词、词干提取等高级功能)
- 排序算法基础(只有 TF-IDF,没有 BM25、学习排序等)
什么时候该用零依赖方案
零依赖搜索引擎适合以下场景:
- 个人博客、文档站点等小规模搜索
- 离线工具、CLI 工具中的搜索功能
- 原型开发和概念验证
- 对部署体积和启动速度有严格要求的环境
- 学习搜索引擎原理的教学项目
不适合的场景:
- 大规模生产环境(百万级以上文档)
- 需要复杂查询语法和高级排序
- 需要分布式和高可用
- 需要中文等复杂语言的分词
总结
只用 Python 标准库构建搜索引擎是一个有趣且有教育意义的项目。它证明了很多时候我们并不需要庞大的依赖栈,标准库已经提供了足够的基础组件。
当然,这不是说 Elasticsearch 和 Whoosh 这样的工具没有价值——它们在大规模和复杂场景下仍然是不可替代的。但对于很多中小型应用来说,一个零依赖的简单方案可能更合适:更简单、更轻量、更易于理解和维护。
这个项目也提醒我们:在添加依赖之前,先想想标准库是否已经足够。有时候,少即是多。
原文链接:https://dev.to/rushikesh_shedage/i-built-a-search-engine-with-zero-dependencies-just-python-314s-standard-library-2ob2