从 N-gram 到 BM25:一次知识库关键词检索架构的演进与优化

在 RAG(Retrieval-Augmented Generation)知识库系统中,很多人首先关注的是 Embedding、向量数据库和大模型,却容易忽略一个非常重要的模块:

关键词检索。

向量检索擅长解决"语义相似"的问题,但对于错误码、函数名、技术名词、产品型号以及业务关键词等内容,传统词法检索往往更加准确。

因此,一个完整的知识库检索系统通常不会只使用向量检索,而是采用:

复制代码
Keyword / Sparse Retrieval
+
Vector Retrieval
+
Fusion
+
Reranker

本文结合一个实际知识库项目中的检索优化过程,介绍关键词检索如何从最初简单的 中文 N-gram + OR 查询,逐步演进到:

复制代码
Query Analyzer
+
ParadeDB BM25
+
pgvector
+
RRF
+
Reranker

整个过程最有价值的地方,并不是最终选择了什么技术,而是:

如何通过真实检索结果发现问题,并一步一步演进检索架构。


一、为什么知识库仍然需要关键词检索?

考虑这样一个 Query:

复制代码
订单超时

假设知识库中存在一份故障排查文档:

复制代码
订单超时问题排查

当订单服务调用库存服务超过 3 秒时,
系统会触发超时重试机制......

对于这种查询,用户非常明确地希望找到:

复制代码
包含"订单超时"这一业务关键词的内容

而不仅仅是寻找:

复制代码
与"订单超时"语义相似的内容

类似的场景还有:

复制代码
ERR_CONNECTION_RESET

runtime.Gosched

CVE-2026-XXXX

OrderService

Redis

HTTP 504

这些 Query 都具有非常明显的字面匹配特征。

因此在 Hybrid Retrieval 中:

复制代码
Keyword Search
→ 找字面相关内容

Vector Search
→ 找语义相关内容

两种方式通常是互补关系。


二、第一版方案:中文 N-gram

项目早期为了降低系统复杂度,并没有直接引入 Elasticsearch 等搜索引擎。

系统本身已经使用:

复制代码
PostgreSQL
+
pgvector

因此第一版关键词检索选择直接基于 PostgreSQL 实现。

核心方案是:

中文 N-gram。

例如:

复制代码
订单超时

经过 1-gram:

复制代码
订
单
超
时

经过 2-gram:

复制代码
订单
单超
超时

最终得到:

复制代码
订
单
超
时
订单
单超
超时

文档入库时,同样对正文生成 N-gram Token,并建立全文索引。

整体流程类似:

复制代码
Document
   ↓
N-gram Tokenizer
   ↓
Keyword Index

查询阶段:

复制代码
Query
   ↓
N-gram Tokenizer
   ↓
Keyword Search

第一版方案具有明显优势:

  • 实现简单;

  • 不需要额外部署搜索服务;

  • 不依赖复杂中文分词器;

  • 中文召回率较高;

  • 可以直接集成 PostgreSQL。

对于项目 MVP 来说,这是一个比较合理的实现。


三、第一个问题:OR 查询导致严重误召回

问题出现在一次关键词搜索测试中。

用户搜索:

复制代码
订单超时

理论上真正目标文档应该是:

复制代码
订单超时问题排查

但系统却返回了很多其他内容。

进一步排查发现,查询实际上被转换成:

复制代码
订 | 单 | 超 | 时 | 订单 | 单超 | 超时

也就是:

复制代码
订
OR 单
OR 超
OR 时
OR 订单
OR 单超
OR 超时

于是只要 Chunk 中出现其中任意一个 Token,都可能进入候选集。

例如某份文档包含:

复制代码
订单创建流程

因为命中:

复制代码
订单

会被召回。

另一份文档包含:

复制代码
HTTP 接口超时处理

因为命中:

复制代码
超时

也会被召回。

甚至某些包含:

复制代码
定时任务

的内容,也可能因为低区分度字符进入宽松候选。

因此问题的本质并不是:

搜索系统召回了完全不包含查询内容的文档。

而是:

1-gram 信息量过低,同时 OR 查询又过于宽松。

最终表现为:

复制代码
High Recall
Low Precision

召回率很高,但精确率明显不足。


四、第二个问题:TopK 不是"必须返回 K 条"

当时系统配置:

复制代码
TopK = 8

假设第一条真正相关结果的分数明显较高,而后面的候选只存在弱匹配。

如果直接执行:

复制代码
ORDER BY score DESC
LIMIT 8;

很容易形成一种错误行为:

既然 TopK 等于 8,就尽量返回 8 条。

实际上:

TopK 表示最多返回 K 条,而不是必须填满 K 条。

例如:

复制代码
TopK = 8

真正有效结果 = 2

那么:

复制代码
最终返回 2 条

完全合理。

强行补齐剩余 6 条低质量候选,反而会降低后续 RAG 的上下文质量。


五、第三个问题:"知识充分"不能只看候选数量

原来的 Retrieval Pipeline 中,还存在一个更隐蔽的问题:

复制代码
结果数量 >= MinCount
        ↓
     知识充分

假设:

复制代码
真正相关 Chunk:2

低质量 Chunk:6

总候选:

复制代码
8

如果只按照数量判断,系统可能认为:

复制代码
知识充分

但真正能够回答问题的证据并不一定足够。

因此:

召回数量不能直接代表知识质量。

更合理的流程应该是:

复制代码
Keyword ─┐
         │
Vector ──┼→ Fusion → Reranker → Sufficiency
         │
Other ───┘

也就是说:

Knowledge Sufficiency 应该尽量根据精排后的有效证据判断,而不是根据最初召回多少候选判断。


六、第一次优化:分层召回

发现 N-gram 的问题后,并没有立即替换整个关键词检索架构。

第一阶段先采取低成本优化:

复制代码
Exact Phrase
    ↓
2-gram Strong Recall
    ↓
1-gram Weak Fallback

例如:

复制代码
订单超时

可以分析成:

复制代码
Exact:

订单超时

Strong:

订单
超时

Weak:

订
单
超
时

原来的:

复制代码
所有 Token 一次 OR

被改造成逐渐放宽检索条件。


七、Query Relaxation:逐步放宽,而不是一次放宽

新的检索流程变成:

复制代码
Query
 ↓
Exact Phrase
 ↓
是否得到高质量结果?
 ├─ Yes → Stop
 └─ No
      ↓
   Strong Terms
      ↓
   是否得到有效候选?
    ├─ Yes → Stop
    └─ No
         ↓
     Weak 1-gram

这种方式可以理解为:

Query Relaxation(查询放宽)。

核心思想非常简单:

查询一开始尽量严格,只有结果不足时才逐渐放宽条件。

因此 1-gram 的定位从:

复制代码
默认召回方式

变成:

复制代码
最后兜底方式

这能够明显减少低区分度汉字带来的误召回。


八、为什么又引入 Query Coverage?

即使减少了 1-gram,普通关键词 OR 查询仍然可能产生噪声。

例如用户搜索:

复制代码
Redis 分布式锁实现

经过分析得到:

复制代码
Redis
分布式锁
实现

如果使用:

复制代码
Redis OR 分布式锁 OR 实现

那么:

复制代码
Redis 缓存设计

只因为包含:

复制代码
Redis

也可能进入候选。

因此可以引入:

Query Coverage。

假设 Query 有 3 个主要关键词。

Chunk A:

复制代码
使用 Redis 实现分布式锁

命中:

复制代码
Redis
分布式锁
实现

Coverage:

复制代码
3 / 3 = 100%

而 Chunk B:

复制代码
Redis 缓存常见问题

只命中:

复制代码
Redis

Coverage:

复制代码
1 / 3 ≈ 33%

因此可以设置:

复制代码
Coverage >= 60%
→ 保留

Coverage < 60%
→ 过滤或降权

Coverage 本质上是在解决:

复制代码
OR 太宽

和:

复制代码
AND 太严格

之间的平衡问题。


九、新的问题:长自然语言 Query 怎么处理?

用户实际使用知识库时,并不会总是输入几个关键词。

例如:

复制代码
请帮我查询订单服务调用库存服务发生超时时,
系统是如何进行重试和补偿的

如果继续直接做字符级 N-gram:

复制代码
请
帮
我
查
询
订
单
服
务
......

Token 数会快速膨胀。

如果这些 Token 再全部 OR:

复制代码
Token 越多
   ↓
查询条件越宽
   ↓
候选越来越多
   ↓
Precision 越差

因此需要认识到:

自然语言问题不能直接等价于关键词检索表达式。


十、引入 Query Analyzer

于是 Retriever 前增加:

复制代码
Query Analyzer

它主要负责:

复制代码
Normalize
↓
Query 类型判断
↓
实体提取
↓
关键词提取
↓
停用词处理

例如:

复制代码
请帮我查询订单服务调用库存服务发生超时时,
系统是如何重试和补偿的

经过分析后可能得到:

复制代码
Entities:

订单服务
库存服务

Keywords:

调用超时
重试
补偿

而:

复制代码
请
帮我
查询
如何

等低价值表达不再直接参与关键词检索。

这时架构逐渐变成:

复制代码
                    Query
                      ↓
               Query Analyzer
                      ↓
          ┌───────────┴───────────┐
          ↓                       ↓
      Short Query              Long Query
          ↓                       ↓
   Exact Phrase              Entity Extract
       +                    Keyword Extract
    Strong Token
          ↓                       ↓
          └───────────┬───────────┘
                      ↓
              Keyword Retrieval

十一、开始出现新的问题:是不是正在自己实现搜索引擎?

到这个阶段,关键词检索模块已经逐渐包含:

复制代码
Tokenizer
Phrase
Strong Token
Weak Token
Coverage
Query Relaxation
Keyword Score
Ranking
Boost
Fallback

如果继续完善,还可能需要:

复制代码
中文分词
专业词典
短语搜索
字段权重
模糊匹配
高亮
相关性评分

这时就必须思考一个工程问题:

我们是不是正在自己重新实现一个简化版全文搜索引擎?

继续不断为 N-gram 增加规则当然也能工作,但系统复杂度和维护成本会越来越高。

因此开始重新调研成熟知识库项目的关键词检索实现。


十二、成熟 RAG 系统的共同趋势

调研成熟知识库项目后,可以发现一个非常明显的趋势:

复制代码
                Query
                  ↓
       ┌──────────┴──────────┐
       ↓                     ↓
Sparse / Keyword         Dense Vector
       ↓                     ↓
       └──────────┬──────────┘
                  ↓
                Fusion
                  ↓
               Reranker

关键词检索通常使用:

复制代码
BM25
全文检索
Elasticsearch
PostgreSQL FTS
Sparse Retrieval

而不是长期依赖:

复制代码
1-gram
+
2-gram
+
大量 OR

于是项目思路发生了变化。

从:

如何继续把自己的 N-gram 检索修好?

变成:

是否应该把底层词法相关性问题交给专业全文搜索组件?


十三、为什么选择 ParadeDB?

项目原本已经使用:

复制代码
PostgreSQL
+
pgvector

如果再引入 Elasticsearch,架构会变成:

复制代码
PostgreSQL
     ↓
数据同步
     ↓
Elasticsearch

还需要额外处理:

复制代码
双写
数据同步
一致性
ES 部署
ES 索引生命周期

对于中小型个人知识库项目而言,这套架构可能偏重。

因此最终考虑:

ParadeDB。

整体仍然围绕 PostgreSQL:

复制代码
             PostgreSQL
                 │
        ┌────────┴────────┐
        ↓                 ↓
   ParadeDB / BM25     pgvector
      Keyword           Vector
        │                 │
        └────────┬────────┘
                 ↓
                RRF
                 ↓
              Reranker

这样能够在不过度增加基础设施复杂度的情况下,引入成熟的 BM25 全文检索能力。


十四、为什么 BM25 比简单 N-gram OR 更适合作为正式关键词检索?

简单 OR 检索更关注:

复制代码
是否命中某个 Token

而 BM25 会综合考虑:

复制代码
Term Frequency
Document Frequency
Document Length

可以简单理解为:

一个词在当前文档中越重要,同时在整个知识库中越少见,它的区分能力通常越强。

例如查询:

复制代码
订单超时

如果整个知识库中大量文档都包含:

复制代码
订单

那么仅仅命中"订单"的区分度并不高。

而同时包含:

复制代码
订单
+
超时

并且主要内容都围绕"订单超时排查"的 Chunk,应该拥有明显更高的相关性。

这比:

复制代码
只要命中任意字符就进入候选

更加适合作为正式搜索系统的相关性排序机制。


十五、Exact Phrase 仍然值得保留

即使引入 BM25,也不意味着所有检索逻辑都应该交给 BM25。

例如:

复制代码
ERR_CONNECTION_RESET

OrderService

Redis Cluster

订单超时

这些 Query 都具有明显的完整短语特征。

因此比较合理的关键词检索策略仍然是:

复制代码
Exact Phrase Boost
+
BM25 Retrieval

完整短语匹配作为高权重信号,BM25 负责更加通用的词法相关性排序。


十六、Query Analyzer 也不能因为用了 ParadeDB 就删除

ParadeDB 解决的是:

怎么搜索。

Query Analyzer 解决的是:

应该搜索什么。

例如:

复制代码
订单服务请求库存服务发生超时时,
为什么会重复扣减库存?

可以经过 Query Analyzer 转换成:

复制代码
订单服务
库存服务
调用超时
重复扣减
库存

再交给搜索系统。

因此:

复制代码
Raw Query
   ↓
Query Rewrite
   ↓
Query Analyzer
   ↓
Retrieval Query

这一层仍然非常重要。


十七、最终 Hybrid Retrieval 架构

最终完整检索链路可以设计为:

复制代码
                    User Query
                        ↓
                  Query Rewrite
                        ↓
                  Query Analyzer
                        ↓
            ┌───────────┴───────────┐
            ↓                       ↓
      Keyword Retrieval        Vector Retrieval
            ↓                       ↓
       ParadeDB BM25            pgvector
            ↓                       ↓
        Keyword TopN            Vector TopN
            └───────────┬───────────┘
                        ↓
                       RRF
                        ↓
                    Reranker
                        ↓
                  Quality Filter
                        ↓
                    Final TopK
                        ↓
             Knowledge Sufficiency
                        ↓
                    RAG Answer

十八、为什么需要 RRF?

BM25 和向量搜索采用的是两套完全不同的评分体系。

例如:

复制代码
BM25 Score = 10.5

向量检索:

复制代码
Cosine Similarity = 0.84

这两个数字不能简单:

复制代码
10.5 + 0.84

因为它们没有统一量纲。

因此可以使用:

RRF(Reciprocal Rank Fusion)

RRF 更关注:

复制代码
结果分别在各个检索通道中的排名

而不是直接比较原始 Score。

例如某个 Chunk:

复制代码
BM25:Rank 1
Vector:Rank 2

另一个:

复制代码
BM25:Rank 18
Vector:Rank 15

显然第一条更值得进入后续精排。


十九、为什么 RRF 后还需要 Reranker?

关键词检索和向量检索的主要目标仍然偏向:

Recall。

也就是尽量先找到可能相关的候选。

例如:

复制代码
BM25 TopN:30

Vector TopN:30

经过 RRF 合并后,仍然可能保留几十条 Candidate。

这些内容不能全部送给 LLM。

因此需要:

复制代码
Reranker

根据:

复制代码
Query + Chunk

进行更加精细的相关性判断。

可以简单理解为:

复制代码
Retriever
→ 找得全

Reranker
→ 排得准

二十、Knowledge Sufficiency 应该放在什么位置?

最开始:

复制代码
召回数量
↓
知识是否充分

这种判断方式是不合理的。

最终更合理的是:

复制代码
BM25
   \
    RRF → Reranker → Knowledge Sufficiency
   /
Vector

Knowledge Sufficiency 可以综合:

复制代码
是否存在有效结果
Rerank Score
有效证据数量
证据是否真正覆盖问题

而不能简单:

复制代码
候选数量 >= 5

就认定知识充分。


二十一、哪些旧逻辑可以逐渐删除?

引入 BM25 后,以前为了修补 N-gram 而设计的很多复杂逻辑都可以逐渐退出主流程:

复制代码
1-gram
2-gram
全部 OR
Strong Token
Weak Token
复杂 Coverage 排序
自定义 Keyword Score

Coverage 可以继续作为:

复制代码
调试指标

但不再承担核心相关性排序职责。

最终代码职责可以更加明确:

复制代码
QueryAnalyzer
      ↓
KeywordSearchRepository
      ↓
ParadeDB BM25


VectorSearchRepository
      ↓
pgvector


RetrievalService
      ↓
Parallel Retrieval


FusionService
      ↓
RRF


Reranker
      ↓
Precision Ranking

二十二、整个关键词检索架构的版本演进

V0:字符级 N-gram

复制代码
1-gram
+
2-gram
+
OR

优点:

复制代码
实现简单
Recall 高

问题:

复制代码
Precision 较差

V1:分层召回

复制代码
Exact Phrase
+
2-gram
+
1-gram Fallback
+
Coverage
+
Query Relaxation

能够明显缓解单字误召回问题。

但搜索规则逐渐复杂。


V1.1:Query Analyzer

增加:

复制代码
Normalize
关键词提取
实体提取
停用词
长短 Query 分流

解决自然语言 Query 直接进行字符级 N-gram 的问题。


V2:BM25 + Vector Hybrid Retrieval

最终:

复制代码
Query Analyzer
      ↓
┌───────────────┐
↓               ↓
BM25          Vector
↓               ↓
└───────┬───────┘
        ↓
       RRF
        ↓
    Reranker
        ↓
    Final TopK

底层词法相关性计算交给成熟全文检索能力。

项目自身则更加专注于:

复制代码
Query Understanding
Hybrid Retrieval
Fusion
Reranking
Knowledge Sufficiency

二十三、这次架构演进真正值得总结的是什么?

最开始并没有为了技术复杂度直接引入搜索引擎,而是采用了简单方案:

复制代码
N-gram

通过真实检索测试发现:

复制代码
Precision 问题

于是先进行低成本优化:

复制代码
Query Relaxation
+
Coverage

继续开发后发现:

复制代码
长 Query
Phrase
Boost
Ranking
中文分词

让自研检索逻辑越来越复杂。

最终才决定:

复制代码
引入专业搜索组件

这是一个非常典型的软件架构演进过程:

复制代码
Simple Solution
      ↓
Real Problem
      ↓
Local Optimization
      ↓
Complexity Growth
      ↓
Architecture Refactoring

技术选型真正需要考虑的,并不是:

哪种技术看起来更加高级?

而是:

当前问题是否已经复杂到值得交给一个专业组件解决。


结语

知识库检索质量,很大程度上决定了最终 RAG 的回答质量。

如果 Retriever 一开始就召回大量错误内容:

复制代码
Bad Retrieval
      ↓
Bad Context
      ↓
LLM
      ↓
Bad Answer

模型能力再强,也很难完全弥补错误上下文带来的影响。

因此,一个更加完整的 RAG 系统,需要逐渐从:

复制代码
能够搜索

发展到:

复制代码
能够稳定召回真正相关的知识

最终形成:

复制代码
Query Understanding
        ↓
Keyword / Sparse Retrieval
        +
Vector Retrieval
        ↓
Fusion
        ↓
Reranker
        ↓
Knowledge Sufficiency
        ↓
Generation

从 N-gram 到 BM25 的这次演进,本质上也是从:

自己实现一个能够工作的关键词查询

逐步走向:

构建一套真正面向 RAG 场景的 Hybrid Retrieval 系统。

而这也是整个关键词检索优化过程中最重要的收获。

相关推荐
IPHWT 零软网络4 小时前
技术方案分享|AI Agent 赋能 IVR 导航,解决传统语音呼叫系统交互瓶颈
人工智能·通信系统·rag·ivr·aiagent·智能语音·语音导航
番茄炒鸡蛋加糖4 小时前
主流 AI 框架 + RAG 落地实战
人工智能·rag·springai
thesky1234568 小时前
27届大模型面试准备(三十二):RAG 进阶——从朴素 RAG 到 Agentic RAG 与多模态检索增强
大模型·rag·agentic rag·graph rag·进阶rag·self-rag·corrective rag
AndrewHZ1 天前
【LLM技术全景】RAG 从原理到实战——检索增强生成完整指南
人工智能·深度学习·算法·llm·检索增强·生成式模型·rag
神奇霸王龙1 天前
Agentic RAG 双硬门屠夫:5 旗舰实测
数据库·人工智能·ai·agent·ai编程·ai写作·rag
Tbisnic1 天前
从算法到工程:构建企业级RAG系统的混合检索实践
python·ai·rag·检索
ZGi.ai2 天前
知识库更新后仍回答旧内容:五层排查指南
知识库·rag·知识检索·zgi
SelectDB技术团队2 天前
Apache Doris AI RAG 实战:从基础 RAG 到知识图谱增强的技术能力与选型
rag
迷路爸爸1802 天前
RAG 优化方案汇总介绍
python·langchain·agent·rag