引言 #
这两个星期的工作主要是对千万文本数据的处理,由于我以前没有接触过类似的数据量,所以我就把我在处理这千万数据的过程中遇到的问题以及解决的方法总结一下
明确目标 #
完成任务之前我们必须要明确自己的目标,首先谈一下数据,数据是两张表,一张是文章列表,一张是文章内容,每篇文章都牵涉到一些人,我们的目标就是给定一些搜索条件然后把最可能相关文章给找出来
这个任务有点像实现一个搜索引擎,我们通过输入关键词把相关的网页寻找出来,简单点来实现就是直接使用SQL
的Like
查询,但是这里存在两个很大的问题
- 搜索精度不准,假如我们搜
张华
可能有关张华硕
的人也会出来 - 搜索耗时太长,在千万级文档中全文搜索速度非常慢
我们希望我们能精确的实现查询,而且我们希望我们的查询能够实现毫秒级的速度。所以我们就尝试使用ElasticSearch
来当我们“数据库”,并且放弃系统默认的分词,自己”手动分词“,来实现精准快速查询
所以我们的目标很简单,将数据从MySQL
“塞”到ElasticSearch
中,然后想办法再"取"出来
第一个拦路虎“MySQL” #
我碰到的第一个拦路虎是数据库的响应速度,为了将数据完整的从数据库里面取出来(新数据还在产生),我按照id从小到大的顺序一小块一小块的从MySQL
中获取出来
一开始程序运行的挺Happy,速度一直很稳定,但是我发现跑了十万之后速度突然慢下来,一开始我以为解析有问题,我开始打断点,调试,找了半天原来是数据库返回数据太慢了。
我们来分析一下这个SQL
为什么这么慢
select * from a order by id limit 10 offset 100000
我们虽然限制返回了10个但是后面有个条件我们必须要后面10万个,为了拿到这10个,MySQL
必须要扫描10万个数据先,虽然我们是在主键上扫描会快一点,但是十万毕竟很大,即使一次主键扫描花0.01ms,乘以十万也是很大的
基本上每次解决数据库速度的时候,我们第一考虑点就是索引,那么我们这里就多聊两句:索引为什么快?
数据库其实就是一堆数据的集合,它提供工具我们快速获取我们想要,用图书馆来打比方,数据库就是图书馆,他们把所以的图书分好类,你想买什么书,按照分类去寻找就行,这样假设你图书馆有一千万本书,你要找《安徒生童话》,你只要按照这个索引(童话书>丹麦>安徒生)就能找到,假如你想找一本《无类》的书你不知道他的分类,你就得把整个图书馆逛一遍才能找到你要的书了,这就是没有索引的下场。
在程序的世界里也一样,你想快速找到一个记录,如果不用索引,那么就得遍历了,运气好一下子就能找到,运气不好一辈子也找不到。在 MySQL
中,索引的背后就是B+树,也就是将数据查找最坏结果降到了一个log2N
级。如果你想了解“树”为什么这么快,可以看看我前面写的的博客
怎么来说明这个索引的作业呢,假设你有一千万数据,你最坏的情况下要进行24次查找,24:1000000 达到惊人的41万倍差距,而且当数据越大这个差距越大,从这里我们就知道索引的威力了。
我们回到前面,为了使用索引,那么我们只能在id(主键)
上做手脚了,我第一个想到的是按照id
分块,但是我仔细看了看数据库,id
不是全部连续的(可能是因为删除过数据),假如我用id
固定的区间来的话,获取到的数据可能部分有部分没有,虽然能够实现但是不够优雅,我还得增加处理空数据的代码。
这时候我想到了,我们第一次获取的id
如果能在后面继续使用,而且更新的话那么我们就能使用上索引了,所以我们只有把每一块数据的最后一个id
记住,然后去这个id
获取下一批数据,这样就能实现又用索引又不用改太多代码。
那么我们的SQL
就改成下面的语句
select * from a where id > 1111 order by id limit 10
通过简单的测试原来需要几分钟才能“掏”出来的数据在几毫秒就取出来了,上万倍的差距。至此我们第一个拦路虎就解决了
PS:在后面看ElasticSearch
文档的时候发现他们也提供了一个scroll
(全库获取)的超级翻页功能,在他们的参数里面也要提供一个scroll_id
,感觉原理应该也是和这个差不多。通过使用索引id
来加速“翻页”
ElasticSearch
存贮和排序
#
接下来我就介绍,我怎么优化存储和编写定制动态DSL
来实现我们想要的功能。
在我介绍之前,我先简单的谈一下我对ElasticSearch
的理解
ElasticSearch简介 #
在我没有真正使用ElasticSearch
之前,我就在知乎上听过它的大名,ElasticSearch
真正让我震惊的是当我把上千万数据导入到它里面去,它能在毫秒级别给你响应,而我在MySQL
调用SQL
进行查询得花几十分钟
我们可以把ElasticSearch
类比成一个数据库,相比于MySQL
它在查询性能上做到了苛刻的,我一开始想好好介绍它是怎么做到的,但是我发现有人已经总结的非常好了,可以看看这份资料,它之所以能做到这么快的原因就是这个:索引+内存+缓存
ElasticSearch
使用倒叙索引让查询时间复杂度降到logN级,使用内存让物理查询速度达到极限,加上一些过滤缓存让其在复杂查询还是简单查询都能保持在一个很平稳的速度
ElasticSearch
相比与MySQL
还有一个特点,就是对大文本搜索的支持,ElasticSearch
对文本默认自动进行分词,并且通过一些高级分类算法(TF/IDF,5.0后使用更加先进的BM25算法),对匹配的文本进行打分,依次返回得分高低列表,而MySQL
在大文本检索只有一个全文索引支持,从实现上来看就是一个加了索引的Like
查询,所以ElasticSearch
在设计的特定算法加持下被称为“搜索引擎”
但是ElasticSearch
同现在商业的搜索引擎,比如Google、百度、Bing这些又有些不同,ElasticSearch
传入的是纯文本,所以它只能使用一些TF/IDF
算法来计算给定关键词与文本的相关项,但是现在商业引擎输入的是网页,所以现在商业引擎比如Google就使用Google Page Rank
算法来再次计算文档相关性。当然现代商业引擎不单仅仅使用Google Page Rank
算法,他还会考虑更加因素进去(比如百度的竞价排行,Google的恶意影响网页排行检测),但是从本质上来说,无论是ElasticSearch
和现代商业引擎都在做同一件事,给匹配项打分,这就是他们与MySQL
的全文检索的不同(MySQL
没有后面打分排行的概率,他只有order by
的这个概率)
设计思路 #
一开始我准备直接使用ElasticSearch
的搜索引擎来对文章
中的涉及到的人进行检索排序,但是我们来考虑这样一件事,假如文章中存在这么一句话:“刘二能吃两碗饭”(涉及到的人是刘二能)。假如我们使用“刘二”去检索,这篇文章中的”刘二能“也能检索到,而我们的目的就是尽可能返回最可能的结果,对于那些不可能的结果一律不返回
所以我们就不能让ElasticSearch
自动帮我们对文本进行分词,但是我们想利用打分
这个机制帮我们完成最可能在最前面返回
所以我们把每篇文章里面的人物解析出来的属性(姓名,出生年月,民族等)设定为keyword
类型,这样ElasticSearch
就不会对这个字段进行分词,查询的时候也必须全匹配才能命中,由于一篇文章可能设计到多个人,所以我把它用一个list
存到一个document
里面
但是这个又引起了另外一个问题,对与一个document
里面的list
,ElasticSearch
会把它进行转换
我们用官方文档的例子解释,我们存入了下面这个document
{
"group" : "fans",
"user" : [
{
"first" : "张",
"last" : "华"
},
{
"first" : "李",
"last" : "四"
}
]
}
ElasticSearch
会把它转换成
{
"group" : "fans",
"user.first" : [ "张", "李" ],
"user.last" : [ "华", "四" ]
}
这样你查询这个人张四
,我们发现上面这个文档也返回了(选了user.first
列表的第一个值,user.last
的第二个值,这个结果明显是错误的,我们怎么才能避免ElasticSearch
的“自作聪明”呢,答案很简单我们把user
声明为nested
对象,这样ElasticSearch
就不会把它拆开了而是把它当做两个文档(有些人可能会说这个会不要影响它的速率,恰恰相反,ElasticSearch
会经常使用类似技术来加速,详情可以看上面的博文)
现在我们解决了重重困难终于要到排序的阶段了,然而我们没有使用string
类型(支持TF/IDF算法)而使用了keyword
类型,导致我们没有办法使用ElasticSearch
提供的高级排序算法,所以我们得自己手动进行提分,怎么来提分呢,很简单使用boost
前面我们提到了我们的文档可能解析出来对象多个属性(姓名、年龄、性别、居住地),但是有些文档也可能没有这些信息,我们查询的时候是有一个信息列表(这个人姓名、年龄、性别等等),所以我们使用boost
对命中的信息越多的进行提分,所以我们最终就能完成命中越多信息的排在越前面,当然命中信息少的也会被筛选出来只不过位置稍微靠后
总结 #
通过这次直面千万数据,让我学习到了不少,虽然一开始目的只想简单搜索出来最匹配的数据,但是在实际过程中,通过不断对产生结果提出问题,最终实现了一个比较满意的产品,整个产品在不断的优化过程中逐渐成型并且稳定,我觉得对我帮助最大就是撰写设计文档,并且在产品成型的过程中把结果反馈上去,最后慢慢迭代一个最好的版本