原帖:http://itren.diandian.com/archives/104
前言
目前大多数提供搜索功能的大型网站,都会对用户输入的拼音进行自动补全,推荐一些用户可能感兴趣的词。由于近期自己做的一个项目也要用到此功能,所以打算亲自分析实现一个自动补全功能。需求分析
这类应用应当根据不同的场景单独设计,通用的设计未必好用,不过可以择各家之长,按需所取。
我的场景是,数据库中存储有1万多个景点名称数据,我希望:
- 当用户输入如"yun"这样的拼音时,能够给出“云台山、云岭”等自动补全信息
- 当用户输入如“yts”这样的缩写时,能够给出“云台山”的自动补全
- 当用户输入如“云tai”这样的混合输入时,也能够给出“云台山”补全信息
- 暂时不考虑输入“台山”补全“云台山”这样的后缀形式
(本需求只是引子,我希望实现千万级以上数据源中的补全)
最原始的做法
如果要改进一个事物,那我们就要先彻底分析原有事物的缺点,比如最原始的实现方法:
- 首先在景点数据库中添加两个新的字段,字段py_full表示当前景点名称的全拼,字段py_short景点名称的拼音缩写。
- 每次录入或更细景点名称的时候,调用工具把名称转换为全拼和缩写。
- 用户在前端输入字符串时,将字符串拆分为字符,依次判断每一个字符是否存在于景点名称、全拼、缩写中,如输入 "云t",则生成类似这样的查询语句:
- select name from 景点 where (name like '%云%' or py_full like '%云%' or py_short like '%云%') and (name like '%t%' or py_full like '%t%' or py_short like '%t%') limit 0,5
这种做法的缺点很明显,就是每次输入字符要进行复杂度为O(n)的查询,n为所以景点的数量,加上like查询对系统性能的影响,这么做对于大数据量(如百万级)是不可接受的。
一般的小型系统,用这种方式是可以被接受的,如果数据只有几万,也完全可以把名称、拼音全部缓存到数据库,内存占用可能也只有几百KB而已。
虽然本系统的数据看起来能够接受此种方式,但是我们此处探讨的是进一步的优化,我们假设数据量为千万级别,那缓存到内存可能就就有上G了,而每次遍历数据库的代价更是非常大。
汉字转为拼音
上文提到了汉字转为拼音和拼音缩写的工具,我们可以通过以下步骤构造:
- 获取某汉字编码及其对应的拼音数据库,网络上有不少下载,我这里用的是GBK编码下的7000个左右汉字对应的拼音,点击此处下载。
- 将这批数据以 汉字、拼音 两个字段的形式保存到数据库,当然也可以为他们建立索引。
- 每次系统启动将数据保存到内存中,编程实现一个 String chineseToPinyin(String word) 函数即可。
网路上也有很多开源的实现,比如pinyin4j,拥有2万多个汉字,但是最终的原理都差不多,上面7000汉字大概占用内存10Bytes * 7000 = 70 KB (假设每个汉字拼音映射平均占用10Bytes)。
进一步的思路
上面的遍历办法既然被否定,那么涉及到字符串匹配,我们还有什么选择呢?
如果使用Trie树组织数据,可以很快速的对源字符串进行匹配,我们可以把输入串转换为拼音全拼,然后根据此全拼建立Trie树,在树节点的key上放置拼音字符,value上为查询到该节点时的完整汉字字符串,如下图:
假设我们有2000万条数据,每条数据有5个汉字,每个汉字转换为拼音字符为15个字符。理论上这棵树的深度最多为15层,每一个节点最多包含26个字符。我们假设每一层正好分配了2000万个字符,这样第一层因为最多只能有26个字符,所以这2000万基本上都是重复数据,之记为26,第二层为26*26 = 676个,只记为676依次类推:
- 第一层:26
- 第二层:676
- 第三层:17576
- 第三层:456976
- 第四层:11881376
- 第五层:超出2000万,按2000万计算
- ...
- 第十五层:2000万
总计约为为,2.2亿个字符。这么多字符,每个字符需要一个节点,每个节点为4Byte的话,也需要880MB的内存,加上2000万条汉字数据,约为20000000*5*4Byte = 400MB内存,再加上节点指针等其他数据,一共至少需要1.5G以上的空间占用(这里主要的指数增长因素为平均字符长度)。
三元搜索树(Ternary Search Tree)针对过多的内存占用解决了一部分问题,它结合二分搜搜的优点,牺牲了一部分查询性能(O(logN),Trie前缀树为O(Len)),来换取较大的空间节省(指数级的降低空间占用,具体可以参考上面的链接)。虽然使用三叉树降低了空间,但是起码也是有几百M的占用的,这些数据最好的情况还是放在硬盘中,而非内存。
比较好的做法
那么,用什么方法即能够保证查询的速度,又能把主要的条目数据存储在磁盘中呢?
我们可以这么来做:
- 对原词条数据进行分词,形成汉字与原条目的倒排索引,可以用Lucene或自己实现,这种需求来说,最好是逐字分词比较好,这样最多可能只有几千个拼音,放在三叉树中的节点不过3、4万左右。
- 把分词后行程的字典转换为拼音全拼和缩写,并把这些拼音放到三叉树中存储,key为拼音字符,value为汉字。
- 把建好的拼音字符树(最多不到1M的大小)放到内存中即可。
- 比如用户输入y,先到内存中的根节点寻找y字符,找到他的value不为空的最近的几个节点,然后根据这些节点的value汉字,到倒排索引中获取真正待推荐的条目即可。
- 同样,也可以把所有字符的缩写也加入到三叉树中,虽然会使整个词典的体积变大一些(1000万个条目,50M以下,假设每个条目的拼音缩写为5个字符),但对大型应用也未尝不好。
// TODO 思路还不完善,需要进一步优化配图
参考文献