📖什么是倒排索引?

搜索引擎最核心的数据结构——把"文档到词"的关系,翻转为"词到文档"的映射

想象你在图书馆找书。正向索引像是一本本翻开每本书看里面有什么内容; 而倒排索引则像是图书馆后面的卡片目录柜—— 你查一个关键词,它直接告诉你哪些书里包含这个词。

FORWARD INDEX

📄 正向索引

Document → [term, term, term, ...]
Doc#1「搜索引擎原理」→ 搜索引擎 使用 倒排索引 加速 文本检索
Doc#2「Python编程」→ Python 是 流行 编程语言
Doc#3「数据库索引」→ 数据库 使用 B+树 索引 优化 查询
INVERTED INDEX

🔄 倒排索引

Term → [{doc_id, tf, positions}, ...]
"搜索引擎"Doc#1(tf=1)
"倒排索引"Doc#1(tf=1), Doc#4(tf=1)
"索引"Doc#1(tf=1), Doc#3(tf=2), Doc#4(tf=1), Doc#5(tf=1)
"Python"Doc#2(tf=3)
📚
词典 (Dictionary / Vocabulary)
所有不同词项的有序集合。通常用 Hash Table 或 B+Tree 存储,支持快速查找 O(1) 或 O(log N)。类似字典的"拼音索引页"。
📋
倒排列表 (Posting List)
每个词项对应的文档列表。每条记录包含:文档ID、词频(TF)、出现位置(positions)。支持交集/并集运算实现布尔查询。
为什么快?
查询时只需查找少数几个词的 posting list 然后做合并操作,无需扫描全部文档。从 O(N) 降到 O(M),M 为匹配文档数。

🔨构建过程:从文档到倒排索引

一步步观察倒排索引是如何从原始文本中自动构建出来的

⚙️ 构建流程实时演示
等待开始...
倒 排
词项 (Term)倒排列表 (Postings)

🧮BM25 评分公式深度解析

信息检索领域最重要的相关性评分算法,被 Elasticsearch、Lucene、SQLite FTS5 等广泛使用

BM25 完整评分函数
score(D,Q) = Σqi ∈ Q   IDF(qi) × f(qi,D) × (k₁ + 1) / [ f(qi,D) + k₁ × (1 - b + b × |D| / avgdl) ]
D
待评分的文档
Q
用户的查询(由多个查询词组成)
qi
查询中的第 i 个词项
f(qi, D)
词 qi 在文档 D 中出现的频率 (Term Frequency)
|D|
文档 D 的长度(词数)
avgdl
语料库中所有文档的平均长度
k₁ ≈ 1.2~2.0
词频饱和参数。越大,TF的影响衰减越慢。通常取 1.5
b ≈ 0.75
长度归一化参数。越大,对长文档的惩罚越重。通常取 0.75
IDF(qi)
log[(N - df(qi) + 0.5) / (df(qi) + 0.5) + 1],N=总文档数,df=含该词的文档数

📈 BM25 各因子直觉理解

📊 TF 归一化曲线 (k₁=1.5) 词频 TF → 得分贡献 → tf=0 tf=2 tf=10 tf→∞ 上界 = k₁+1 = 2.5 📊 IDF 曲线 (N=10000) 文档频率 df → 罕见词(df=1) df=100 df=1000 常见词(df=5000) ★ 罕见词 IDF 高 → 区分力强 | ★ 常见词 IDF 低 → 几乎无区分价值

🐍完整 Python 实现

自包含、可直接运行的倒排索引实现,含 BM25 评分、布尔查询、短语查询

数据结构定义 (Data Structures)
@dataclass class Posting: """倒排列表中的一条记录""" doc_id: int # 文档ID frequency: int = 0 # 词频 TF(在该文档中出现次数) positions: List[int] # 出现位置列表(用于短语查询) @dataclass class TermEntry: """词典中每个词项的条目""" term: str # 词项本身 df: int = 0 # 文档频率(多少个文档包含此词) postings: List[Posting] # 倒排列表 @property def idf(self) -> float: # IDF = log(N / df) + 1 (加1避免零值) return math.log(self._total_docs / self.df) + 1 @dataclass class Document: """待索引的文档""" doc_id: int title: str content: str @property def full_text(self) -> str: return f"{self.title} {self.content}"
InvertedIndex 核心类 — 分词 & 构建
class Tokenizer: """分词器:英文按单词,中文按字符,支持停用词过滤""" STOP_WORDS = {'的', '是', '了', '在', 'the', 'a', 'an', 'is', 'are', ...} def tokenize(self, text: str) -> List[Tuple[str, int]]: """返回 [(token, position), ...] """ text = text.lower() tokens = [] pos = 0 pattern = re.compile(r'[a-z0-9_]+|[\u4e00-\u9fff]') for match in pattern.finditer(text): tok = match.group() if tok not in self.STOP_WORDS: tokens.append((tok, pos)) pos += 1 return tokens class InvertedIndex: def __init__(self, tokenizer=None): self.tokenizer = tokenizer or Tokenizer() self.index: Dict[str, TermEntry] = {} # 词典 → 词项条目 self.documents: Dict[int, Document] = {} # 文档存储 self.doc_count: int = 0 self.avg_doc_length: float = 0.0 # 平均文档长度(BM25用) self.k1 = 1.5 # BM25参数 self.b = 0.75 def add_document(self, doc: Document): """添加文档到索引(核心构建逻辑)""" self.documents[doc.doc_id] = doc self.doc_count += 1 tokens = self.tokenizer.tokenize(doc.full_text) # 统计每个词的出现位置 temp_index = defaultdict(list) for token, pos in tokens: temp_index[token].append(pos) # 更新全局倒排索引 for token, positions in temp_index.items(): if token not in self.index: self.index[token] = TermEntry(term=token, df=0, postings=[]) entry = self.index[token] entry.df += 1 entry.postings.append(Posting( doc_id=doc.doc_id, frequency=len(positions), positions=positions, )) # 更新平均文档长度 total = self.avg_doc_length * (self.doc_count - 1) + len(tokens) self.avg_doc_length = total / self.doc_count
完整使用示例 (Demo)
# === 1. 准备文档集 === documents = [ Document(1, "搜索引擎原理", "搜索引擎使用倒排索引来加速文本检索..."), Document(2, "Python编程指南", "Python是一种流行的编程语言..."), Document(3, "数据库索引技术", "数据库使用B+树索引优化查询性能..."), Document(4, "AI Agent记忆系统", "AI Agent的记忆系统需要高效检索机制..."), ] # === 2. 构建索引 === indexer = InvertedIndex(Tokenizer(use_stop_words=True)) indexer.build_index(documents) # === 3. 布尔搜索 === results = indexer.search_boolean("倒排 索引") # → [1, 4] # === 4. BM25 相关性搜索 === results = indexer.search_bm25("索引 查询 性能", top_k=5) # → [(3, 3.82), (1, 2.15), (5, 1.87), ...] for rank, (doc_id, score) in enumerate(results, 1): print(f"#{rank} [{score:.4f}] 《{indexer.documents[doc_id].title}》") # === 5. 精确短语查询 === results = indexer.search_phrase("倒排索引") # → [1] (只有Doc#1中这两个字是紧挨着的) # === 6. 查看统计 === print(indexer.get_statistics()) # {'总文档数': 4, '词典大小': 67, '总posting数': 142, ...}