高效的的关键字查找和检测(哈希表和Trie前缀树和FastCheck)在实际使用中的性能
前言:看到dudu发的博文中似乎最近的db压力来源于关键字检测,以前只关注了倒排索引,于是好奇经典的关键字查找在实际生产中性能到底是一个什么数量级?
为什么不用倒排索引
在一个文本中找到给定的关键字最快的做法是倒排索引,比如平常使用的各种搜索框如google,还有咱们在生产中分析日志时用的ES也是。它的优点是海量的文本和海量的关键字,缺点是搜索的文本加入现有索引很慢。所以它不适合:评论、即时通讯、微博、脏字过滤等对发布时间有要求的场景,而这些场景正好跟倒排索引是反过来的。
如果不用Trie树
那么如何实现一个高效的及时关键字检测算法,这里为了简单期间只关注FindFirst,而FindAll 同理。首先第一个否定的算法就是IndexOf(keyword)方法,这里假定输入文本长度n,检测的关键词数量m,关键字平均长度l,复杂度就是O(n*m*l),复杂度很高。在不知道Trie树之前很容易想到的实现是Hashtable,我想大多数程序员也是这样想的,复杂度直接降为O(n+m*l),这里要提一下好多写java和csharp的同学在实现这个算法的时候会调用substring,然后导致内存飙升,其实这个临时空间完全不需要的。下面是我用python写的一个实现,这里为简单期间回避了hashtable中碰撞的情况,一般用单向链表还有树来解决,因为知道trie了没必要去做的很完善,只是提供一个对比的数量级,而哈希表的完整实现只会比这个更慢。
def test3():
with open('./sample_post', encoding='utf-8') as f:
test_post = f.read()
spam_words={}
with open('././SpamWordsCN.min.txt', encoding='utf-8') as f:
words=[line.strip() for line in f]
for w in words:
if len(w)>0:
spam_words[w[0]]=w
import time
start = time.time()
times = 500
while times > 0:
for i,c in enumerate(test_post):
if c in spam_words:
is_found=True
for j,sc in enumerate(spam_words):
if sc!=test_post[i+j]:
# compare failed
is_found=False
break
# found
if is_found:
#print('找到了%s'%spam_words)
break
times -= 1
end = time.time()
print('程序运行时间:%s毫秒' % ((end - start) * 1000))
测试条件和运行环境
- 输入文本长度:5388 from word count
- 关键字数量:575
- 平均长度:3 目测
- 测试次数:500
- 运行环境:Ryzen 5 3600 6-core 3.6GHz
多次运行后会降为650ms左右
HashTable 总结
哈希的原理就是通过对关键字的预处理,把首字母的关键字检索降为O(1),整个过滤只需要O(n)即可5k的字符过滤650ms时间对大多数小项目小网站足够了,如果是即时通信、站内消息也是勉强足够了。但是作为一个服务的话,100并发下50ms都太大了,而且这里还只是非重复的关键字,重复的关键字是脏词最常见的形式,那么可不可以更快一点。
Trie 出场
在计算机科学中,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。— from 维基百科
关于Trie的原理可以查看Tool.Good的文章:https://www.cnblogs.com/toolgood/p/6284718.html
Trie的实现
高达2.3k星的实现:ToolGood.Words,每秒3亿的过滤效率,有多个语言版本的实现,其中以ccharp为主,python的 “咳咳”有点尴尬,从文件命名上看就不友好,而且似乎作者用来检测的字词有点过于简短,四五个关键词,不超过20个字符的文本输入完全测试不出压力。接下来试试Trie的威力,同样先从python试起,下面是测试代码:
from StringSearch import StringSearch
with open('./sample_post',encoding='utf-8') as f:
test_post = f.read()
with open('././SpamWordsCN.min.txt',encoding='utf-8') as f:
spam_words = [line.rstrip() for line in f]
import time
search = StringSearch()
search.SetKeywords(spam_words)
start = time.time()
times = 500
while times > 0:
f = search.FindFirst(test_post)
times -= 1
end = time.time()
print('程序运行时间:%s毫秒' % ((end - start) * 1000))
Trie的实现只有之前哈希表的五分之一
测试环境下过滤效率大概是 2千万/每秒 ,看来csharp 3亿确实是可能的。 它之所以快是因为哈西表只解决了输入字符中完全不需要匹配的n,而Trie把关键字的复杂度也降低为O(max(l))。白话一点就是哈西表把indexof中遍历m遍降低为一遍,而Trie把哈希表中关键词的检测也降低为max(l),这里的l指最长的关键字长度,也可以理解为关键字的每一个字匹配都像哈希中的实现一样只需要一次匹配。O(n+max(l))的复杂度已经很低,效率极高。对于要求不高的项目直接可以耦合进现在的处理中。那么可不可以更快一点?
Fastcheck 和 TTMP
这里还有两个号称更快的算法FastCheck和TTMP,咳咳,只搜索到了博客园的帖子,不信你google。因为是改进算法,其中fastcheck在tool.goods中有实现,期待将来能再做一个评测比较。他们比Trie快在,适当的增加了空间换了可观的时间,比如fastcheck利用了类似bitmap的原理考虑了字符出现的位置,所以它在短关键字和过滤替换场景上的表现会很好。
- FastCheck:https://www.cnblogs.com/xingd/archive/2008/02/01/1061800.html
- TTMP:https://www.cnblogs.com/sumtec/archive/2008/02/01/1061742.html
还是前面的问题可以不可以更快一点?
接下篇《从125ms到17ms,一次关键字检测过滤服务的优化》