我们正在准备在Postgres中进行全文搜索。 第一部分

UPD 第二部分


本文是有关如何在PostgreSQL中最佳配置全文本搜索的系列文章的第一部分。 我最近不得不在工作中解决一个类似的问题-对于在此主题上至少缺少一些理智的材料,我感到非常惊讶。 我的战斗经验很丰富。


领带


我支持一个相对较大的项目,该项目可以对文档进行公开搜索。 该数据库包含约500,000个文档,总容量约为3.6 GB。 搜索的本质是:用户填写一个表格,其中既有全文查询,又有数据库中各个字段(包括join-s)进行的过滤。


搜索通过Sphinx进行了工作(或更有效),但效果不是很好。 主要问题如下:


  1. 索引消耗了大约8 GB的RAM。 在具有8 GB RAM的服务器上,这是一个问题。 内存交换,导致了糟糕的性能。
  2. 该索引在大约40分钟内建立。 毫无疑问,搜索结果是否一致;索引每天发布一次。
  3. 搜索工作了很长时间 。 进行了特别长时间的请求,这对应于大量文档:必须将大量id-shnik从狮身人面像传输到数据库,并根据后端的相关性进行排序。

由于这些问题,出现了任务-优化全文搜索。 此任务有两个解决方案:


  1. 收紧Sphinx:配置实时索引,将要过滤的属性存储在索引中。
  2. 使用内置的FTS PostgreSQL。

决定实施第二种解决方案:这样,您可以本地提供索引的自动更新,摆脱两个服务之间的长时间通信,并监视一个服务而不是两个服务。


这似乎是一个很好的解决方案。 但是问题摆在面前。


让我们从头开始。


我们天真地使用全文搜索


如文档所述,全文本搜索需要使用tsvectortsquery 。 第一个以搜索优化的形式存储文档的文本,第二个以全文查询的形式存储。


要搜索PostgreSQL,有函数to_tsvectorplainto_tsqueryto_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,并按相关性从高到低的顺序对其进行排序。 一切似乎都还好吗? 不行


上面的方法有很多缺点:


  1. 我们不使用索引进行搜索。
  2. 表格的每一行都会调用ts_vector函数。
  3. 对该表的每一行调用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存储在数据库中。 当然,在更改包含文档的表中的数据时,您需要通过后端使用数据库中的触发器进行更新。


有两种方法可以做到这一点:


  1. tsvector类型的列添加到包含文档的表中。
  2. 创建一个单独的表,并与文档表进行一对一通信,并将向量存储在那里。

第一种方法的优点:搜索中缺少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索引可以做到这一点。 有关它的详细信息,例如,可以在其作者演讲中找到。 它存储有关请求的其他信息,使您可以直接评估所谓的信息。 tsvectortsquery之间的“距离”,并在扫描索引后立即产生排序结果。


但是,抛出GIN并安装RUM并不值得。 它有缺点,有优点和有应用限制-我将在下一篇文章中对此进行介绍。

Source: https://habr.com/ru/post/zh-CN442170/


All Articles