在使用 PostgreSQL 的 pg_textsearch 扩展进行全文检索时,如果你计划使用分区表,那么需要格外注意。本文的核心观点是:在这种情况下,你应当避免使用分区表。这一点在其 GitHub 仓库的说明中也有提及。
其根本原因在于,pg_textsearch 用来计算 BM25 得分的核心统计信息存储在索引里,而非全局的数据库视图中。由于 PostgreSQL 本身没有“全局索引”的概念,当数据分布在多个分区时,每个分区都会维护自己独立的统计信息集。这直接导致了跨分区查询时,基于 BM25 的排序结果可能失去可比性。
核心原因:分区本地统计信息
当 pg_textsearch 在分区表上运行时,其依赖的关键统计信息是在每个分区级别独立维护的,主要包括:
- 文档总数 (
total_docs)
- 平均文档长度 (
avg_doc_len)
- 用于计算 IDF(逆文档频率)的词项文档频率
这意味着,一个词项在“小”分区和“大”分区中的重要性(IDF值)计算基准是不同的。
实际影响:得分不可直接比较
这会产生两种场景:
- 单分区查询:查询只命中一个分区时,系统使用该分区的本地统计信息,可以计算出准确且在该分区内有意义的 BM25 得分。
- 跨分区查询:当查询需要扫描多个分区时,每个分区内的文档得分是依据其自身分区统计信息独立计算的。由于统计基准(如总文档数)不同,从不同分区返回的得分无法直接进行跨分区比较和排序。
项目测试文件中的示例清晰地展示了这个问题:
-- 在小分区(仅3个文档)中搜索 ‘database’ 的得分
SELECT content, ROUND((content <@> ‘database’)::numeric, 4) as score
FROM partition_small;
-- 结果示例:-0.4901
-- 在大分区(10个文档)中搜索同一个词 ‘database’ 的得分
SELECT content, ROUND((content <@> ‘database’)::numeric, 4) as score
FROM partition_large;
-- 结果示例:-1.4816
可以看到,相同的搜索词 ‘database’ 在两个不同大小的分区中得到了完全不同的 BM25 得分。这是因为 IDF 的计算依赖于 log( (N - n + 0.5) / (n + 0.5) ) 这样的公式,其中 N(分区总文档数)在partition_small和partition_large中分别是3和10,导致计算出的逆文档频率和最终得分大相径庭。
给你的实用建议
如果你的应用场景要求对全表数据(或跨越多个分区)的 BM25 得分进行排序和比较,例如返回“全局最相关”的文档,那么使用分区表会引入不准确性和混乱。你可以考虑以下替代方案:
- 放宽分区窗口:对于按时间分区的数据,如果查询通常需要比较近期数据和历史数据的相关性,可以考虑将单个分区的时间范围设置得足够大,使其能覆盖大多数查询的需求,让查询尽可能落在单一分区内。
- 精心设计分区键:采用一种能确保绝大多数查询都自然落在单个分区内的分区策略(例如,按明确的业务维度分区,而非单纯按时间)。
- 评估分区必要性:在设计涉及全文检索工作负载的表结构时,需要仔细权衡分区带来的管理优势与全局相关性排序失效之间的利弊。
技术原理备注
BM25 得分的具体计算在函数 bm25_text_bm25query_score 中实现。该函数会从索引中读取上述统计信息来完成计算。关键在于,对于分区表,索引及其携带的统计信息是按分区构建的,因此计算自然也是分区级别的,无法获得全局统一的统计视角。
|