当前位置:   article > 正文

Postgresql杂谈 23——Postgresql中的全文检索_postgresql 全文索引

postgresql 全文索引

       今天我们来聊一下全文检索,想必做搜索相关业务朋友对这个概念不会陌生,尤其是做搜索引擎,或者类似CSDN、知乎类的社区网站,全文检索是逃不开的业务。文,即文章、文档。全文搜索就是给定关键词,在所有的文档数据中找到符合关键词的文档。不管是哪种业务模式下的全文检索功能,其实大体的实现思路类似,如下所示:

       使用文字进行描述,就是:

(1)获取原始文档数据。

(2)对文档进行分析,分词(所为分词,就是按照分词符,如空格,将一句话分隔成若干的单词)

(3)存档存入数据库,并通过分词建立索引。

(4)查询时根据关键词,通过索引查询到索引指向的数据。

       Postgresql本身就支持全文检索的功能,尤其是Postgresql10.0之后,对于全文检索的支持更加成熟。配合它的GIN索引,Postgresql的全文检索具有很高的查询性能。下面,我们来演示下Postgresql中全文索引的使用。

一、测试数据的准备

       在继续下面的内容之前,我们还是要先创建一个测试表,用来进行后面内容的演示:

  1. postgres=# create table blog (id serial,recoredtime timestamp default now(),content text);
  2. CREATE TABLE
  3. postgres=# \d+ blog
  4. Table "public.blog"
  5. Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
  6. -------------+-----------------------------+-----------+----------+----------------------------------+----------+--------------+-------------
  7. id | integer | | not null | nextval('blog_id_seq'::regclass) | plain | |
  8. recoredtime | timestamp without time zone | | | now() | plain | |
  9. content | text | | |

       可以看到,笔者创建了一个名叫blog的测试表,用来模拟博客网站存储的博文,表中一共有3个字段:

  • id —— 为每一篇博文分配的唯一的自增长ID
  • recoredtime —— 博文提交的时间,默认是提交的当前时间
  • content —— 博文的内容

       接下来,向blog表里面插入一些测试数据:

  1. postgres=# COPY blog(content) FROM '/data/1.txt';
  2. COPY 20
  3. postgres=# COPY blog(content) FROM '/data/2.txt';
  4. COPY 12
  5. postgres=# COPY blog(content) FROM '/data/3.txt';
  6. COPY 6
  7. postgres=# COPY blog(content) FROM '/data/4.txt';
  8. COPY 22
  9. postgres=# COPY blog(content) FROM '/data/5.txt';
  10. COPY 9

       笔者通过Copy命令将位于本地的5个文本文件的内容插入到了blog表中,每个文本文件里面实际上都是一片英文的文章,但是Copy命令遇到换行符会结束然后插入新行,所以每篇文章实际插入了多行数据。

  1. postgres=# select count(*) from blog;
  2. -[ RECORD 1 ]
  3. count | 69

       可以看到,整个blog表一共有69行数据,而且其中不乏空行。但是为了验证在大数据量下全文检索的性能,69行数据还是远远不够的,我们可以以1.txt为源数据,重复插入:

  1. postgres=# do $$
  2. postgres$# declare
  3. postgres$# v_idx integer := 1;
  4. postgres$# begin
  5. postgres$# while v_idx < 10000 loop
  6. postgres$# v_idx = v_idx+1;
  7. postgres$# COPY blog(content) FROM '/data/1.txt';
  8. postgres$# end loop;
  9. postgres$# end $$;
  10. DO

       最终我们插入了200W+的数据:

  1. postgres=# select count(*) from blog;
  2. count
  3. --------
  4. 201009
  5. (1 row)

       接下来,我们就以这200W+行数据为数据源来介绍下Postgresql中全文检索功能的使用。

二、Postgresql的全文检索原理

       Postgresql会对长文本进行分词,分词的标准一般是按照空格进行拆分。分词之后长文本实际上被分成了很多个key的集合,这个key的集合叫做tsvector。所有的搜索都是在tsvector中进行的。

       我们先来简单验证下,Postgresql是怎么对一个简单文本字符串进行分词的:

  1. postgres=# select 'I will be back'::tsvector;
  2. tsvector
  3. ------------------------
  4. 'I' 'back' 'be' 'will'
  5. (1 row)

       我们将长字符串声明成了tsvector类型,Postgresql就自动按照空格对其进行了分词,并打印出来。Postgresql中也提供了一个to_tsvector的函数,可以实现类似的功能:

  1. postgres=# select to_tsvector('I will be back');
  2. to_tsvector
  3. -------------
  4. 'back':4
  5. (1 row)

       先看下to_tsvector函数返回的结果,第4行‘back’是提取的关键字,冒号后面的4表示词在句子中的位置。有朋友一定会觉得奇怪,为什么前面使用tsvector分词时分出来4个词,而使用to_tsvector只分出来了back一个?实际上tssvector会自动忽略掉I、Well等等这类主语词或者谓词、虚词,这也很容易理解,因为这类词往往是量最多,但是却很少使用来进行查询的。

       我们再来看一个Postgresql官方文档中使用to_tsvector的例子:

  1. postgres=# SELECT to_tsvector('english', 'a fat cat sat on a mat - it ate a fat rats');
  2. to_tsvector
  3. -----------------------------------------------------
  4. 'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4
  5. (1 row)

       在上面这个例子中我们看到,作为结果的tsvector不包含词a、on或it,词rats变成了rat,并且标点符号-被忽略了。

       to_tsvector函数在内部调用了一个解析器,它把文档文本分解成记号并且为每一种记号分配一个类型。对于每一个记号,会去查询一个词典列表,该列表会根据记号的类型而变化。第一个识别记号的词典产生一个或多个正规化的词位来表示该记号。例如,rats变成rat是因为一个词典识别到该词rats是rat的复数形式。一些词会被识别为停用词,这将导致它们被忽略,因为它们出现得太频繁以至于在搜索中起不到作用。在我们的例子中有a、on和it是停用词。如果在列表中没有词典能识别该记号,那它将也会被忽略。在这个例子中标点符号-就属于这种情况,因为事实上没有词典会给它分配记号类型(空间符号),即空间记号不会被索引。对于解析器、词典以及要索引哪些记号类型是由所选择的文本搜索配置决定的。可以在同一个数据库中有多种不同的配置,并且有用于很多种语言的预定义配置。在我们的例子中,我们使用用于英语的默认配置english。

       介绍完了tsvector,要完成全文检索功能,我们还需要引入另外一个类型——检索条件tsquery,tsquery是一个由简单逻辑运算符组成的字符串,如下:

  1. postgres=# select 'we & back'::tsquery;
  2. tsquery
  3. ---------------
  4. 'we' & 'back'
  5. (1 row)

       'we & back'的意思就是查询条件中既包含‘we’这个单词,也包括'back'这个单词。如果使用这个tsquery进行查询,就可以组成类似下面的SQL语句:

  1. postgres=# select 'I will be back'::tsvector @@ 'we & back'::tsquery;
  2. ?column?
  3. ----------
  4. f
  5. (1 row)

       上面查询语句的意思,当然就是在'I will be back'这个tsvector类型中确认是不是符合既包含‘we’这个单词,也包括'back'这个单词?答案当然是否定的,所以结果为false。如果我们把tsquery中的&换成|,就会是另一种结果:

  1. postgres=# select 'I will be back'::tsvector @@ 'we | back'::tsquery;
  2. ?column?
  3. ----------
  4. t
  5. (1 row)

       在'I will be back'中查找,确认其是否满足含有‘we’或者'back',因为它含有'back',索引结果就是true。

       和tsvector类似,将字符串转换成tsquery类型,Postgresql也提供了对应的to_tsquery函数:

  1. postgres=# select to_tsquery('we & back');
  2. to_tsquery
  3. ------------
  4. 'back'
  5. (1 row)

       从上面的结果中也可以看到,to_tsquery也忽略了we这个主语单词。

三、在数据表中使用全文检索

       前面,我们介绍了Postgresql中实现全文检索的原理,接下来,开始在之前创建的blog表中使用全文检索。我们从上文中了解到:Postgresql实现全文检索是在tsvector类型之上的,因此要想在blog表中实现这一功能,我们还必须添加一个tsvector的列,在此列的基础之上进行全文检索。

       先添加列:

  1. postgres=# alter table blog add column tscontent tsvector;
  2. ALTER TABLE

       加完成列之后,然后将content里面的内容分词转换成tsvector类型:

  1. postgres=# update blog set tscontent=to_tsvector(content);
  2. UPDATE 69

       然后,在此基础之上,我们进行查找包含单词mother的数据行,为了方便查看性能,我们在执行计划里面去执行:

  1. postgres=# explain (analyze,verbose,buffers,costs,timing) select * from blog where tscontent @@ 'mother'::tsquery;
  2. QUERY PLAN
  3. -----------------------------------------------------------------------------------------------------------------------------------
  4. Gather (cost=1000.00..302075.58 rows=1 width=347) (actual time=18140.837..18140.878 rows=1 loops=1)
  5. Output: id, recoredtime, content, tscontent
  6. Workers Planned: 2
  7. Workers Launched: 2
  8. Buffers: shared hit=16156 read=273456
  9. -> Parallel Seq Scan on public.blog (cost=0.00..301075.48 rows=1 width=347) (actual time=14030.975..17958.129 rows=0 loops=3)
  10. Output: id, recoredtime, content, tscontent
  11. Filter: (blog.tscontent @@ '''mother'''::tsquery)
  12. Rows Removed by Filter: 733663
  13. Buffers: shared hit=16156 read=273456
  14. Worker 0: actual time=6087.447..17868.909 rows=1 loops=1
  15. Buffers: shared hit=5343 read=88589
  16. Worker 1: actual time=17864.982..17864.982 rows=0 loops=1
  17. Buffers: shared hit=5292 read=86997
  18. Planning Time: 1.990 ms
  19. Execution Time: 18144.970 ms
  20. (16 rows)

      从上面的执行计划信息中可以看到,整个查询采用了并行扫描全表,一共查询到了1条数据,查询实际耗时18144.970ms。为了加快查询速度,我们还可以在tscontent字段上加上GIN索引:

  1. postgres=# create index on blog using gin(tscontent);
  2. CREATE INDEX

       创建成功GIN索引之后,再次执行查询计划:

  1. postgres=# explain (analyze,verbose,buffers,costs,timing) select * from blog where tscontent @@ 'mother'::tsquery;
  2. QUERY PLAN
  3. ----------------------------------------------------------------------------------------------------------------------------
  4. Bitmap Heap Scan on public.blog (cost=28.00..32.01 rows=1 width=347) (actual time=3.176..3.179 rows=1 loops=1)
  5. Output: id, recoredtime, content, tscontent
  6. Recheck Cond: (blog.tscontent @@ '''mother'''::tsquery)
  7. Heap Blocks: exact=1
  8. Buffers: shared hit=1 read=3
  9. -> Bitmap Index Scan on blog_tscontent_idx (cost=0.00..28.00 rows=1 width=0) (actual time=0.690..0.691 rows=1 loops=1)
  10. Index Cond: (blog.tscontent @@ '''mother'''::tsquery)
  11. Buffers: shared hit=1 read=2
  12. Planning Time: 2.786 ms
  13. Execution Time: 3.238 ms
  14. (10 rows)

       加了索引之后,再去查询,查询过程走了索引,采用位图扫描,整个的查询过程只消耗了3.238ms,单单从数字上比较,性能提高了6000倍不止。

四、使用tsquery的全文查询和like模糊查询的性能比较

       有的朋友可能会有疑问:如果全文搜索使用like等模糊查询方式是不是也可以实现呢?可以实现,但是如果使用like等模糊查询,主要有两个弊端:

(1)like模糊查询要进行全表扫描,查询起来会相当吃力,性能很低;

(2)查询结果中包含了所有mother这个字符串的数据,无法做到精确匹配。

       我们可以再次在执行计划中使用like模糊查询测试下:

  1. ^Cpostgres=explain (analyze,verbose,buffers,costs,timing) select * from blog where content like '%mother%';
  2. QUERY PLAN
  3. ------------------------------------------------------------------------------------------------------------------------------------------
  4. Gather (cost=1000.00..313181.99 rows=111627 width=649) (actual time=6209.370..18396.139 rows=110048 loops=1)
  5. Output: id, recoredtime, content, tscontent
  6. Workers Planned: 2
  7. Workers Launched: 2
  8. Buffers: shared hit=16145 read=273467
  9. -> Parallel Seq Scan on public.blog (cost=0.00..301019.29 rows=46511 width=649) (actual time=6248.323..16996.203 rows=36683 loops=3)
  10. Output: id, recoredtime, content, tscontent
  11. Filter: (blog.content ~~ '%mother%'::text)
  12. Rows Removed by Filter: 696980
  13. Buffers: shared hit=16145 read=273467
  14. Worker 0: actual time=6249.688..16447.847 rows=21945 loops=1
  15. Buffers: shared hit=5242 read=66229
  16. Worker 1: actual time=6286.170..16309.549 rows=21570 loops=1
  17. Buffers: shared hit=5181 read=65342
  18. Planning Time: 0.109 ms
  19. Execution Time: 18484.319 ms
  20. (16 rows)

       因为也采用的是并行的全表扫描,所以使用like查询的耗时和使用索引前的全文检索耗时差不多,用了18484.319 ms。而且从查询结果中,我们可以看到,使用模糊查询我们查出来了110048条结果,而实际上包含mothor这个单词的数据只有一行。

五、支持中文全文检索的zhparser

       可能细心的朋友已经发现,我们现在做的全文检索功能,是完全建立在检索英文的基础之上的。实际上,Postgresql默认的全文检索只支持英文,如果需要支持中文的全文检索,我们需要安装zhparser插件。由于篇幅有限,笔者就不再这里展开了,如果感兴趣可以自行百度或google。

六、总结

       按照惯例,我们还是对本篇的内容进行总结:

(1)Postgresql支持全文检索的功能,它提供了两个类型tsvector和tsquery分别表示全文检索索引的集合以及查询条件

(2)全文检索的原理就是将长的字符串按照空格进行分词,将分词存入到类型为tsvector的集合中,tsvector中存储每个单词和其在长语句中的位置。

(3)tsquery类型是由查询的key和&、|等逻辑运算符拼接在一起。

(4)在某个表上进行全文检索,需要创建专门的tsvector类型的字段,而且字段上可以创建gin索引来加速查询。

(5)Postgresql默认的全文检索只支持英文,需要需要使用支持中文的全文检索,需要安装zhparser插件。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/136846
推荐阅读
相关标签
  

闽ICP备14008679号