倒排索引核心结构是键为词项、值为(文档id,位置)元组列表的字典;需统一预处理、稳定文档id、用defaultdict构建、归并求交、过滤停用词与词干化。

倒排索引的核心结构到底长什么样
倒排索引不是某种黑盒数据结构,它本质上就是一个 dict:键是分词后的词项(term),值是包含该词项的文档 ID 列表(或带位置信息的元组列表)。别被“索引”二字吓住——你用 {'python': [1, 3], 'code': [1, 2, 3]} 这种结构就已迈出第一步。关键不在“存”,而在“怎么建”和“怎么查”。
- 文档 ID 必须稳定且可追溯,推荐用整数序号或文件路径哈希(如
hashlib.md5(b'./docs/api.md').hexdigest()[:8]),避免用内存地址或临时变量名 - 分词必须统一:中文要用
jieba或pkuseg,英文至少做小写 + 去标点(re.sub(r'[^a-zA-Z0-9\s]', '', text).lower().split()) - 不建议直接存原始文本行,而应存
(doc_id, position)元组,否则无法支持短语搜索和高亮
用 defaultdict(list) 构建索引比手动检查快得多
手写 if term not in index: index[term] = [] 看似清晰,但每次查找都多一次哈希计算,对百万级词项会明显拖慢构建速度。Python 的 defaultdict(list) 是更自然的选择:
from collections import defaultdict
index = defaultdict(list)
for doc_id, text in enumerate(documents):
tokens = tokenize(text) # 你的分词函数
for pos, term in enumerate(tokens):
index[term].append((doc_id, pos))
-
defaultdict在首次访问不存在的 key 时自动调用list(),无额外判断开销 - 如果后续要序列化(比如存到磁盘),记得转成普通
dict:dict(index),否则 pickle 会报错 - 注意内存:如果文档极大、词项极多,
defaultdict本身不压缩,容易 OOM;此时应考虑分块构建或用sqlite3落盘
查询时 AND/OR 操作不能靠 Python 列表推导硬算
用户搜 "python AND code",你若写 set(index['python']) & set(index['code']),看似简洁,但实际在反复构造大集合,性能随文档量指数下降。更务实的做法是:
- 对单个词项,直接取
index.get('python', []),返回的是已排序的 doc_id 列表(因插入顺序即文档遍历顺序) - 多词 AND:用双指针归并(类似合并两个有序数组),时间复杂度 O(m+n),远优于集合交集
- 多词 OR:直接拼接后去重,
list(set(a + b))可接受,但注意去重后丢失顺序;如需按相关性排序,得另加打分逻辑 - 若查不到某词项(
index.get(term)返回None),整个 AND 查询结果为空,可提前退出
停用词和词干化不是“锦上添花”,而是索引可用的前提
没过滤停用词(如 'the', 'is', '的', '了'),索引体积可能膨胀 3–5 倍,且严重稀释倒排链长度,导致查询变慢、内存暴涨。词干化(如 'running' → 'run')则决定召回率。
- 英文停用词表直接用
nltk.corpus.stopwords.words('english'),但注意先nltk.download('stopwords') - 中文停用词建议用哈工大或百度开源列表(如
hit_stopwords.txt),别自己手写几条应付 - 词干化优先选
nltk.stem.PorterStemmer(轻量)或nltk.stem.WordNetLemmatizer(准确但重),避免用正则粗暴截断(如删 ing/ed) - 所有预处理(分词→去停用→词干化)必须在构建索引和查询时完全一致,否则查
'runs'永远匹配不到index['run']
真正卡住人的往往不是结构设计,而是分词一致性、停用词覆盖不全、以及把倒排链当无序列表暴力求交——这些细节不抠清楚,索引建得再“标准”,一上线就变慢。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











