天天看點

PostgreSQL的全文檢索插件zhparser的中文分詞效果

PostgreSQL支援全文檢索,其内置的預設的分詞解析器采用空格分詞。因為中文的詞語之間沒有空格分割,是以這種方法并不适用于中文。要支援中文的全文檢索需要額外的中文分詞插件。網上查了下,可以給PG用的開源中文分詞插件有兩個:nlpbamboo和zhparser。但是nlpbamboo是托管在googlecode上的,而googlecode被封了,下載下傳不友善。下面嘗試采用zhparser進行中文的全文檢索。

zhparser是基于Simple Chinese Word Segmentation(SCWS)中文分詞庫實作的一個PG擴充,作者是 amutu,源碼URL為https://github.com/amutu/zhparser。

http://www.xunsearch.com/scws/down/scws-1.2.2.tar.bz2

tar xvf scws-1.2.2.tar.bz2

cd scws-1.2.2

./configure

make install

https://github.com/amutu/zhparser/archive/master.zip

確定PostgreSQL的二進制指令路徑在PATH下,然後解壓并進入zhparser目錄後,編譯安裝zhparser。

SCWS_HOME=/usr/local make && make install

連接配接到目标資料庫進行中文全文檢索的配置

點選(此處)折疊或打開

-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)

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

CREATE TEXT SEARCH CONFIGURATION

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映射,以實作屏蔽詞和同義詞歸并等功能。

testdb=# select to_tsvector('testzhcfg','南京市長江大橋');

       to_tsvector

-------------------------

 '南京市':1 '長江大橋':2

(1 row)

中文分詞有最大比對,最細粒度等各種常用算法。上面的分詞結果沒有把'長江大橋'拆成'長江'和'大橋'兩個詞,是以SCWS估計是采取的最大比對的分詞算法。

分詞算法的優劣一般通過3個名額衡量。

效率:

  索引和查詢的效率

召回率:

  提取出的正确資訊條數 /  樣本中的資訊條數 

準确率:

  提取出的正确資訊條數 /  提取出的資訊條數

分詞的粒度越粗,效率越高,但遺漏的可能性也會高一點,即召回率受影響。具體到上面的例子,用'南京&大橋'就沒法比對到。

testdb=# select to_tsvector('testzhcfg','南京市長江大橋') @@ '南京&大橋';

 ?column?

----------

 f

效率,召回率和準确率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 "南京市長江大橋"

南京市 南京 長江大橋 長江 大橋 

| 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 "南京市長江大橋"

南京市 南京 市 長江大橋 長江 大橋 江 橋 

這樣切分後,"南京 & 橋"也可以比對。

再變态一點,對短詞和所有單字做複合切分。

[root@hanode1 zhparser-0.1.4]# scws -c utf8  -d dict.utf8.xdb  -r rules.utf8.ini -M 9 "南京市長江大橋"

南京市 南京 南 京 市 長江大橋 長江 大橋 長 江 大 橋 

這樣切分基本上可以不再遺漏比對了,但是效率肯定受影響。

上面的選項是加在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,可以看到效果。

                               to_tsvector

-------------------------------------------------------------------------

 '南京':2 '南京市':1 '大橋':6 '市':3 '橋':8 '江':7 '長江':5 '長江大橋':4

testdb=# select to_tsvector('testzhcfg','南京市長江大橋') @@ '南京 & 橋';

 t

tsquery也會被複合切分:

testdb=# select to_tsquery('testzhcfg','南京市長江大橋');

                              to_tsquery

-----------------------------------------------------------------------

 '南京市' & '南京' & '市' & '長江大橋' & '長江' & '大橋' & '江' & '橋'

這可能不是我們需要的,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)

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

Time: 11.586 ms

要回避這個問題可以做兩套解析器,一套給tsvector用做複合切分;一套給tsquery用,不做複合切分。或者像上面測試例子中那樣不對查詢字元串做分詞,由應用端直接輸入tsquery(不過這樣做會有别的問題,後面會提到)。

無意中發現一個奇怪的現象,'南大'被無視了:

testdb=# select to_tsvector('testzhcfg','南大') ;

 to_tsvector

-------------

'北大','東大'甚至'西大'都沒問題:

testdb=# select to_tsvector('testzhcfg','南大 北大 東大 西大') ;

        to_tsvector

----------------------------

 '東大':2 '北大':1 '西大':3

調查發現原因在于它們被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;

             to_tsvector

-------------------------------------

 '東大':3 '北大':2 '南大':1 '西大':4

詞典收錄的詞畢竟有限,遇到新詞就不認識了。不斷完善詞典可以緩解這個問題,但不能從根本上避免。

'微信'沒有被識别出來:

testdb=# select to_tsvector('testzhcfg','微信');

  to_tsvector

---------------

 '信':2 '微':1

testdb=# select to_tsvector('testzhcfg','微信') @@ '微信';

雖然這個詞沒有被識别出來,但是我們隻要對tsquery采用相同分詞方法,就可以比對。

testdb=# select to_tsvector('testzhcfg','微信') @@ to_tsquery('testzhcfg','微信');

但是,利用拆開的單字做比對,檢索的效率肯定不會太好。SCWS還提供了一種解決方法(-U),可以對連續的閑散單字做二進制切分。

[root@hanode1 zhparser-0.1.4]# scws -c utf8  -d dict.utf8.xdb  -r rules.utf8.ini -U "微信微網誌"

微信 信微 微網誌

| TextLen:   12                  |

| 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。

但是二進制切分也有缺點,會産生歧義詞和無意義的詞。而且如果這些連續的閑散單字真的是單字的話,二字聚合後就不能再做單字比對了。

zhparser的安裝和配置非常容易,分詞效果也不錯,可以滿足一般的場景。如果有更高的要求需要做一些定制。

<a href="http://amutu.com/blog/wp-content/uploads/2013/05/postgresql%E4%B9%8B%E5%85%A8%E6%96%87%E6%90%9C%E7%B4%A2%E7%AF%87.pdf">postgresql之全文搜尋篇</a>

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

繼續閱讀