跳到主要内容
Cowers://
全部文章
检索与 RAG

混合检索选型:BM25 与向量该放进哪个库

关键词和语义两路检索可以合并在 PostgreSQL、Elasticsearch 或 Milvus 中,选型重点是检索能力、数据同步与运维边界。

混合检索选型:BM25 与向量该放进哪个库

修订说明(核实于 2026-08-13,东八区) 本篇按 ParadeDB / PostgreSQL / Milvus 官方文档复核过,主要修正:

  • ParadeDB 查询语法过时:评分函数是 pdb.score(id) 不是 paradedb.score(id);基础匹配用 |||(OR)/ &&&(AND),@@@ 是 query builder 用法。
  • 关键词 CTE 少了顶层 ORDER BYLIMIT 20 拿到的不保证是 BM25 前 20 名。
  • pdb.jieba 已确认是官方支持的 tokenizer,原文的「拼写未确认」提示已撤下。
  • 能力对比表中 PG「高亮/纠错/联想弱」已过时:pg_search 现已支持高亮、模糊查询、聚合、自动补全。
  • 规模上限、「PG 一把梭」等结论补上了适用前提。

以下两条经核对,原文是对的,报告的质疑不成立,予以保留:

  • USING paradedb 是 0.25.0 起的正式语法(USING bm25 降级为兼容别名),不是笔误。
  • Milvus 2.5 的全文检索确实集成 Tantivy,这是官方博客明确写的。

锁定版本:ParadeDB pg_search v2 API(0.25.0+)、PostgreSQL 16+、Milvus 2.5+。

阅读提示 本笔记的前置阅读是 BM25 算法原理(BM25 怎么算分)和 RAG概念(为什么 RAG 需要两路召回)。那两篇解决「是什么」,本篇只回答一个选型问题:关键词一路和语义一路,应该放进哪个存储,代价最小。

默认读者已经理解稀疏检索与稠密检索的区别,正在为一个真实项目决定要不要引入 ES 或 Milvus。

本篇不讲 BM25 公式推导,不讲切块与 Embedding 模型选择(见 RAG优化方案),不讲系统怎么分层(见 RAG架构设计)。本篇只管检索能力放在哪个进程里

⚠️ = 常见陷阱 🆚 = 对比说明 💡 = 机制或选择建议

目录


核心概念 BM25 曾经是「必须上 Elasticsearch」的主要理由——因为只有它有。现在 PostgreSQL(pg_search)和 Milvus(2.5+)都把 BM25 内置了,于是选型问题从「要不要上 ES」变成了「把关键词和语义两路合并到哪个进程,运维代价最小」。对中小项目,答案通常是 PG:pgvector 管语义,pg_search 管关键词,两者都是扩展,不新增服务、不搬数据。

一、这个选型问题是怎么冒出来的?

三五年前的标准架构是三个库拼起来:PG 存业务数据,ES 做关键词检索,向量库做语义检索。

拼三个库的直接原因是能力不可替代:BM25 只有 ES 有,向量索引只有向量库有。你没得选。

现在这个前提塌了。三家都在往对方的地盘扩:

  • PostgreSQLpg_search 就有了 BM25,装 pgvector 就有了向量索引;
  • Elasticsearch 8.0 起支持 HNSW 向量检索;
  • Milvus 2.5 起内置 BM25 全文检索。

于是「BM25」不再是任何一家的独占卖点,选型的判断依据必须换一个——换成运维边界:这个能力是跑在你已有的进程里,还是要新起一个集群。

为什么判断依据是「进程」而不是「性能」 中小项目的检索性能瓶颈几乎从来不是引擎本身,而是数据同步:ES/Milvus 的数据要从 PG 同步过去,于是你要处理同步延迟、失败重试、双写不一致、全量重建。这些工作量通常远超「换个更快的引擎」带来的收益。 能力差不多的时候,选进程数少的那个。

二、说「PG 不支持中文 BM25」时,说的是哪一层?

这句话在网上到处都是,但它同时是对的和错的——取决于问的是哪一层。分成三层就不会再混。

混合检索 PostgreSQL能力三层边界 图:PG 检索能力的三层边界——裸核、扩展、外部中间件,以及运维成本的真正分界线。

图从左到右读:左边两层都在同一个 PG 进程内,红色虚线右边才是独立服务。「少维护一个中间件」省下的是跨过虚线时才产生的那三项成本(部署监控、数据同步、无法 JOIN),而不是省下某个算法。

三层的具体能力:

装什么 有 BM25 吗 切中文吗 运维成本
裸核 Built-in 什么都不装 ✗ 只有 ts_rank ✗ 整句当一个 token 0
扩展 Extension CREATE EXTENSION pg_search ✓ 真 BM25 ✓ 自带 jieba 几乎 0,仍是一个进程
外部中间件 部署 ES / Milvus ✓(需装插件) 一整套集群

所以准确的说法是:

PG 裸核既没有 BM25 也不切中文;但装上 pg_search 扩展后,BM25 和中文分词都在 PG 进程内部解决,不需要外部中间件。

裸核的 ts_rank 差在哪 常见的说法是「ts_rank 词频线性叠加、不做长度归一化」——这是不准确的。PG 对同一个词的第 j 次出现按 1/(j+1)² 衰减累加(见源码 src/backend/utils/adt/tsrank.ccalc_rank_or;这是实现细节,官方文档没有写成公式,跨大版本前值得再确认一次),这个级数收敛,饱和其实比 BM25 还激进;文档长度归一化也有,就是 ts_rank 的第四个参数(normalization 位掩码),只是默认关闭。

它真正的两个短板是:

  1. 没有 IDF——不知道「的」是废词、「Tantivy」是稀有词,缺少全库统计;
  2. 无法在索引层早停——必须扫完所有命中行才能排序,拿不到 top-K 提前退出。

知道真实短板才能做对决策:这两点都是索引结构决定的,写个自定义打分函数补救不了,只能换引擎。

三、混合检索到底指什么?

混合检索(Hybrid Search)是一种思想,不是某个产品的功能。 只要同时用了关键词和语义两路召回并做了融合,就叫混合检索,跟在哪个库里实现无关。

混合检索 两路召回与RRF融合 图:关键词一路走分词与倒排索引、由 BM25 打分;语义一路走 Embedding 与 HNSW、由向量距离打分;最后用 RRF 按名次融合。

图从上往下读:查询进来后分叉成两条独立的路,各自用完全不同的索引结构和打分体系跑完,最后在底部融合。关键结论是最下面那行——两路都命中的文档会排到最前,这正是混合检索的全部价值。

为什么单独一路都不够:

  • 只用向量:问「公司 2024 年 Q4 财报营收多少」,2024年Q4 这种精确 token 向量不敏感,容易召回 2023 年的。
  • 只用 BM25:问「赚钱最多的是哪个季度」,BM25 认不出 赚钱最多营收最高 是一回事。

RAG 场景两种问法都会出现,所以混合检索值得作为基线去评测。注意这是「值得先试」,不是「必然更好」:如果你的查询高度同质(比如全是自然语言长问句,几乎没有专有名词和编号),单路向量检索可能已经够用,加一路 BM25 只是徒增复杂度。用自己的标注集比一次,让数据来定。

RRF 里的 60 是什么 RRF(Reciprocal Rank Fusion,倒数排名融合)的公式是 Σ 1/(k + rank)k 取 60 是原论文的经验值。

它的作用是压平相邻名次的差距1/(60+1)1/(60+2) 只差 1.6%,所以第 1 名和第 2 名几乎等价。

⚠️ 但别推过头——RRF 依然关心名次,只是不关心「差一两名」这种细节。拉开距离看就很明显:

名次 得分 1/(60+rank) 相对第 1 名
1 0.0164 100%
5 0.0154 94%
20 0.0125 76%
100 0.0063 38%

第 100 名只有第 1 名的 38%,差距是实打实的。准确的说法是:RRF 对头部的微小名次波动不敏感,但对「排在前面还是后面」依然敏感;两路都命中的文档能拿两份分,所以通常会浮上来——这是倾向,不是硬规则,单路的绝对头名照样可能赢过双路的中游。

它鲁棒的真正原因是根本不用分数,只用名次。余弦距离(02)和 BM25 分(0几十)是两套不可比的量纲,直接加权相加需要反复调参,换个数据集就失效;换成名次就没有这个问题。

四、三个候选方案差在哪?

三家都能做混合检索,差别在你要为此维护几个进程,以及检索结果能不能直接和业务数据 JOIN

维度 PG(pgvector + pg_search) Elasticsearch Milvus
关键词一路 pg_search(Tantivy 内核) 原生 BM25,本行 2.5+ 内置 sparse-BM25(同样是 Tantivy)
语义一路 pgvector(HNSW) 8.0+ 支持 HNSW 本行,最专业
融合方式 自己写 RRF(SQL CTE) rank: { rrf: {} } hybrid_search + RRFRanker
新增进程数 0 1 套集群 1 套集群
数据同步 不需要 需要,有延迟和不一致 需要
能否 JOIN 业务表 ,就是普通 SQL 不能,要回查 PG 不能
权限过滤 SQL WHERE 直接下推,可用 RLS 需把 ACL 同步过去并保证及时失效 同左,靠 scalar filter
高亮/纠错/联想 已支持pdb.snippet()、模糊查询、聚合、自动补全) 开箱即用,生态最全
规模上限 见下方说明 见下方说明 见下方说明

⚠️ 「规模上限」这一格没有可靠答案,别当容量规划用 原文写的是「PG 千万级 / ES 亿级 / Milvus 亿级最强」。这组数字缺少全部关键前提:向量维度、索引类型与参数、单机还是集群、内存多大、可接受的召回率和 P99 延迟、写入 QPS。任何一项变化都能让「上限」挪动一个数量级——同样是 1000 万条,768 维 HNSW 全内存和 1536 维磁盘索引完全是两回事。

能说的只有定性结论:

  • PG + pgvector 的 HNSW 索引需要常驻内存,内存是先撞到的墙,通常比 Milvus 更早;
  • Milvus 从设计上就为分片和横向扩展做了准备,规模越大优势越明显;
  • ES 在关键词一路成熟,向量一路是后加的,超大向量规模下不如 Milvus。

真要定容量,只有一条路:拿自己的数据和维度做压测。 网上任何「PG 撑得住 N 条」的说法都默认了一组你不知道的前提。

PG 的「进程」说法要精确一点 表里「新增进程数 0」指的是不新增独立的数据服务——不用再部署、监控、备份第二套系统,也不用写同步管道。

但 PostgreSQL 本身是多进程架构(每个连接一个 backend 进程,外加若干后台进程),pg_searchpgvector 是加载进这些进程的扩展,不是「所有东西跑在同一个进程里」。这个区别在做资源规划时有意义:扩展的内存和 CPU 开销会落在 PG 的进程里,和你的业务查询抢同一份资源。检索负载重时,该考虑读副本隔离,而不是以为「反正没新增进程所以没成本」。

💡 三句话版本:

  • ES 是靠 BM25 吃饭的老牌搜索引擎,强在检索周边功能(高亮、聚合、纠错);
  • Milvus 是靠向量吃饭的,把 BM25 请来当副手,强在向量规模;
  • PG 是为了让你不必引入前两者,把两样都搬进了自己进程里,强在零同步、能 JOIN

💡 怎么选

  • 已经在用 PG,检索规模在自己压测能撑住的范围内,团队没有专职运维 → 先用 PG 试。对多数「已有 PG + 没有专职检索团队」的项目,这是成本最低的起点——但它是起点假设,不是普适结论,仍要用自己的数据验证。
  • 已经有在跑的 ES 集群 → 直接在 ES 里加向量字段,别再引入第三个库。
  • 向量规模大到 pgvector 的 HNSW 索引装不进内存,或压测显示 P99 延迟稳定超出你的预算 → 向量那一路拆到 Milvus,关键词一路可以留在 PG。(判断依据是你自己压测的结果,不是某个固定的条数或毫秒数。)

注意这三条的判断依据都是你已有什么规模,没有一条是「谁的 BM25 更好」——那已经不再是区分点了。

五、PG 方案怎么落地?

⚠️ 本节代码有版本敏感性 ParadeDB(pg_search)的 API 经历过至少两次不兼容大改(0.15 前后、v2 API)。下面按 v2 API(0.25.0+) 写:索引访问方法自 0.25.0 起正式更名为 paradedbUSING bm25 保留为向后兼容别名。

pdb.jieba官方支持的中文 tokenizer(0.21.0 起还加了繁简转换),不是笔误。同系列还有 pdb.chinese_lindera(词级)和字符级的 chinese_compatible 可选。

仍然建议:执行前用目标版本跑一遍并看 EXPLAIN,确认索引真的被用上了而不是退化成顺序扫描。

1. 装扩展

CREATE EXTENSION vector;      -- 语义一路
CREATE EXTENSION pg_search;   -- 关键词一路(BM25)

2. 建表与两套索引

CREATE TABLE docs (
  id        SERIAL PRIMARY KEY,
  content   TEXT,
  embedding vector(768)       -- bge-base-zh 是 768 维
);
 
-- 语义索引
CREATE INDEX ON docs USING hnsw (embedding vector_cosine_ops);
 
-- 关键词索引:索引类型现已更名为 paradedb,分词器用 cast 语法指定
CREATE INDEX docs_bm25 ON docs
USING paradedb (id, (content::pdb.jieba))
WITH (key_field = 'id');

写入时只写原文和向量,pg_search 会自己维护倒排索引:

INSERT INTO docs (content, embedding) VALUES ($1, $2);

3. 混合查询:用 CTE 手写 RRF

PG 目前没有 Milvus 那样的原生 hybrid_search,两路要自己融合。下面是能跑的骨架:

WITH semantic AS (                                  -- 语义一路,取 top 20
  SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $1) AS rank
  FROM docs ORDER BY embedding <=> $1 LIMIT 20
),
keyword AS (                                        -- 关键词一路,取 top 20
  SELECT id, ROW_NUMBER() OVER (ORDER BY pdb.score(id) DESC) AS rank
  FROM docs
  WHERE content ||| $2                              -- ||| = 匹配任一词(OR)
  ORDER BY pdb.score(id) DESC                       -- ← 必须有,否则 LIMIT 取的是任意 20 行
  LIMIT 20
),
fused AS (
  SELECT
    COALESCE(s.id, k.id) AS id,
    COALESCE(1.0 / (60 + s.rank), 0)                -- 未命中的一路贡献 0
      + COALESCE(1.0 / (60 + k.rank), 0) AS rrf_score
  FROM semantic s
  FULL OUTER JOIN keyword k ON s.id = k.id          -- 必须 FULL,两路地位对等
)
SELECT d.*, f.rrf_score
FROM fused f JOIN docs d ON d.id = f.id
ORDER BY f.rrf_score DESC
LIMIT 10;

四个关键点,也正是最容易写错的地方:

  1. FULL OUTER JOIN——只被一路命中的文档必须保留,见下面的陷阱 3
  2. COALESCE(..., 0)——未命中的一路应当贡献 0 分,而不是让整个表达式变 NULL
  3. 先融合再回表——fused 只处理 ID 和分数,最后才 JOIN 回 docs 取完整行,避免在两路 CTE 里重复搬运大字段。
  4. 两路 CTE 都必须有顶层 ORDER BY——见下面的陷阱 0。

⚠️ 陷阱 0:ROW_NUMBER() OVER (ORDER BY ...) 不等于 ORDER BY 这是最隐蔽的一个,原文的关键词 CTE 就踩了:

-- ❌ 错的
SELECT id, ROW_NUMBER() OVER (ORDER BY pdb.score(id) DESC) AS rank
FROM docs WHERE content ||| $2 LIMIT 20

窗口函数里的 ORDER BY 只决定 rank 这一列怎么编号,不决定结果集按什么顺序返回。 没有顶层 ORDER BY 时,LIMIT 20 从满足 WHERE 的行里取任意 20 行——具体取哪些取决于执行计划,可能是索引扫描顺序,也可能是堆表物理顺序。

后果特别难查: 你确实拿到了 20 行、每行也确实有 1~20 的 rank,看起来完全正常。但这 20 行是全体命中文档里的任意 20 个,BM25 真正的第 1 名很可能根本不在里面。融合结果因此持续偏差,而 SQL 不报任何错。

正确写法: 顶层补上 ORDER BY pdb.score(id) DESC,再 LIMIT 20。语义一路(ORDER BY embedding <=> $1 LIMIT 20)本来就是对的,容易让人误以为两路对称——它们不对称,因为语义那一路的 ORDER BY 写在了外层

六、常见陷阱

⚠️ 陷阱 1:用 pg_trgm 做中文搜索 错误操作: 看到教程说「PG 装 pg_trgm 就能支持中文搜索」,于是建了 GIN trigram 索引。

实际结果: LIKE '%关键词%' 确实变快了,但搜「全文检索」时,「检索全文」「全文检索技术」「检索」得分几乎一样,排序完全没有相关性可言。

原因: pg_trgm三元组模糊匹配,把字符串切成三字符片段算重合度,用途是容错(potos 能搜到 photos)和加速 LIKE

它能给出一个排序——similarity(a, b)<-> 距离算子都可以拿来 ORDER BY。但那是字符串表面相似度,回答的是「这两串字长得像不像」,不是「这篇文档对这个查询有多相关」。它不理解「词」的概念,没有 IDF(不知道哪个词稀有),没有文档长度归一化,也不区分命中的是标题还是正文。中文场景更糟:三字符窗口横跨词边界,「北京大学」和「京大学生」的 trigram 重合度很高,但语义无关。

准确的说法是:pg_trgm 做的是相似度排序,不是相关性排序。这两件事在英文短字段(人名、商品名)上偶尔能凑合,在中文长文检索上完全不能替代 BM25。

正确做法: 要相关性排序就用 pg_search(BM25 + 倒排索引)。pg_trgm 保留给拼写容错和模糊匹配场景,两者可以共存,各管各的。

⚠️ 陷阱 2:照抄网上的 paradedb.create_bm25() 错误操作: 按中文教程执行 CALL paradedb.create_bm25(index_name => ..., text_fields => '{...}'),然后用 SELECT * FROM posts_bm25 WHERE posts_bm25 @@@ '...' 查询。

实际结果: 报错,函数不存在;或在旧版本上能跑,升级后整套查询失效。

原因: 这是 ParadeDB 0.x 时期的 legacy API(把索引名当表查的 schema-based 模式),早已废弃。官方文档里这部分已经挪到 /legacy/ 路径下了。中文社区的教程绝大多数停留在这一代。

正确做法: 用标准 CREATE INDEX ... USING paradedb (...) WITH (key_field='id'),查询直接查原表SELECT * FROM docs WHERE content ||| '关键词'。以及一条通用习惯——pg_search 相关的写法一律以官方文档为准,不要信搜索引擎排在前面的中文博客,这个扩展的 API 变动频率远高于一般 PG 扩展。

⚠️ 陷阱 3:RRF 用了 INNER JOIN 错误操作: 融合两路时写成 FROM docs JOIN semantic USING (id) FULL OUTER JOIN keyword USING (id)

实际结果: 两种失效叠加——① 被 BM25 命中但没进向量 top-20 的文档被 INNER JOIN 直接丢弃;② 侥幸留下的行里 rankNULL,而 1.0/(60 + NULL) 结果是 NULL,整个 rrf_scoreNULL,排序彻底失效。

原因: 混合检索的两路地位是对等的,任何一路单独命中的文档都必须进入候选集——这恰恰是做混合检索的全部意义。用 INNER JOIN 等于宣布「向量没召回的一律不要」,那还不如只用向量。而 NULL 参与算术运算结果恒为 NULL,是 SQL 三值逻辑的基本行为。

正确做法: 两路之间用 FULL OUTER JOIN,每一路的贡献用 COALESCE(1.0/(60+rank), 0) 兜底,融合完再 JOIN 回主表。见上一节的完整 SQL

⚠️ 陷阱 4:为「以后可能要扩」提前上 ES 错误操作: 项目只有几十万文档,但担心将来长到亿级,一开始就架 ES 集群。

实际结果: 立刻要处理 PG→ES 的数据同步(延迟、失败重试、双写不一致、全量重建),权限数据要额外同步一份到 ES 并保证及时失效。这些成本从第一天就产生,而 ES 的能力在几十万文档规模下完全用不上。

(注:「检索结果还得回 PG 捞完整行」不是必然的——ES 默认保存 _source,可以直接返回原始文档。真正跑不掉的是同步:一旦文档在 PG 里更新或权限变更,ES 那份副本就进入了不一致窗口。你要么接受这个窗口,要么为它写一套对账逻辑。)

原因: 这是把未来才产生的收益换成了当下就要付的成本。而且假设本身站不住——从 PG 迁到 ES 并不需要重写业务逻辑,检索层本就该被封装在一个接口后面(见 RAG架构设计)。

正确做法:pgvector + pg_search 起步,把检索封装成一个可替换的接口。等真正撞到规模瓶颈(见下一节的具体信号)再迁——那时你还多了一份真实的线上查询模式数据,迁移方案反而更准确。

七、什么时候该跨过那条线?

「多少万行该上 ES」是个没有标准答案的问题——行数从来不是真正的触发条件,并发量、字段数、是否要聚合才是。与其记一个拍脑袋的阈值,不如记这些可观测的信号

信号 说明 该往哪走
索引重建时间超出维护窗口 单次全量重建要几小时 考虑外部引擎
BM25 查询 p99 持续超标 加索引、调参都压不下来 ES
需要高亮、拼写纠错、搜索联想 PG 生态在这块确实弱 ES
需要复杂聚合分析(facet、时间序列下钻) ES 的强项 ES
向量规模上亿,或延迟要求 20ms 内 pgvector 的 HNSW 会吃紧 Milvus
检索负载明显影响业务库 读写争抢同一个实例 先试只读副本,再考虑拆分

💡 拆分不必一次拆完 两路是可以分开迁的。最常见的渐进路径是:向量那一路先拆到 Milvus(因为向量规模先撑不住),关键词一路继续留在 PG。融合逻辑本就在应用层,两路来自不同的库并不影响 RRF——RRF 只需要两个名次列表。

这也反过来说明了陷阱 4为什么不成立:迁移是可以切片进行的,不需要一开始就赌。


速查表

我想要 用什么 不要用
中文关键词相关性排序 pg_search + jieba 分词器 pg_trgm(不做相关性)
拼写容错、模糊 LIKE 加速 pg_trgm pg_search(不是它的活)
语义/同义表达召回 pgvector + HNSW 任何关键词方案
两者都要(RAG 常见基线) pgvector + pg_search + 手写 RRF 未经评测就断定只需一路
高亮、纠错、联想、复杂聚合 ES 生态最全;pg_search 也已支持高亮/模糊/聚合 PG 裸核
向量规模超出单机内存 Milvus pgvector(HNSW 需常驻内存)
名词 一句话
BM25 关键词相关性打分公式,k1 管词频饱和,b 管长度惩罚,详见 BM25 算法原理
Tantivy Rust 写的全文检索库,受 Lucene 启发(不是移植);pg_search 和 Milvus 2.5 的 BM25 都用它
稀疏向量 BM25 的另一种表示形式,绝大部分维度为 0,配倒排索引使用
稠密向量 Embedding 模型的输出,每一维都有值,配 HNSW/IVF 使用
RRF 名次而非分数融合多路结果,Σ 1/(60+rank)
||| / &&& pg_search v2 的 BM25 匹配算子,分别是「命中任一词(OR)」和「全部命中(AND)」。@@@ 用于 query builder 等复杂查询;@@ 则是 PG 裸核的 tsquery 匹配,三者别混
pdb.score(id) pg_search v2 的 BM25 打分函数(旧写法 paradedb.score() 已更名)

复习重点

  1. BM25 已经不是选型的区分点。 PG、ES、Milvus 三家都有了,判断依据要换成运维边界:这个能力跑在你已有的进程里,还是要新起一个集群。
  2. 「PG 不支持中文 BM25」只在裸核那一层成立。 装上 pg_search 后,BM25 和 jieba 中文分词都在 PG 进程内解决,不新增服务、不搬数据。
  3. 裸核 ts_rank 的真实短板是没有 IDF、无法索引层早停,不是「词频线性叠加」——它其实有 1/(j+1)² 衰减。这两个短板由索引结构决定,写函数补救不了。
  4. 混合检索是思想不是功能。 关键词一路认字、语义一路认意思,RAG 两种问法都会遇到,所以两路都得要。
  5. RRF 只用名次不用分数,这是它能融合两套不可比量纲的原因;k=60 的作用是压平头部差距。
  6. 融合时两路必须 FULL OUTER JOIN + COALESCE 兜底,用 INNER JOIN 会丢掉单路命中的文档,NULL 参与算术会让排序失效。
  7. 中小项目默认答案:pgvector + pg_search 一把梭,把检索封装成可替换接口,等撞到具体信号(重建超窗口、p99 超标、需要高亮聚合、向量上亿)再迁,而且可以两路分开迁。
  8. pg_search 的 API 变动频繁paradedb.create_bm25() 已废弃,一律以官方文档为准,别信中文博客。

进一步学习

  • ParadeDB 官方文档 —— 建索引与 tokenizer 的当前写法(本篇代码的权威来源)
  • ParadeDB 博客《Hybrid Search in PostgreSQL: The Missing Manual》—— 官方版 RRF 写法
  • Milvus 官方文档 Full Text Search —— 若将来要拆向量那一路
  • RAG架构设计 —— 检索层该怎么封装成可替换接口
  • RAG优化方案 —— 两路召回之后的重排、评测与调优