PostgreSQL的全文检索插件zhparser的中文分词效果

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扩展

点击(此处)折叠或打开

  1. -bash-4.1$ psql testdb
  2. psql (9.4.0)
  3. Type “help” for help.
  4. testdb=# create extension zhparser;
  5. CREATE EXTENSION

安装zhparser扩展后多一个叫“zhparser”的解析器

点击(此处)折叠或打开

  1. testdb=# \dFp
  2.          List of text search parsers
  3.    Schema | Name | Description
  4. ————+———-+———————
  5.  pg_catalog | default | default word parser
  6.  public | zhparser |
  7. (2 rows)

zhparser可以将中文切分成下面26种token
点击(此处)折叠或打开

  1. testdb=# select ts_token_type(‘zhparser’);
  2.               ts_token_type
  3. —————————————–
  4.  (97,a,adjective)
  5.  (98,b,”differentiation (qu bie)”)
  6.  (99,c,conjunction)
  7.  (100,d,adverb)
  8.  (101,e,exclamation)
  9.  (102,f,”position (fang wei)”)
  10.  (103,g,”root (ci gen)”)
  11.  (104,h,head)
  12.  (105,i,idiom)
  13.  (106,j,”abbreviation (jian lue)”)
  14.  (107,k,head)
  15.  (108,l,”tmp (lin shi)”)
  16.  (109,m,numeral)
  17.  (110,n,noun)
  18.  (111,o,onomatopoeia)
  19.  (112,p,prepositional)
  20.  (113,q,quantity)
  21.  (114,r,pronoun)
  22.  (115,s,space)
  23.  (116,t,time)
  24.  (117,u,auxiliary)
  25.  (118,v,verb)
  26.  (119,w,”punctuation (qi ta biao dian)”)
  27.  (120,x,unknown)
  28.  (121,y,”modal (yu qi)”)
  29.  (122,z,”status (zhuang tai)”)
  30. (26 rows)

2.2 创建使用zhparser作为解析器的全文搜索的配置

点击(此处)折叠或打开

  1. testdb=# CREATE TEXT SEARCH CONFIGURATION testzhcfg (PARSER = zhparser);
  2. CREATE TEXT SEARCH CONFIGURATION

2.3 往全文搜索配置中增加token映射

点击(此处)折叠或打开

  1. testdb=# ALTER TEXT SEARCH CONFIGURATION testzhcfg ADD MAPPING FOR n,v,a,i,e,l WITH simple;
  2. ALTER TEXT SEARCH CONFIGURATION

上面的token映射只映射了名词(n),动词(v),形容词(a),成语(i),叹词(e)和习用语(l)6种,这6种以外的token全部被屏蔽。词典使用的是内置的simple词典,即仅做小写转换。根据需要可以灵活定义词典和token映射,以实现屏蔽词和同义词归并等功能。

3.中文分词测试

点击(此处)折叠或打开

  1. testdb=# select to_tsvector(‘testzhcfg’,’南京市长江大桥’);
  2.        to_tsvector
  3. ————————-
  4.  ‘南京市’:1 ‘长江大桥’:2
  5. (1 row)

中文分词有最大匹配,最细粒度等各种常用算法。上面的分词结果没有把’长江大桥’拆成’长江’和’大桥’两个词,所以SCWS估计是采取的最大匹配的分词算法。
分词算法的优劣一般通过3个指标衡量。
效率:
  索引和查询的效率
召回率:
  提取出的正确信息条数 /  样本中的信息条数 
准确率:
  提取出的正确信息条数 /  提取出的信息条数
分词的粒度越粗,效率越高,但遗漏的可能性也会高一点,即召回率受影响。具体到上面的例子,用’南京&大桥’就没法匹配到。

点击(此处)折叠或打开

  1. testdb=# select to_tsvector(‘testzhcfg’,’南京市长江大桥’) @@ ‘南京&大桥’;
  2.  ?column?
  3. ———-
  4.  f
  5. (1 row)

效率,召回率和准确率3个指标往往不能兼顾,所以不能笼统的说最大匹配好还是不好。但是如果特别在乎召回率,SCWS也提供了一些选项进行调节。下面是scws命令可接受的参数。
http://www.xunsearch.com/scws/docs.php#utilscws

点击(此处)折叠或打开

  1. 1. **$prefix/bin/scws** 这是分词的命令行工具,执行 scws -h 可以看到详细帮助说明。
  2. “`
  3. Usage: scws [options] [[-i] input] [[-o] output]
  4. “`
  5. * _-i string|file_ 要切分的字符串或文件,如不指定则程序自动读取标准输入,每输入一行执行一次分词
  6. * _-o file_ 切分结果输出保存的文件路径,若不指定直接输出到屏幕
  7. * _-c charset_ 指定分词的字符集,默认是 gbk,可选 utf8
  8. * _-r file_ 指定规则集文件(规则集用于数词、数字、专有名字、人名的识别)
  9. * _-d file[:file2[:…]]_ 指定词典文件路径(XDB格式,请在 -c 之后使用)
  10. “`
  11. 自 1.1.0 起,支持多词典同时载入,也支持纯文本词典(必须是.txt结尾),多词典路径之间用冒号(:)隔开,
  12. 排在越后面的词典优先级越高。
  13. 文本词典的数据格式参见 scws-gen-dict 所用的格式,但更宽松一些,允许用不定量的空格分开,只有<词>是必备项目,
  14. 其它数据可有可无,当词性标注为“!”(叹号)时表示该词作废,即使在较低优先级的词库中存在该词也将作废。
  15. “`
  16. * _-M level_ 复合分词的级别:1~15,按位异或的 1|2|4|8 依次表示 短词|二元|主要字|全部字,缺省不复合分词。
  17. * _-I_ 输出结果忽略跳过所有的标点符号
  18. * _-A_ 显示词性
  19. * _-E_ 将 xdb 词典读入内存 xtree 结构 (如果切分的文件很大才需要)
  20. * _-N_ 不显示切分时间和提示
  21. * _-D_ debug 模式 (很少用,需要编译时打开 –enable-debug)
  22. * _-U_ 将闲散单字自动调用二分法结合
  23. * _-t num_ 取得前 num 个高频词
  24. * _-a [~]attr1[,attr2[,…]]_ 只显示某些词性的词,加~表示过滤该词性的词,多个词性之间用逗号分隔
  25. * _-v_ 查看版本

通过-M指定短词的复合分词,可以得到细粒度的分词。
默认是最大匹配:

点击(此处)折叠或打开

  1. [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)     |
    +——————————–+

指定短词的复合分词,可以对长词再进行复合切分。

点击(此处)折叠或打开

  1. [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)     |
    +——————————–+

这样切分后”南京 & 大桥”也可以匹配。

甚至可以把重要的单字也切出来。

点击(此处)折叠或打开

  1. [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)     |
    +——————————–+

这样切分后,”南京 & 桥”也可以匹配。

再变态一点,对短词和所有单字做复合切分。

点击(此处)折叠或打开

  1. [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:

点击(此处)折叠或打开

  1. 9. `void scws_set_multi(scws_t s, int mode)` 设定分词执行时是否执行针对长词复合切分。(例:“中国人”分为“中国”、“人”、“中国人”)。
  2.    > **参数 mode** 复合分词法的级别,缺省不复合分词。取值由下面几个常量异或组合:
  3.    >
  4.    > – SCWS_MULTI_SHORT 短词
  5.    > – SCWS_MULTI_DUALITY 二元(将相邻的2个单字组合成一个词)
  6.    > – SCWS_MULTI_ZMAIN 重要单字
  7.    > – SCWS_MULTI_ZALL 全部单字

修改zhparser.c,追加scws_set_multi()的调用
zhparser.c:

点击(此处)折叠或打开

  1. static void init(){
  2.         char sharepath[MAXPGPATH];
  3.         char * dict_path,* rule_path;
  4.         if (!(scws = scws_new())) {
  5.                 ereport(ERROR,
  6.                                 (errcode(ERRCODE_INTERNAL_ERROR),
  7.                                  errmsg(“Chinese Parser Lib SCWS could not init!\”%s\””,””
  8.                                        )));
  9.         }
  10.         get_share_path(my_exec_path, sharepath);
  11.         dict_path = palloc(MAXPGPATH);
  12.         snprintf(dict_path, MAXPGPATH, “%s/tsearch_data/%s.%s”,
  13.                         sharepath, “dict.utf8”, “xdb”);
  14.         scws_set_charset(scws, “utf-8”);
  15.         scws_set_dict(scws,dict_path, SCWS_XDICT_XDB);
  16.         rule_path = palloc(MAXPGPATH);
  17.         snprintf(rule_path, MAXPGPATH, “%s/tsearch_data/%s.%s”,
  18.                         sharepath, “rules.utf8”, “ini”);
  19.         scws_set_rule(scws ,rule_path);
  20.         scws_set_multi(scws ,SCWS_MULTI_SHORT|SCWS_MULTI_ZMAIN);//追加代码
  21. }

重新编译安装zhparser后,再restart PostgreSQL,可以看到效果。

点击(此处)折叠或打开

  1. testdb=# select to_tsvector(‘testzhcfg’,’南京市长江大桥’);
  2.                                to_tsvector
  3. ————————————————————————-
  4.  ‘南京’:2 ‘南京市’:1 ‘大桥’:6 ‘市’:3 ‘桥’:8 ‘江’:7 ‘长江’:5 ‘长江大桥’:4
  5. (1 row)
  6. testdb=# select to_tsvector(‘testzhcfg’,’南京市长江大桥’) @@ ‘南京 & 桥’;
  7.  ?column?
  8. ———-
  9.  t
  10. (1 row)

tsquery也会被复合切分:

点击(此处)折叠或打开

  1. testdb=# select to_tsquery(‘testzhcfg’,’南京市长江大桥’);
  2.                               to_tsquery
  3. ———————————————————————–
  4.  ‘南京市’ & ‘南京’ & ‘市’ & ‘长江大桥’ & ‘长江’ & ‘大桥’ & ‘江’ & ‘桥’
  5. (1 row)

这可能不是我们需要的,tsquery切的太细会影响查询效率。做了个简单的测试,走gin索引,按这个例子对tsquery复合切分会比默认的最大切分慢了1倍。

点击(此处)折叠或打开

  1. testdb=# \d tb1
  2.     Table “public.tb1”
  3.  Column | Type | Modifiers
  4. ——–+——+———–
  5.  c1 | text |
  6. Indexes:
  7.     “tb1idx1” gin (to_tsvector(‘testzhcfg’::regconfig, c1))
  8. testdb=# insert into tb1 select ‘南京市长江大桥’ from generate_series(1,10000,1);
  9. testdb=# explain analyze select count(*) from tb1 where to_tsvector(‘testzhcfg’, c1) @@ ‘南京市 & 长江大桥’::tsquery;
  10.                                                            QUERY PLAN
  11. ——————————————————————————————————————————–
  12.  Aggregate (cost=348.53..348.54 rows=1 width=0) (actual time=6.077..6.077 rows=1 loops=1)
  13.    -> Bitmap Heap Scan on tb1 (cost=109.51..323.53 rows=10001 width=0) (actual time=3.186..4.917 rows=10001 loops=1)
  14.          Recheck Cond: (to_tsvector(‘testzhcfg’::regconfig, c1) @@ ”’南京市” & ”长江大桥”’::tsquery)
  15.          Heap Blocks: exact=64
  16.          -> Bitmap Index Scan on tb1idx1 (cost=0.00..107.01 rows=10001 width=0) (actual time=3.154..3.154 rows=10001 loops=1)
  17.                Index Cond: (to_tsvector(‘testzhcfg’::regconfig, c1) @@ ”’南京市” & ”长江大桥”’::tsquery)
  18.  Planning time: 0.117 ms
  19.  Execution time: 6.127 ms
  20. (8 rows)
  21. Time: 6.857 ms
  22. testdb=# explain analyze select count(*) from tb1 where to_tsvector(‘testzhcfg’, c1) @@ ‘南京市 & 南京 & 市 & 长江大桥 & 长江 & 大桥 & 江 & 桥’::tsquery;
  23.                                                                                QUERY PLAN
  24. ————————————————————————————————————————————————
  25. ————————-
  26.  Aggregate (cost=396.53..396.54 rows=1 width=0) (actual time=10.823..10.823 rows=1 loops=1)
  27.    -> Bitmap Heap Scan on tb1 (cost=157.51..371.53 rows=10001 width=0) (actual time=7.923..9.631 rows=10000 loops=1)
  28.          Recheck Cond: (to_tsvector(‘testzhcfg’::regconfig, c1) @@ ”’南京市” & ”南京” & ”市” & ”长江大桥” & ”长江” & ”大桥” & ”江”
  29.  & ”桥”’::tsquery)
  30.          Heap Blocks: exact=64
  31.          -> Bitmap Index Scan on tb1idx1 (cost=0.00..155.01 rows=10001 width=0) (actual time=7.885..7.885 rows=10000 loops=1)
  32.                Index Cond: (to_tsvector(‘testzhcfg’::regconfig, c1) @@ ”’南京市” & ”南京” & ”市” & ”长江大桥” & ”长江” & ”大桥” & ”
  33. 江” & ”桥”’::tsquery)
  34.  Planning time: 0.111 ms
  35.  Execution time: 10.879 ms
  36. (8 rows)
  37. Time: 11.586 ms

要回避这个问题可以做两套解析器,一套给tsvector用做复合切分;一套给tsquery用,不做复合切分。或者像上面测试例子中那样不对查询字符串做分词,由应用端直接输入tsquery(不过这样做会有别的问题,后面会提到)。

3.其它问题

3.1 ‘南大’被无视了

无意中发现一个奇怪的现象,’南大’被无视了:

点击(此处)折叠或打开

  1. testdb=# select to_tsvector(‘testzhcfg’,’南大’) ;
  2.  to_tsvector
  3. ————-
  4. (1 row)

‘北大’,’东大’甚至’西大’都没问题:

点击(此处)折叠或打开

  1. testdb=# select to_tsvector(‘testzhcfg’,’南大 北大 东大 西大’) ;
  2.         to_tsvector
  3. —————————-
  4.  ‘东大’:2 ‘北大’:1 ‘西大’:3
  5. (1 row)

调查发现原因在于它们被SCWS解析出来的token类型不同:

点击(此处)折叠或打开

  1. testdb=# select ts_debug(‘testzhcfg’,’南大 北大 东大 西大’) ;
  2.                 ts_debug
  3. —————————————–
  4.  (j,”abbreviation (jian lue)”,南大,{},,)
  5.  (n,noun,北大,{simple},simple,{北大})
  6.  (n,noun,东大,{simple},simple,{东大})
  7.  (n,noun,西大,{simple},simple,{西大})
  8. (4 rows)

‘南大’被识别为j(简略词),而之前并没有为j创建token映射。现在加上j的token映射,就可以了。

点击(此处)折叠或打开

  1. testdb=# ALTER TEXT SEARCH CONFIGURATION testzhcfg ADD MAPPING FOR j WITH simple;
  2. ALTER TEXT SEARCH CONFIGURATION
  3. testdb=# select to_tsvector(‘testzhcfg’,’南大 北大 东大 西大’) ;
  4.              to_tsvector
  5. ————————————-
  6.  ‘东大’:3 ‘北大’:2 ‘南大’:1 ‘西大’:4
  7. (1 row)

3.2 新词的识别

词典收录的词毕竟有限,遇到新词就不认识了。不断完善词典可以缓解这个问题,但不能从根本上避免。
‘微信’没有被识别出来:

点击(此处)折叠或打开

  1. testdb=# select to_tsvector(‘testzhcfg’,’微信’);
  2.   to_tsvector
  3. —————
  4.  ‘信’:2 ‘微’:1
  5. (1 row)

  6. testdb=# select to_tsvector(‘testzhcfg’,’微信’) @@ ‘微信’;
  7.  ?column?
  8. ———-
  9.  f
  10. (1 row)

虽然这个词没有被识别出来,但是我们只要对tsquery采用相同分词方法,就可以匹配。

点击(此处)折叠或打开

  1. testdb=# select to_tsvector(‘testzhcfg’,’微信’) @@ to_tsquery(‘testzhcfg’,’微信’);
  2.  ?column?
  3. ———-
  4.  t
  5. (1 row)

但是,利用拆开的单字做匹配,检索的效率肯定不会太好。SCWS还提供了一种解决方法(-U),可以对连续的闲散单字做二元切分。

点击(此处)折叠或打开

  1. [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

点击(此处)折叠或打开

  1. 10. `void scws_set_duality(scws_t s, int yes)` 设定是否将闲散文字自动以二字分词法聚合。
  2.    > **参数 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

Leave a Reply