timescale/pg_textsearch

GitHub: timescale/pg_textsearch

一个 PostgreSQL 扩展,在数据库内原生提供 BM25 相关性排序的全文本搜索能力,让用户无需部署外部搜索引擎即可获得高性能文本检索。

Stars: 3532 | Forks: 95

# pg_textsearch [![CI](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/aabe6f358e145120.svg)](https://github.com/timescale/pg_textsearch/actions/workflows/ci.yml) [![基准测试](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/3fde5e8742145122.svg)](https://timescale.github.io/pg_textsearch/benchmarks/) [![Coverity 扫描](https://scan.coverity.com/projects/32822/badge.svg)](https://scan.coverity.com/projects/pg_textsearch) 为 Postgres 提供的现代排名文本搜索。 - 简单的语法:`ORDER BY content <@> 'search terms'` - 带有可配置参数 (k1, b) 的 BM25 排名 - 兼容 Postgres 文本搜索配置(英语、法语、德语等) - 通过 Block-Max WAND 优化实现快速 top-k 查询 - 针对大表的并行索引构建 - 支持分区表 - 业界领先的性能和可扩展性 🚀 **状态**:v1.0.0 - 生产环境就绪。有关即将推出的功能,请参阅 [ROADMAP.md](ROADMAP.md)。 ![Tapir 和朋友们](https://static.pigsec.cn/wp-content/uploads/repos/2026/04/09ec220685145126.png) ## 历史说明 该项目最初的名字是 Tapir - **T**extual **A**nalysis for **P**ostgres **I**nformation **R**etrieval (Postgres 信息检索的文本分析)。我们仍然使用貘(tapir)作为我们的吉祥物,并且该名称在源代码的各个地方都有出现。 ## PostgreSQL 版本兼容性 pg_textsearch 支持 PostgreSQL 17 和 18。 ## 安装 ### 预编译二进制文件 从 [发布页面](https://github.com/timescale/pg_textsearch/releases) 下载预编译的二进制文件。 适用于 Linux 和 macOS(amd64 和 arm64),PostgreSQL 17 和 18。 ### 从源码构建 ``` cd /tmp git clone https://github.com/timescale/pg_textsearch cd pg_textsearch make make install # may need sudo ``` ## 入门指南 pg_textsearch 必须通过 `shared_preload_libraries` 加载。将以下内容添加到 `postgresql.conf` 并重启服务器: ``` shared_preload_libraries = 'pg_textsearch' # add to existing list if needed ``` 然后启用扩展(每个数据库执行一次): ``` CREATE EXTENSION pg_textsearch; ``` 创建一个包含文本内容的表 ``` CREATE TABLE documents (id bigserial PRIMARY KEY, content text); INSERT INTO documents (content) VALUES ('PostgreSQL is a powerful database system'), ('BM25 is an effective ranking function'), ('Full text search with custom scoring'); ``` 在文本列上创建一个 pg_textsearch 索引 ``` CREATE INDEX docs_idx ON documents USING bm25(content) WITH (text_config='english'); ``` ## 查询 使用 `<@>` 操作符获取最相关的文档 ``` SELECT * FROM documents ORDER BY content <@> 'database system' LIMIT 5; ``` 注意:`<@>` 返回负的 BM25 分数,因为 Postgres 在操作符上只支持 `ASC` 顺序的索引扫描。分数越低表示匹配度越好。 索引会从列中自动检测。要显式指定索引: ``` SELECT * FROM documents WHERE content <@> to_bm25query('database system', 'docs_idx') < -1.0; ``` 支持的操作: - `text <@> 'query'` - 根据查询对文本进行评分(自动检测索引) - `text <@> bm25query` - 使用显式索引规范对文本进行评分 ### 验证索引使用情况 使用 EXPLAIN 检查查询计划: ``` EXPLAIN SELECT * FROM documents ORDER BY content <@> 'database system' LIMIT 5; ``` 对于小型数据集,PostgreSQL 可能更倾向于顺序扫描。强制使用索引: ``` SET enable_seqscan = off; ``` 注意:即使 EXPLAIN 显示为顺序扫描,`<@>` 和 `to_bm25query` 也始终使用索引来获取 BM25 评分所需的语料库统计信息(文档计数、平均长度)。 ### 使用 WHERE 子句进行过滤 过滤与 BM25 索引扫描的交互有两种方式: **预过滤** 使用单独的索引(B-tree 等)在评分前减少行数: ``` -- Create index on filter column CREATE INDEX ON documents (category_id); -- Query filters first, then scores matching rows SELECT * FROM documents WHERE category_id = 123 ORDER BY content <@> 'search terms' LIMIT 10; ``` **后过滤** 首先应用 BM25 索引扫描,然后过滤结果: ``` SELECT * FROM documents WHERE content <@> to_bm25query('search terms', 'docs_idx') < -5.0 ORDER BY content <@> 'search terms' LIMIT 10; ``` **性能考量**: - **预过滤权衡**:如果过滤器匹配到大量行(例如 10 万+),对所有这些行进行评分可能会非常耗时。当 BM25 索引能够利用 top-k 优化(ORDER BY + LIMIT)来避免对每个匹配文档进行评分时,其效率最高。 - **后过滤权衡**:索引在过滤*之前*返回 top-k 结果。如果你的 WHERE 子句过滤掉了大部分结果,你得到的行数可能会少于请求的数量。可以通过增加 LIMIT 来弥补,然后在应用代码中重新限制。 - **最佳实践**:使用高选择性的条件(匹配行数小于 10%)进行预过滤,然后让 BM25 通过 ORDER BY + LIMIT 对减少后的数据集进行评分。 这与 [pgvector 中的过滤行为](https://github.com/pgvector/pgvector?tab=readme-ov-file#filtering) 类似,其近似索引也会在索引扫描后应用过滤。 ## 索引 在你的文本列上创建 BM25 索引: ``` CREATE INDEX ON documents USING bm25(content) WITH (text_config='english'); ``` ### 索引选项 - `text_config` - 要使用的 PostgreSQL 文本搜索配置(必填) - `k1` - 词频饱和度参数(默认为 1.2) - `b` - 长度归一化参数(默认为 0.75) ``` CREATE INDEX ON documents USING bm25(content) WITH (text_config='english', k1=1.5, b=0.8); ``` 同时支持不同的文本搜索配置: ``` -- English documents with stemming CREATE INDEX docs_en_idx ON documents USING bm25(content) WITH (text_config='english'); -- Simple text processing without stemming CREATE INDEX docs_simple_idx ON documents USING bm25(content) WITH (text_config='simple'); -- Language-specific configurations CREATE INDEX docs_fr_idx ON french_docs USING bm25(content) WITH (text_config='french'); CREATE INDEX docs_de_idx ON german_docs USING bm25(content) WITH (text_config='german'); ``` ## 数据类型 ### bm25query `bm25query` 类型表示带有可选索引上下文的 BM25 评分查询: ``` -- Create a bm25query with index name (required for WHERE clause and standalone scoring) SELECT to_bm25query('search query text', 'docs_idx'); -- Returns: docs_idx:search query text -- Embedded index name syntax (alternative form using cast) SELECT 'docs_idx:search query text'::bm25query; -- Returns: docs_idx:search query text -- Create a bm25query without index name (only works in ORDER BY with index scan) SELECT to_bm25query('search query text'); -- Returns: search query text ``` **注意**:在 PostgreSQL 18 中,使用单冒号(`:`)的内嵌索引名称语法允许查询计划器即使在提前评估 SELECT 子句表达式时也能确定索引名称。这确保了跨不同查询评估策略的兼容性。 #### bm25query 函数 函数 | 描述 --- | --- to_bm25query(text) → bm25query | 创建不包含索引名称的 bm25query(仅用于 ORDER BY) to_bm25query(text, text) → bm25query | 使用查询文本和索引名称创建 bm25query text <@> bm25query → double precision | BM25 评分操作符(返回负分数) bm25query = bm25query → boolean | 相等比较 ## 性能 pg_textsearch 索引使用 memtable 架构来实现高效的写入。与其他索引类型一样,在加载数据后创建索引速度更快。 ``` -- Load data first INSERT INTO documents (content) VALUES (...); -- Then create index CREATE INDEX docs_idx ON documents USING bm25(content) WITH (text_config='english'); ``` ### 并行索引构建 pg_textsearch 支持并行索引构建,以加快大型表的索引构建速度。 Postgres 会根据表大小和配置自动使用并行工作进程。 ``` -- Configure parallel workers (optional, uses server defaults otherwise) SET max_parallel_maintenance_workers = 4; SET maintenance_work_mem = '256MB'; -- At least 64MB required for parallel builds -- Create index (parallel workers used automatically for large tables) CREATE INDEX docs_idx ON documents USING bm25(content) WITH (text_config='english'); ``` **注意:** 计划器要求 `maintenance_work_mem >= 64MB` 才能启用并行索引构建。如果内存不足,构建会静默回退到串行模式。 当使用并行构建时,你会看到一条提示: ``` NOTICE: parallel index build: launched 4 of 4 requested workers ``` 对于分区表,如果分区足够大,每个分区将使用并行工作进程独立构建其索引。这允许对超大型分区数据集进行高效索引。 ### 性能调优 #### 强制合并段 索引跨层级将数据存储在多个段中(类似于 LSM 树)。在批量加载或持续的增量插入之后,可能会积累多个段;将它们合并为一个段可以通过减少扫描的段数来提高查询速度: ``` SELECT bm25_force_merge('docs_idx'); ``` 这类似于 Lucene 的 `forceMerge(1)`。它将所有段重写为单个段并回收释放的页面。最好在大型批量插入后使用,而不是在持续的写入流量期间使用。 #### 结合 ORDER BY 使用 LIMIT Top-k 查询(`ORDER BY ... LIMIT n`)可启用 Block-Max WAND 优化,该优化会跳过那些无法贡献给顶部结果的倒排列表块。 如果没有 LIMIT 子句,索引将回退为对所有匹配文档进行评分,最高可达 `pg_textsearch.default_limit`。 ``` -- Fast: BMW skips non-competitive blocks SELECT * FROM documents ORDER BY content <@> 'search terms' LIMIT 10; -- Slower: scores up to default_limit documents SELECT * FROM documents ORDER BY content <@> 'search terms'; ``` #### 段压缩 压缩默认开启,通常可以改善索引大小和查询性能(需要读取的页面更少)。只有在你观察到解压缩开销成为工作负载的瓶颈时才应禁用: ``` SET pg_textsearch.compress_segments = off; ``` #### 影响索引构建的 Postgres 设置 设置 | 效果 --- | --- `max_parallel_maintenance_workers` | CREATE INDEX 使用的并行工作进程数(默认为 2) `maintenance_work_mem` | 每个工作进程的内存;并行构建必须 >= 64MB #### pg_textsearch GUCs 设置 | 默认值 | 描述 --- | --- | --- `pg_textsearch.default_limit` | 1000 | 没有 LIMIT 子句时评分的最大文档数 `pg_textsearch.compress_segments` | on | 压缩新段中的倒排块 `pg_textsearch.segments_per_level` | 8 | 自动压缩前每层的段数 (2-64) `pg_textsearch.bulk_load_threshold` | 100000 | 触发自动溢出的每次事务词数(0 = 禁用) `pg_textsearch.memtable_spill_threshold` | 32000000 | 触发自动溢出的倒排条目数(0 = 禁用) #### 溢出阈值 `memtable_spill_threshold` 控制内存中的索引何时刷入磁盘段。当 memtable 达到该数量的倒排条目时,它会在事务提交时自动溢出。`bulk_load_threshold` 根据单个事务中的词条数触发溢出。两者都能在保持内存使用受限的同时维持良好的查询性能。 **崩溃恢复**:memtable 在启动时从堆中重建,因此如果 Postgres 在溢出到磁盘之前崩溃,数据也不会丢失。 ## 监控 ``` -- Check index usage SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetch FROM pg_stat_user_indexes WHERE indexrelid::regclass::text ~ 'pg_textsearch'; ``` ## 示例 ### 基础搜索 ``` CREATE TABLE articles (id serial PRIMARY KEY, title text, content text); CREATE INDEX articles_idx ON articles USING bm25(content) WITH (text_config='english'); INSERT INTO articles (title, content) VALUES ('Database Systems', 'PostgreSQL is a powerful relational database system'), ('Search Technology', 'Full text search enables finding relevant documents quickly'), ('Information Retrieval', 'BM25 is a ranking function used in search engines'); -- Find relevant documents SELECT title, content <@> 'database search' as score FROM articles ORDER BY score; ``` 同时支持不同的语言和自定义参数: ``` -- Different languages CREATE INDEX fr_idx ON french_articles USING bm25(content) WITH (text_config='french'); CREATE INDEX de_idx ON german_articles USING bm25(content) WITH (text_config='german'); -- Custom parameters CREATE INDEX custom_idx ON documents USING bm25(content) WITH (text_config='english', k1=2.0, b=0.9); ``` ## 限制 ### 不支持短语查询 BM25 索引存储的是词频而不是词位置,因此它本身无法评估像 `"database system"` 这样的短语查询。你可以通过将 BM25 排名与后过滤相结合来模拟短语匹配: ``` -- BM25 ranks candidates; subquery over-fetches to account for -- post-filter eliminating non-phrase matches SELECT * FROM ( SELECT *, content <@> 'database system' AS score FROM documents ORDER BY score LIMIT 100 -- over-fetch ) sub WHERE content ILIKE '%database system%' ORDER BY score LIMIT 10; ``` 由于后过滤会消除某些结果,因此内部的 LIMIT 应大于所需的结果数量。 ### 不支持表达式索引 每个 BM25 索引只覆盖一个文本列。你不能在像 `lower(title) || ' ' || content` 这样的表达式上创建索引。作为一种变通方法,可以使用生成列: ``` ALTER TABLE documents ADD COLUMN search_text text GENERATED ALWAYS AS ( COALESCE(title, '') || ' ' || COALESCE(content, '') ) STORED; CREATE INDEX ON documents USING bm25(search_text) WITH (text_config = 'english'); ``` ### 无内置分面搜索 pg_textsearch 不提供专用的分面操作符,但标准的 Postgres 查询机制可以处理常见的分面模式: ``` -- Filter by category (assumes a B-tree index on category) SELECT * FROM documents WHERE category = 'engineering' ORDER BY content <@> 'search terms' LIMIT 10; -- Compute facet counts over BM25-matched results SELECT category, count(*) FROM documents WHERE content <@> to_bm25query('search terms', 'docs_idx') < -1.0 GROUP BY category; ``` ### 插入/更新性能 memtable 架构旨在支持高效的写入,但持续的写入密集型工作负载尚未完全优化。对于初始数据加载,在加载数据后创建索引比增量插入更快。这是一个正在积极开发的领域。 ### 无后台压缩 段压缩目前是在 memtable 溢出操作期间同步运行的。写入密集型工作负载可能会在溢出期间观察到压缩延迟。后台压缩计划在未来版本中发布。 ### 分区表 分区表上的 BM25 索引使用**分区局部统计信息**。每个分区维护自己的: - 文档计数(`total_docs`) - 平均文档长度(`avg_doc_len`) - 用于 IDF 计算的按词条文档频率 这意味着: - 针对单个分区的查询使用该分区的统计信息计算准确的 BM25 分数 - 跨多个分区的查询返回按分区独立计算的分数,这些分数在不同分区之间可能无法直接比较 **示例**:如果分区 A 有 1000 个文档,分区 B 有 10 个文档,则术语 "database" 在每个分区中将有不同的 IDF 值。来自两个分区的结果将具有不同比例的分数。 **建议**: - 对于时间分区数据,当分数可比性很重要时,请查询单个分区 - 使用查询自然针对单个分区的分区方案 - 在为搜索工作负载设计分区策略时,请考虑此行为 ``` -- Query single partition (scores are accurate within partition) SELECT * FROM docs WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01' ORDER BY content <@> 'search terms' LIMIT 10; -- Cross-partition query (scores computed per-partition) SELECT * FROM docs ORDER BY content <@> 'search terms' LIMIT 10; ``` ### 词长限制 pg_textsearch 继承了 PostgreSQL tsvector 2047 个字符的词长限制。 超出此限制的词在标记化处理期间将被忽略(并附带一条 INFO 消息)。 这由 PostgreSQL 文本搜索实现中的 `MAXSTRLEN` 定义。 对于典型的自然语言文本,永远不会遇到此限制。它可能会影响包含超长 token 的文档,例如 base64 编码的数据、长 URL 或连接的标识符。 此行为类似于其他搜索引擎: - Elasticsearch:截断 token(可通过 `truncate` 过滤器配置,默认为 10 个字符) - Tantivy:默认截断为 255 个字节 ### PL/pgSQL 和存储过程 隐式的 `text <@> 'query'` 语法依赖于计划器钩子来自动检测 BM25 索引。这些钩子不会在 PL/pgSQL DO 块、函数或存储过程内部运行。 **在 PL/pgSQL 内部**,请使用带有 `to_bm25query()` 的显式索引名称: ``` -- This won't work in PL/pgSQL: -- SELECT * FROM docs ORDER BY content <@> 'search terms' LIMIT 10; -- Use explicit index name instead: SELECT * FROM docs ORDER BY content <@> to_bm25query('search terms', 'docs_idx') LIMIT 10; ``` (PL/pgSQL 外部的)常规 SQL 查询同时支持这两种形式。 ## 故障排除 ``` -- List available text search configurations SELECT cfgname FROM pg_ts_config; -- List BM25 indexes SELECT indexname FROM pg_indexes WHERE indexdef LIKE '%USING bm25%'; ``` ## 安装说明 如果你的机器有多个 Postgres 安装,请指定 `pg_config` 的路径: ``` export PG_CONFIG=/Library/PostgreSQL/18/bin/pg_config # or 17 make clean && make && make install ``` 如果遇到编译错误,请安装 Postgres 开发文件: ``` # Ubuntu/Debian sudo apt install postgresql-server-dev-17 # for PostgreSQL 17 sudo apt install postgresql-server-dev-18 # for PostgreSQL 18 ``` ## 参考 ### 索引选项 选项 | 类型 | 默认值 | 描述 --- | --- | --- | --- text_config | string | 必填 | 要使用的 PostgreSQL 文本搜索配置 k1 | real | 1.2 | 词频饱和度参数(0.1 到 10.0) b | real | 0.75 | 长度归一化参数(0.0 到 1.0) ### 文本搜索配置 可用的配置取决于你的 Postgres 安装: ``` # SELECT cfgname FROM pg_ts_config; cfgname ------------ simple arabic armenian basque catalan danish dutch english finnish french german greek hindi hungarian indonesian irish italian lithuanian nepali norwegian portuguese romanian russian serbian spanish swedish tamil turkish yiddish (29 rows) ``` 通过诸如 [zhparser](
标签:Block-Max WAND, BM25算法, DNS解析, pg_textsearch, PostgreSQL, SEO检索, Timescale, Top-k查询, 信息检索, 全文检索, 分区表支持, 国家安全局, 大数据检索, 客户端加密, 并行索引构建, 幻觉缓解, 开源项目, 数据库扩展, 数据库插件, 数据检索, 文本分析, 测试用例, 相关性排序, 科技, 高性能搜索