首页 > 经验记录 > 关于 PostgreSQL 全文检索的实战 —— 中文分词、查询、索引、权重、排序

关于 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 拓展就可以引入两个索引 gingist, 需要注意的是执行语句需要提权到 postgres 用户。

CREATE EXTENSION pg_trgm;

gin和gist的区别就是 gin查询更快, 但是构建速度可能会慢一点。 而 gist 的构建速度快, 查询会慢一点。
一般建议预计数据量不大时可以使用gist索引, 如果预计数据量很大请直接上gin。
 
 
 

PostgreSQL 真的很香。特性太棒了, 做东西又快又好。
就光这个全文搜索, 中小体量的应用直接用 PostgreSQL 提供的支持可以减少很多麻烦。 既省去了运维新的中间件, 也节约了服务器资源。
最终实现的效果还强力。
 
就我亲身体验而言, 爽
 
 

           


1 COMMENT

  1. skyray2020-12-29 14:13

    可以,学到了

EA PLAYER &

历史记录 [ 注意:部分数据仅限于当前浏览器 ]清空

      00:00/00:00