從一個(gè)模糊詞查詢需求的處理方案討論到一種極速匹配方案的實(shí)現(xiàn)

背景

某天,我們的產(chǎn)品經(jīng)理突然找到我拧额,說(shuō)我們的廣告業(yè)務(wù)上線后效果不錯(cuò),但是需要做敏感詞過(guò)濾處理玉凯,需要接入一個(gè)模糊詞詞典和一個(gè)精確詞詞典势腮。然后我拿到了這兩份詞典,兩份違禁詞加起來(lái)總量近100w條漫仆。

這個(gè)需求簡(jiǎn)單來(lái)說(shuō)就是如果用戶的查詢?cè)~中命中了違禁詞的話是不能出廣告的捎拯。如:用戶query=怎么去故宮博物院,因?yàn)槊辛斯蕦m博物院,所以不能出廣告署照。 最終我合并了這兩份詞表祸泪,并寫(xiě)了一個(gè)高效的實(shí)現(xiàn)。

今天打算就其中的模糊匹配部分單獨(dú)抽出來(lái)討論一下其實(shí)現(xiàn)方案建芙,并開(kāi)源了一個(gè)我自己用go寫(xiě)的開(kāi)箱即用的高性能匹配方案倉(cāng)庫(kù)地址没隘。

理論基礎(chǔ)

如果想要開(kāi)箱即用的實(shí)現(xiàn),可以跳過(guò)理論拉到最下面禁荸。

對(duì)于一個(gè)web應(yīng)用來(lái)說(shuō)右蒲,這種大量的字符串,一個(gè)一個(gè)匹配肯定是不行的赶熟。 肯定是需要高性能的匹配方案瑰妄。

使用字典樹(shù)(trie樹(shù))進(jìn)行匹配是一個(gè)方案, 但是達(dá)不到高性能映砖。

因?yàn)閷?duì)于模糊匹配來(lái)說(shuō)间坐,使用字典樹(shù)需要不斷回退。

如:我們的字典里有下面這兩個(gè)詞:

怎么去天安門(mén)

故宮博物院

query=怎么去故宮博物院邑退。

字典樹(shù)匹配過(guò)程如下:

第一次匹配:q=怎么去故宮博物院

怎->么->去->故(fail)

到 故 這個(gè)詞失敗后進(jìn)行回退竹宋,回退到第二個(gè)詞進(jìn)行匹配

第二次匹配:q=么去故宮博物院

么(fail)

回退到第三個(gè)詞

第三次匹配:q=去故宮博物院

去(fail)

回退到第四個(gè)詞

第四次匹配:q=故宮博物院

故->宮->博->物->院(success)

匹配完成。

可以看到如果對(duì)于查詢?cè)~比較多的情況下地技,不斷回退導(dǎo)致了性能下降蜈七。如果剛好又遇到跟字典中命中了大量前綴,更是一個(gè)災(zāi)難莫矗。

那如何做到高性能呢宪潮?這時(shí)需要用到Aho–Corasick算法

該算法在1975年產(chǎn)生于貝爾實(shí)驗(yàn)室,是著名的多模匹配算法趣苏。其本質(zhì)是一個(gè)自動(dòng)機(jī)狡相,下面我們稱為AC自動(dòng)機(jī)。

我們先看下其特點(diǎn)食磕。

AC自動(dòng)機(jī):

1.高速完成多模式匹配而不用回溯尽棕,與模式串的數(shù)量和長(zhǎng)度無(wú)關(guān),只與查詢文本長(zhǎng)度有關(guān)彬伦,時(shí)間復(fù)雜度O(n)滔悉。

2.基礎(chǔ)結(jié)構(gòu)為trie樹(shù)。

3.KMP的思想:為trie樹(shù)所有節(jié)點(diǎn)建立fail指針单绑。與KMP不同的是:AC的fail指針是指向最長(zhǎng)后綴回官。

基本構(gòu)造:

goto表:成功轉(zhuǎn)移到另一個(gè)狀態(tài)

failure表:不可成功轉(zhuǎn)移時(shí),則使用該表轉(zhuǎn)移到下一個(gè)狀態(tài)

output表:命中的模式串

查詢:

在goto表里查找搂橙,如果不能成功轉(zhuǎn)移到下個(gè)狀態(tài)歉提,就通過(guò)failure表轉(zhuǎn)移,在每步轉(zhuǎn)移的同時(shí),判斷output是否可輸出

構(gòu)建方法:

goto表:trie樹(shù)

output表:trie樹(shù)葉子節(jié)點(diǎn)+在fail表構(gòu)建時(shí)的可輸出節(jié)點(diǎn)

failure表:逐層構(gòu)建

? ? 1)根節(jié)點(diǎn)的fail為0苔巨,深度為1的節(jié)點(diǎn)所有的fail=0

? ? 2)深度>=2的節(jié)點(diǎn)N版扩,從其父節(jié)點(diǎn)Pn轉(zhuǎn)移到N需要接收字符C,則測(cè)試goto(fail(Pn),C)侄泽,如果成功礁芦,則fail(N)=goto(fail(Pn),C)

? ? 3)如果失敗,則測(cè)試goto(fail(fail(Pn)),C)悼尾,如此反復(fù)柿扣,肯定可以找到一個(gè)有效狀態(tài)(最終往上到根節(jié)點(diǎn)時(shí),如果轉(zhuǎn)移失敗闺魏,則fail(N)=0)窄刘。

以經(jīng)典的ushers為例,模式串是he/ she/ his /hers舷胜。構(gòu)建的自動(dòng)機(jī)如圖:

AC自動(dòng)機(jī)的基礎(chǔ)是trie樹(shù),trie樹(shù)的實(shí)現(xiàn)決定了AC自動(dòng)機(jī)的效率高低, 我們看下常用的兩種實(shí)現(xiàn)活翩。 Hash trie樹(shù) VS Double Array Trie樹(shù)

Hash trie

實(shí)現(xiàn)起來(lái)相對(duì)簡(jiǎn)單

理想狀態(tài)下取詞復(fù)雜度O(1)

hash空間占用量大烹骨,hash函數(shù)也有性能消耗,數(shù)據(jù)量很大的情況下很難保證O(1)

另外含有大量指針材泄,對(duì)于有g(shù)c的語(yǔ)言來(lái)說(shuō)沮焕,不利于gc

用go來(lái)定義就是:

type node struct {

? ? val? ? ? rune

? ? term? ? bool

? ? depth? ? int

? ? children map[rune]*node

}

Double Array trie

實(shí)現(xiàn)起來(lái)較復(fù)雜

取詞復(fù)雜度O(1)

完全壓縮到兩個(gè)數(shù)組里,去掉了指針拉宗,結(jié)構(gòu)比較簡(jiǎn)單峦树,兼顧了查詢效率和空間存儲(chǔ)

用go來(lái)定義就是:

type doubleArrayTrie struct {

? ? base? []int

? ? check []int

}

我們著重說(shuō)下雙數(shù)組trie樹(shù)。

Double Array Trie

核心:

Base數(shù)組:維持鏈路關(guān)系旦事,存儲(chǔ)轉(zhuǎn)義基數(shù)(即子節(jié)點(diǎn)的索引)魁巩。

Check數(shù)組:和base數(shù)組等長(zhǎng),用來(lái)確認(rèn)轉(zhuǎn)移的正確性姐浮。存儲(chǔ)上一個(gè)狀態(tài)谷遂。

對(duì)于一個(gè)字符串s,當(dāng)前字符狀態(tài)為t卖鲤,增加一個(gè)字符c轉(zhuǎn)義到狀態(tài)tc肾扰。則:

base[t]+c = base[tc]

check[tc] = t

構(gòu)建方式:

1.依次處理每個(gè)詞的每個(gè)字來(lái)構(gòu)建 2.按每個(gè)詞的首字,然后依次處理構(gòu)建

避免沖突移動(dòng)的方法

排序蛋逾,邊構(gòu)建樹(shù)邊構(gòu)建雙數(shù)組集晚,對(duì)于一群兄弟節(jié)點(diǎn)尋找一個(gè)插入點(diǎn),使得這些節(jié)點(diǎn)未被占用区匣。

如果進(jìn)行排序后偷拔,構(gòu)建起來(lái)就會(huì)快很多。使用第二種方式構(gòu)建會(huì)減少?zèng)_突次數(shù)及數(shù)組長(zhǎng)度,加快構(gòu)建時(shí)間条摸,且更易于構(gòu)建悦污。在構(gòu)建Double Array trie時(shí),受到了darts-java開(kāi)源項(xiàng)目的啟發(fā)钉蒲,在此深表感謝

DFS生成樹(shù)切端。

type Node struct {

? ? Code? ? ? ? ? ? ? ? ? ? rune

? ? Left, Right, Depth, Base int

? ? Children? ? ? ? ? ? ? ? []*Node

}

Code:字符編碼

Left,Right為這個(gè)節(jié)點(diǎn)在字典中索引范圍

Depth表示深度

Base為該節(jié)點(diǎn)的轉(zhuǎn)移基數(shù),葉子節(jié)點(diǎn)為-1

base[begin+code]=子節(jié)點(diǎn)的begin

check[begin+code]=begin

構(gòu)建過(guò)程:

? ? 1.令base[0]=1;check[0]=0顷啼,排序踏枣,對(duì)根節(jié)點(diǎn)獲取子節(jié)點(diǎn)

? ? 2.對(duì)每一群兄弟節(jié)點(diǎn),尋找一個(gè)begin值使得數(shù)組未被占用钙蒙,則每個(gè)兄弟節(jié)點(diǎn)的check[begin+code]=begin;

? ? 3.對(duì)兄弟節(jié)點(diǎn)的每個(gè)節(jié)點(diǎn)茵瀑,如果沒(méi)有孩子,則令base為負(fù)值躬厌,否則為該節(jié)點(diǎn)的子節(jié)點(diǎn)的begin值

雙數(shù)組樹(shù)和hash樹(shù)構(gòu)建完成后马昨,進(jìn)行對(duì)比。

運(yùn)行機(jī)器:2? Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz Dict:61w條(黑名單)扛施;字?jǐn)?shù):5872778 待匹配:96330條(用戶請(qǐng)求query); 字?jǐn)?shù):887790

可以看到鸿捧,使用dfs生成樹(shù)相比hash樹(shù),詞典構(gòu)建時(shí)間增加到50s,但內(nèi)存使用降低40.79%疙渣,查詢效率提升16.97%匙奴。

問(wèn)題:

1.現(xiàn)在實(shí)現(xiàn)的雙數(shù)組trie樹(shù)build時(shí)間有些長(zhǎng),能否縮短構(gòu)建時(shí)間妄荔?

2.AC自動(dòng)機(jī)的構(gòu)建是BFS泼菌,這需要我們構(gòu)建完trie樹(shù)后再構(gòu)建AC自動(dòng)機(jī),這一步也會(huì)增加時(shí)間啦租,能否邊構(gòu)建雙數(shù)組trie樹(shù)的時(shí)候邊構(gòu)建AC自動(dòng)機(jī)哗伯?

3.查詢效率確實(shí)比hash trie高,這個(gè)查詢時(shí)間能否再度優(yōu)化篷角?

問(wèn)題關(guān)鍵點(diǎn):

1.轉(zhuǎn)移基數(shù)存儲(chǔ)的子節(jié)點(diǎn)的索引笋颤,如果改為BFS構(gòu)建,如何保證AC自動(dòng)機(jī)的Fail狀態(tài)和雙數(shù)組Trie樹(shù)狀態(tài)對(duì)應(yīng)内地,同步構(gòu)建如何保證構(gòu)建過(guò)程可控

2.dfs構(gòu)建時(shí)增加了一個(gè)單獨(dú)的葉子節(jié)點(diǎn)伴澄,這個(gè)節(jié)點(diǎn)能否省掉?如果可以不但能減少一次查詢阱缓,同時(shí)也會(huì)減少構(gòu)建時(shí)間非凌,降低內(nèi)存占用,提升查詢效率荆针。

解決辦法:

1.bfs生成樹(shù)

2.葉子節(jié)點(diǎn)壓縮

? ? 1)縮短了為孩子節(jié)點(diǎn)尋找未被占用空間的時(shí)間敞嗡,減少構(gòu)建時(shí)間

? ? 2)縮小了數(shù)組占用的空間

? ? 3)提升了查詢速度

type Node struct {

? ? Code? ? ? ? ? ? ? ? ? ? ? ? ? ? rune

? ? Depth, Base, Index, Left, Right int

? ? Term? ? ? ? ? ? ? ? ? ? ? ? ? ? bool

? ? Children? ? ? ? ? ? ? ? ? ? ? ? []*Node

}

Term標(biāo)識(shí)當(dāng)前節(jié)點(diǎn)是否葉子節(jié)點(diǎn)

Index標(biāo)識(shí)當(dāng)前節(jié)點(diǎn)在雙數(shù)組中的索引

BFS生成樹(shù)構(gòu)建步驟:

1.令base[0]=1;check[0]=0; 獲取根節(jié)點(diǎn)的子節(jié)點(diǎn)

2.為每個(gè)子節(jié)點(diǎn)和其孩子節(jié)點(diǎn)設(shè)置base&check:

? ? 對(duì)每個(gè)子節(jié)點(diǎn)獲取孩子節(jié)點(diǎn),為孩子節(jié)點(diǎn)尋找空閑插入點(diǎn)begin颁糟,如果當(dāng)前節(jié)點(diǎn)為葉子節(jié)點(diǎn),則該節(jié)點(diǎn)base取反喉悴,同時(shí)設(shè)置孩子節(jié)點(diǎn)的Base[pos]=begin棱貌。Check[pos]=parent.Index

優(yōu)化后,再來(lái)看下對(duì)比:

運(yùn)行機(jī)器:2? Intel(R) Xeon(R) CPU E5-2630 v2 @ 2.60GHz Dict:61w條(黑名單)箕肃;字?jǐn)?shù):5872778 待匹配:96330條(用戶請(qǐng)求query); 字?jǐn)?shù):887790

可以看到構(gòu)建時(shí)間大幅下降婚脱,內(nèi)存占用下降11.11%,查詢效率提升12.4%勺像。

AC自動(dòng)機(jī)的一個(gè)極速實(shí)現(xiàn)

有了上面的基礎(chǔ)后障贸,實(shí)現(xiàn)ac自動(dòng)機(jī)就容易了。但是理論誰(shuí)都會(huì)講幾句吟宦,真正實(shí)現(xiàn)起來(lái)并不那么容易篮洁。

下面介紹下筆者用go寫(xiě)的一個(gè)非常快速的實(shí)現(xiàn)殃姓。倉(cāng)庫(kù)地址

基于雙數(shù)組Trie樹(shù)的一種極速實(shí)現(xiàn)袁波。可用于模糊匹配蜗侈,違禁詞標(biāo)記篷牌。

常用使用場(chǎng)景為:

1.聊天時(shí),對(duì)用戶輸入的query進(jìn)行命中違禁詞時(shí)進(jìn)行屏蔽宛篇。

?2.判斷用戶輸入詞是否命中了違禁詞黑名單,然后就行后續(xù)邏輯處理薄湿。

等需要高性能模糊匹配的場(chǎng)景叫倍。

優(yōu)化

本算法除了基于雙數(shù)組進(jìn)行構(gòu)建外,還做有額外的優(yōu)化豺瘤,保證更高的效率:

1.在構(gòu)造算法時(shí)吆倦,雙數(shù)組和fail指針及輸出項(xiàng)會(huì)同時(shí)構(gòu)建,大幅度提升構(gòu)建速度坐求,減少字典構(gòu)建時(shí)間蚕泽。

2.壓縮輸出項(xiàng),減少構(gòu)建時(shí)長(zhǎng)及內(nèi)存占用及gc桥嗤。

3.壓縮了葉子節(jié)點(diǎn)须妻,減少內(nèi)存占用,提升檢索效率泛领。

對(duì)壓縮輸出項(xiàng)的解釋?zhuān)?/p>

如:違禁詞詞典為:

????he

????she

輸入為: ushe荒吏,則返回的命中項(xiàng)為:she.

解釋?zhuān)喝绻蛔鲚敵鲰?xiàng)壓縮,則命中項(xiàng)為:he,she渊鞋;其最長(zhǎng)輸出項(xiàng)為she绰更。

考慮到在實(shí)際業(yè)務(wù)中瞧挤,不管是對(duì)違禁詞做屏蔽(如上文中的ushe,屏蔽違禁詞后為u***)儡湾,還是只判斷是否命中了違禁詞特恬,都不需要冗余的輸出項(xiàng),對(duì)輸出項(xiàng)做壓縮徐钠,不僅可以減少構(gòu)建時(shí)長(zhǎng)癌刽,還能減少內(nèi)存占用,對(duì)于go來(lái)說(shuō)丹皱,還可以減少gc掃描妒穴。

性能

對(duì)比一個(gè)star數(shù)較多的cloudflare/ahocorasick 并返回相同的結(jié)果(使用api:MultiPatternIndexes(content []rune) []int)進(jìn)行對(duì)比(對(duì)比代碼見(jiàn)test文件)。

字典:dictionary.txt 字符串個(gè)數(shù):153151摊崭。字符數(shù):401552 (平均每條2.6個(gè)字符)

待檢索文件:text.txt 字符數(shù):815006

(字典和待檢索文件已在倉(cāng)庫(kù)內(nèi))

運(yùn)行機(jī)器:聯(lián)想小新pro13 Ryzen 5 3550H

對(duì)待檢索文件匹配100遍

go版本:1.15

待匹配前先進(jìn)行一次gc(runtime.Gc())

可以看出讼油,性能比cloudflare/ahocorasick快64%(檢索一遍81.5w的字符僅需54ms),得益于雙數(shù)組實(shí)現(xiàn)和對(duì)輸出項(xiàng)的優(yōu)化,執(zhí)行一次gc的時(shí)間可以忽略不記(持有的指針僅為四個(gè)數(shù)組header)呢簸。另外占用內(nèi)存僅為14.2M矮台。

但得益于上面的各種優(yōu)化,使得構(gòu)建時(shí)間優(yōu)化到2.4s根时,雖然仍較cloudflare/ahocorasick長(zhǎng)瘦赫,但是考慮到構(gòu)建時(shí)較復(fù)雜,而且大部分應(yīng)用只需在啟動(dòng)時(shí)構(gòu)建一次蛤迎,秒級(jí)對(duì)于感官時(shí)間較短确虱,構(gòu)建時(shí)長(zhǎng)可控(筆者用一個(gè)業(yè)務(wù)的61w行黑名單詞測(cè)試,構(gòu)建時(shí)間3.5s)替裆,但帶來(lái)的更高的可量化的各項(xiàng)指標(biāo)收益校辩,這個(gè)是值得的。

倉(cāng)庫(kù)地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末辆童,一起剝皮案震驚了整個(gè)濱河市宜咒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌把鉴,老刑警劉巖故黑,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異庭砍,居然都是意外死亡场晶,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門(mén)怠缸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)峰搪,“玉大人,你說(shuō)我怎么就攤上這事凯旭「懦埽” “怎么了使套?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)鞠柄。 經(jīng)常有香客問(wèn)我侦高,道長(zhǎng),這世上最難降的妖魔是什么厌杜? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任奉呛,我火速辦了婚禮,結(jié)果婚禮上夯尽,老公的妹妹穿的比我還像新娘瞧壮。我一直安慰自己,他們只是感情好匙握,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布咆槽。 她就那樣靜靜地躺著,像睡著了一般圈纺。 火紅的嫁衣襯著肌膚如雪秦忿。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,730評(píng)論 1 289
  • 那天蛾娶,我揣著相機(jī)與錄音灯谣,去河邊找鬼。 笑死蛔琅,一個(gè)胖子當(dāng)著我的面吹牛胎许,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播罗售,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼辜窑,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了莽囤?” 一聲冷哼從身側(cè)響起谬擦,我...
    開(kāi)封第一講書(shū)人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤切距,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體鞭达,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疲酌,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了葡幸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片最筒。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蔚叨,靈堂內(nèi)的尸體忽然破棺而出床蜘,到底是詐尸還是另有隱情辙培,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布邢锯,位于F島的核電站扬蕊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏丹擎。R本人自食惡果不足惜尾抑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蒂培。 院中可真熱鬧再愈,春花似錦、人聲如沸护戳。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)灸异。三九已至府适,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間肺樟,已是汗流浹背檐春。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留么伯,地道東北人疟暖。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像田柔,于是被迫代替她去往敵國(guó)和親俐巴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容