在項目中如果要實現(xiàn)全文檢索,最普通的方法就是通過數(shù)據(jù)庫查詢語句like '%keywords%'妒御,但是這種方法在數(shù)據(jù)量多的情況下效率很低镇饺。目前最主流的方法是集成一個搜索引擎,通過調(diào)用相關(guān)API來實現(xiàn)創(chuàng)建索引以及搜索的功能惋啃。目前比較常見的開源搜索軟件:Lucene监右、Solr、ElasticSearch绒瘦、Sphinx、CoreSeek惰帽。
對于一個比較大型的項目來說,集成一個搜索引擎是必須的券册。但是對于一些比較小型的項目垂涯,不想使用like語句航邢,集成一個搜索引擎又需要很大的成本。如果該項目的數(shù)據(jù)庫是PostgreSQL操骡,那么就可以使用PostgreSQL的全文檢索功能赚窃。不過PostgreSQL的全文檢索功能效率是比不上那些開源搜索軟件的。
遺憾的是是掰,PostgreSQL默認(rèn)沒有中文分詞功能辱匿,因此要實現(xiàn)PostgreSQL的中文分詞功能必須使用擴(kuò)展插件。我通過百度找到了兩種中文分詞插件匾七,pg_jieba和Zhparser。pg_jieba是基于結(jié)巴中文分詞的丁频,zhparser是基于SCWS邑贴。
pg_jieba:https://github.com/jaiminpan/pg_jieba
Zhparser:https://github.com/amutu/zhparser
測試過程中發(fā)現(xiàn)pg_jieba和Zhparser都能夠比較方便的實現(xiàn)全文檢索的功能。
不過碰到特殊的分詞會出現(xiàn)問題胁勺,比如數(shù)據(jù)庫里的公司地址字段會出現(xiàn)浙江省余杭市独旷,浙江省余杭區(qū)寥裂,浙江省余杭這種情況案疲,分詞的結(jié)果分別是“余杭市”、“余杭區(qū)”以及“余杭”诺舔。搜索“余杭”關(guān)鍵字只會出現(xiàn)浙江省余杭的相關(guān)數(shù)據(jù)备畦,余杭市和余杭區(qū)的數(shù)據(jù)并不會出現(xiàn)。這種情況褥赊,pg_jieba可以通過使用自定義的分詞詞典來解決莉恼,Zhparser也有相關(guān)功能。但是構(gòu)建分詞詞典比較麻煩尿背,有好多分詞結(jié)果需要處理捶惜。我只是想實現(xiàn)一個簡單的全文檢索功能,幸好Zhparser有相關(guān)的參數(shù)進(jìn)行配置坞淮。
另外個問題就是標(biāo)點符號對分詞結(jié)果的影響陪捷。pg_jieba的分詞結(jié)果是不受標(biāo)點符號影響的,而Zhparser會受到嚴(yán)重影響啡直。
德哥 PostgreSQL 如何高效解決 按任意字段分詞檢索的問題 - case1
德哥的這篇文章中有寫關(guān)于SCWS分詞的問題苍碟,逗號會對分詞結(jié)果產(chǎn)生影響,文中采用的方法是使用replace函數(shù)將逗號替換成空格微峰。實際測試中蜓肆,我發(fā)現(xiàn)+ - . \ ;等符號也會對分詞結(jié)果產(chǎn)生影響谋币。由于公司有的表的詳情字段是通過富文本編輯器進(jìn)行編輯的症概,難免會出現(xiàn)各種標(biāo)點符號,所以這種替換的方法就不可取了诅蝶,幸好Zhparser有相關(guān)參數(shù)配置能夠忽略標(biāo)點符號的影響募壕。
再加上Zhparser的star數(shù)比pg_jieba的要高(從眾心理不可取)筐眷,所以我最后選用了Zhparser來進(jìn)行中文分詞习柠。
集成Zhparser中文分詞插件
1.安裝中文分詞插件SCWS和Zhparser
SCWS:https://github.com/hightman/scws
Zhparser:https://github.com/amutu/zhparser
2.安裝完成之后照棋,執(zhí)行以下三條sql語句來啟用中文分詞,生成一個叫testzhcfg的解釋器溶锭。
CREATE EXTENSION zhparser;
CREATE TEXT SEARCH CONFIGURATION testzhcfg(PARSER=zhparser);
ALTER TEXT SEARCH CONFIGURATION testzhcfg ADD MAPPING FOR n,v,a,i,e,l WITH simple;
3.通過下面這條語句設(shè)定分詞執(zhí)行時針對長詞進(jìn)行復(fù)合切分符隙,比如余杭區(qū)分詞時會被分成“余杭”和“余杭區(qū)”,不設(shè)置這條語句則分詞結(jié)果只有“余杭區(qū)”拱绑。
alter role all set zhparser.multi_short=on;
通過下面這條語句來使分詞時忽略標(biāo)點符號的影響丽蝎。
alter role all set zhparser.punctuation_ignore=on;
有兩種方法進(jìn)行全文檢索,第一種是在搜索的時候進(jìn)行分詞操作红省。第二種是將分詞的結(jié)果存儲到新增的列當(dāng)中国觉,通過觸發(fā)器更新這個字段,然后在該字段上建索引痕寓。第一種方法的優(yōu)點是創(chuàng)建索引簡單,占用空間少厂抽,缺點是每次執(zhí)行查詢都需要調(diào)用to_tsvector函數(shù)來確保索引值關(guān)聯(lián)筷凤。第二種方法的優(yōu)點是查詢的速度快(無需每次調(diào)用to_tsvector函數(shù)),缺點是需要新增一個列藐守,消耗更多的存儲空間卢厂。
以zl_company表為例
第一種方法
create index idx_zl_company on zl_company using gin(to_tsvector('testzhcfg',coalesce(name,'')||coalesce(address,'')));//創(chuàng)建gin索引
使用函數(shù)coalesce來確保字段為NULL的也可以建立索引。該語句對name和address字段進(jìn)行索引慎恒,也可以擴(kuò)展別的字段融柬。
explain analyse select * from zl_company where to_tsvector('testzhcfg',coalesce(name,'')||coalesce(address,'')) @@ to_tsquery('testzhcfg','余杭');
通過這條語句就可以查詢“余杭”關(guān)鍵字對應(yīng)的結(jié)果,如果有多個關(guān)鍵字粒氧,比如要查詢余杭的公司可以通過to_tsquery('testzhcfg','余杭&公司')外盯。
第二種方法
alter table zl_company add column tsv tsvector;//新建字段類型是tsvector
update zl_company set tsv = to_tsvector('testzhcfg',coalesce(name,'')||coalesce(address,''));//更新該字段
create index idx_zl_company on zl_company using gin(tsv);//創(chuàng)建gin索引
explain analyse select * from zl_company where tsv @@ to_tsquery('testzhcfg','余杭');
還需要創(chuàng)建一個觸發(fā)器來更新tsv字段的值
create trigger tsvectorupdate before insert or update
on zl_company for each row execute procedure
tsvector_update_trigger(tsv, 'testzhcfg', name, address);
與like的效率對比
以1000萬條數(shù)據(jù)為例
測試之后發(fā)現(xiàn)不管搜索的關(guān)鍵字是什么饱苟,like的搜索速度比較穩(wěn)定,而PostgreSQL中文檢索的搜索速度跟該搜索關(guān)鍵字的數(shù)據(jù)量有很大的關(guān)系肋殴。有個很尷尬的情況坦弟,這是我從正式庫里復(fù)制的一個數(shù)據(jù)庫表,里面加了將進(jìn)1000萬條相同的浙江某公司測試數(shù)據(jù)烙懦,只有幾十萬條的數(shù)據(jù)是正式數(shù)據(jù)赤炒,分詞的結(jié)果反而比like還要慢亏较,正式的數(shù)據(jù)也不大可能會出現(xiàn)1000萬條分詞相同的數(shù)據(jù)掩缓。如果真的出現(xiàn)了,我想總數(shù)據(jù)量應(yīng)該都上億了巡通,這種時候不能再在代碼層面上進(jìn)行優(yōu)化了舍哄,應(yīng)該對數(shù)據(jù)庫進(jìn)行分表分庫等。
采用第二種方法后速度有明顯提升弥锄。
如果PostgreSQL的版本為9.6以上籽暇,還能采用rum索引饥追,速度比gin索引更快。詳情看:PostgreSQL 全文檢索加速 快到?jīng)]有朋友 - RUM索引接口(潘多拉魔盒)
為Zhparser添加自定義中文分詞詞典的可以看:如何使用中文分詞和自定義中文分詞詞典