【无标题】

pgsql_全文检索_使用空间换时间的方法支持中文搜索

一、环境

  • PostgreSQL 14.2, compiled by Visual C++ build 1914, 64-bit

二、引言

提到全文检索首先想到的就是ES(ElasticSearch)和Lucene,专业且强大。对于一些小众场景对于搜索要求不高,数据量也不大的情况,

上ES等有些繁重,增加工作量还增加了后期运维成本。

PgSql也支持全文检索原理和ES一样,支持分词和反向索引(倒排索引),比如数据量只有几十万时,可以考虑直接使用DB去做查询。

三、帮助文档

四、概念

ES执行全文检索的逻辑是:

  1. 需要对目标内容(文档)做分词,分词是将内容拆分成各个独立的词,每个词会有词频和在内容中的位置等信息;
  2. 使用分词后的内容生成索引文件,这个就是生成倒排索引的阶段,是每个词关联到不同的文档;
  3. 查询时需要对查询关键词进行分词,跟第一步很像只带分词的文档长度小了很多;
  4. 使用查询关键词匹配索引文件,按照词频、相似度、权重等指标对目标文档检索并按得分对文档排序;
  5. 返回最终匹配的文档(记录);

五、PgSQL全文检索基础

PgSQL全局检索前需要了解三个基础概念:文档、查询、操作符。

tsvector类型表示一个为文本搜索优化的形式下的文档,tsquery类型表示一个文本查询。

  • tsvector(文档)类型

    tsvector是一个数据类型,和varchar、integer类似。一个tsvector值是一个排序的可区分词位的列表,记录了分词后的词条、词频、词位、权重信息。

    SELECT to_tsvector('hello word!hello word!');

    'hello':1,3 'word':2,4

其中hello是词条后边的数据是词位,逗号分割的是多个词条在文档中的位置,词位的数量可以反应该词在文档中的词频。

  • tsquery(查询)类型

    对多个查询关键词做与或非逻辑表达,支持的逻辑操作符有:&(与)、|(或)和!(非);

    select to_tsquery('hello & word');

    'hello' & 'word'

  • 匹配操作符@@

    使用"查询"检索"文档",返回一个true/false的结果,标记操作是否匹配;

    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

六、排序&计算匹配得分

  • 排序有两个函数支持:ts_rank()、ts_rank_cd()
    他们都会参考词频、相似度,但ts_rank_cd()会计算覆盖密度排名。

    -- 计算文档中同时包含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。

  • 将字符串转tsvector类型
    • 原始文档"hello word! hello word!"

    • 分词 select to_tsvector('hello word! hello word!');

    • 自定义权重:select 'hello:1A,3B word:2C,4D'::tsvector;

      • 其中的权重值A、B、C、D是人为加的,需要满足下列格式要求;
      • 1.多个词条用空格分隔;
      • 2.每个词条后用冒号(:)分隔,冒号左边是词右边是词位、词频、权重信息;

      --文档分词
      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 <b>word</b>!hello <b>word</b>!

九、提高性能使用 GIN 和 GiST 索引

有两种索引可以被用来加速全文搜索。注意全文搜索并非一定需要索引,但是在一个定期会被搜索的列上,通常需要有一个索引。

  • CREATE INDEX name ON table USING GIN(column);
    • 创建一个基于 GIN(通用倒排索引)的索引。column必须是tsvector类型。
  • CREATE INDEX name ON table USING GIST(column);
    • 创建一个基于 GiST(通用搜索树)的索引。column可以是tsvector或tsquery类型。

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类型索引。

两种方法各有利弊,使用是权衡考虑。

相关推荐
七星静香12 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员13 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU13 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie617 分钟前
在IDEA中使用Git
java·git
Elaine20239132 分钟前
06 网络编程基础
java·网络
G丶AEOM34 分钟前
分布式——BASE理论
java·分布式·八股
落落鱼201334 分钟前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀35 分钟前
LRU缓存算法
java·算法·缓存
镰刀出海38 分钟前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试
阿伟*rui3 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel