倒排索引 (Inverted Index)
倒排索引:翻转过来的世界
算法背后的故事:19 世纪的索引卡片
在计算机出现之前,历史学家和研究人员面临着一个噩梦。如果你想在拥有 10,000 本书的图书馆里找到所有提到“自由”的地方,你必须读完每一本书。
为了解决这个问题,图书馆员开始创建 词彙索引 (Concordance)。他们会拿出一本书,列出其中每一个唯一的单词,并在一张小索引卡片上记下该单词出现的每个页码。
他们不再记录:书 A [单词 1, 单词 2, 单词 3…] 而是将其翻转为:单词 1 [书 A, 书 B, 书 D…]
这次“翻转”改变了人类知识的历史。它让研究人员能在几秒钟内从大海中捞出那根针。今天,这种起源于 19 世纪的技术——现在被称为倒排索引——成为了 Elasticsearch、Lucene 以及你使用的每一个搜索框的核心技术。
为什么需要它?
在标准数据库(如 SQL)中,你通常通过 ID 寻找一行数据。但在搜索引擎中,你是通过内容来寻找“文档”。
- 痛点: 如果你有 1 亿篇博客,搜索“常用算法”,标准数据库必须扫描每一篇文章的每一个字 ()。这需要花费数分钟。
- 解决方案: 倒排索引。当一篇文档被保存时,我们将其切碎成单词(Token),并存储一张“哪个单词属于哪篇文档”的地图。当你搜索时,我们只需要查地图 ()。
算法是如何“思考”的
这个算法像是一个严谨的图书馆员,执行着一种“反向映射”:
分词 (Tokenization): 它拿出一个句子,比如“To be or not to be”,将其切碎成独立的单词:
[to, be, or, not]。它会忽略大小写和标点符号。大翻转 (Inversion): 它不再记忆“文档 1 包含这些单词”,而是构建一张以 单词 为 Key 的巨大表格。
to[文档 1, 文档 2, 文档 5]be[文档 1, 文档 3]or[文档 1]
求交集 (Intersection): 当你搜索“to be”时,图书馆员查看
to和be的列表,找到它们的交集:即同时出现在两个列表中的文档。
工程决策:速度的代价
倒排索引是 Elasticsearch 搜索极快、但写入相对较慢的原因。
- 权衡: 每次你添加一篇文档,系统都必须更新索引中成千上万个词条的列表。这在计算上是昂贵的,并且会占用大量磁盘空间。
- 压缩: 因为这些列表(倒排列表 Postings List)可能非常巨大,工程师们使用了精巧的位打包 (Bit-packing) 技术,以确保索引能足够小,甚至能放进内存 (RAM)。
实现参考 (Python)
from collections import defaultdict
def create_inverted_index(documents):
"""
documents: 字典格式 {doc_id: "内容字符串"}
"""
index = defaultdict(set)
for doc_id, content in documents.items():
# 1. 分词:转换为小写并拆分为单词
words = content.lower().split()
# 2. 翻转:建立 单词 -> 文档ID 的映射
for word in words:
index[word].add(doc_id)
return index
def search(query, index):
query_words = query.lower().split()
if not query_words:
return set()
# 3. 求交集:找到包含查询中所有单词的文档
results = index.get(query_words[0], set())
for word in query_words[1:]:
results = results.intersection(index.get(word, set()))
return results
# 使用示例
# docs = {
# 1: "Common algorithms for engineers",
# 2: "The soul of a new machine",
# 3: "How to find common ground"
# }
# idx = create_inverted_index(docs)
# print(search("common", idx)) # 输出: {1, 3}小结
倒排索引教会我们,如果你想快速找到东西,就必须停止通过“我有什么”的视角观察,转而通过“人们想要什么”的视角观察。通过翻转世界,我们将“读完所有书”这一不可能的任务,变成了“查阅地图”这一简单动作。
