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
PostgreSQL的全文检索插件zhparser的中文分词效果
原文:http://blog.chinaunix.net/uid-20726500-id-4820580.html