剖析搜索引擎背后的经典数据结构和算法

124 阅读9分钟

剖析搜索引擎背后的经典数据结构和算法

所谓技术驱动是指,搜索引擎实现起来,技术难度非常大,技术的好坏直接决定了这个产品的核心竞争力

整体系统

如何在一台机器上,实现搜索引擎?内存8GB,硬盘100GB

搜索引擎:

  1. 搜集:爬虫爬网页
  2. 分析:网页内容抽取,分词,构建临时索引
  3. 索引:倒排索引
  4. 查询:响应用户请求,返回查询结果

搜集

把互联网看作有向图,每个页面看作一个顶点,1页面包含2页面链接,连有向边,利用图的遍历搜索算法遍历互联网网页,搜索引擎BFS,找到一个页面作为种子网页链接放入队列,然后开始BFS

待爬取网页链接文件:links.bin

队列,从这个文件取链接爬页面

用文件存储网页链接好处:断点爬取:就算断电网页链接不会丢失

如何解析页面获取链接?

页面是大字符串,用字符串匹配算法,搜索一个网页标签,顺序读取之间的字符串

网页判重文件:bloom_filter.bin

如果把布隆过滤器存储在内存,那机器宕机重启布隆过滤器就被清空了,白干了,怎么解决?

定期(半小时)将布隆过滤器持久化到磁盘,存储在bloom_filter.bin,这样就算宕机也只是丢失布隆过滤器中的数据

原始网页存储文件:doc_raw.bin

我们可以把多个网页存储在一个文件中。每个网页之间,通过一定的标识进行分隔

image.png

当然,这样的一个文件也不能太大,因为文件系统对文件的大小也有一定的限制。所以,我们可以设置每个文件的大小不能超过一定的值(比如 1GB)。随着越来越多的网页被添加到文件中,文件的大小就会越来越大,当超过 1GB 的时候,我们就创建一个新的文件,用来存储新爬取的网页。

一台机器硬盘100GB,一个网页平均大小64KB,可以存储100w-200w左右网页,假设机器带宽10MB,下载100GB网页,大约10000秒,也就是爬取100多万个网页只要几个小时

网页链接以及编号对应文件:doc_id.bin

网页编号:每个网页分配的唯一的ID,方便对网页分析、索引

如何给网页编号?

按网页被爬取先后顺序,从小大依次编号。具体:维护一个中心的计数器,爬到一个网页,就从计数器拿一个好吗分配给这个网页,然后计数器加1,存储网页同时将网页链接和编号对应关系存储在doc_id.bin文件中

links.bin 和bloom_filter.bin 这两个文件是爬虫自身所用的。另外的两个(doc_raw.bin、doc_id.bin)是作为搜集阶段的成果,供后面的分析、索引、查询用的。

分析

抽取网页文本信息

网页是半结构化数据,里面夹杂着各种标签、JavaScript 代码、CSS 样式。对于搜索引擎来说,它只关心网页中的文本信息,也就是,网页显示在浏览器中时,能被用户肉眼看到的那部分信息。我们如何从半结构化的网页中,抽取出搜索引擎关系的文本信息呢?

半结构化数据:HTML语法规范

这个抽取的过程,大体可以分为两步:

  1. 去掉 JavaScript 代码、CSS 格式以及下拉框中的内容(因为下拉框在用户不操作的情况下,也是看不到的)。也就是,,/option>这三组标签之间的内容。我们可以利用 AC 自动机这种多模式串匹配算法,在网页这个大字符串中,一次性查找, , 这三个关键词。当找到某个关键词出现的位置之后,我们只需要依次往后遍历,直到对应结束标签(, , </option)为止。而这期间遍历到的字符串连带着标签就应该从网页中删除。
  2. 去掉所有 HTML 标签。这一步也是通过字符串匹配算法来实现的。过程跟第一步类似

分词并创建临时索引

对于英文网页来说,分词非常简单。我们只需要通过空格、标点符号等分隔符,将每个单词分割开来就可以了。但是,对于中文来说,分词就复杂太多了,一种比较简单的思路,基于字典和规则的分词方法。其中,字典也叫词库,里面包含大量常用的词语(我们可以直接从网上下载别人整理好的)。我们借助词库并采用最长匹配规则,来对文本进行分词。所谓最长匹配,也就是匹配尽可能长的词语。比如要分词的文本是“中国人民解放了”,我们词库中有“中国”“中国人”“中国人民”“中国人民解放军”这几个词,那我们就取最长匹配,也就是“中国人民”划为一个词,而不是把“中国”、“中国人“划为一个词。具体到实现层面,我们可以将词库中的单词,构建成 Trie 树结构,然后拿网页文本在 Trie 树中匹配。

每个网页的文本信息在分词完成之后,我们都得到一组单词列表。我们把单词与网页之间的对应关系,写入到一个临时索引文件中(tmp_Index.bin),这个临时索引文件用来构建倒排索引文件。临时索引文件的格式如下:

image.png

在临时索引文件中,我们存储的是单词编号,也就是图中的 term_id,而非单词本身。这样做的目的主要是为了节省存储的空间。那这些单词的编号是怎么来的呢?

给单词编号的方式,跟给网页编号类似。我们维护一个计数器,每当从网页文本信息中分割出一个新的单词的时候,我们就从计数器中取一个编号,分配给它,然后计数器加一。在这个过程中,我们还需要使用散列表,记录已经编过号的单词。在对网页文本信息分词的过程中,我们拿分割出来的单词,先到散列表中查找,如果找到,那就直接使用已有的编号;如果没有找到,我们再去计数器中拿号码,并且将这个新单词以及编号添加到散列表中当所有的网页处理(分词及写入临时索引)完成之后,我们再将这个单词跟编号之间的对应关系,写入到磁盘文件中,并命名为 term_id.bin。

经过分析阶段,我们得到了两个重要的文件。它们分别是临时索引文件(tmp_index.bin)和单词编号文件(term_id.bin)。

索引

索引阶段主要负责将分析阶段产生的临时索引,构建成倒排索引。倒排索引( Invertedindex)中记录了每个单词以及包含它的网页列表

如何通过临时索引文件,构建出倒排索引文件呢?

多路归并排序

image.png

除了倒排文件之外,我们还需要一个文件,来记录每个单词编号在倒排索引文件中的偏移位置。我们把这个文件命名为 term_offset.bin。这个文件的作用是,帮助我们快速地查找某个单词编号在倒排索引中存储的位置,进而快速地从倒排索引中读取单词编号对应的网页编号列表。

image.png

经过索引阶段的处理,我们得到了两个有价值的文件,它们分别是倒排索引文件(index.bin)和记录单词编号在索引文件中的偏移位置的文件(term_offset.bin)。

查询

doc_id.bin:记录网页链接和编号之间的对应关系。

term_id.bin:记录单词和编号之间的对应关系。

index.bin:倒排索引文件,记录每个单词编号以及对应包含它的网页编号列表。

term_offsert.bin:记录每个单词编号在倒排索引文件中的偏移位置。

除了倒排索引文件(index.bin)比较大之外,其他的都比较小。为了方便快速查找数据,我们将其他三个文件都加载到内存中,并且组织成散列表这种数据结构

当用户在搜索框中,输入某个查询文本的时候,我们先对用户输入的文本进行分词处理。假设分分词之后,我们得到 k 个单词。我们拿这 k 个单词,去 term_id.bin 对应的散列表中,查找对应的单词编号。经过这个查询之后,我们得到了这 k 个单词对应的单词编号。我们拿这 k 个单词编号,去 term_offset.bin 对应的散列表中,查找每个单词编号在倒排索引文件中的偏移位置。经过这个查询之后,我们得到了 k 个偏移位置。

我们拿这 k 个偏移位置,去倒排索引(index.bin)中,查找 k 个单词对应的包含它的网页编号列表。经过这一步查询之后,我们得到了 k 个网页编号列表。我们针对这 k 个网页编号列表,统计每个网页编号出现的次数。具体到实现层面,我们可以借助散列表来进行统计。统计得到的结果,我们按照出现次数的多少,从小到大排序。出现次数越多,说明包含越多的用户查询单词(用户输入的搜索文本,经过分词之后的单词)。

经过这一系列查询,我们就得到了一组排好序的网页编号。我们拿着网页编号,去doc_id.bin 文件中查找对应的网页链接,分页显示给用户就可以了。

思考

1.搜索引擎为什么选择广度优先策略,而不是深度优先策略呢?

因为搜索引擎要优先爬取权重较高的页面,离种子网页越近,较大可能权重更高,广度优先更合适。