UPD 第二部分
本文是有关如何在PostgreSQL中最佳配置全文本搜索的系列文章的第一部分。 我最近不得不在工作中解决一个类似的问题-对于在此主题上至少缺少一些理智的材料,我感到非常惊讶。 我的战斗经验很丰富。
领带
我支持一个相对较大的项目,该项目可以对文档进行公开搜索。 该数据库包含约500,000个文档,总容量约为3.6 GB。 搜索的本质是:用户填写一个表格,其中既有全文查询,又有数据库中各个字段(包括join-s)进行的过滤。
搜索通过Sphinx进行了工作(或更有效),但效果不是很好。 主要问题如下:
- 索引消耗了大约8 GB的RAM。 在具有8 GB RAM的服务器上,这是一个问题。 内存交换,导致了糟糕的性能。
- 该索引在大约40分钟内建立。 毫无疑问,搜索结果是否一致;索引每天发布一次。
- 搜索工作了很长时间 。 进行了特别长时间的请求,这对应于大量文档:必须将大量id-shnik从狮身人面像传输到数据库,并根据后端的相关性进行排序。
由于这些问题,出现了任务-优化全文搜索。 此任务有两个解决方案:
- 收紧Sphinx:配置实时索引,将要过滤的属性存储在索引中。
- 使用内置的FTS PostgreSQL。
决定实施第二种解决方案:这样,您可以本地提供索引的自动更新,摆脱两个服务之间的长时间通信,并监视一个服务而不是两个服务。
这似乎是一个很好的解决方案。 但是问题摆在面前。
让我们从头开始。
我们天真地使用全文搜索
如文档所述,全文本搜索需要使用tsvector
和tsquery
。 第一个以搜索优化的形式存储文档的文本,第二个以全文查询的形式存储。
要搜索PostgreSQL,有函数to_tsvector
, plainto_tsquery
, to_tsquery
。 要对结果进行排名,请使用ts_rank
。 它们的用法很直观,并且在文档中有很好的描述,因此我们不再赘述它们的用法。
使用它们的传统搜索查询如下所示:
SELECT id, ts_rank(to_tsvector("document_text"), plainto_tsquery('')) FROM documents_document WHERE to_tsvector("document_text") @@ plainto_tsquery('') ORDER BY ts_rank(to_tsvector("document_text"), plainto_tsquery('')) DESC;
我们推导出文本中带有“查询”一词的文档ID,并按相关性从高到低的顺序对其进行排序。 一切似乎都还好吗? 不行
上面的方法有很多缺点:
- 我们不使用索引进行搜索。
- 表格的每一行都会调用ts_vector函数。
- 对该表的每一行调用ts_rank函数。
所有这些导致搜索花费很长时间的事实。 在战斗基础上解释结果:
Gather Merge (actual time=420289.477..420313.969 rows=58742 loops=1) Workers Planned: 2 Workers Launched: 2 -> Sort (actual time=420266.150..420267.935 rows=19581 loops=3) Sort Key: (ts_rank(to_tsvector(document_text), plainto_tsquery(''::text))) DESC Sort Method: quicksort Memory: 2278kB -> Parallel Seq Scan on documents_document (actual time=65.454..420235.446 rows=19581 loops=3) Filter: (to_tsvector(document_text) @@ plainto_tsquery(''::text)) Rows Removed by Filter: 140636 Planning time: 3.706 ms Execution time: 420315.895 ms
420秒! 一个请求!
该基地还产生了很多形式的[54000] word is too long to be indexed
。 不用担心。 原因是在我的数据库中是在所见即所得编辑器中创建的文档。 它插入了很多 在任何可能的地方,并且连续5.4万。 Postgres会忽略此长度的单词,并写出无法禁用的vorning。
我们将尝试解决所有提到的问题,并加快搜索速度。
我们天真地优化了搜索
当然,我们不会使用战斗基地-我们将创建一个测试基地。 它包含约12,000个文档。 示例中的请求在35秒左右的时间内执行。 漫长的时间!
说明结果 Sort (actual time=35431.874..35432.208 rows=3593 loops=1) Sort Key: (ts_rank(to_tsvector(document_text), plainto_tsquery(''::text))) DESC Sort Method: quicksort Memory: 377kB -> Seq Scan on documents_document (actual time=8.470..35429.261 rows=3593 loops=1) Filter: (to_tsvector(document_text) @@ plainto_tsquery(''::text)) Rows Removed by Filter: 9190 Planning time: 0.200 ms Execution time: 35432.294 ms
索引
首先,当然,您需要添加一个索引。 最简单的方法:功能索引。
CREATE INDEX idx_gin_document ON documents_document USING gin (to_tsvector('russian', "document_text"));
这样的索引将创建很长时间-在测试基础上花费了大约26秒。 他需要遍历数据库,并为每个记录调用to_tsvector函数。 尽管它仍然可以将搜索速度提高到12秒,但它的长度仍然无法原谅!
说明结果 Sort (actual time=12213.943..12214.327 rows=3593 loops=1) Sort Key: (ts_rank(to_tsvector('russian'::regconfig, document_text), plainto_tsquery(''::text))) DESC Sort Method: quicksort Memory: 377kB -> Bitmap Heap Scan on documents_document (actual time=3.849..12212.248 rows=3593 loops=1) Recheck Cond: (to_tsvector('russian'::regconfig, document_text) @@ plainto_tsquery(''::text)) Heap Blocks: exact=946 -> Bitmap Index Scan on idx_gin_document (actual time=0.427..0.427 rows=3593 loops=1) Index Cond: (to_tsvector('russian'::regconfig, document_text) @@ plainto_tsquery(''::text)) Planning time: 0.109 ms Execution time: 12214.452 ms
重复调用to_tsvector
要解决此问题,您需要将tsvector
存储在数据库中。 当然,在更改包含文档的表中的数据时,您需要通过后端使用数据库中的触发器进行更新。
有两种方法可以做到这一点:
- 将
tsvector
类型的列添加到包含文档的表中。 - 创建一个单独的表,并与文档表进行一对一通信,并将向量存储在那里。
第一种方法的优点:搜索中缺少join-s。
第二种方法的优点:带有文档的表中缺少额外的数据,它的大小与以前相同。 使用备份,您无需浪费时间和时间在tsvector
,根本不需要备份。
两次旅行都导致磁盘上的数据变成原来的两倍:存储了文档文本及其向量。
我为自己选择了第二种方法,它的优势对我来说更为重要。
索引创建 CREATE INDEX idx_gin_document ON documents_documentvector USING gin ("document_text");
新的搜索查询 SELECT documents_document.id, ts_rank("text", plainto_tsquery('')) FROM documents_document LEFT JOIN documents_documentvector ON documents_document.id = documents_documentvector.document_id WHERE "text" @@ plainto_tsquery('') ORDER BY ts_rank("text", plainto_tsquery('')) DESC;
将数据添加到链接表并创建索引。 在测试的基础上,添加数据花费了24秒,而创建索引仅花费了2.7秒 。 如我们所见,更新索引和数据并没有明显加快,但是索引本身现在可以非常快速地更新。
搜索加速了多少次?
Sort (actual time=48.147..48.432 rows=3593 loops=1) Sort Key: (ts_rank(documents_documentvector.text, plainto_tsquery(''::text))) DESC Sort Method: quicksort Memory: 377kB -> Hash Join (actual time=2.281..47.389 rows=3593 loops=1) Hash Cond: (documents_document.id = documents_documentvector.document_id) -> Seq Scan on documents_document (actual time=0.003..2.190 rows=12783 loops=1) -> Hash (actual time=2.252..2.252 rows=3593 loops=1) Buckets: 4096 Batches: 1 Memory Usage: 543kB -> Bitmap Heap Scan on documents_documentvector (actual time=0.465..1.641 rows=3593 loops=1) Recheck Cond: (text @@ plainto_tsquery(''::text)) Heap Blocks: exact=577 -> Bitmap Index Scan on idx_gin_document (actual time=0.404..0.404 rows=3593 loops=1) Index Cond: (text @@ plainto_tsquery(''::text)) Planning time: 0.410 ms Execution time: 48.573 ms
没有连接的指标要求:
SELECT id, ts_rank("text", plainto_tsquery('')) AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank;
结果:
排序(实际时间= 44.339..44.487行= 3593循环= 1)
排序键:(ts_rank(文本,plainto_tsquery('query'::文本)))
排序方式:quicksort内存:265kB
->对documents_documentvector进行位图堆扫描(实际时间= 0.692..43.682行= 3593循环= 1)
重新检查条件:(文本@@ plainto_tsquery('query'::文本))
堆块:准确= 577
->对idx_gin_document进行位图索引扫描(实际时间= 0.577..0.577行= 3593循环= 1)
索引条件:(文本@@ plainto_tsquery('query'::文本))
计划时间:0.182毫秒
执行时间:44.610毫秒
太不可思议了! 尽管join和ts_rank
仍然ts_rank
。 已经是一个可以接受的结果,大多数时间不是通过搜索,而是通过为每行计算ts_rank
的。
ts_rank
多重通话
似乎我们已经成功解决了除此以外的所有问题。 44毫秒是不错的交货时间。 幸福的结局似乎很接近? 在那里!
在不使用ts_rank
情况下运行相同的查询并比较结果。
没有ts_rank要求:
SELECT document_id, 1 AS rank FROM documents_documentvector WHERE "text" @@ plainto_tsquery('') ORDER BY rank;
结果:
Bitmap Heap Scan on documents_documentvector (actual time=0.503..1.609 rows=3593 loops=1) Recheck Cond: (text @@ plainto_tsquery(''::text)) Heap Blocks: exact=577 -> Bitmap Index Scan on idx_gin_document (actual time=0.439..0.439 rows=3593 loops=1) Index Cond: (text @@ plainto_tsquery(''::text)) Planning time: 0.147 ms Execution time: 1.715 ms
1.7毫秒! 快三十倍! 对于战斗基地,结果约为150毫秒1.5秒。 无论如何,两者之间的差异是一个数量级,而1.5秒并不是您要等待来自基础的答案的时间。 怎么办
您不能根据相关性关闭排序;不能减少要计数的行数(数据库应为所有ts_rank
文档计算ts_rank
,否则无法排序)。
在Internet上的某些地方,建议缓存最频繁的请求(并因此调用ts_rank)。 但是我不喜欢这种方法:正确选择正确的查询非常困难,而且错误的查询仍然会降低搜索速度。
我非常希望在经过索引之后,数据像Sphinx一样以已经排序的形式出现。 不幸的是,在PostgreSQL的盒子里什么也做不了。
但是我们很幸运-RUM索引可以做到这一点。 有关它的详细信息,例如,可以在其作者的演讲中找到。 它存储有关请求的其他信息,使您可以直接评估所谓的信息。 tsvector
和tsquery
之间的“距离”,并在扫描索引后立即产生排序结果。
但是,抛出GIN并安装RUM并不值得。 它有缺点,有优点和有应用限制-我将在下一篇文章中对此进行介绍。