最近, 我的一位同事提出了一個要求: 如何在 redis 中創建內容篩選器?他想根據一系列臟話過濾收到的信息。這是一個相當常見的用例–任何接受用戶輸入的應用可能都希望至少對不恰當的單詞進行粗略掃描。我首先想到的是, 他應該使用布魯姆過濾器, 但我想知道是否有更好的選擇, 所以我想測試我的假設。我從這里得到了一個不好的詞的列表, 并做了一些初步測試的文本尤利西斯由詹姆斯·喬伊斯 (一個大的書在公共領域與許多有趣的語言!)。

對于那些不熟悉的人來說, bloom 篩選器是一種概率 (p11c) 數據結構, 可以測試是否存在。這種方法一開始看起來很嚇人, 但它真的很簡單: 將項目添加到 bloom 篩選器中, 然后檢查是否存在于 bloom 篩選器中。bloom 篩選器是概率的, 因為它們無法明確回答存在性問題–它們要么告訴您一個項目肯定不在篩選器中, 要么可能不在篩選器中。換句話說, 與布魯姆過濾器誤報是可能的, 但假陰性不是.布魯姆過濾器真正酷的是, 它們使用的空間的一小部分 (與實際存儲項目相比), 并且可以以適度的計算復雜性確定可能的存在。我的妻子是一名文學教授, 我很開心地選擇了尤利西斯參加這次考試, 提醒我這本書的主角叫布魯姆。擬 合。

回到手頭的問題上來。你想做的是把給定信息中的所有單詞都拿出來, 找到一個已知的壞詞列表的交集。然后你確定有問題的單詞, 或者拒絕或者手動篩選它們。

為了使事情盡可能易于使用, 并避免任何網絡瓶頸, 我在 lua 中實現了所有解決方案, 并從 redis 內部運行它們。我使用了一個 lua 表, 其中的鍵是不合適的單詞, 值是 “true”, 以防止在所有腳本中發出不需要的 redis 命令。

關于消息處理的說明: 在腳本中, 我使用了一個非常簡單的方法, 只是按空間拆分。誠然, 這本身在實踐中是行不通的, 因為你需要處理標點符號、數字等。在這種情況下, 它工作得很好, 因為我們以同樣的方式處理腳本, 而不是試圖測量 lua 字符串拆分的速度。

使用內容篩選集

除了 bloom 篩選器之外, 解決此問題的另一種方法是使用 redis set。我做的第一件事就是使用 sadd 命令和超過1600個參數將臟話添加到 set 鍵中。我拿了壞單詞列表, 用雙引號包圍了每個單詞, 然后加上了 sadd 和密鑰的前綴, 最后將其保存到一個文本文件 (badwords-sets. txt)。我執行它與這個重新的 cli 管道技巧:

$ cat ./badwords-set.txt | redis-cli -a YourRedisPassword

一個簡單的方法是將信息分解成文字, 并在消息中的每個單詞上運行 sism置于以識別單詞。我最初的想法是, 命令的數量之多將使這種情況效率低下。讓我們看看它是如何實現的:

local words = {}

for i in string.gmatch(ARGV[1], "%S+") do
 words[string.lower(i)] = true
end

local badwords = {}
for word,v in pairs(words) do
 if redis.call('SISMEMBER', KEYS[1], word) == 1 then
   table.insert(badwords,word)
 end
end

return badwords

我們在這里所做的就是拆分第一個參數 (在本例中為消息), 然后遍歷單詞并為每個參數運行 sism直er。如果 sismember 返回 1, 那么我們知道這是一個壞詞, 并將其添加到一個表, 我們返回到 redis讓我們用 sadd 和 sinter 來做這件事。同樣, 這似乎是一噸的命令, 但下面是它在 lua 中的外觀:

local words = {}

for i in string.gmatch(ARGV[1], "%S+") do
 words[string.lower(i)] = true
end

for word,v in pairs(words) do
 redis.call('SADD',KEYS[1],word)
end

local badwords = redis.call('SINTER',KEYS[1],KEYS[2])
redis.call('UNLINK',KEYS[1])

return badwords

這是相同的拆分例程, 但我沒有為每個單詞頒發 sism開出, 而是將其添加到使用 sadd 設置的臨時 redis 中。然后, 我使用啊, 我使用啊, 用 “辛特” 命令, 把這個臨時集合和我們的壞話交織在一起。最后, 我擺脫了與 unlink 的臨時設置, 并返回了啊, 是我的結果。使用此方法, 請記住您正在寫信給 redis。這對持久性和分片有一些影響, 因此 lua 腳本的相對簡單隱藏了一些操作復雜性。

內容過濾與綻放

對于我的第二種方法, 我將所有的臟話添加到布魯姆過濾器中。與 sadd 一樣, 您可以一次向帶有 bf 的 bloom 篩選器添加多個項目。命令。事實上, 我拿了包含所有壞單詞和 sadd 命令的文本文件, 只是替換了命令 (現在是 bf。madd) 和鍵。

使用此解決方案, 請務必記住, bloom 篩選器是概率的, 它們可以返回誤報。根據您的使用情況, 這可能需要對結果進行二次檢查, 或者只是將其用作手動過程的屏幕。

首先, 讓我們看一下 lua 腳本, 以檢查是否存在臟話:

local words = {}

for i in string.gmatch(ARGV[1], "%S+") do
 words[string.lower(i)] = true
end


local badwords = {}
for word,v in pairs(words) do
 if redis.call('BF.EXISTS', KEYS[1], word) == 1 then
   table.insert(badwords,word)
 end
end

return badwords

類似于 sismamer 方法, 我們可以將傳入字符串拆分為唯一的單詞, 然后運行 bf。每個單詞都有例外。Cfo如果篩選器中可能存在, 則存在 existts 將返回 1; 如果篩選器肯定不存在, 則返回0。此解決方案將消息中的每個單詞與篩選器進行比較, 將任何壞單詞插入表中, 并在最后將其返回。

我們還可以使用 rebloom 模塊, 該模塊提供 bf。命令檢查多個字符串與篩選器。它的功能與 bf 相同??梢? 但可以讓你一次做得更多。然而, 這一次的事情變得有些棘手。運行多個命令 (即使是來自 lua 的命令) 會給 redis 帶來一些開銷, 因此, 理想情況下, 您希望運行的命令越少越好, 讓 redis 實際檢查數據, 而不是花費資源來解釋數千個命令。最好通過最大化參數的數量來最大限度地減少命令調用 (這至少是理論)。

我的第一次嘗試只是把《尤利西斯》 (45, 834) 的所有獨特之語 (45, 834 字) 都推入 bf 的一個命令中。使用解包功能的 mexistts。這并沒有成功, 因為 lua 的固定堆棧大小遠遠小于我的45k 參數。這迫使我拆分我的 bf。mexist 分成更小的單詞塊。嘗試和錯誤幫助我發現, 我可以安全地添加約 7, 000個參數到 bf。在盧亞抱怨之前, 麥克斯命令。這是以 lua 腳本的復雜性為代價的, 但這意味著我可以運行66個命令, 而不是超過45k。

下面是腳本的最終外觀:

local words = {}

for i in string.gmatch(ARGV[1], "%S+") do
 words[string.lower(i)] = true
end

local function bloomcheck(key,wordstocheck,dest)
 for k, v in ipairs(redis.call('BF.MEXISTS',key,unpack(wordstocheck))) do
   if v == 1 then
       table.insert(dest,wordstocheck[k])
    end
 end
end

local possiblebad = {}
local temp = {}
local i = 0;
for word,v in pairs(words) do
 table.insert(temp,word)
 i = i+1;
 if i == 7000 then
   bloomcheck(KEYS[1],temp,possiblebad)
   temp = {}
   i = 0;
 end
end
bloomcheck(KEYS[1],temp,possiblebad)

return possiblebad

正如你所看到的, 這開始是大量的 lua 代碼最后, 它返回所有可能的壞話。

測試

從 cli 運行 lua 腳本并不有趣, 因為您正在處理大型參數, 并且事情可能會變得棘手。因此, 我使用基準. js 框架編寫了一個小型 node. js 腳本, 多次運行我的測試, 并在每次運行時獲得正確的統計信息。除了尤利西斯, 我還運行它的1000和500字的 lorem ipsum 隨機文本集, 以獲得較短的消息的感覺。所有這些測試都是在我的開發筆記本電腦上運行的, 因此除了相對差異之外, 它們的含義微不足道。

讓我們來看看結果:

Ulyses Bad Word Detection

均值 標準偏差 錯誤的邊距
入門級 (第4) 0.25 2477065875

(252.478ms)
0.06331450637410971 0.026739796333390907027
妹妹 (1) 0.17 3657477090625秒 (179.356 毫秒) 0.034054707030413132506 0.0117993526261322626679
Cfo墨西哥菜 (第2次) 0.1925117373762064 (192.52 毫秒) 0.05 1589253435185 0.019619660405252156
Cfo考試 (第3次) 0.21505565530769238 (23.556ms) 0.04 1931428091619434 0.01640265013395354

1000 words of Lorem Ipsom Bad Word Detection

均值 標準偏差 錯誤的邊距
燒結機 (第2次) 0.000 55343038665705 (0.55 毫秒) 0.0000 4478206422951418 0.0000 10447163737135063
妹妹 (1) 0.00087821613392592 (0.49 毫秒) 0.00007453275890098545 0.0000 1826260593074143636
Cfo墨西哥菜 (第3次) 0.00057705050503133733705 (0.58 毫秒) 0.00884234034283731551 0.0000 199593614949282812
Cfo考試 (4) 0.0006705186154166669 (0.67 毫秒) 0.000035960357334688716 0.00000 8888 8754646998835

500 words of Lorem Ipsom Bad Word Detection

均值 標準偏差 錯誤的邊距
入門級 (第4) 0.0007199102990995225 (0.72 毫秒) 0.000 2662192424373751274 0.0000 686 1610037055691
妹妹 (1) 0.000512735960882813 (0.51 毫秒) 0.000042812670318675034 0.000103288558277771371371371371373年
Cfo墨西哥菜 (第2次) 0.0005467939453535285 (0.55 毫秒) 0.004135890147573503 0.00000 94877778886686
Cfo考試 (第3次) 0.0007052701613834288 (0.71 毫秒) 0.00013292929264339345952 0.000032836237873305535

結論

我覺得這些結果很有趣。我最初的假設是不正確的這種情況-布魯姆過濾器不是最快的。在所有測試中, 普通的老 ssammer 運行得更快。除此之外, 我的一些重要外賣包括:

  • 尤利西斯是這種類型的事情不尋常的工作量, 你很可能不會有很多詹姆斯·喬伊斯提交史詩小說

響應時間因小數毫秒而異, 因此這些差異是不可察覺的。

  • 如果 sism黃芪有能力接受可變參數 (它沒有), 這很可能會使它更快在這種情況下。
  • 500字中的一些手段比1000字的手段長。我懷疑在這些相對較小的工作負載上花費的大部分時間都在開銷中。
  • 那你為什么要使用布魯姆過濾器呢?存儲效率。讓我們看最后一件事:

    127.0.0.1:6379> MEMORY USAGE badwordsset
    (integer) 68409
    127.0.0.1:6379> MEMORY USAGE badwords
    (integer) 4228

    在這里, “壞詞” 是布魯姆過濾器, 而 “壞詞集” 是集結構。布魯姆過濾器比集合小16X 以上。在這種情況下, 大小并不那么重要, 因為它是一個單一的列表。但是, 想象一下, 如果一個網站上的每個用戶都有自己的臟話過濾器?這真的可以使用 set 結構來添加, 但使用 bloom 篩選器會更容易容納。而這一測試表明, 響應時間非常相似。

    Comments are closed.