dan*_*njo 8 postgresql performance index full-text-search postgresql-9.3
我有一个包含从文本文档中提取的数据的表。数据存储在一个名为的列"CONTENT"
中,我使用 GIN 创建了该索引:
CREATE INDEX "File_contentIndex"
ON "File"
USING gin
(setweight(to_tsvector('english'::regconfig
, COALESCE("CONTENT", ''::character varying)::text), 'C'::"char"));
Run Code Online (Sandbox Code Playgroud)
我使用以下查询对表执行全文搜索:
SELECT "ITEMID",
ts_rank(setweight(to_tsvector('english', coalesce("CONTENT",'')), 'C') ,
plainto_tsquery('english', 'searchTerm')) AS "RANK"
FROM "File"
WHERE setweight(to_tsvector('english', coalesce("CONTENT",'')), 'C')
@@ plainto_tsquery('english', 'searchTerm')
ORDER BY "RANK" DESC
LIMIT 5;
Run Code Online (Sandbox Code Playgroud)
File 表包含 250 000 行,每个"CONTENT"
条目由一个随机单词和一个所有行都相同的文本字符串组成。
现在,当我搜索一个随机单词(在整个表中命中 1 个)时,查询运行得非常快(<100 毫秒)。但是,当我搜索出现在所有行中的单词时,查询运行速度非常慢(10 分钟或更长时间)。
EXPLAIN ANALYZE
显示对于 1-hit 搜索,先执行位图索引扫描,然后执行位图堆扫描。对于慢速搜索,改为执行Seq Scan,这需要很长时间。
当然,在所有行中使用相同的数据是不现实的。但是由于我无法控制用户上传的文本文档,也无法控制他们执行的搜索,因此可能会出现类似的情况(搜索在 DB 中出现率很高的术语)。在这种情况下,如何提高搜索查询的性能?
运行 PostgreSQL 9.3.4
查询计划来自EXPLAIN ANALYZE
:
快速搜索(在 DB 中命中 1 次)
"Limit (cost=2802.89..2802.90 rows=5 width=26) (actual time=0.037..0.037 rows=1 loops=1)"
" -> Sort (cost=2802.89..2806.15 rows=1305 width=26) (actual time=0.037..0.037 rows=1 loops=1)"
" Sort Key: (ts_rank(setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char"), '''wfecg'''::tsquery))"
" Sort Method: quicksort Memory: 25kB"
" -> Bitmap Heap Scan on "File" (cost=38.12..2781.21 rows=1305 width=26) (actual time=0.030..0.031 rows=1 loops=1)"
" Recheck Cond: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''wfecg'''::tsquery)"
" -> Bitmap Index Scan on "File_contentIndex" (cost=0.00..37.79 rows=1305 width=0) (actual time=0.012..0.012 rows=1 loops=1)"
" Index Cond: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''wfecg'''::tsquery)"
"Total runtime: 0.069 ms"
Run Code Online (Sandbox Code Playgroud)
慢速搜索(数据库中有 25 万次点击)
"Limit (cost=14876.82..14876.84 rows=5 width=26) (actual time=519667.404..519667.405 rows=5 loops=1)"
" -> Sort (cost=14876.82..15529.37 rows=261017 width=26) (actual time=519667.402..519667.402 rows=5 loops=1)"
" Sort Key: (ts_rank(setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char"), '''cyberspace'''::tsquery))"
" Sort Method: top-N heapsort Memory: 25kB"
" -> Seq Scan on "File" (cost=0.00..10541.43 rows=261017 width=26) (actual time=2.097..519465.953 rows=261011 loops=1)"
" Filter: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''cyberspace'''::tsquery)"
" Rows Removed by Filter: 6"
"Total runtime: 519667.429 ms"
Run Code Online (Sandbox Code Playgroud)
Erw*_*ter 11
...每个 CONTENT 条目由一个随机单词和一个对所有行都相同的文本字符串组成。
所有行都相同的文本字符串就是空运。如果需要显示它,请将其删除并连接到视图中。
显然,你知道:
当然,这是不现实的......但由于我无法控制文本......
运行 PostgreSQL 9.3.4
虽然仍在 Postgres 9.3 上,但您至少应该升级到最新版本(当前为 9.3.9)。项目官方推荐:
我们始终建议所有用户针对正在使用的任何主要版本运行最新的可用次要版本。
更好的是,升级到9.4,它对GIN 索引进行了重大改进。
一些文本搜索功能的成本在 9.4 版本之前被严重低估了。在即将到来的 9.5 版本中,这个成本增加了 100 倍,就像@jjanes 在他最近的回答中描述的那样:
正如您在提交消息中看到的,to_tsvector()
是这些功能之一。您可以立即应用更改(作为超级用户):
ALTER FUNCTION to_tsvector (regconfig, text) COST 100;
Run Code Online (Sandbox Code Playgroud)
这应该让很多更可能是你的功能使用索引。
核心问题是 Postgres 必须计算ts_rank()
260k 行 ( rows=261011
)的排名,然后才能按顺序排序并选择前 5 位。即使您已经按照讨论解决了其他问题,这也会很昂贵。它本质上是一个K 最近邻 (KNN) 问题,并且有相关案例的解决方案。但是我想不出适合您的情况的通用解决方案,因为排名计算本身取决于用户输入。我会尝试尽早消除大部分低排名的比赛,以便只需要对少数优秀候选人进行全面计算。
我能想到的一种方法是将全文搜索与三元组相似性搜索相结合——这为 KNN 问题提供了一个可行的实现。通过这种方式,您可以预先选择带有LIKE
谓词的“最佳”匹配项作为候选(LIMIT 50
例如在子查询中),然后根据您在主查询中的排名计算选择排名靠前的 5 行。
或者在同一个查询中应用两个谓词,并根据三元组的相似性(这会产生不同的结果)选择最接近的匹配项,就像在这个相关的答案中一样:
我做了更多的研究,你不是第一个遇到这个问题的人。关于 pgsql-general 的相关帖子:
正在进行工作以最终实现一个tsvector <-> tsquery
操作符。
Oleg Bartunov 和 Alexander Korotkov 甚至提出了一个工作原型(使用><
as 操作符而不是<->
当时),但是在 Postgres 中集成非常复杂,GIN 索引的整个基础结构必须重新设计(大部分现在已经完成)。
我确定了另一个增加查询速度的因素。根据文档:
对于标准查询,GIN 索引不是有损的,但它们的性能在对数上取决于唯一词的数量。(但是,GIN 索引仅存储值的单词(词素)
tsvector
,而不是它们的权重标签。因此,在使用涉及权重的查询时需要重新检查表行。)
大胆强调我的。一旦涉及到权重,就必须从堆中获取每一行(不仅仅是廉价的可见性检查),并且必须去除长值,这会增加成本。但似乎有一个解决方案:
再次查看您的索引,开始似乎没有意义。您为单个列分配权重,只要您不连接具有不同权重的其他列,这毫无意义。
COALESCE()
只要您实际上没有连接更多列,这也是没有意义的。
简化索引:
CREATE INDEX "File_contentIndex" ON "File" USING gin
(to_tsvector('english', "CONTENT");
Run Code Online (Sandbox Code Playgroud)
你的查询:
SELECT "ITEMID", ts_rank(to_tsvector('english', "CONTENT")
, plainto_tsquery('english', 'searchTerm')) AS rank
FROM "File"
WHERE to_tsvector('english', "CONTENT")
@@ plainto_tsquery('english', 'searchTerm')
ORDER BY rank DESC
LIMIT 5;
Run Code Online (Sandbox Code Playgroud)
对于匹配每一行的搜索词来说仍然很昂贵,但可能要少得多。
所有这些问题结合在一起,您第二次查询 520 秒的疯狂成本开始变得有意义。但可能还有更多问题。你配置你的服务器了吗?
所有关于性能优化的常用建议都适用。
如果您不使用双引号 CaMeL-case 标识符,它会让您的生活更轻松: