PostgreSQL 中的高效全文搜索,在另一列上排序

Posted

技术标签:

【中文标题】PostgreSQL 中的高效全文搜索,在另一列上排序【英文标题】:Efficient full text search in PostgreSQL, sorting on another column 【发布时间】:2021-11-06 01:56:34 【问题描述】:

在 PostgreSQL 中,如何有效地在一列上进行全文搜索,在另一列上进行排序?

假设我有一个表 tbl 与列 abc,... 和许多(>一百万)行。我想对列 a 进行全文搜索,然后按其他列对结果进行排序。

所以我从 a 列创建了一个 tsvector va

ALTER TABLE tbl
ADD COLUMN va tsvector GENERATED ALWAYS AS (to_tsvector('english', a)) STORED;

为此创建一个索引iva

CREATE INDEX iva ON tbl USING GIN (va);

还有一个索引ib 用于列b

CREATE INDEX ib ON tbl (b);

然后我像这样查询

SELECT * FROM tbl WHERE va @@ to_tsquery('english', 'test') ORDER BY b LIMIT 100

现在 Postgres 的明显执行策略是:

    对于使用ib的频繁词进行索引扫描,过滤va @@ 'test'::tsquery,并在100个匹配后停止,

    同时使用iva 对稀有词进行(位图)索引扫描,条件是 va @@ 'test'::tsquery,然后手动对b进行排序

不过,Postgres 的查询规划器似乎没有考虑词频:

LIMIT 较低(例如 100)它总是使用策略 1(正如我使用 EXPLAIN 检查的那样),在我的情况下,对于罕见(或未出现)的单词需要一分钟以上的时间。但是,如果我通过设置一个大的(或没有)LIMIT 来欺骗它使用策略 2,它会在一毫秒内返回!

反之,如果LIMIT 更大(例如200),它总是使用策略 2,该策略适用于稀有词,但对于常用词非常慢

那么如何让 Postgres 在每种情况下都使用一个好的查询计划?

由于目前似乎没有办法让 Postgres 自动选择正确的计划,

如何获取包含特定词位的行数,以便决定最佳策略?

SELECT COUNT(*) FROM tbl WHERE va @@ to_tsquery('english', 'test') 非常慢(对于 10000 行中出现的词位约 1 秒),ts_stat 似乎也无济于事,除了建立我自己的词频列表)

然后我如何告诉 Postgres 使用这个策略?


这是一个具体的例子

我有一个表 items 有 150 万行,我在其上进行文本搜索的 tsvector 列 v3 和我在其上排序的列 rating。在这种情况下,如果 LIMIT 小于或等于 135,我确定查询规划器始终选择策略 1,否则选择策略 2

这里是罕见词“aberdeen”(出现在 132 行中)的解释分析,限制为 135:

EXPLAIN (ANALYZE, BUFFERS) SELECT nm FROM items WHERE v3 @@ to_tsquery('english', 'aberdeen')
  ORDER BY rating DESC NULLS LAST LIMIT 135

Limit  (cost=0.43..26412.78 rows=135 width=28) (actual time=5915.455..499917.390 rows=132 loops=1)
  Buffers: shared hit=4444267 read=2219412
  I/O Timings: read=485517.381
  ->  Index Scan using ir on items  (cost=0.43..1429202.13 rows=7305 width=28) (actual time=5915.453..499917.242 rows=132 loops=1)
        Filter: (v3 @@ '''aberdeen'''::tsquery)"
        Rows Removed by Filter: 1460845
        Buffers: shared hit=4444267 read=2219412
        I/O Timings: read=485517.381
Planning:
  Buffers: shared hit=253
Planning Time: 1.270 ms
Execution Time: 499919.196 ms

并且限制为 136:

EXPLAIN (ANALYZE, BUFFERS) SELECT nm FROM items WHERE v3 @@ to_tsquery('english', 'aberdeen')
  ORDER BY rating DESC NULLS LAST LIMIT 136

Limit  (cost=26245.53..26245.87 rows=136 width=28) (actual time=29.870..29.889 rows=132 loops=1)
  Buffers: shared hit=57 read=83
  I/O Timings: read=29.085
  ->  Sort  (cost=26245.53..26263.79 rows=7305 width=28) (actual time=29.868..29.876 rows=132 loops=1)
        Sort Key: rating DESC NULLS LAST
        Sort Method: quicksort  Memory: 34kB
        Buffers: shared hit=57 read=83
        I/O Timings: read=29.085
        ->  Bitmap Heap Scan on items  (cost=88.61..25950.14 rows=7305 width=28) (actual time=1.361..29.792 rows=132 loops=1)
              Recheck Cond: (v3 @@ '''aberdeen'''::tsquery)"
              Heap Blocks: exact=132
              Buffers: shared hit=54 read=83
              I/O Timings: read=29.085
              ->  Bitmap Index Scan on iv3  (cost=0.00..86.79 rows=7305 width=0) (actual time=1.345..1.345 rows=132 loops=1)
                    Index Cond: (v3 @@ '''aberdeen'''::tsquery)"
                    Buffers: shared hit=3 read=2
                    I/O Timings: read=1.299
Planning:
  Buffers: shared hit=253
Planning Time: 1.296 ms
Execution Time: 29.932 ms

这里是 LIMIT 135 的常用词“游戏”(出现在 240464 行中):

EXPLAIN (ANALYZE, BUFFERS) SELECT nm FROM items WHERE v3 @@ to_tsquery('english', 'game')
  ORDER BY rating DESC NULLS LAST LIMIT 135

Limit  (cost=0.43..26412.78 rows=135 width=28) (actual time=3.240..542.252 rows=135 loops=1)
  Buffers: shared hit=2876 read=1930
  I/O Timings: read=529.523
  ->  Index Scan using ir on items  (cost=0.43..1429202.13 rows=7305 width=28) (actual time=3.239..542.216 rows=135 loops=1)
        Filter: (v3 @@ '''game'''::tsquery)
        Rows Removed by Filter: 867
        Buffers: shared hit=2876 read=1930
        I/O Timings: read=529.523
Planning:
  Buffers: shared hit=208 read=45
  I/O Timings: read=15.626
Planning Time: 25.174 ms
Execution Time: 542.306 ms

并且限制为 136:

EXPLAIN (ANALYZE, BUFFERS) SELECT nm FROM items WHERE v3 @@ to_tsquery('english', 'game')
  ORDER BY rating DESC NULLS LAST LIMIT 136
  
Limit  (cost=26245.53..26245.87 rows=136 width=28) (actual time=69419.656..69419.675 rows=136 loops=1)
  Buffers: shared hit=1757820 read=457619
  I/O Timings: read=65246.893
  ->  Sort  (cost=26245.53..26263.79 rows=7305 width=28) (actual time=69419.654..69419.662 rows=136 loops=1)
        Sort Key: rating DESC NULLS LAST
        Sort Method: top-N heapsort  Memory: 41kB
        Buffers: shared hit=1757820 read=457619
        I/O Timings: read=65246.893
        ->  Bitmap Heap Scan on items  (cost=88.61..25950.14 rows=7305 width=28) (actual time=110.959..69326.343 rows=240464 loops=1)
              Recheck Cond: (v3 @@ '''game'''::tsquery)
              Rows Removed by Index Recheck: 394527
              Heap Blocks: exact=49894 lossy=132284
              Buffers: shared hit=1757817 read=457619
              I/O Timings: read=65246.893
              ->  Bitmap Index Scan on iv3  (cost=0.00..86.79 rows=7305 width=0) (actual time=100.537..100.538 rows=240464 loops=1)
                    Index Cond: (v3 @@ '''game'''::tsquery)
                    Buffers: shared hit=1 read=60
                    I/O Timings: read=26.870
Planning:
  Buffers: shared hit=253
Planning Time: 1.195 ms
Execution Time: 69420.399 ms

【问题讨论】:

什么版本?规划器不是静态的。 Postgres 13,“非静态”是什么意思? 非静态 = 从一个版本到另一个版本得到改进。 这对我来说似乎做得很好。您能否从具体案例中向我们展示具体的 EXPLAIN (ANALYZE, BUFFERS),并用定量值替换“>一百万”、“稀有”和“频繁”?如果可能,请先打开 track_io_timing。 @jjanes 我现在已经按照你的要求给出了一个具体的例子 【参考方案1】:

这个不好解决:全文搜索需要GIN索引,但是GIN索引不支持ORDER BY。另外,如果你有ORDER BY的B-tree索引和全文搜索的GIN索引,可以使用位图索引扫描组合它们,但是位图索引扫描也不支持ORDER BY

如果您创建自己的“停用词”列表,其中包含数据中的所有常用词(除了正常的英语停用词),我认为这是一种可能性。然后,您可以定义使用该停用词文件的文本搜索字典和使用该字典的文本搜索配置english_rare

然后您可以使用该配置创建全文索引,并分两步进行查询:

    寻找稀有词:

    SELECT *
    FROM (SELECT *
          FROM tbl
          WHERE va @@ to_tsquery('english_rare', 'test')
          OFFSET 0) AS q
    ORDER BY b LIMIT 100;
    

    带有OFFSET 0 的子查询将阻止优化器扫描b 上的索引。

    对于稀有词,这将很快返回正确的结果。对于频繁出现的单词,这不会返回任何内容,因为to_tsquery 将返回一个空结果。要区分因单词未出现而导致的未命中和因单词频繁出现而导致的未命中,请注意以下注意事项:

    NOTICE:  text-search query contains only stop words or doesn't contain lexemes, ignored
    

    寻找常用词(如果第一个查询通知您):

    SELECT *
    FROM (SELECT *
          FROM tbl
          ORDER BY b) AS q
    WHERE va @@ to_tsquery('english', 'test')
    LIMIT 100;
    

    注意我们这里使用的是正常的英文配置。这将始终扫描b 上的索引,并且对于频繁的搜索词将相当快。

【讨论】:

感谢您的回答!它似乎比我的第一个想法好得多(启动两个并发查询并在其他答案后立即杀死一个需要时间的查询......)。我现在担心的是,“常用词”字典可能会很大,这取决于我为频繁选择的阈值。我做了我的测试,然后回到你身边 是的,编写该文件是我想法的致命弱点。 也许还有一点需要注意的是,对于第一个查询,如果搜索多个单词,OR操作符是必须的。它提出了一些有趣的问题:)。 一个包含多个单词的搜索字符串被解释为“and”,这没有问题。但是如果搜索字符串包含|,那对我的策略来说将是一个问题。要么禁止,要么拆分搜索字符串并分别处理每个元素。 @Vincent,我现在报告了我的经历【参考方案2】:

我的场景的解决方案,我认为它适用于许多实际案例:

让 Postgres 始终或大部分使用“稀有词策略”(2. 在问题中)。原因是用户总是应该有可能按相关性排序(例如使用ts_rank),在这种情况下不能使用另一种策略,因此必须确保“稀有词策略”对所有搜索都足够快。

要强制 Postgres 使用这种策略,可以使用子查询,正如 Laurenz Albe 指出的那样:

SELECT * FROM 
    (SELECT * FROM tbl WHERE va @@ to_tsquery('english', 'test') OFFSET 0) AS q
ORDER BY b LIMIT 100;

或者,可以简单地将LIMIT 设置得足够高(同时只获取所需数量的结果)。

我可以获得足够的性能(几乎所有查询都需要

首先针对包含每个文档最相关部分(例如标题和摘要)的较小 ts_vector 进行每次搜索,并仅在第一个查询产生的结果不足时检查完整文档。 特别处理非常频繁出现的词,例如只允许它们与其他词进行 AND 组合(将它们添加到停用词是有问题的,因为这些词在出现在短语中时没有得到合理的处理) 增加 RAM 并增加 shared_buffers 以便可以缓存整个表(我目前为 8.5 GB)

对于这些优化还不够的情况,为了获得更好的性能所有查询(即那些按相关性排序,这是最难的),我认为一个将不得不使用更复杂的文本搜索索引而不是 GIN。 RUM index extension 看起来很有希望,但我还没有尝试过。


PS:与我在问题中的观察相反,我现在发现在某些情况下,规划者确实会考虑词频并朝着正确的方向做出决策:

对于稀有词,它选择“稀有词策略”的边界LIMIT低于频繁词,在一定范围内这个选择似乎很好。然而,这绝不是可靠的,有时选择是非常错误的,例如对于低LIMITs,它也会为非常罕见或不出现的单词选择“常用词策略”,这会导致非常慢。

这似乎取决于许多因素,而且似乎不可预测。

【讨论】:

以上是关于PostgreSQL 中的高效全文搜索,在另一列上排序的主要内容,如果未能解决你的问题,请参考以下文章

选择一列上的值在另一列上具有相同的一组值

在一列上排名表,同时在另一列上排序

在一个列上应用 distinct 并在另一列上按 count 排序

如何在另一列上显示html表格的计算

在另一列上分组后查找列值的最大出现次数

在另一列上复制在某些条件下具有空值的列