提到全文检索首先想到的就是ES(ElasticSearch)和Lucene,专业且强大。对于一些小众场景对于搜索要求不高,数据量也不大的情况,
上ES等有些繁重,增加工作量还增加了后期运维成本。
PgSql也支持全文检索原理和ES一样,支持分词和反向索引(倒排索引),比如数据量只有几十万时,可以考虑直接使用DB去做查询。
ES执行全文检索的逻辑是:
PgSQL全局检索前需要了解三个基础概念:文档、查询、操作符。
tsvector类型表示一个为文本搜索优化的形式下的文档,tsquery类型表示一个文本查询。
SELECT to_tsvector('hello word!hello word!');
------
'hello':1,3 'word':2,4
其中hello是词条后边的数据是词位,逗号分割的是多个词条在文档中的位置,词位的数量可以反应该词在文档中的词频。
select to_tsquery('hello & word');
------
'hello' & 'word'
SELECT
to_tsvector('hello word!') @@ query q1,
to_tsvector('hello word1!') @@ query q2,
to_tsvector('hello word2!') @@ query q3
from to_tsquery('hello & word') query;
------
q1 q2 q3
true false false
-- 计算文档中同时包含hello和word的文档得分
SELECT ts_rank_cd(to_tsvector('hello word!'), to_tsquery('hello & word'));
SELECT ts_rank(to_tsvector('hello word!'), to_tsquery('word & word'));
--
SELECT ts_rank_cd (to_tsvector('hello word!'), query)
,ts_rank (to_tsvector('hello word!'),query)
from to_tsquery('hello & word') query;
tsvector是一个标准的DB类型,是类型就可以做显示转换,在pgsql中类型显示转换的操作符是两个冒号(:。
前面用到的to_tsvector()函数,默认会按照英文的语法使用空格对文档进行分词,把文档分词后做词频统计。
pgsql支持的权重值有四个,按照权重从大到小分别是:A、 B、C、D。
--文档分词
select to_tsvector('hello word! hello word!');
select 'hello:1A,3B word:2C,4D'::tsvector;
--词频影响得分
SELECT
ts_rank(to_tsvector('hello word!'),query) rank1,
ts_rank(to_tsvector('hello word! hello word!'),query) rank2
from to_tsquery('hello & word') query;
----
rank1 rank2
0.09910322 0.34000534
rank2中word出现两次,所以在计算得分时rank2比rank1高。
--权重影响得分
SELECT
ts_rank('hello:1,3 word:2,4'::tsvector,query) rank1,
ts_rank('hello:1A word:2A'::tsvector,query) rank2
from to_tsquery('word') query;
----------
rank1 rank2
0.075990885 0.6079271
word词条在rank1的词频,比rank2词频高,但通过权重控制,最终词频低的得分变高了。
高亮显示比较简单使用 tsquery 类型对文档内的关键字加上html的b标签。
--高亮
SELECT 'ts_headline'
,ts_headline ('hello word!hello word!',query)
from to_tsquery('word') query;
------
hello word!hello word!
有两种索引可以被用来加速全文搜索。注意全文搜索并非一定需要索引,但是在一个定期会被搜索的列上,通常需要有一个索引。
GIN 索引是更好的文本搜索索引类型。作为倒排索引,每个词(词位)在 其中都有一个索引项,其中有压缩过的匹配位置的列表。多词搜索可以找到 第一个匹配,然后使用该索引移除缺少额外词的行。GIN 索引只存储 tsvector值的词(词位),并且不存储它们的权重标签。因此, 在使用涉及权重的查询时需要一次在表行上的重新检查。
一个 GiST 索引是有损的,这表示索引可能产生假匹配,并且有必要检查真实的表行来消除这种假匹配(PostgreSQL在需要时会自动做这一步)。GiST 索引之所以是有损的,是因为每一个文档在索引中被表示为一个定长的签名。该签名通过哈希每一个词到一个 n 位串中的一个单一位来产生,通过将所有这些位 OR 在一起产生一个 n 位的文档签名。当两个词哈希到同一个位位置时就会产生假匹配。如果查询中所有词都有匹配(真或假),则必须检索表行查看匹配是否正确。
GiST 索引可以被覆盖,例如使用INCLUDE子句。 包含的列可以具有没有任何 GiST 操作符类的数据类型。 包含的属性将非压缩存储。
有损性导致的性能下降归因于不必要的表记录(即被证实为假匹配的记录)获取。因为表记录的随机访问是较慢的,这限制了 GiST 索引的可用性。假匹配的可能性取决于几个因素,特别是唯一词的数量,因此推荐使用词典来缩减这个数量。
对于简单的全文检索场景,使用pgsql就可以实现,对于检索的基础概念如文档、查询和操作符,词频、权重、排序、高亮都简单说明。
pgsql默认的to_tsvector()函数只支持使用空格进行分词,对于中文这个函数就不好用了。
对于中文分词有两个方案解决:1>使用pgsql的中文分词插件;2>利用空间换时间的方法,在记录写入db前利用java的jieba等分词组件对文档分词,并按
tsvector格式拼接,独立一列记录分词后的类型。如果需要提高检索效率,考虑在tsvector字段上添加GIN类型索引。
两种方法各有利弊,使用是权衡考虑。