关于 PostgreSQL 全文检索的实战 —— 中文分词、查询、索引、权重、排序

如果有人新启一个项目需要实现全文检索的功能。 不少人可能第一时间就想到ES, 但ES实在是有点重了。 再怎么说也要吃个2G内存吧。

并且由于ES是个中间件, 使用的话系统复杂度又会有一个指数级的上升,  我认为可预计的需要搜索的数据量级不会达到
千万级别

的话, 没太大必要。

干脆直接用 PostgreSQL 解决这个问题得了, 使用简单, 功能强大。  支持各种常规的搜索条件(与、或、非、包含、被包含、距离等) 、也可以添加索引, 如果机器够顶, 就算是亿级别的数据也可以控制在毫秒级返回。 架构上能简化很多。

一般的个人项目、 中小公司的项目,   需要搜索的数据能达到千万这个量么?

我由于自身维护了一个网站,当时的技术选型DB层就是使用的 Posrgres 。 自然, 也会有搜索相关的功能 ,所以我对此有一定使用层面的经验分享

下面就来从
字段、函数、分词、使用、实践、 示例

等方面概述一下如何优雅使用 PostgreSQL 来进行全文检索。

相关文档

这俩文档对不太熟悉 PostgreSQL 全文搜索的小伙伴应该会有一定作用。

看下操作符和官方示例可以更好的知道如何使用。

文本搜索的函数与操作符中文说明: 

http://www.postgres.cn/docs/9.3.4/functions-textsearch.html

PostgreSQL 文本搜索文档 : 

https://www.postgresql.org/docs/9.4/textsearch-controls.html

核心函数、关键字

全文搜索的思想主要分两步:

  • 将文本解析为对应的倒排索引
  • 搜索索引来找到对应的文本

要使用 PostgreSQL 的全文搜索,  那么就要了解到底是要用什么关键字和函数来 构建索引
搜索
, 这里主要涉及到
一个关键字


四个函数

可以看上边的文档了解函数的具体参数

关键字 tsvector

tsvector 是 PostgreSQL 内置的一种字段类型,  用来保存的是分词后的结果 (文本向量)  它是由 [词,序列, 权重] 三个东西共同组成的, 权重可能会没有

函数: 

  • to_tsvector()

    • 分词用,  将文本转为向量。 用它可以将字符串转成上边说的 tsvector ,  遗憾的是默认不支持中文分词
  • to_tsquery()

    • 构建搜索的关键字, 支持各种符号表示条件。 详情查看文档
  • setweight()

    • 设置关键词权重, 总共四个权重从高到低为 A-B-C-D
  • ts_rank ()

    • 排序用, 可以根据 to_tsquery 和 tsvector 的匹配度计算

一个简单的使用:

//查询表中包含 aaa 并且包含 BBB或CCC任意一个 的记录
SELECT * FROM table
WHERE to_tsvector('parser_name', field) @@ to_tsquery('aaa & (bbb | ccc)')

最佳实践

最重要的就是分词逻辑。

如果你去网上搜索 PostgreSQL 中文分词, 你应该会找到洋洋洒洒一大堆叫你装什么插件然后分词的。

我觉得不太行, 一个是安装起来麻烦,二个是难以控制。

我推荐的方式是不装插件, 在 应用程序内部分词
, 将
分词后的结果加上权重 设置进待搜索列的字段中

优势: 

  1. 不需要折腾, 省时间
  2. 冗余向量字段, 空间换时间,避免每次查询时还得调用 to_tsvector 、setweight 去计算分词结果和权重  。
  3. 程序分词, 更自由地选择分词器
  4. 自定义字典方便

而程序内部分词, 一般都有相应的库开箱即用, 比如Java 使用 jieba 分词, 直接引入依赖就能用

<dependency>
    <groupId>com.huaban</groupId>
    <artifactId>jieba-analysis</artifactId>
    <version>1.0.2</version>
</dependency>

引入后就可以很轻松的使用这个库来进行中文分词, 效果也挺不错的

private List<String> segmente(String text, JiebaSegmenter.SegMode mode) {
    JiebaSegmenter segmenter = new JiebaSegmenter();
    List<SegToken> tokens = segmenter.process(text, mode);
 
    List<String> result = tokens.stream()
            .map(tk -> tk.word)
            .filter(t -> t != null && !t.isBlank())
            .collect(Collectors.toCollection(LinkedList::new));
 
    return result;
}

当我们可以轻松的获取分词结果时, 那么接下来要做的就很简单了。


构建倒排索引:

针对于我们的业务模型找出 需要搜索的字段
分配权重
, 分词后 配合 setweight 函数插入到表中的 tsvector 类型字段


全文检索: 

使用 to_tsquery 构建搜索关键字,  匹配到结果后使用 ts_rank 进行排序

例子: 

这里都是我个人项目中的真实SQL,  将其去敏后贴出, 下边详细解释这些SQL在干什么。

使用的是 MyBatis ,  #{} 为 变量。

1、 构建索引,  这是一个构建索引的SQL,  在数据行创建完毕后执行

UPDATE table1
SET tokens = setweight(to_tsvector('simple', #{aTokens}), 'A') ||
             setweight(to_tsvector('simple', #{bTokens}), 'B')
WHERE id = #{id}

tokens 字段即为我这个数据模型的用来保存文本向量的字段, 即一个 tsvector 类型的字段。

这里就是在程序端分词完毕后将分词的结果设置进此字段中。需要注意的是 to_tsvector('simple', xxx)
表示自定义分词,
PostgreSQL会 按照空格进行分词

, 所以我在
传入值的时候

将所有的词都用
空格分隔并进行了拼接

由于我这个数据模型有两个字段需要搜索, 并且搜索时 权重有优先级之分
。 可以理解为 标题、简介 这种优先级关系。 所以我在设置时
使用 setweight 将其中一个的优先级设置为 A, 另一个设置为 B

。 ‘||’ 符号不是或, 是PostgreSQL的拼接符号

2、检索,  这是一个全文检索SQL,用户点击查询时,  将搜索内容分词后传入执行

SELECT hc.id                     AS id,
       hc.name                   AS name,
       hc.create_at              AS createAt,
       hc.create_time            AS createTime,
       ts_rank(hc.tokens, query) AS score
FROM table1 hc,
     to_tsquery('simple', #{keyword}) query
WHERE hc.tokens @@ query
ORDER BY score DESC

keyword 即用户试图搜索的内容, 在
程序端分词完毕后

使用 ‘|’ (或) 拼接传入。使用 to_tsquery 来构建查询。

使用 @@
语法来匹配已经构建完毕的倒排索引, 只有匹配到我传入的词组任意一个内容才会被筛选出来。

筛选完毕了还不行, 用户搜索时当然期望查到最匹配的数据,所以这里用 ts_rank() 函数来
计算 筛选结果的索引&查询关键字 之间的分数

,  计算完毕后 ORDER BY 排序没啥好说的。

注意点:

构建索引时一般拆分粒度会尽可能的细,  最好用索引模式。 而针对用户的搜索内容一般使用搜索模式, 拆分粒度粗一点。

冗余的索引字段在业务中没有任何作用, 只用来搜索, 所以在其他业务里最好不要查询。

如果使用了 ORM 框架,  请给实体类的字段加上 @Transient
注解 (Java)

索引

当数据量庞大时, 那么不可避免地查询速度就会变慢, 此时就需要去加索引。

PostgreSQL自然也提供了强大的索引支持, 使用以下语句增加 pg_trgm
拓展就可以引入两个索引 gin
gist
, 需要注意的是执行语句需要提权到 postgres 用户。

CREATE EXTENSION pg_trgm;

gin和gist的区别就是 gin查询更快, 但是构建速度可能会慢一点。 而 gist 的构建速度快, 查询会慢一点。

一般建议预计数据量不大时可以使用gist索引, 如果预计数据量很大请直接上gin。

PostgreSQL 真的很香。特性太棒了, 做东西又快又好。

就光这个全文搜索, 中小体量的应用直接用 PostgreSQL 提供的支持可以减少很多麻烦。 既省去了运维新的中间件, 也节约了服务器资源。

最终实现的效果还强力。

就我亲身体验而言, 爽

编码妙♂妙♂屋
我还没有学会写个人说明!

你也可能喜欢

评论已经被关闭。

插入图片