赞
踩
今天我们来聊一下全文检索,想必做搜索相关业务朋友对这个概念不会陌生,尤其是做搜索引擎,或者类似CSDN、知乎类的社区网站,全文检索是逃不开的业务。文,即文章、文档。全文搜索就是给定关键词,在所有的文档数据中找到符合关键词的文档。不管是哪种业务模式下的全文检索功能,其实大体的实现思路类似,如下所示:
使用文字进行描述,就是:
(1)获取原始文档数据。
(2)对文档进行分析,分词(所为分词,就是按照分词符,如空格,将一句话分隔成若干的单词)
(3)存档存入数据库,并通过分词建立索引。
(4)查询时根据关键词,通过索引查询到索引指向的数据。
Postgresql本身就支持全文检索的功能,尤其是Postgresql10.0之后,对于全文检索的支持更加成熟。配合它的GIN索引,Postgresql的全文检索具有很高的查询性能。下面,我们来演示下Postgresql中全文索引的使用。
在继续下面的内容之前,我们还是要先创建一个测试表,用来进行后面内容的演示:
- postgres=# create table blog (id serial,recoredtime timestamp default now(),content text);
- CREATE TABLE
- postgres=# \d+ blog
- Table "public.blog"
- Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
- -------------+-----------------------------+-----------+----------+----------------------------------+----------+--------------+-------------
- id | integer | | not null | nextval('blog_id_seq'::regclass) | plain | |
- recoredtime | timestamp without time zone | | | now() | plain | |
- content | text | | |
可以看到,笔者创建了一个名叫blog的测试表,用来模拟博客网站存储的博文,表中一共有3个字段:
接下来,向blog表里面插入一些测试数据:
- postgres=# COPY blog(content) FROM '/data/1.txt';
- COPY 20
- postgres=# COPY blog(content) FROM '/data/2.txt';
- COPY 12
- postgres=# COPY blog(content) FROM '/data/3.txt';
- COPY 6
- postgres=# COPY blog(content) FROM '/data/4.txt';
- COPY 22
- postgres=# COPY blog(content) FROM '/data/5.txt';
- COPY 9
笔者通过Copy命令将位于本地的5个文本文件的内容插入到了blog表中,每个文本文件里面实际上都是一片英文的文章,但是Copy命令遇到换行符会结束然后插入新行,所以每篇文章实际插入了多行数据。
- postgres=# select count(*) from blog;
- -[ RECORD 1 ]
- count | 69
可以看到,整个blog表一共有69行数据,而且其中不乏空行。但是为了验证在大数据量下全文检索的性能,69行数据还是远远不够的,我们可以以1.txt为源数据,重复插入:
- postgres=# do $$
- postgres$# declare
- postgres$# v_idx integer := 1;
- postgres$# begin
- postgres$# while v_idx < 10000 loop
- postgres$# v_idx = v_idx+1;
- postgres$# COPY blog(content) FROM '/data/1.txt';
- postgres$# end loop;
- postgres$# end $$;
-
- DO
最终我们插入了200W+的数据:
- postgres=# select count(*) from blog;
- count
- --------
- 201009
- (1 row)
接下来,我们就以这200W+行数据为数据源来介绍下Postgresql中全文检索功能的使用。
Postgresql会对长文本进行分词,分词的标准一般是按照空格进行拆分。分词之后长文本实际上被分成了很多个key的集合,这个key的集合叫做tsvector。所有的搜索都是在tsvector中进行的。
我们先来简单验证下,Postgresql是怎么对一个简单文本字符串进行分词的:
- postgres=# select 'I will be back'::tsvector;
- tsvector
- ------------------------
- 'I' 'back' 'be' 'will'
- (1 row)
我们将长字符串声明成了tsvector类型,Postgresql就自动按照空格对其进行了分词,并打印出来。Postgresql中也提供了一个to_tsvector的函数,可以实现类似的功能:
- postgres=# select to_tsvector('I will be back');
- to_tsvector
- -------------
- 'back':4
- (1 row)
先看下to_tsvector函数返回的结果,第4行‘back’是提取的关键字,冒号后面的4表示词在句子中的位置。有朋友一定会觉得奇怪,为什么前面使用tsvector分词时分出来4个词,而使用to_tsvector只分出来了back一个?实际上tssvector会自动忽略掉I、Well等等这类主语词或者谓词、虚词,这也很容易理解,因为这类词往往是量最多,但是却很少使用来进行查询的。
我们再来看一个Postgresql官方文档中使用to_tsvector的例子:
- postgres=# SELECT to_tsvector('english', 'a fat cat sat on a mat - it ate a fat rats');
- to_tsvector
- -----------------------------------------------------
- 'ate':9 'cat':3 'fat':2,11 'mat':7 'rat':12 'sat':4
- (1 row)
在上面这个例子中我们看到,作为结果的tsvector不包含词a、on或it,词rats变成了rat,并且标点符号-被忽略了。
to_tsvector函数在内部调用了一个解析器,它把文档文本分解成记号并且为每一种记号分配一个类型。对于每一个记号,会去查询一个词典列表,该列表会根据记号的类型而变化。第一个识别记号的词典产生一个或多个正规化的词位来表示该记号。例如,rats变成rat是因为一个词典识别到该词rats是rat的复数形式。一些词会被识别为停用词,这将导致它们被忽略,因为它们出现得太频繁以至于在搜索中起不到作用。在我们的例子中有a、on和it是停用词。如果在列表中没有词典能识别该记号,那它将也会被忽略。在这个例子中标点符号-就属于这种情况,因为事实上没有词典会给它分配记号类型(空间符号),即空间记号不会被索引。对于解析器、词典以及要索引哪些记号类型是由所选择的文本搜索配置决定的。可以在同一个数据库中有多种不同的配置,并且有用于很多种语言的预定义配置。在我们的例子中,我们使用用于英语的默认配置english。
介绍完了tsvector,要完成全文检索功能,我们还需要引入另外一个类型——检索条件tsquery,tsquery是一个由简单逻辑运算符组成的字符串,如下:
- postgres=# select 'we & back'::tsquery;
- tsquery
- ---------------
- 'we' & 'back'
- (1 row)
'we & back'的意思就是查询条件中既包含‘we’这个单词,也包括'back'这个单词。如果使用这个tsquery进行查询,就可以组成类似下面的SQL语句:
- postgres=# select 'I will be back'::tsvector @@ 'we & back'::tsquery;
- ?column?
- ----------
- f
- (1 row)
上面查询语句的意思,当然就是在'I will be back'这个tsvector类型中确认是不是符合既包含‘we’这个单词,也包括'back'这个单词?答案当然是否定的,所以结果为false。如果我们把tsquery中的&换成|,就会是另一种结果:
- postgres=# select 'I will be back'::tsvector @@ 'we | back'::tsquery;
- ?column?
- ----------
- t
- (1 row)
在'I will be back'中查找,确认其是否满足含有‘we’或者'back',因为它含有'back',索引结果就是true。
和tsvector类似,将字符串转换成tsquery类型,Postgresql也提供了对应的to_tsquery函数:
- postgres=# select to_tsquery('we & back');
- to_tsquery
- ------------
- 'back'
- (1 row)
从上面的结果中也可以看到,to_tsquery也忽略了we这个主语单词。
前面,我们介绍了Postgresql中实现全文检索的原理,接下来,开始在之前创建的blog表中使用全文检索。我们从上文中了解到:Postgresql实现全文检索是在tsvector类型之上的,因此要想在blog表中实现这一功能,我们还必须添加一个tsvector的列,在此列的基础之上进行全文检索。
先添加列:
- postgres=# alter table blog add column tscontent tsvector;
- ALTER TABLE
加完成列之后,然后将content里面的内容分词转换成tsvector类型:
- postgres=# update blog set tscontent=to_tsvector(content);
- UPDATE 69
然后,在此基础之上,我们进行查找包含单词mother的数据行,为了方便查看性能,我们在执行计划里面去执行:
- postgres=# explain (analyze,verbose,buffers,costs,timing) select * from blog where tscontent @@ 'mother'::tsquery;
- QUERY PLAN
- -----------------------------------------------------------------------------------------------------------------------------------
- Gather (cost=1000.00..302075.58 rows=1 width=347) (actual time=18140.837..18140.878 rows=1 loops=1)
- Output: id, recoredtime, content, tscontent
- Workers Planned: 2
- Workers Launched: 2
- Buffers: shared hit=16156 read=273456
- -> 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)
- Output: id, recoredtime, content, tscontent
- Filter: (blog.tscontent @@ '''mother'''::tsquery)
- Rows Removed by Filter: 733663
- Buffers: shared hit=16156 read=273456
- Worker 0: actual time=6087.447..17868.909 rows=1 loops=1
- Buffers: shared hit=5343 read=88589
- Worker 1: actual time=17864.982..17864.982 rows=0 loops=1
- Buffers: shared hit=5292 read=86997
- Planning Time: 1.990 ms
- Execution Time: 18144.970 ms
- (16 rows)
从上面的执行计划信息中可以看到,整个查询采用了并行扫描全表,一共查询到了1条数据,查询实际耗时18144.970ms。为了加快查询速度,我们还可以在tscontent字段上加上GIN索引:
- postgres=# create index on blog using gin(tscontent);
- CREATE INDEX
创建成功GIN索引之后,再次执行查询计划:
- postgres=# explain (analyze,verbose,buffers,costs,timing) select * from blog where tscontent @@ 'mother'::tsquery;
- QUERY PLAN
- ----------------------------------------------------------------------------------------------------------------------------
- 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)
- Output: id, recoredtime, content, tscontent
- Recheck Cond: (blog.tscontent @@ '''mother'''::tsquery)
- Heap Blocks: exact=1
- Buffers: shared hit=1 read=3
- -> 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)
- Index Cond: (blog.tscontent @@ '''mother'''::tsquery)
- Buffers: shared hit=1 read=2
- Planning Time: 2.786 ms
- Execution Time: 3.238 ms
- (10 rows)
加了索引之后,再去查询,查询过程走了索引,采用位图扫描,整个的查询过程只消耗了3.238ms,单单从数字上比较,性能提高了6000倍不止。
有的朋友可能会有疑问:如果全文搜索使用like等模糊查询方式是不是也可以实现呢?可以实现,但是如果使用like等模糊查询,主要有两个弊端:
(1)like模糊查询要进行全表扫描,查询起来会相当吃力,性能很低;
(2)查询结果中包含了所有mother这个字符串的数据,无法做到精确匹配。
我们可以再次在执行计划中使用like模糊查询测试下:
- ^Cpostgres=explain (analyze,verbose,buffers,costs,timing) select * from blog where content like '%mother%';
- QUERY PLAN
- ------------------------------------------------------------------------------------------------------------------------------------------
- Gather (cost=1000.00..313181.99 rows=111627 width=649) (actual time=6209.370..18396.139 rows=110048 loops=1)
- Output: id, recoredtime, content, tscontent
- Workers Planned: 2
- Workers Launched: 2
- Buffers: shared hit=16145 read=273467
- -> 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)
- Output: id, recoredtime, content, tscontent
- Filter: (blog.content ~~ '%mother%'::text)
- Rows Removed by Filter: 696980
- Buffers: shared hit=16145 read=273467
- Worker 0: actual time=6249.688..16447.847 rows=21945 loops=1
- Buffers: shared hit=5242 read=66229
- Worker 1: actual time=6286.170..16309.549 rows=21570 loops=1
- Buffers: shared hit=5181 read=65342
- Planning Time: 0.109 ms
- Execution Time: 18484.319 ms
- (16 rows)
因为也采用的是并行的全表扫描,所以使用like查询的耗时和使用索引前的全文检索耗时差不多,用了18484.319 ms。而且从查询结果中,我们可以看到,使用模糊查询我们查出来了110048条结果,而实际上包含mothor这个单词的数据只有一行。
可能细心的朋友已经发现,我们现在做的全文检索功能,是完全建立在检索英文的基础之上的。实际上,Postgresql默认的全文检索只支持英文,如果需要支持中文的全文检索,我们需要安装zhparser插件。由于篇幅有限,笔者就不再这里展开了,如果感兴趣可以自行百度或google。
按照惯例,我们还是对本篇的内容进行总结:
(1)Postgresql支持全文检索的功能,它提供了两个类型tsvector和tsquery分别表示全文检索索引的集合以及查询条件
(2)全文检索的原理就是将长的字符串按照空格进行分词,将分词存入到类型为tsvector的集合中,tsvector中存储每个单词和其在长语句中的位置。
(3)tsquery类型是由查询的key和&、|等逻辑运算符拼接在一起。
(4)在某个表上进行全文检索,需要创建专门的tsvector类型的字段,而且字段上可以创建gin索引来加速查询。
(5)Postgresql默认的全文检索只支持英文,需要需要使用支持中文的全文检索,需要安装zhparser插件。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。