向量检索擅长理解“意思相近”,却未必能稳定识别产品 SKU、零件编号、地名和其他精确词项。现在,AlloyDB 与 Cloud SQL for PostgreSQL 17+ 预览支持基于开源 pg_textsearch 扩展的原生 BM25 索引,可以把关键词检索、向量检索和业务数据放进同一个 PostgreSQL 后端,减少额外搜索集群带来的数据复制、ETL 与同步延迟。
向量搜索为什么还需要 BM25
在 RAG、商品搜索和数据 Agent 中,两类查询经常同时出现:
- “能长得比房子还高的树”依赖语义理解,适合向量检索。
- “California”“SKU-AX19”或某个具体型号依赖词项匹配,关键词检索通常更可靠。
PostgreSQL 自带全文搜索可以通过 ts_rank 排序,但摘要指出,它不具备 BM25 的完整统计能力。BM25 主要补上三点:
- 逆文档频率(IDF):罕见词比到处出现的常见词更有区分度。
- 词频饱和:某个词重复 50 次,不会简单地获得 50 倍优势。
- 文档长度归一化:避免长文档仅因为包含更多词就天然排在前面。
因此,BM25 并不是向量搜索的替代品。更实用的设计是让向量搜索负责语义召回,让 BM25 负责关键词相关性,再把两份候选结果融合起来。
需要注意,严格的标识符查询仍应保留普通索引。例如 SKU 必须完全相等时,优先使用 B-tree 和 WHERE sku = $1;BM25 更适合排序相关的文本检索,而不是替代数据库约束与精确过滤。
用一张表验证 BM25 排序
以下示例假设数据库运行 PostgreSQL 17+,并且所在的 AlloyDB 或 Cloud SQL 实例已经开放 pg_textsearch 预览扩展。执行扩展安装可能需要相应数据库权限。
这是一份可以直接改造的最小 SQL:
-- 启用 BM25 扩展
CREATE EXTENSION IF NOT EXISTS pg_textsearch;
DROP TABLE IF EXISTS cymbal_products;
CREATE TABLE cymbal_products (
uniq_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
product_name text NOT NULL,
product_description text NOT NULL
);
INSERT INTO cymbal_products (product_name, product_description) VALUES
('California Sycamore', 'A tall California tree suitable for large outdoor spaces'),
('Dwarf Cherry Tree', 'A compact cherry tree for patios and small gardens'),
('Cherry Blossom', 'An ornamental flowering cherry tree with pink blossoms'),
('Indoor Fern', 'A shade-loving indoor plant for humid rooms');
CREATE INDEX idx_products_description_bm25
ON cymbal_products
USING bm25 (product_description)
WITH (text_config = 'english');
SELECT
uniq_id,
product_name,
product_description <@> 'cherry tree' AS bm25_score
FROM cymbal_products
ORDER BY bm25_score
LIMIT 5;
<@> 是 pg_textsearch 提供的 BM25 排序操作符。在这里,分数越负,匹配越强,因此查询使用升序排列。
示例中的 text_config = 'english' 是因为样例数据为英文。生产环境不要直接照搬:索引配置必须与数据语言、分词方式和查询语言保持一致,并通过自己的语料验证召回率。对于中英文混合文本、带连字符的 SKU 或特殊领域缩写,还需要单独测试分词结果。
把 BM25 与向量结果合成一份排名
混合搜索一般不是把两个原始分数直接相加。向量距离与 BM25 分数的量纲、范围和方向不同,直接相加很难稳定调参。摘要中的方案使用 Reciprocal Rank Fusion(RRF):只依赖每个结果在各自列表中的名次,再生成统一分数。
常见形式如下:
RRF(d) = Σ 1 / (k + rank_i(d))
其中 rank_i(d) 是文档在第 i 个检索通道中的名次,k 常用来降低头部名次之间过大的差异。示例采用 60。
AlloyDB:ScaNN 与内置混合搜索 UDF
AlloyDB 可以在同一张表上创建 BM25 索引和 ScaNN 向量索引:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS alloydb_scann;
CREATE INDEX cymbal_products_embeddings_scann
ON cymbal_products
USING scann (product_embedding cosine);
随后可以使用 AlloyDB 提供的混合搜索 UDF,把向量通道与文本通道提交给 ai.hybrid_search,由函数通过 RRF 合并结果。使用前需要启用并配置 google_ml_integration,同时确认 product_embedding 的维度与所选嵌入模型一致。
Cloud SQL:使用 CTE 手动完成 RRF
Cloud SQL 可以使用 pgvector 的 HNSW 索引处理语义检索,再用 CTE 合并 BM25 排名。下面假设表中已有 product_embedding 向量列,并已配置 google_ml_integration 的模型访问权限:
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS google_ml_integration;
CREATE INDEX IF NOT EXISTS product_hnsw_idx
ON cymbal_products
USING hnsw (product_embedding vector_cosine_ops);
WITH query_params AS (
SELECT google_ml.embedding(
'text-embedding-005',
'trees that grow taller than houses'
)::vector AS query_vector
),
keyword_results AS (
SELECT
uniq_id,
product_name,
ROW_NUMBER() OVER (
ORDER BY product_description <@> 'California'
) AS rank_kw
FROM cymbal_products
ORDER BY product_description <@> 'California'
LIMIT 10
),
semantic_results AS (
SELECT
p.uniq_id,
p.product_name,
ROW_NUMBER() OVER (
ORDER BY p.product_embedding <=> q.query_vector
) AS rank_vec
FROM cymbal_products AS p
CROSS JOIN query_params AS q
ORDER BY p.product_embedding <=> q.query_vector
LIMIT 10
)
SELECT
COALESCE(k.uniq_id, s.uniq_id) AS uniq_id,
COALESCE(k.product_name, s.product_name) AS product_name,
COALESCE(1.0 / (60 + k.rank_kw), 0) +
COALESCE(1.0 / (60 + s.rank_vec), 0) AS rrf_score
FROM keyword_results AS k
FULL OUTER JOIN semantic_results AS s
ON k.uniq_id = s.uniq_id
ORDER BY rrf_score DESC
LIMIT 5;
运行前需要把模型名称、向量维度、表名和列名改成自己的配置。这里两个通道权重相同;如果关键词精度更重要,可以将关键词项乘以更高权重,例如:
0.7 * COALESCE(1.0 / (60 + k.rank_kw), 0) +
0.3 * COALESCE(1.0 / (60 + s.rank_vec), 0) AS rrf_score
权重不应凭感觉确定。更稳妥的做法是准备一组带期望结果的查询集,分别测量纯 BM25、纯向量和不同 RRF 权重下的 Recall@K、MRR 或 NDCG。
上线前应检查什么
原生 BM25 的核心价值不只是少部署一个服务,而是减少数据副本和一致性边界。商品描述更新后,关键词索引与业务行位于同一个数据库中,不再需要等待外部搜索后端完成同步。不过,这不意味着搜索系统从此没有成本。
上线前建议检查以下事项:
- 版本与可用性:该能力处于预览阶段,并面向 PostgreSQL 17+;先确认实例版本、区域和扩展权限。
- 索引开销:BM25、HNSW 或 ScaNN 都会占用存储,并增加写入和维护成本。
- 候选集大小:RRF 只能融合各通道已经召回的结果;每个通道只取 10 条可能不足,也可能最省延迟。
- 语言与分词:用真实查询验证产品型号、缩写、连字符和多语言内容。
- 精确过滤:租户、权限、库存和地域等条件应在 SQL 中明确过滤,不能只依赖相关性排名。
- 嵌入一致性:文档和查询必须使用兼容的嵌入模型与相同维度。
- 预览风险:在正式生产采用前评估接口变化、升级路径、备份恢复和回退方案。
如果应用已经把主数据放在 AlloyDB 或 Cloud SQL 中,这项能力值得先从一个边界清晰的检索场景开始验证。先建立纯 BM25 基线,再加入向量召回和 RRF;只有离线指标与真实点击数据都证明混合搜索更好时,才逐步扩大流量。这样既能获得语义理解与精确关键词匹配的互补优势,也能避免为了“混合”而引入不必要的查询复杂度。