RUM
为了全文搜索更加快,RUM索引可以看做是在GIN基础上的扩展。可以从https://github.com/postgrespro/rum下载使用。
使用GIN索引的一些限制
GIN索引允许使用tsvector和tsquery类型执行快速的全文本搜索。但是,使用GIN索引进行全文搜索存在几个问题:
- 排序慢。需要有关词汇的位置信息才能进行排序。 GIN索引不存储词素的位置信息。因此,在索引扫描之后,我们需要额外的堆扫描以检索词素位置。
- 短语搜索在GIN搜索中也比较慢。该问题与词素搜索的问题类似。需要位置信息来执行短语搜索。
- 时间戳排序缓慢。 GIN索引无法在带有词素的索引中存储相关信息。因此,要执行其他的堆扫描,有额外的其他开销。
下图显示了GIN和RUM的不同,添加了addinfo,可以存储位置或者时间戳。
RUM的缺点是构建索引和插入数据花费的时间比GIN慢。这是因为我们需要存储除密钥之外的其他信息,并且RUM使用WAL记录。
短语搜索
全文搜索查询可以包含特殊运算符,这些运算符考虑了词素之间的距离。例如,我们可以找到文档中的«hand»与 «thigh»之间有两个不同的单词:
postgres=# select to_tsvector('Clap your hands, slap your thigh') @@
to_tsquery('hand <3> thigh');
?column?
----------
t
(1 row)
我们也可以指出单词必须一个接一个地定位:
postgres=# select to_tsvector('Clap your hands, slap your thigh') @@
to_tsquery('hand <-> slap');
?column?
----------
t
(1 row)
常规GIN索引可以返回包含两个词素的文档,但是我们只能通过查看tsvector来检查它们之间的距离:
postgres=# select to_tsvector('Clap your hands, slap your thigh');
to_tsvector
--------------------------------------
'clap':1 'hand':3 'slap':4 'thigh':6
(1 row)
在RUM索引中,每个词素不仅引用表行,而且每个TID都提供了词素在文档中出现的位置列表。
postgres=# create extension rum;
postgres=# create index on ts using rum(doc_tsv);
在上图中灰色方块包含了词素的位置信息
postgres=# select ctid, left(doc,20), doc_tsv from ts;
ctid | left | doc_tsv
-------+----------------------+---------------------------------------------------------
(0,1) | Can a sheet slitter | 'sheet':3,6 'slit':5 'slitter':4
(0,2) | How many sheets coul | 'could':4 'mani':2 'sheet':3,6 'slit':8 'slitter':7
(0,3) | I slit a sheet, a sh | 'sheet':4,6 'slit':2,8
(1,1) | Upon a slitted sheet | 'sheet':4 'sit':6 'slit':3 'upon':1
(1,2) | Whoever slit the she | 'good':7 'sheet':4,8 'slit':2 'slitter':9 'whoever':1
(1,3) | I am a sheet slitter | 'sheet':4 'slitter':5
(2,1) | I slit sheets. | 'sheet':3 'slit':2
(2,2) | I am the sleekest sh | 'ever':8 'sheet':5,10 'sleekest':4 'slit':9 'slitter':6
(2,3) | She slits the sheet | 'sheet':4 'sit':6 'slit':2
(9 rows)
当指定了fastupdate参数时,GIN还提供了延迟插入。但在RUM中已经删除。
下面举一个类似生产环境的例子,数据来源mail_message
fts=# alter table mail_messages add column tsv tsvector;
fts=# set default_text_search_config = default;
fts=# update mail_messages
set tsv = to_tsvector(body_plain);
...
UPDATE 356125
通过GIN索引搜索的执行计划如下:
fts=# create index tsv_gin on mail_messages using gin(tsv);
fts=# explain (costs off, analyze)
select * from mail_messages where tsv @@ to_tsquery('hello <-> hackers');
QUERY PLAN
---------------------------------------------------------------------------------
Bitmap Heap Scan on mail_messages (actual time=2.490..18.088 rows=259 loops=1)
Recheck Cond: (tsv @@ to_tsquery('hello <-> hackers'::text))
Rows Removed by Index Recheck: 1517
Heap Blocks: exact=1503
-> Bitmap Index Scan on tsv_gin (actual time=2.204..2.204 rows=1776 loops=1)
Index Cond: (tsv @@ to_tsquery('hello <-> hackers'::text))
Planning time: 0.266 ms
Execution time: 18.151 ms
(8 rows)
从计划中可以看出,通过使用GIN索引,返回了1776个匹配项,其中剩余259个匹配项,在recheck阶段删除了1517个匹配项。
让我们删除GIN,创建RUM索引
fts=# drop index tsv_gin;
fts=# create index tsv_rum on mail_messages using rum(tsv);
使用RUM索引搜索的执行计划,可以看到索引包含了所有需要查询的行,提高了查询效率
fts=# explain (costs off, analyze)
select * from mail_messages
where tsv @@ to_tsquery('hello <-> hackers');
QUERY PLAN
--------------------------------------------------------------------------------
Bitmap Heap Scan on mail_messages (actual time=2.798..3.015 rows=259 loops=1)
Recheck Cond: (tsv @@ to_tsquery('hello <-> hackers'::text))
Heap Blocks: exact=250
-> Bitmap Index Scan on tsv_rum (actual time=2.768..2.768 rows=259 loops=1)
Index Cond: (tsv @@ to_tsquery('hello <-> hackers'::text))
Planning time: 0.245 ms
Execution time: 3.053 ms
(7 rows)
排序相关
为了方便地按所需顺序返回文档,RUM索引支持排序运算符,我们在与GiST相关的文章中对此进行了讨论。 RUM定义了运算符<=>,该运算符返回(«tsvector»)与(«tsquery»)之间的距离。例如:
fts=# select to_tsvector('Can a sheet slitter slit sheets?') <=>l to_tsquery('slit');
?column?
----------
16.4493
(1 row)
fts=# select to_tsvector('Can a sheet slitter slit sheets?') <=> to_tsquery('sheet');
?column?
----------
13.1595
(1 row)
在相对比较大的数据上比较GIN和RUM:
fts=# explain (costs off, analyze)
select * from mail_messages
where tsv @@ to_tsquery('hello & hackers')
order by ts_rank(tsv,to_tsquery('hello & hackers'))
limit 10;
QUERY PLAN
---------------------------------------------------------------------------------------------
Limit (actual time=27.076..27.078 rows=10 loops=1)
-> Sort (actual time=27.075..27.076 rows=10 loops=1)
Sort Key: (ts_rank(tsv, to_tsquery('hello & hackers'::text)))
Sort Method: top-N heapsort Memory: 29kB
-> Bitmap Heap Scan on mail_messages (actual ... rows=1776 loops=1)
Recheck Cond: (tsv @@ to_tsquery('hello & hackers'::text))
Heap Blocks: exact=1503
-> Bitmap Index Scan on tsv_gin (actual ... rows=1776 loops=1)
Index Cond: (tsv @@ to_tsquery('hello & hackers'::text))
Planning time: 0.276 ms
Execution time: 27.121 ms
(11 rows)
GIN索引返回了1776行,然后通过排序选取了前10行。
下面使用了RUM索引,看到可以使用简单的索引扫描来执行查询,无序执行额外的查询,也不需要单独再进行排序。
fts=# explain (costs off, analyze)
select * from mail_messages
where tsv @@ to_tsquery('hello & hackers')
order by tsv <=> to_tsquery('hello & hackers')
limit 10;
QUERY PLAN
--------------------------------------------------------------------------------------------
Limit (actual time=5.083..5.171 rows=10 loops=1)
-> Index Scan using tsv_rum on mail_messages (actual ... rows=10 loops=1)
Index Cond: (tsv @@ to_tsquery('hello & hackers'::text))
Order By: (tsv <=> to_tsquery('hello & hackers'::text))
Planning time: 0.244 ms
Execution time: 5.207 ms
(6 rows)
额外信息
RUM索引和GIN索引都可以建立在多个字段上。但是,尽管GIN独立存储每一列中的词素,而RUM却使我们能够将主字段(在本例中为 tsvector)与附加字段关联。为此,我们需要使用专门的运算符类«rum_tsvector_addon_ops»:
fts=# create index on mail_messages using rum(tsv RUM_TSVECTOR_ADDON_OPS, sent)
WITH (ATTACH='sent', TO='tsv');
我们能使用这个索引返回存储在额外字段排序的结果:
fts=# select id, sent, sent <=> '2017-01-01 15:00:00'
from mail_messages
where tsv @@ to_tsquery('hello')
order by sent <=> '2017-01-01 15:00:00'
limit 10;
id | sent | ?column?
---------+---------------------+----------
2298548 | 2017-01-01 15:03:22 | 202
2298547 | 2017-01-01 14:53:13 | 407
2298545 | 2017-01-01 13:28:12 | 5508
2298554 | 2017-01-01 18:30:45 | 12645
2298530 | 2016-12-31 20:28:48 | 66672
2298587 | 2017-01-02 12:39:26 | 77966
2298588 | 2017-01-02 12:43:22 | 78202
2298597 | 2017-01-02 13:48:02 | 82082
2298606 | 2017-01-02 15:50:50 | 89450
2298628 | 2017-01-02 18:55:49 | 100549
(10 rows)
这里我们搜索和’2017-01-01 15:00:00’时间点最近的行,并按时间差进行排序。如果要获得指定日期之前或者之后的结构,可以使用 <=| 或者 |=>操作符。
ts=# explain (costs off)
select id, sent, sent <=> '2017-01-01 15:00:00'
from mail_messages
where tsv @@ to_tsquery('hello')
order by sent <=> '2017-01-01 15:00:00'
limit 10;
QUERY PLAN
---------------------------------------------------------------------------------
Limit
-> Index Scan using mail_messages_tsv_sent_idx on mail_messages
Index Cond: (tsv @@ to_tsquery('hello'::text))
Order By: (sent <=> '2017-01-01 15:00:00'::timestamp without time zone)
(4 rows)
如果创建索引时没有字段关联的附加信息,则对于类似的查询,我们不得不对索引扫描的所有结果进行排序。
除了日期,我们也可以将其他数据类型的字段添加到RUM索引中。几乎所有基本类型都支持。例如,在线商店可以按新颖性(date),价格(numeric),受欢迎程度或折扣值(int,float)快速显示商品。
其他操作类
让我们从rum_tsvector_hash_ops和rum_tsvector_hash_addon_ops开始介绍,它们类似于已经讨论过的 rum_tsvector_ops和 rum_tsvector_addon_ops,但是索引存储的是词素的哈希码,而不是词素本身。这样可以减小索引的大小,当然搜索的准确性会降低,需要重新检查。此外,索引不再支持部分匹配的搜索。
rum_tsquery_ops操作类,它使我们能够解决“逆向”问题:比如查找与文档匹配的查询。例子:
fts=# create table categories(query tsquery, category text);
fts=# insert into categories values
(to_tsquery('vacuum | autovacuum | freeze'), 'vacuum'),
(to_tsquery('xmin | xmax | snapshot | isolation'), 'mvcc'),
(to_tsquery('wal | (write & ahead & log) | durability'), 'wal');
fts=# create index on categories using rum(query);
fts=# select array_agg(category)
from categories
where to_tsvector(
'Hello hackers, the attached patch greatly improves performance of tuple
freezing and also reduces size of generated write-ahead logs.'
) @@ query;
array_agg
--------------
{vacuum,wal}
(1 row)
剩余的操作类rum_anyarray_ops和rum_anyarray_addon_ops是操纵数组相关的。
索引大小和WAL(write-ahead log)
很显然,由于RUM比GIN存储更多的信息,因此RUM索引更大。
rum | gin | gist | btree
--------+--------+--------+--------
457 MB | 179 MB | 125 MB | 546 MB
可以看到大小比GIN大很多,这也是更加快速搜索的代价。
RUM是一个扩展插件,也就是说,可以在不对系统核心进行任何修改的情况下安装RUM。当时变动索引相关的WAL比GIN的更大,可以通过以下多次删除和插入数据,查看产生的日志量。
可以通过pg_current_wal_location(早起版本可以使用pg_current_xlog_location)函数查看日志的位移量,来查看产生日志的多少。
fts=# select pg_current_wal_location() as start_lsn \gset
fts=# insert into mail_messages(parent_id, sent, subject, author, body_plain, tsv)
select parent_id, sent, subject, author, body_plain, tsv
from mail_messages where id % 100 = 0;
INSERT 0 3576
fts=# delete from mail_messages where id % 100 = 99;
DELETE 3590
fts=# vacuum mail_messages;
fts=# insert into mail_messages(parent_id, sent, subject, author, body_plain, tsv)
select parent_id, sent, subject, author, body_plain, tsv
from mail_messages where id % 100 = 1;
INSERT 0 3605
fts=# delete from mail_messages where id % 100 = 98;
DELETE 3637
fts=# vacuum mail_messages;
fts=# insert into mail_messages(parent_id, sent, subject, author, body_plain, tsv)
select parent_id, sent, subject, author, body_plain, tsv from mail_messages
where id % 100 = 2;
INSERT 0 3625
fts=# delete from mail_messages where id % 100 = 97;
DELETE 3668
fts=# vacuum mail_messages;
fts=# select pg_current_wal_location() as end_lsn \gset
fts=# select pg_size_pretty(:'end_lsn'::pg_lsn - :'start_lsn'::pg_lsn);
pg_size_pretty
----------------
3114 MB
(1 row)
可以看到,WAL大约为3 GB。但是,如果我们对GIN索引重复相同的实验,则只会占用700 MB左右的空间。
索引相关属性
amname | name | pg_indexam_has_property
--------+---------------+-------------------------
rum | can_order | f
rum | can_unique | f
rum | can_multi_col | t
rum | can_exclude | t -- f for gin
索引层相关属性
name | pg_index_has_property
---------------+-----------------------
clusterable | f
index_scan | t -- f for gin
bitmap_scan | t
backward_scan | f
请注意,与GIN不同,RUM支持索引扫描。否则,不可能在带有limit子句的查询中精确返回所需数目的结果。不需要相应地使用«gin_fuzzy_search_limit»参数。因此,该索引可用于支持排它约束。
列相关属性:
name | pg_index_column_has_property
--------------------+------------------------------
asc | f
desc | f
nulls_first | f
nulls_last | f
orderable | f
distance_orderable | t -- f for gin
returnable | f
search_array | f
search_nulls | f
此处的区别在于RUM支持排序运算符。但是,并非对所有运算符类都是如此,例如,对于«tsquery_ops»来说为false。
参考:https://github.com/postgrespro/rum