PostgreSQL支持全文检索,其内置的缺省的分词解析器采用空格分词。因为中文的词语之间没有空格分割,所以这种方法并不适用于中文。要支持中文的全文检索需要额外的中文分词插件。网上查了下,可以给PG用的开源中文分词插件有两个:nlpbamboo和zhparser。但是nlpbamboo是托管在googlecode上的,而googlecode被封了,下载不方便。下面尝试采用zhparser进行中文的全文检索。
zhparser是基于Simple Chinese Word Segmentation(SCWS)中文分词库实现的一个PG扩展,作者是 amutu,源码URL为https://github.com/amutu/zhparser。
1. 安装
1.1 下载SCWS
http://www.xunsearch.com/scws/down/scws-1.2.2.tar.bz2
1.2 编译和安装SCWS
tar xvf scws-1.2.2.tar.bz2
cd scws-1.2.2
./configure
make install
1.3 下载zhparser
https://github.com/amutu/zhparser/archive/master.zip
1.4 编译和安装zhparser
确保PostgreSQL的二进制命令路径在PATH下,然后解压并进入zhparser目录后,编译安装zhparser。
SCWS_HOME=/usr/local make && make install
2 配置中文全文检索
连接到目标数据库进行中文全文检索的配置
2.1 安装zhparser扩展
- -bash-4.1$ psql testdb
- psql (9.4.0)
- Type "help" for help.
-
- testdb=# create extension zhparser;
- CREATE EXTENSION
安装zhparser扩展后多一个叫“zhparser”的解析器
- testdb=# \dFp
- List of text search parsers
- Schema | Name | Description
- ------------+----------+---------------------
- pg_catalog | default | default word parser
- public | zhparser |
- (2 rows)
zhparser可以将中文切分成下面26种token
点击(
此处
)折叠或打开
- testdb=# select ts_token_type('zhparser');
- ts_token_type
- -----------------------------------------
- (97,a,adjective)
- (98,b,"differentiation (qu bie)")
- (99,c,conjunction)
- (100,d,adverb)
- (101,e,exclamation)
- (102,f,"position (fang wei)")
- (103,g,"root (ci gen)")
- (104,h,head)
- (105,i,idiom)
- (106,j,"abbreviation (jian lue)")
- (107,k,head)
- (108,l,"tmp (lin shi)")
- (109,m,numeral)
- (110,n,noun)
- (111,o,onomatopoeia)
- (112,p,prepositional)
- (113,q,quantity)
- (114,r,pronoun)
- (115,s,space)
- (116,t,time)
- (117,u,auxiliary)
- (118,v,verb)
- (119,w,"punctuation (qi ta biao dian)")
- (120,x,unknown)
- (121,y,"modal (yu qi)")
- (122,z,"status (zhuang tai)")
- (26 rows)
2.2 创建使用zhparser作为解析器的全文搜索的配置
- testdb=# CREATE TEXT SEARCH CONFIGURATION testzhcfg (PARSER = zhparser);
- CREATE TEXT SEARCH CONFIGURATION
2.3 往全文搜索配置中增加token映射
- testdb=# ALTER TEXT SEARCH CONFIGURATION testzhcfg ADD MAPPING FOR n,v,a,i,e,l WITH simple;
- ALTER TEXT SEARCH CONFIGURATION
上面的token映射只映射了名词(n),动词(v),形容词(a),成语(i),叹词(e)和习用语(l)6种,这6种以外的token全部被屏蔽。词典使用的是内置的simple词典,即仅做小写转换。根据需要可以灵活定义词典和token映射,以实现屏蔽词和同义词归并等功能。
3.中文分词测试
- testdb=# select to_tsvector('testzhcfg','南京市长江大桥');
- to_tsvector
- -------------------------
- '南京市':1 '长江大桥':2
- (1 row)
中文分词有最大匹配,最细粒度等各种常用算法。上面的分词结果没有把'长江大桥'拆成'长江'和'大桥'两个词,所以SCWS估计是采取的最大匹配的分词算法。
分词算法的优劣一般通过3个指标衡量。
效率:
索引和查询的效率
召回率:
提取出的正确信息条数 / 样本中的信息条数
准确率:
提取出的正确信息条数 / 提取出的信息条数
分词的粒度越粗,效率越高,但遗漏的可能性也会高一点,即
召回率受影响。具体到上面的例子,用'南京&大桥'就没法匹配到。
- testdb=# select to_tsvector('testzhcfg','南京市长江大桥') @@ '南京&大桥';
- ?column?
- ----------
- f
- (1 row)
效率,召回率和准确率3个指标往往不能兼顾,所以不能笼统的说最大匹配好还是不好。但是如果特别在乎
召回率,SCWS也提供了一些选项进行调节。下面是scws命令可接受的参数。
http://www.xunsearch.com/scws/docs.php#utilscws
- 1. **$prefix/bin/scws** 这是分词的命令行工具,执行 scws -h 可以看到详细帮助说明。
- ```
- Usage: scws [options] [[-i] input] [[-o] output]
- ```
- * _-i string|file_ 要切分的字符串或文件,如不指定则程序自动读取标准输入,每输入一行执行一次分词
- * _-o file_ 切分结果输出保存的文件路径,若不指定直接输出到屏幕
- * _-c charset_ 指定分词的字符集,默认是 gbk,可选 utf8
- * _-r file_ 指定规则集文件(规则集用于数词、数字、专有名字、人名的识别)
- * _-d file[:file2[:...]]_ 指定词典文件路径(XDB格式,请在 -c 之后使用)
- ```
- 自 1.1.0 起,支持多词典同时载入,也支持纯文本词典(必须是.txt结尾),多词典路径之间用冒号(:)隔开,
- 排在越后面的词典优先级越高。
-
- 文本词典的数据格式参见 scws-gen-dict 所用的格式,但更宽松一些,允许用不定量的空格分开,只有是必备项目,
- 其它数据可有可无,当词性标注为“!”(叹号)时表示该词作废,即使在较低优先级的词库中存在该词也将作废。
- ```
- * _-M level_ 复合分词的级别:1~15,按位异或的 1|2|4|8 依次表示 短词|二元|主要字|全部字,缺省不复合分词。
- * _-I_ 输出结果忽略跳过所有的标点符号
- * _-A_ 显示词性
- * _-E_ 将 xdb 词典读入内存 xtree 结构 (如果切分的文件很大才需要)
- * _-N_ 不显示切分时间和提示
- * _-D_ debug 模式 (很少用,需要编译时打开 --enable-debug)
- * _-U_ 将闲散单字自动调用二分法结合
- * _-t num_ 取得前 num 个高频词
- * _-a [~]attr1[,attr2[,...]]_ 只显示某些词性的词,加~表示过滤该词性的词,多个词性之间用逗号分隔
- * _-v_ 查看版本
通过-M指定短词的复合分词,可以得到细粒度的分词。
默认是最大匹配:
- [root@hanode1 tsearch_data]# scws -c utf8 -d dict.utf8.xdb -r rules.utf8.ini "南京市长江大桥"
南京市 长江大桥
+--[scws(scws-cli/1.2.2)]----------+
| TextLen: 21 |
| Prepare: 0.0021 (sec) |
| Segment: 0.0003 (sec) |
+--------------------------------+
-
指定短词的复合分词,可以对长词再进行复合切分。
- [root@hanode1 tsearch_data]# scws -c utf8 -d dict.utf8.xdb -r rules.utf8.ini -M 1 "南京市长江大桥"
南京市 南京 长江大桥 长江 大桥
+--[scws(scws-cli/1.2.2)]----------+
| TextLen: 21 |
| Prepare: 0.0020 (sec) |
| Segment: 0.0002 (sec) |
+--------------------------------+
-
这样切分后"南京 & 大桥"也可以匹配。
甚至可以把重要的单字也切出来。
- [root@hanode1 zhparser-0.1.4]# scws -c utf8 -d dict.utf8.xdb -r rules.utf8.ini -M 5 "南京市长江大桥"
南京市 南京 市 长江大桥 长江 大桥 江 桥
+--[scws(scws-cli/1.2.2)]----------+
| TextLen: 21 |
| Prepare: 0.0020 (sec) |
| Segment: 0.0002 (sec) |
+--------------------------------+
-
这样切分后,"南京 & 桥"也可以匹配。
再变态一点,对短词和所有单字做复合切分。
- [root@hanode1 zhparser-0.1.4]# scws -c utf8 -d dict.utf8.xdb -r rules.utf8.ini -M 9 "南京市长江大桥"
南京市 南京 南 京 市 长江大桥 长江 大桥 长 江 大 桥
+--[scws(scws-cli/1.2.2)]----------+
| TextLen: 21 |
| Prepare: 0.0021 (sec) |
| Segment: 0.0003 (sec) |
+--------------------------------+
-
这样切分基本上可以不再遗漏匹配了,但是效率肯定受影响。
上面的选项是加在scws命令上的,也可以通过scws_set_multi()函数加到zhparser(
libscws)上。
http://www.xunsearch.com/scws/docs.php#libscws:
- 9. `void scws_set_multi(scws_t s, int mode)` 设定分词执行时是否执行针对长词复合切分。(例:“中国人”分为“中国”、“人”、“中国人”)。
-
- > **参数 mode** 复合分词法的级别,缺省不复合分词。取值由下面几个常量异或组合:
- >
- > - SCWS_MULTI_SHORT 短词
- > - SCWS_MULTI_DUALITY 二元(将相邻的2个单字组合成一个词)
- > - SCWS_MULTI_ZMAIN 重要单字
- > - SCWS_MULTI_ZALL 全部单字
修改
zhparser.c,追加
scws_set_multi()的调用
zhparser.c:
- static void init(){
- char sharepath[MAXPGPATH];
- char * dict_path,* rule_path;
-
- if (!(scws = scws_new())) {
- ereport(ERROR,
- (errcode(ERRCODE_INTERNAL_ERROR),
- errmsg("Chinese Parser Lib SCWS could not init!\"%s\"",""
- )));
- }
- get_share_path(my_exec_path, sharepath);
- dict_path = palloc(MAXPGPATH);
-
- snprintf(dict_path, MAXPGPATH, "%s/tsearch_data/%s.%s",
- sharepath, "dict.utf8", "xdb");
- scws_set_charset(scws, "utf-8");
- scws_set_dict(scws,dict_path, SCWS_XDICT_XDB);
-
- rule_path = palloc(MAXPGPATH);
- snprintf(rule_path, MAXPGPATH, "%s/tsearch_data/%s.%s",
- sharepath, "rules.utf8", "ini");
- scws_set_rule(scws ,rule_path);
- scws_set_multi(scws ,SCWS_MULTI_SHORT|SCWS_MULTI_ZMAIN);//追加代码
- }
重新编译安装zhparser后,再restart PostgreSQL,可以看到效果。
- testdb=# select to_tsvector('testzhcfg','南京市长江大桥');
- to_tsvector
- -------------------------------------------------------------------------
- '南京':2 '南京市':1 '大桥':6 '市':3 '桥':8 '江':7 '长江':5 '长江大桥':4
- (1 row)
-
- testdb=# select to_tsvector('testzhcfg','南京市长江大桥') @@ '南京 & 桥';
- ?column?
- ----------
- t
- (1 row)
tsquery也会被复合切分:
- testdb=# select to_tsquery('testzhcfg','南京市长江大桥');
- to_tsquery
- -----------------------------------------------------------------------
- '南京市' & '南京' & '市' & '长江大桥' & '长江' & '大桥' & '江' & '桥'
- (1 row)
这可能不是我们需要的,tsquery切的太细会影响查询效率。做了个简单的测试,走gin索引,按这个例子对
tsquery复合切分会比默认的最大切分慢了1倍。
- testdb=# \d tb1
- Table "public.tb1"
- Column | Type | Modifiers
- --------+------+-----------
- c1 | text |
- Indexes:
- "tb1idx1" gin (to_tsvector('testzhcfg'::regconfig, c1))
-
- testdb=# insert into tb1 select '南京市长江大桥' from generate_series(1,10000,1);
-
- testdb=# explain analyze select count(*) from tb1 where to_tsvector('testzhcfg', c1) @@ '南京市 & 长江大桥'::tsquery;
- QUERY PLAN
- --------------------------------------------------------------------------------------------------------------------------------
- Aggregate (cost=348.53..348.54 rows=1 width=0) (actual time=6.077..6.077 rows=1 loops=1)
- -> Bitmap Heap Scan on tb1 (cost=109.51..323.53 rows=10001 width=0) (actual time=3.186..4.917 rows=10001 loops=1)
- Recheck Cond: (to_tsvector('testzhcfg'::regconfig, c1) @@ '''南京市'' & ''长江大桥'''::tsquery)
- Heap Blocks: exact=64
- -> Bitmap Index Scan on tb1idx1 (cost=0.00..107.01 rows=10001 width=0) (actual time=3.154..3.154 rows=10001 loops=1)
- Index Cond: (to_tsvector('testzhcfg'::regconfig, c1) @@ '''南京市'' & ''长江大桥'''::tsquery)
- Planning time: 0.117 ms
- Execution time: 6.127 ms
- (8 rows)
-
- Time: 6.857 ms
- testdb=# explain analyze select count(*) from tb1 where to_tsvector('testzhcfg', c1) @@ '南京市 & 南京 & 市 & 长江大桥 & 长江 & 大桥 & 江 & 桥'::tsquery;
- QUERY PLAN
-
- ------------------------------------------------------------------------------------------------------------------------------------------------
- -------------------------
- Aggregate (cost=396.53..396.54 rows=1 width=0) (actual time=10.823..10.823 rows=1 loops=1)
- -> Bitmap Heap Scan on tb1 (cost=157.51..371.53 rows=10001 width=0) (actual time=7.923..9.631 rows=10000 loops=1)
- Recheck Cond: (to_tsvector('testzhcfg'::regconfig, c1) @@ '''南京市'' & ''南京'' & ''市'' & ''长江大桥'' & ''长江'' & ''大桥'' & ''江''
- & ''桥'''::tsquery)
- Heap Blocks: exact=64
- -> Bitmap Index Scan on tb1idx1 (cost=0.00..155.01 rows=10001 width=0) (actual time=7.885..7.885 rows=10000 loops=1)
- Index Cond: (to_tsvector('testzhcfg'::regconfig, c1) @@ '''南京市'' & ''南京'' & ''市'' & ''长江大桥'' & ''长江'' & ''大桥'' & ''
- 江'' & ''桥'''::tsquery)
- Planning time: 0.111 ms
- Execution time: 10.879 ms
- (8 rows)
-
- Time: 11.586 ms
要回避这个问题可以做两套解析器,一套给tsvector用做复合切分;一套给tsquery用,不做复合切分。或者像上面测试例子中那样不对查询字符串做分词,由应用端直接输入tsquery(不过这样做会有别的问题,后面会提到)。
3.其它问题
3.1 '南大'被无视了
无意中发现一个奇怪的现象,'南大'被无视了:
- testdb=# select to_tsvector('testzhcfg','南大') ;
- to_tsvector
- -------------
-
- (1 row)
'北大','东大'甚至'西大'都没问题:
- testdb=# select to_tsvector('testzhcfg','南大 北大 东大 西大') ;
- to_tsvector
- ----------------------------
- '东大':2 '北大':1 '西大':3
- (1 row)
调查发现原因在于它们被SCWS解析出来的token类型不同:
- testdb=# select ts_debug('testzhcfg','南大 北大 东大 西大') ;
- ts_debug
- -----------------------------------------
- (j,"abbreviation (jian lue)",南大,{},,)
- (n,noun,北大,{simple},simple,{北大})
- (n,noun,东大,{simple},simple,{东大})
- (n,noun,西大,{simple},simple,{西大})
- (4 rows)
'南大'被识别为j(简略词),而之前并没有为j创建token映射。现在加上j的token映射,就可以了。
- testdb=# ALTER TEXT SEARCH CONFIGURATION testzhcfg ADD MAPPING FOR j WITH simple;
- ALTER TEXT SEARCH CONFIGURATION
- testdb=# select to_tsvector('testzhcfg','南大 北大 东大 西大') ;
- to_tsvector
- -------------------------------------
- '东大':3 '北大':2 '南大':1 '西大':4
- (1 row)
3.2 新词的识别
词典收录的词毕竟有限,遇到新词就不认识了。不断完善词典可以缓解这个问题,但不能从根本上避免。
'微信'没有被识别出来:
- testdb=# select to_tsvector('testzhcfg','微信');
- to_tsvector
- ---------------
- '信':2 '微':1
- (1 row)
-
- testdb=# select to_tsvector('testzhcfg','微信') @@ '微信';
- ?column?
- ----------
- f
- (1 row)
虽然这个词没有被识别出来,但是我们只要对tsquery采用相同分词方法,就可以匹配。
- testdb=# select to_tsvector('testzhcfg','微信') @@ to_tsquery('testzhcfg','微信');
- ?column?
- ----------
- t
- (1 row)
但是,利用拆开的单字做匹配,检索的效率肯定不会太好。SCWS还提供了一种解决方法(-U),可以对连续的闲散单字做二元切分。
- [root@hanode1 zhparser-0.1.4]# scws -c utf8 -d dict.utf8.xdb -r rules.utf8.ini -U "微信微博"
微信 信微 微博
+--[scws(scws-cli/1.2.2)]----------+
| TextLen: 12 |
| Prepare: 0.0020 (sec) |
| Segment: 0.0001 (sec) |
+--------------------------------+
-
对zhparser,可以像之前那样,修改
zhparser.c,通过调用scws_set_duality()函数设置这个选项。
http://www.xunsearch.com/scws/docs.php#libscws
- 10. `void scws_set_duality(scws_t s, int yes)` 设定是否将闲散文字自动以二字分词法聚合。
-
- > **参数 yes** 如果为 1 表示执行二分聚合,0 表示不处理,缺省为 0。
但是二元切分也有缺点,会产生歧义词和无意义的词。而且如果这些
连续的
闲散单字真的是单字的话,二字聚合后就不能再做单字匹配了。
4. 总结
zhparser的安装和配置非常容易,分词效果也不错,可以满足一般的场景。如果有更高的要求需要做一些定制。
5. 参考
postgresql之全文搜索篇
http://www.postgresql.org/docs/9.4/static/textsearch.html
http://www.xunsearch.com/scws/docs.php
http://www.xunsearch.com/scws/api.php
http://amutu.com/blog/zhparser/
http://my.oschina.net/Kenyon/blog/82305?p=1#comments
http://blog.163.com/digoal@126/blog/static/163877040201252141010693/
http://francs3.blog.163.com/blog/static/405767272015065565069/
http://www.cnblogs.com/flish/archive/2011/08/08/2131031.html
http://wenku.baidu.com/link?url=wD7QgE8iNY-UshcSIWkVMUmpTa-dCsnYmn187XZhWuA5Hljt73raE25Wa8dFm_5IADD2T6y5Ur_JeCtouwszayjEUudLQN3pNJqZWN5ofFG
http://www.cnblogs.com/lvpei/archive/2010/08/04/1792409.html
http://blog.2ndquadrant.com/text-search-strategies-in-postgresql/
http://wenku.baidu.com/link?url=va4FRRibEfCdm731U420y5rxcnCDFTDY5Y7ElDbKdUNbusnEz8zLHt3bZlUaDqDQfLigkgycwdp4iWbRlvr2DV3P2bTeJlwipaNqNTughdK
http://jingyan.baidu.com/article/77b8dc7f2af94e6174eab6a2.html