分词

全文索引的实现要靠 PgSQL 的 gin 索引。分词功能 PgSQL 内置了英文、西班牙文等,但中文分词需要借助开源插件 zhparser;

安装

要使用 zhparser,我们首先要安装 SCWS 分词库,SCWS 是 Simple Chinese Word Segmentation 的首字母缩写(即:简易中文分词系统),其 GitHub 项目地址为 hightman-scws,我们下载之后可以直接安装。

安装完后,就可以在命令行中使用 scws 命令进行测试分词了, 其参数主要有:

-c utf8 指定字符集
-d dict 指定字典 可以是 xdb 或 txt 格式
-M 复合分词的级别, 1~15,按位异或的 1|2|4|8 依次表示 短词|二元|主要字|全部字,默认不复合分词,这个参数可以帮助调整到最想要的分词效果。
zhpaser

安装scws

wget http://www.xunsearch.com/scws/down/scws-1.2.2.tar.bz2

tar xvf scws-1.2.2.tar.bz2   

cd scws-1.2.2  

./configure --prefix=/usr/local/scws ; make ; make install

ls -al /usr/local/scws/lib/libscws.la

/usr/local/scws/bin/scws -h

cd /usr/local/scws/etc

wget http://www.xunsearch.com/scws/down/scws-dict-chs-gbk.tar.bz2

wget http://www.xunsearch.com/scws/down/scws-dict-chs-utf8.tar.bz2

tar xvjf scws-dict-chs-gbk.tar.bz2

tar xvjf scws-dict-chs-utf8.tar.bz2

安装 zhparser

wget https://github.com/amutu/zhparser/archive/master.zip

cd zh*

SCWS_HOME=/usr/local/scws make && make install

chown -R postgres:postgres /usr/pgsql-13/share/tsearch_data/

注册使用

登陆 PgSQL 使用 CREATE EXTENSION zhparser; 启用插件;
添加分词配置

CREATE TEXT SEARCH CONFIGURATION parser_name (PARSER = zhparser); // 添加配置

ALTER TEXT SEARCH CONFIGURATION parser_name ADD MAPPING FOR n,v,a,i,e,l,j WITH simple; // 设置分词规则 (n 名词 v 动词等,详情阅读下面的文档)

给某一列的分词结果添加 gin 索引 create index idx_name on table using gin(to_tsvector('parser_name', field));

配置

以下配置在PG9.2及以上版本使用,这些选项是用来控制字典加载行为和分词行为的,这些选项都不是必须的,默认都为false(即如果没有在配置文件中设置这些选项,则zhparser的行为与将下面的选项设置为false一致)。


忽略所有的标点等特殊符号: zhparser.punctuation_ignore = f

闲散文字自动以二字分词法聚合: zhparser.seg_with_duality = f

将词典全部加载到内存里: zhparser.dict_in_memory = f

短词复合: zhparser.multi_short = f

散字二元复合: zhparser.multi_duality = f

重要单字复合: zhparser.multi_zmain = f

全部单字复合: zhparser.multi_zall = f

在命令行中使用上一节中介绍的 scws 命令测试分词配置,如我认为复合等级为 7 时分词结果最好,则我在 postgresql.conf添加配置

zhparser.multi_short = true #短词复合: 1
zhparser.multi_duality = true  #散字二元复合: 2
zhparser.multi_zmain = true  #重要单字复合: 4
zhparser.multi_zall = false  #全部单字复合: 8

SQL

查询中我们可以使用最简单的 SELECT * FROM table WHERE to_tsvector('parser_name', field) @@ 'word' 来查询 field 字段分词中带有 word 一词的数据;

使用 to_tsquery() 方法将句子解析成各个词的组合向量,如 国家大剧院 的返回结果为 '国家' & '大剧院' & '大剧' & '剧院' ,当然我们也可以使用 & | 符号拼接自己需要的向量;在查询 长句 时,可以使用 SELECT * FROM table WHERE to_tsvector('parser_name', field) @@ to_tsquery('parser_name','words');

有时候我们想像 MySQL 的 SQL_CALC_FOUND_ROWS 语句一样同步返回结果条数,则可以使用 SELECT COUNT(*) OVER() AS score FROM table WHERE ...,PgSQL 会在每一行数据添加 score 字段存储查询到的总结果条数;

到这里,普通的全文检索需求已经实现了。

优化

我们接着对分词效果和效率进行优化:

存储分词结果

我们可以使用一个字段来存储分词向量,并在此字段上创建索引来更优地使用分词索引:

ALTER TABLE table ADD COLUMN tsv_column tsvector;           // 添加一个分词字段
UPDATE table SET tsv_column = to_tsvector('parser_name', coalesce(field,''));   // 将字段的分词向量

更新到新字段中

CREATE INDEX idx_gin_zhcn ON table USING GIN(tsv_column);   // 在新字段上创建索引
CREATE TRIGGER trigger_name BEFORE INSERT OR UPDATE  ON table FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger(tsv_column, 'parser_name', field); // 创建一个更新分词触发器

这样,再进行查询时就可以直接使用 SELECT * FROM table WHERE tsv_column @@ 'keyword' 了。

这里需要注意,这时候在往表内插入数据的时候,可能会报错,提示指定 parser_name 的 schema, 这时候可以使用 \dF 命令查看所有 text search configuration 的参数:

               List of text search configurations
   Schema   |    Name    |              Description
------------+------------+---------------------------------------
 pg_catalog | english    | configuration for english language
 public     | myparser   |

注意 schema 参数,在创建 trigger 时需要指定 schema, 如上面,就需要使用 public.myparser。

添加自定义词典

我们可以在网上下载 xdb 格式的词库来替代默认词典,词库放在 share/tsearch_data/ 文件夹下才能被 PgSQL 读取到,默认使用的词库是 dict.utf8.xdb。要使用自定义词库,可以将词库放在词库文件夹后,在 postgresql.conf 配置 zhparser.extra_dict="mydict.xdb" 参数;

当我们只有 txt 的词库,想把这个词库作为默认词库该怎么办呢?使用 scws 带的scwe-gen-dict 工具或网上找的脚本生成 xdb 后放入词库文件夹后,在 PgSQL 中分词一直报错,读取词库文件失败。我经过多次实验,总结出了一套制作一个词典文件的方法:

准备词库源文件 mydict.txt:词库文件的内容每一行的格式为词 TF IDF 词性,词是必须的,而 TF 词频(Term Frequency)、IDF 反文档频率(Inverse Document Frequency) 和 词性 都是可选的,除非确定自己的词典资料是对的且符合 scws 的配置,不然最好还是留空,让 scws 自已确定;
在 postgresql.conf 中设置 zhparser.extra_dicts = "mydict.txt" 同时设置 zhparser.dict_in_memory = true;
命令行进入 PgSQL,执行一条分词语句 select to_tsquery('parser', '随便一个词') ,分词会极慢,请耐心(请保证此时只有一个分词语句在执行);
分词成功后,在/tmp/目录下找到生成的 scws-xxxx.xdb 替换掉 share/tsearch_data/dict.utf8.xdb;
删除刚加入的 extra_dicts dict_in_memory 配置,重启服务器。
扩展
由于查询的是 POI 的名称,一般较短,且很多词并无语义,又考虑到用户的输入习惯,一般会输入 POI 名称的前几个字符,而且 scws 的分词准确率也不能达到100%,于是我添加了名称的前缀查询来提高查询的准确率,即使用 B树索引 实现 LIKE '关键词%' 的查询。这里需

这里要注意的是,创建索引时要根据字段类型配置 操作符类,不然索引可能会不生效,如在 字段类型为 varchar 的字段上创建索引需要使用语句CREATE INDEX idx_name ON table(COLUMN varchar_pattern_ops),这里的 varcharpatternops 就是操作符类,操作符类的介绍和选择可以查看文档:11.9. 操作符类和操作符族。

自此,一个良好的全文检索系统就完成了。

新增分词

# insert into zhparser.zhprs_custom_word values('资金压力');
--删除词insert into zhprs_custom_word(word, attr) values('word', '!');
--\d zhprs_custom_word 查看其表结构,支持TD, IDF
# select sync_zhprs_custom_word();
 sync_zhprs_custom_word ------------------------
 (1 row) test=# \q --sync 后重新建立连接

总结

简单的数据迁移并不是终点,后续要做的还有很多,如整个系统的数据同步、查询效率优化、查询功能优化(添加拼音搜索、模糊搜索)等。特别是查询效率,不知道是不是我配置有问题,完全达不到那种 E级毫秒 的速度,1kw 的数据效率在进行大结果返回时就大幅下降(200ms),只好老老实实地提前进行了分表,目前百万级查询速度在 20ms 以内,优化还有一段路要走。

不过这次倒是对 技术的“生态”有了个更深的体会,这方面 PgSQL 确实和 MySQL 差远了,使用 MySQL 时再奇葩的问题都能在网上快速找到答案,而 PgSQL 就尴尬了,入门级的问题搜索 stackoverflow 来来回回就那么几个对不上的回答。虽然也有阿里的“德哥”一样的大神在辛苦布道,但用户的数量才是根本。不过,随着 PgSQL 越来越完善,使用它的人一定会越来越多的,我这篇文章也算是为 PgSQL 加温了吧,哈哈~希望能帮到后来的使用者。

其他测试

-- ts_parse

SELECT * FROM ts_parse('zhparser', 'hello world! 2010年保障房建设在全国范围内获全面启动,从中央到地方纷纷加大 了保障房的建设和投入力度 。2011年,保障房进入了更大规模的建设阶段。住房城乡建设部党组书记、部长姜伟新去年底在全国住房城乡建设工作会议上表示,要继续推进保障性安居工程建设。');

-- test to_tsvector

SELECT to_tsvector('testzhcfg','“今年保障房新开工数量虽然有所下调,但实际的年度在建规模以及竣工规模会超以往年份,相对应的对资金的需求也会创历>史纪录。”陈国强说。在他看来,与2011年相比,2012年的保障房建设在资金配套上的压力将更为严峻。');

-- test to_tsquery

SELECT to_tsquery('testzhcfg', '保障房资金压力');