You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

140 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 10 | 搜索引擎设计:信息搜索怎么避免大海捞针?
你好,我是李智慧。
在[04讲](https://time.geekbang.org/column/article/489014)中我们讨论了大型分布式网络爬虫的架构设计但是网络爬虫只是从互联网获取信息海量的互联网信息如何呈现给用户还需要使用搜索引擎完成。因此我们准备开发一个针对全网内容的搜索引擎产品名称为“Bingoo”。
Bingoo的主要技术挑战包括
1. 针对爬虫获取的海量数据,如何高效地进行数据管理;
2. 当用户输入搜索词的时候,如何快速查找包含搜索词的网页内容;
3. 如何对搜索结果的网页内容进行排序,使排在搜索结果列表前面的网页,正好是用户期望看到的内容。
## 概要设计
一个完整的搜索引擎包括分布式爬虫、索引构造器、网页排名算法、搜索器等组成部分Bingoo的系统架构如下。
![图片](https://static001.geekbang.org/resource/image/4b/9d/4b1d91247766bacb920a4af7bd8ec09d.jpg?wh=1920x1440)
分布式爬虫通过存储服务器将爬取的网页存储到分布式文件集群HDFS为了提高存储效率网页将被压缩后存储。存储的时候网页一个文件挨着一个文件地连续存储存储格式如下。
![图片](https://static001.geekbang.org/resource/image/fa/99/fa2bb56330b9ab2d3dd78e5c379f1599.jpg?wh=1920x253)
每个网页被分配得到一个8字节长整型docIDdocID之后用2个字节记录网页的URL的长度之后4个字节记录压缩后网页内容数据的长度所有存储的网页的头14个字节都是同样的格式。之后存储URL字符串和压缩后的网页内容数据。读取文件的时候先读14个字节的头信息根据头信息中记录的URL长度和数据长度再读取对应长度的URL和网页内容数据。
搜索引擎能够快速查找的核心就是利用索引根据用户的查询内容查找匹配的索引根据索引列表构建结果页面。索引的构造主要通过索引构造器完成索引构造器读取HDFS中的网页内容解压缩后提取网页中的单词构建一个“docID->单词列表”的正排索引。然后,索引构造器再根据这个正排索引构建一个“单词->docID列表”的倒排索引“docID列表”就是包含了这个单词的所有网页列表。利用这个倒排索引搜索器可以快速获得用户搜索词对应的所有网页。
网页中所有的单词构成了一个词典实际上词典就是一个Hash表key就是单词value就是倒排索引的网页列表。虽然互联网页的内容非常庞大但是使用到的单词其实是非常有限的。根据Google的报告256M内存可以存放1400万个单词这差不多就是英文单词的全部了。
在构建索引的过程中因为要不断修改索引列表还要进行排序所以有很多操作是需要进行加锁同步完成的。对于海量的互联网页的计算这样的索引构建速度太慢了。因此我们设计了64个索引桶根据docID取模将不同网页分配到不同的桶中在每个桶中分别进行索引构建通过并行计算来加快索引处理速度。
索引构造器在读取网页内容、构造索引的时候还会调用URL提取器将网页中包含的URL提取出来构建一个链接关系表。链接关系表的格式是“docID->docID”前一个docID是当前网页的docID后一个docID是当前网页中包含的URL对应的docID。一个网页中会包含很多个URL也就是会构建出很多个这样的链接关系。后面会利用这个链接关系表使用PageRank排名算法对所有网页进行打分排名当索引器得到查找的网页列表时利用PageRank值进行排名最终呈现给用户保证用户最先看到的网页是最接近用户期望的结果页面。
## 详细设计
一个运行良好的搜索引擎的核心技术就是索引和排名,所以我们将分别说明这两种技术要点。
#### 索引
索引构造器从HDFS读取网页内容后解析每个页面提取网页里的每个单词。如果是英文那么每个单词都用空格分隔比较容易如果是中文需要使用中文分词器才能提取到每个单词比如“高并发架构”使用中文分词器得到的就是“高并发”、“架构”两个词。
首先索引构造器将所有的网页都读取完构建出所有的“docID->单词列表”正排索引。
![图片](https://static001.geekbang.org/resource/image/82/09/82a7104f75267b7bd0158b5755567809.jpg?wh=1920x397)
然后遍历所有的正排索引再按照“单词→docID列表”的方式组织起来就是倒排索引了。
![图片](https://static001.geekbang.org/resource/image/d6/61/d67b951a58e1ab8d0c79d908f1e7e361.jpg?wh=1920x550)
我们这个例子中只有两个单词、7个网页。事实上Bingoo数以千亿的网页就是这样通过倒排索引组织起来的网页数量虽然庞大但是单词数却是比较有限的。所以整个倒排索引的大小相比于网页数量要小得多。Bingoo将每个单词对应的网页列表存储在硬盘中而单词则存储在内存的Hash表也就是词典中词典示例
![图片](https://static001.geekbang.org/resource/image/b3/41/b3612d6a169fca54946f300b17644d41.jpg?wh=1920x550)
对于部分热门的单词整个网页列表也可以存储在内存中相当于缓存。在词典中每个单词记录下硬盘或者内存中的网页列表地址这样只要搜索单词就可以快速得到对应的网页地址列表。Bingoo根据列表中的网页编号docID展示对应的网页信息摘要就完成了海量数据的快速检索。
如果用户的搜索词正好是一个单词,比如“高并发”,那么直接查找词典,得到网页列表就完成查找了。但是如果用户输入的是一个句话,那么搜索器就需要将这句话拆分成几个单词,然后分别查找倒排索引。这样的话,得到的就是几个网页列表,还需要对这几个网页列表求交集,才能得到最终的结果列表。
比如,用户输入“高并发架构”进行搜索,那么搜索器就会拆分成两个词:“高并发”、“架构”,得到两个倒排索引:
高并发->2,3,5,7
架构->1,2,4
需要对这两个倒排索引求交集也就是同时包含“高并发”和“架构”的网页才是符合搜索要求的结果最终的交集结果应该是只有一篇网页即docID为2的满足要求。
列表求交集最简单的实现就是双层for循环但是这种算法的时间复杂度是O(n^2)我们的网页列表长度n可能有千万级甚至更高这样的计算效率太低。
一个改进的算法是**拉链法**我们将网页列表先按照docID的编号进行排序得到的就是这样两个有序链表
![图片](https://static001.geekbang.org/resource/image/64/84/6422e8ac0da90ffcb1218552a4471d84.jpg?wh=1920x400)
同时遍历两个链表如果其中一个链表当前指向的元素小于另一个链表当前指向的元素那么这个链表就继续向前遍历如果两个链表当前指向的元素相同该元素就是交集元素记录在结果列表中依此继续向前遍历直到其中一个链表指向自己的尾部nil。
拉链法的时间复杂度是O(2n),远优于双层循环。但是对于千万级的数据而言,还是太慢。我们还可以采用**数据分片**的方式进行并行计算,以实现性能优化。
比如我们的docID分布在\[0, 1万亿)区间而每个倒排索引链表平均包含1千万个docID。我们把所有的docID按照1千亿进行数据分片就会得到10个区间\[0, 1千亿)\[1千亿2千亿)……\[9千亿1万亿)。每个倒排索引链表大致均匀分布在这10个区间我们就可以依照这10个区间范围将每个要遍历的链表切分为10片每片大约包含1百万个docID。两个链表只在自己对应的分片内求交集即可因此我们可以启动10个线程对10个分片进行并行计算速度可提高10倍。
事实上两个1千万长度的链表求交集最终的结果可能不过几万也就是说大部分的比较都是不相等的。比如下面的例子。
![图片](https://static001.geekbang.org/resource/image/1f/94/1f17583a47a03498f0bd7bc6ab426194.jpg?wh=1920x400)
第一个链表遍历到自己的最后一个元素,才和第二个链表的第一个元素相同。那么第一个链表能不能跳过前面那些元素呢?很自然,我们想到可以用**跳表**来实现,如下图。
![图片](https://static001.geekbang.org/resource/image/c1/cb/c178c81263abfcd21e4b836d724347cb.jpg?wh=1920x544)
**跳表实际上是在链表上构建多级索引**在索引上遍历可以跳过底层的部分数据我们可以利用这个特性实现链表的跳跃式比较加快计算速度。使用跳表的交集计算时间复杂度大约是O(log(n))。
此外,虽然搜索引擎利用倒排索引已经能很快得到搜索结果了,但搜索引擎应用还会使用缓存对搜索进行加速,将整个搜索词对应的搜索结果直接放入缓存,以减少倒排索引的访问压力,以及不必要的集合计算。
#### PageRank排名算法
Bingoo使用PageRank算法进行网页结果排名以保证搜索结果更符合用户期待。
PageRank算法会根据网页的链接关系给网页打分。如果一个网页A包含另一个网页B的超链接那么就认为A网页给B网页投了一票。一个网页得到的投票越多说明自己越重要越重要的网页给自己投票自己也越重要。
PageRank算法就是计算每个网页的PageRank值最终的搜索结果也是以网页的PageRank值排序展示给用户。事实证明这种排名方法非常有效PageRank值更高的网页确实更满足用户的搜索期望。
以下面四个网页A、B、C、D举例带箭头的线条表示链接。
![图片](https://static001.geekbang.org/resource/image/25/d9/25394ced1fc911df279e993c7b4ab7d9.jpg?wh=1920x495)
B网页包含了A、D两个页面的超链接相当于B网页给A、D每个页面投了一票如果初始的时候所有页面都是1分那么经过这次投票后B给了A和D每个页面1/2分B包含了A、D两个超链接所以每个投票值1/2分自己从C页面得到1/3分C包含了A、B、D三个页面的超链接每个投票值1/3分
而A页面则从B、C、D分别得到1/21/31分。用公式表示就是
$\\small PRA = \\frac{PRB}{2}+\\frac{PRC}{3}+\\frac{PRD}{1}$
等号左边是经过一次投票后A页面的PageRank分值等号右边每一项的分子是包含A页面超链接的页面的PageRank分值分母是该页面包含的超链接数目。
这样经过一次计算后每个页面的PageRank分值就会重新分配重复同样的算法过程经过几次计算后根据每个页面PageRank分值进行排序就得到一个页面重要程度的排名表。根据这个排名表将用户搜索出来的网页结果排序排在前面的通常也正是用户期待的结果。
但是这个算法还有个问题如果某个页面只包含指向自己的超链接其他页面不断给它送分而自己一分不出随着计算执行次数越多它的分值也就越高这显然是不合理的。这种情况就像下图所示的A页面只包含指向自己的超链接。
![图片](https://static001.geekbang.org/resource/image/e1/45/e1d72f3ff887d48ff6b0bc67d9a00845.jpg?wh=1920x530)
解决方案是设想浏览一个页面的时候有一定概率不是点击超链接而是在地址栏输入一个URL访问其他页面表示在公式上就是
$\\small PRA = \\alpha(\\frac{PRB}{2}+\\frac{PRC}{3}+\\frac{PRD}{1})+\\frac{1-\\alpha}{4}$
上面$\\small 1-\\alpha$就是跳转到其他任何页面的概率通常取经验值0.15(即$\\small \\alpha$ 为0.85)因为有一定概率输入的URL是自己的所以加上上面公式最后一项其中分母4表示所有网页的总数。
那么对于N个网页任何一个页面$\\small P\_{i}$的PageRank计算公式如下
$\\small PageRankP\_{i}=\\alpha \\sum\_{P\_{j}\\in M(P\_{i})}^{}{\\frac{PageRank(P\_{j})}{L(P\_{j})}} + \\frac{1-\\alpha}{N}$
公式中,$\\small P\_{j}\\in M(P\_{i})$ 表示所有包含有$\\small P\_{i}$超链接的$\\small P\_{j}$$\\small L(P\_{j})$表示$\\small P\_{j}$页面包含的超链接数N表示所有的网页总和。由于Bingoo要对全世界的网页进行排名所以这里的N是一个万亿级的数字。
计算开始的时候将所有页面的PageRank值设为1带入上面公式计算每个页面都得到一个新的PageRank值。再把这些新的PageRank值带入上面的公式继续得到更新的PageRank值如此迭代计算直到所有页面的PageRank值几乎不再有大的变化才停止。
## 小结
PageRank算法我们现在看起来平平无奇但是正是这个算法造就了Google近2万亿美元的商业帝国。在Google之前Yahoo已经是互联网最大的搜索引擎公司。按照一般的商业规律如果一个创新公司不能带来十倍的效率或者体验提升就根本没有机会挑战现有的巨头。而Google刚一出现就给Yahoo和旧有的搜索引擎世界带来摧枯拉朽的扫荡用户体验的提升不止十倍这其中的秘诀正是PageRank。
二十几年前,我刚刚接触编程的时候,我们中国也有很多这样的编程英雄,王选、王江民、求伯君、雷军等等,他们几乎凭一己之力就创造出一个行业。正是对这些英雄们的崇拜和敬仰,引领我在编程这条路上一直走下去。软件编程是一个可以创造奇迹的地方,而不只是为了混碗饭吃。梦想不能当饭吃,但是梦想带来的可不止是一碗饭。
## 思考题
PageRank的计算需要在万亿级的数据上进行多次迭代计算才能完成。数据量和计算量都非常大如何完成这样的计算也就是说具体编程实现是怎样的
欢迎在评论区分享你的思考,我们共同进步。