本文引用自: http://blog.chinaunix.net/uid-20726500-id-4820580.html

防止文章丢失才进行复制

 

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

    相关文章:

    • 2021-12-30
    • 2022-12-23
    • 2022-01-07
    • 2021-06-17
    • 2021-07-13
    • 2022-12-23
    猜你喜欢
    • 2022-12-23
    • 2022-12-23
    • 2022-12-23
    • 2021-09-11
    • 2021-05-31
    • 2022-02-13
    • 2022-03-07
    相关资源
    相似解决方案