TECH ARTICLES
搜索引擎向量检索RAG深度学习

搜索核心算法完全指南:从倒排索引到深度学习检索

Jackie Zhan2026-07-05
目录
一、搜索引擎的"心脏":倒排索引原理 二、经典检索算法:BM25 与 TF-IDF 三、向量检索:ANN 近似最近邻搜索 四、稠密 vs 稀疏:两种检索范式的世纪对决 五、混合检索:让 1+1 大于 2 六、Query 理解:让搜索引擎真正"看懂"你 七、排序学习 LTR:从人工调参到机器学习 八、总结:搜索算法的进化之路

你打开百度,敲下"苹果",按回车。0.3 秒后,你看到了超过 1 亿条结果。

但你有没有想过:这 0.3 秒里,到底发生了什么?

很多人以为搜索就是一个"匹配"的过程——你输入"苹果",系统找到包含"苹果"两个字的网页,返回给你。听起来简单得像查字典。

但如果你真这么理解,那你错过了一个极其精彩的世界。

真实的搜索引擎里,至少有七个"关卡",每一关都在决定哪些网页值得出现在你面前。倒排索引负责"大海捞针",BM25 和 TF-IDF 给相关性打分,向量检索把语义理解带进来,混合检索把多种武器组合使用,Query 理解让系统知道你搜的到底是水果还是手机,排序学习(LTR)则像一位经验老到的编辑,把最终结果排成最符合你需求的顺序。

这篇文章,就是这七关的完整攻略。读完你会发现:一个"简单"的搜索框,背后藏着半个计算机科学史的智慧结晶。


一、搜索引擎的"心脏":倒排索引原理

先从一个你每天都在经历的场景说起。

假设你是图书馆管理员,你的任务是:用户说一个词,你能立刻告诉他哪些书里提到了这个词。传统做法是把每本书翻一遍,找到那个词。这叫正排索引——以文档为中心,去找词。

但用户问的词五花八门,如果每次都翻遍所有书,图书馆得烧了。于是你换了个思路:建一张大表,按字母顺序列出所有词,每个词后面跟一个"哪些书里有这个词"的清单。这张表,就是倒排索引(Inverted Index)。

你不需要大海捞针——因为你手里已经有一张"针"的位置图。这就是 Google、Elasticsearch、Meilisearch 等所有主流搜索引擎的核心数据结构。

倒排索引是怎么建出来的?

假设有三篇文档:

经过分词和标准化处理后,构建出的倒排索引结构如下:

词典 (Lexicon) 深度学习 机器学习 搜索 应用 推荐 倒排列表 (Posting List) Doc A, Doc C (DF=2) Doc A, Doc B (DF=2) Doc B, Doc C (DF=2) Doc A, B, C (DF=3) Doc B (DF=1)
倒排索引结构:词典 + 倒排列表。DF = Document Frequency(包含该词的文档数)

每个倒排列表里,不仅记录"哪些文档有这个词",还记录词在每个文档中的位置信息——这在短语查询和邻近查询时非常有用。

from collections import defaultdict

def build_inverted_index(documents):
    """构建倒排索引"""
    index = defaultdict(list)
    for doc in documents:
        doc_id = doc["id"]
        words = doc["text"].split()
        seen = set()
        for pos, word in enumerate(words):
            if word not in seen:
                index[word].append({"doc_id": doc_id, "pos": pos})
                seen.add(word)
    return dict(index)

def search(index, query):
    """简单词频计数检索"""
    doc_scores = defaultdict(int)
    for word in query.split():
        if word in index:
            for posting in index[word]:
                doc_scores[posting["doc_id"]] += 1
    return sorted(doc_scores.keys(), key=lambda d: doc_scores[d], reverse=True)

docs = [
    {"id": 1, "text": "深度学习是机器学习的子领域"},
    {"id": 2, "text": "机器学习广泛应用于搜索推荐"},
    {"id": 3, "text": "深度学习在搜索系统中的应用"},
]
print(search(build_inverted_index(docs), "深度学习"))  # [1, 3]

倒排索引的厉害之处在于:它的检索复杂度是 O(k),其中 k 是查询词的数量,与文档总数无关。这意味着,即使文档量从 100 万增长到 100 亿,查询速度仍然只取决于你敲了几个字。

一句话理解
正排索引是"拿着书找词",倒排索引是"按字母查字典"——把"大海捞针"变成了"字典查字"。

二、经典检索算法:BM25 与 TF-IDF

有了倒排索引,我们知道哪些文档包含哪些词了。但问题来了:如果查询"深度学习",文档 A 和文档 C 都包含"深度学习",该把谁排前面?这就是相关性评分要解决的问题。

TF-IDF:词有多重要?

TF-IDF 是信息检索领域最经典、最朴素、也最经受住时间考验的算法之一。它的核心思想:

数据说话
TF-IDF(词 t, 文档 d) = TF(t,d) × IDF(t)
其中 IDF(t) = log(总文档数 / 包含词 t 的文档数)

但 TF-IDF 有一个致命问题:它不会考虑词在文档中的饱和效应。一个词在文档里出现 100 次,就真的比出现 10 次重要 10 倍吗?不一定——可能只是作者在凑字数。

BM25:更聪明的 TF-IDF

BM25(Best Matching 25)在 1994 年被提出,是所有现代搜索引擎相关性评分的基础。它在 TF-IDF 的基础上引入了两个关键改进:词频饱和文档长度归一化

BM25 核心公式 Score = Σ IDF(qᵢ) · (tf · (k₁+1)) / (tf + k₁·(1-b+b·|D|/avgdl)) tf = 词在文档中出现次数 k₁ ∈ [1.2, 2.0] 控制饱和速度 |D| = 文档长度 avgdl = 平均文档长度 b = 0.75 控制长度归一化力度
BM25 评分公式。核心洞察:词频贡献有上限,长短文档公平竞争。

当 tf 较小时,分数随词频线性增长;但当 tf 超过某个阈值,继续增加词频带来的分数提升就微乎其微了。这就是"饱和效应"——说三遍重要,说三十遍不会更重要。

BM25 统治了信息检索领域近 30 年。Elasticsearch 从 Lucene 3.0 开始就把默认算法换成了 BM25。

反例警示
千万别直接拿原始 TF-IDF 分数做排序!不同文档长度、不同字段长度差异太大,会让短文档天然占优。BM25 的长度归一化解决了这个问题,这也是为什么 ES 默认用它。
import math

def bm25_score(doc, query, avg_doc_len, k1=1.5, b=0.75):
    """计算单篇文档对查询的 BM25 得分"""
    doc_len = len(doc.split())
    score = 0.0
    for term in query.split():
        tf = doc.lower().split().count(term.lower())
        if tf == 0: continue
        df = 100  # 假设有 100 篇文档包含该词
        idf = math.log((10000 - df + 0.5) / (df + 0.5) + 1)
        numerator = tf * (k1 + 1)
        denominator = tf + k1 * (1 - b + b * doc_len / avg_doc_len)
        score += idf * numerator / denominator
    return score

doc = "深度学习在搜索系统中的应用非常广泛"
query = "深度学习 搜索"
print(f"BM25 Score: {bm25_score(doc, query, avg_doc_len=200):.4f}")
实战案例
在 Elasticsearch 中调整 BM25 参数是最常见的调优手段:`k1` 调大让长文档更容易命中,`b` 调大让短长文档分数更公平。大多数场景下默认参数够用,但在垂直领域(比如法律文档检索)调参往往能带来 10-20% 的 NDCG 提升。

三、向量检索:ANN 近似最近邻搜索

BM25 很好,但它有一个根本性局限:它只能做字面匹配。你搜"苹果",它能找到包含"苹果"两个字的文档。但"苹果手机"和"iPhone 16 Pro"语义上高度相关,BM25 给它们打的分可能天差地别——因为这两个短语几乎没有字面重叠。这就是语义鸿沟。

打破这个僵局的方法,是把文本变成向量。现代的做法是用深度学习模型(如 BERT、text2vec、M3E、bge-m3)把一段文本编码成一个固定长度的向量,通常是 768 维或 1024 维的浮点数数组。这个过程叫向量化编码

向量化之后,"苹果"和"iPhone"在向量空间里会非常接近——因为它们在语义上指代同一种东西。向量检索的本质,就是在这个高维空间里,找到和你查询向量最相似的那些文档向量。

ANN:大海捞针的正确姿势

向量空间里可能有几十亿个向量,暴力计算两两相似度是不现实的。这就要靠 ANN(Approximate Nearest Neighbor,近似最近邻搜索)了。它的核心思想是:放弃精确找到最近的那一个,换来搜索速度 100 倍以上的提升。

HNSW:跳表 + 多层图的暴力美学

HNSW(Hierarchical Navigable Small World)是目前工业界最流行的 ANN 算法,Elasticsearch 8.0+、Milvus、FAISS 都支持它。

它的设计精妙极了:想象你在一个陌生的大城市里找最近的地铁站。如果城市被分成多个区,每区有一个"导航员"认识区内所有著名地标,你就可以先问导航员,快速缩小范围。HNSW 就是这个原理:用多层图结构,从粗糙层快速收敛到精确层。

Layer 2 (顶层 · 快速粗筛) Node-A Node-B Node-C Node-D Layer 1 (中层 · 区域导航) A1 A2 B1 B2 C1 C2 D1 D2 Layer 0 (底层 · 精确搜索 · 密集连接) a1 a2 a3 b1 b2 b3 c1 c2 c3 d1 搜索路径
HNSW 多层图搜索:上层快速粗筛 → 下层精确收敛。底层节点全连通保证精度。

IVF-PQ:聚类 + 压缩的工程美学

另一种主流 ANN 算法是 IVF-PQ(倒排索引文件 + Product Quantization)。它的思路是:先通过聚类把向量空间划分成若干"区域"(IVF),查询时先找到最相关的几个区域,再在区域内做精确搜索;每个向量还通过 PQ 压缩成短编码,大幅减少内存占用。

这两种算法各有优劣:HNSW 查询精度高、延迟低,但对内存占用大;IVF-PQ 内存效率高,适合超大规模向量库,但查询精度略逊。在实际生产中,Milvus、Faiss 支持混合使用——先用 IVF 粗筛,再用 HNSW 精排。

补充说明
余弦相似度和欧氏距离是向量检索中最常用的两种相似度度量。HNSW 默认用余弦相似度,IVF-PQ(Faiss)默认用内积(IP),可以统一转换为余弦相似度:cosine = IP / (|A| × |B|)。

四、稠密 vs 稀疏:两种检索范式的世纪对决

说到向量检索和传统检索,你可能已经注意到了:这两种方法本质上是在用不同的方式表示"文本"。

传统的 TF-IDF、BM25 用的是稀疏向量(Sparse Vector)——每个词占一个维度,绝大多数维度是 0。这是自然语言最直观的表示方式,可解释性强,但你搜什么词,系统就只能在那些词上打分。

而用深度学习模型编码出来的叫稠密向量(Dense Vector)——每个维度都有值(不为 0),维度数固定(768、1024 等)。信息被压缩、抽象地分布在所有维度上。这就是为什么"苹果"和"iPhone"在稠密向量空间里能很接近——因为模型学会了把它们映射到相似的位置。

稀疏检索 (Sparse) 深度学习 机器学习 应用 系统 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 ... 词袋模型 · 维度 = 词表大小(可能数十万) 绝大多数为 0 · 可解释性强 代表:BM25、TF-IDF 稠密检索 (Dense) 深度学习 → [0.23, -0.15, 0.87, ...] [0.12, -0.08, 0.45, 0.91, ...] 维度固定(768 / 1024 / 1536) 所有维度均有值 · 语义感知 代表:BERT embedding、Sentence-BERT
稀疏 vs 稠密检索:维度表示的本质差异

这两种方法各有擅长:

维度稀疏检索 (BM25)稠密检索 (Vector)
匹配方式字面匹配(exact match)语义匹配(semantic match)
查询词依赖强依赖,用户必须用对词弱依赖,同义词自动理解
未见词处理完全无法处理(OOV)泛化能力强
可解释性高(哪个词命中一目了然)低(黑盒向量运算)
精确检索✅ 强(人名、型号、ID 等)❌ 弱
同义词/语义❌ 需要额外处理✅ 天然支持

一个经典的例子:用户搜"手机",BM25 找不到一篇只写"iPhone"但从没出现"手机"这个词的文章,但向量检索可以。这就是稠密检索最大的价值——跨越语言的语义鸿沟。

关键区别
稀疏检索擅长"精准打击"——你知道要找什么词,它能精确命中。稠密检索擅长"语义理解"——你不知道该用什么词,它能猜到你的意思。两者不是替代关系,而是互补关系。

五、混合检索:让 1+1 大于 2

既然稀疏检索和稠密检索各有优劣,那最好的方案就是——两个都要

混合检索(Hybrid Search)就是同时利用稀疏和稠密检索结果,再通过特定策略合并打分。实际生产中,这是 RAG(检索增强生成)系统的标配。

关键问题是:两种检索结果的打分尺度完全不同——BM25 分数可能是几十到几百,向量余弦相似度是 0 到 1 之间。怎么比?

RRF:无需调参的融合神器

Reciprocal Rank Fusion(RRF,倒数排名融合)是一种经典的无参融合方法:

数据说话
RRF(D) = Σ 1 / (k + rank(d))
其中 k 是一个小常数(通常 k=60),rank(d) 是文档 d 在各检索结果中的排名位置。

它的核心思想是:只关心"排第几",不关心"分多少"。文档在 BM25 结果里排第 3,在向量结果里排第 5,那它的 RRF 分数就是 1/(60+3) + 1/(60+5) = 0.0159 + 0.0154 = 0.0313。

这个方法的好处是:不需要任何训练,不需要调参,对两种检索结果的尺度差异完全免疫。

from collections import defaultdict

def rrf_fusion(results_list, k=60):
    """
    RRF 倒数排名融合
    results_list: 多个检索结果列表,每个元素是按相关性排序的 doc_id 列表
    """
    scores = defaultdict(float)
    for results in results_list:
        for rank, doc_id in enumerate(results, 1):
            scores[doc_id] += 1.0 / (k + rank)
    return sorted(scores.keys(), key=lambda d: scores[d], reverse=True)

# 示例
bm25_results = [1, 3, 5, 2]   # BM25 返回的文档 ID 排序
vector_results = [3, 1, 4, 2] # 向量检索返回的文档 ID 排序
final = rrf_fusion([bm25_results, vector_results])
print(final)  # 融合后的最终排序
BM25 检索 稀疏 · 字面匹配 向量检索 稠密 · 语义理解 RRF 融合 1/(k+rank) 最终排序结果 稀疏+稠密协同
混合检索架构:BM25 + 向量检索 → RRF 融合 → 最终排序

在 Elasticsearch 8.11+ 中,可以使用 knn 查询和 query 查询的组合,Elasticsearch 内置了 RRF 支持。Pinecone、Weaviate 等向量数据库也都支持混合检索。

实战案例
在 RAG 场景中,混合检索几乎是标配。BM25 负责精确匹配实体、型号、ID 等,向量检索负责语义理解。实测中,混合检索往往比单一检索的 Recall@10 高出 15-30%。

六、Query 理解:让搜索引擎真正"看懂"你

到目前为止,我们默认用户输入的 query 是可以直接检索的。但现实中,用户的输入五花八门:可能有拼写错误、可能口语化严重、可能词不达意。

Query 理解,就是让搜索引擎真正"看懂"你在找什么。它包括三个核心环节:

1. 分词:词是语言的最小意义单元吗?

分词(Tokenization)是搜索的第一道关。中国人说话不像英文用空格分词,"机器学习"是一个词还是两个词?"深度学习模型训练"怎么切?

常见的中文分词工具包括 jieba、HanLP、THULAC 等。但对于搜索场景,简单基于词典的分词往往不够——"机器学习"可能被切成"机器"+"学习",也可能被保留为一个整体。这取决于词典质量和上下文。

现代做法是用子词分词(Subword Tokenization),如 BPE、WordPiece、SentencePiece。它们把文本切成更小的、频率最高的子字符串单元,在开放域词汇和精准分词之间取得平衡。这也是 BERT 等模型的分词方式。

2. 意图识别:你到底想要什么?

用户输入"苹果",你真的知道用户想要什么吗?水果?手机?股票?

意图识别(Intent Detection)的目标是把用户模糊的输入归类到一个或多个意图类别。比如电商搜索中,常见的意图包括:找商品、找品牌、比价格、看评价。

实现方式通常是训练一个文本分类模型(如 BERT + Classifier),在标注数据充足的情况下,准确率可以达到 90% 以上。

3. 纠错:搜错了也能找到对的结果

用户打错字是家常便饭——"搜索算法"打成了"搜素算法","机器学习"打成了"机哭学习"。如果系统傻傻地按错误的词去检索,结果一定是灾难性的。

Query 纠错(Query Correction / Spelling Correction)主要有两种策略:

值得注意的是:纠错要谨慎。学术查询"CNN"如果被纠错成"CN",结果会完全跑偏。所以通常只对低置信度的纠错进行自动应用,高置信度意图保持不变。

常见误解
很多人以为 Query 理解只是一个"预处理"步骤,不影响检索质量。实际上,意图识别决定了返回哪个类别的内容,纠错决定了能不能找到正确的内容。Query 理解做砸了,后面所有环节都白搭。

七、排序学习 LTR:从人工调参到机器学习

好了,现在你已经有了候选文档列表,并且有了相关性分数。最后一步:把这些文档排成什么顺序展示给用户?

最朴素的做法是:直接按相关性分数排序。但这忽略了一个关键问题——相关性不等于用户体验

一个文档可能和查询"非常相关"(BM25 分数高),但用户真正想要的,可能是更新、更权威、点击率更高的文档。这就需要排序学习(Learning to Rank, LTR)。

LTR 的核心思想

LTR 把搜索排序问题建模成一个机器学习问题。它的输入是每个文档的多个"特征"(feature),输出是排序分数。

这些特征包括:

模型的任务是:给定查询和文档特征,预测一个分数,使得最终排序能最大化用户满意度(如 NDCG、MAP 等评估指标)。

三大 LTR 方法

类型代表算法特点
PointwiseLightGBM + 回归每个文档独立打分,简单但忽略了文档间关系
PairwiseLambdaMART、RankNet学习文档对的相对顺序,更接近排序本质
ListwiseListNet、LambdaRank直接优化整个列表的 NDCG,效果最好但计算成本高

在实际生产中,LambdaMART(基于梯度提升树的 Pairwise 方法)是最流行的选择。它在 LightGBM、XGBoost 中都有现成实现,训练快、效果好、特征工程友好。

# LambdaMART 训练示例(使用 LightGBM)
import lightgbm as lgb
from sklearn.model_selection import train_test_split

# 假设有特征矩阵 X 和 relevance label(0-4 的相关性等级)
X_train, X_test, y_train, y_test = train_test_split(X, labels, test_size=0.2)

# LightGBM LambdaRank
train_data = lgb.Dataset(X_train, label=y_train)
test_data = lgb.Dataset(X_test, label=y_test, reference=train_data)

params = {
    "objective": "lambdarank",   # 指定 LambdaRank 目标
    "metric": "ndcg",             # 用 NDCG 作为评估指标
    "ndcg_eval_at": [5, 10],      # 评估 NDCG@5 和 NDCG@10
    "num_leaves": 63,
    "learning_rate": 0.05,
}

model = lgb.train(
    params,
    train_data,
    num_boost_round=500,
    valid_sets=[test_data],
    callbacks=[lgb.early_stopping(50)]
)

# 预测排序
predictions = model.predict(X_test)  # 输出每个文档的排序分数

值得注意的是,LTR 模型的效果高度依赖标注数据的质量。通常需要人工标注"查询-文档"对的相关性等级(0-4 或 0-3),这是最耗时、成本最高的环节。

一句话理解
LTR 就是用机器学习来学习"什么文档应该排在前面"。它把排序从"人工调参"变成了"数据驱动",让用户点击、浏览、收藏这些真实行为来教系统怎么排序。

八、总结:搜索算法的进化之路

好,我们走完了整个搜索算法的七个关卡。现在来回顾一下:

  1. 倒排索引:解决了"在海量文档中快速找到包含特定词的文档"的问题
  2. BM25 / TF-IDF:解决了"哪些文档更相关"的打分问题,引入了词频饱和和长度归一化
  3. 向量检索 + ANN:解决了"语义相近但字面不同的文档"的匹配问题
  4. 稠密 vs 稀疏:两种检索范式各有优劣,分别擅长精确匹配和语义理解
  5. 混合检索:用 RRF 等方法把多种检索结果融合,获得最佳覆盖
  6. Query 理解:让搜索引擎真正"看懂"用户在找什么,而不是机械匹配
  7. 排序学习 LTR:用机器学习整合所有信号,生成最终排序

这七个环节串起来,就是现代搜索引擎(如 Elasticsearch + 向量插件)或 RAG 系统(如 RAGFlow、AnythingLLM)的核心技术栈。

我认为,搜索算法的进化史,本质上是一部"让机器越来越懂人话"的历史。从字面匹配到语义理解,从人工调参到数据驱动,每一步都在弥合人类表达和机器理解之间的鸿沟。

而大模型时代的到来,把这个进程又往前推进了一大步——现在,Query 理解和文档理解都可以由 LLM 来完成,搜索正在从"找信息"变成"找答案"。

但无论技术怎么变,搜索的核心问题永远不会变:用户想要的,到底是什么?

这个问题,值得每一个搜索工程师持续思考。