PostgreSQL支持全文检索,其内置的缺省的分词解析器采用空格分词。因为中文的词语之间没有空格分割,所以这种方法并不适用于中文。要支持中文的全文检索需要额外的中文分词插件。网上查了下,可以给PG用的开源中文分词插件有两个:nlpbamboo和zhparser。但是nlpbamboo是托管在googlecode上的,而googlecode被封了,下载不方便。下面尝试采用zhparser进行中文的全文检索。
zhparser是基于Simple Chinese Word Segmentation(SCWS)中文分词库实现的一个PG扩展,作者是 amutu,源码URL为。
1. 安装
1.1 下载SCWS
1.2 编译和安装SCWS
tar xvf scws-1.2.2.tar.bz2
cd scws-1.2.2
./configure
make install
1.3 下载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命令可接受的参数。
-
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)上。
-
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()函数设置这个选项。
-
10. `void scws_set_duality(scws_t s, int yes)` 设定是否将闲散文字自动以二字分词法聚合。
-
-
> **参数 yes** 如果为 1 表示执行二分聚合,0 表示不处理,缺省为 0。
但是二元切分也有缺点,会产生歧义词和无意义的词。而且如果这些
连续的闲散单字真的是单字的话,二字聚合后就不能再做单字匹配了。
4. 总结
zhparser的安装和配置非常容易,分词效果也不错,可以满足一般的场景。如果有更高的要求需要做一些定制。
5. 参考
postgresql之全文搜索篇
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://www.cnblogs.com/lvpei/archive/2010/08/04/1792409.html
http://blog.2ndquadrant.com/text-search-strategies-in-postgresql/
阅读(22562) | 评论(0) | 转发(0) |