搜索引擎的管道流程| 青训营笔记

160 阅读7分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。由于我们小组决定进行做搜索引擎,经过网上搜索和学习,得知搜索引擎整体架构大致可以分为搜集,预处理,索引,查询。

1. 搜集

我们可以给爬虫一组优质种子网页的链接,然后对这些网页通过广度优先遍历不断遍历这些网页,爬取网页内容,提取出其中的链接,不断将其放入到待爬取队列,然后爬虫不断地从 url 的待爬取队列里提取出 url 进行爬取,重复以上过程。在此期间可以启动多个爬虫并行爬取,这样速度会快很多。

其中必不可少的就是判重,需要对url进行去重操作,有以下几个方法:

  1. 散列表,将每个待抓取 url 存在散列表里,每次要加入待爬取 url 时都通过这个散列表来判断一下是否爬取过。

    但运用此办法可能会需要会出巨大的空间代价。如果用散列表实现的话,由于散列表为了避免过多的冲突,需要较小的装载因子(假设哈希表要装载 10 个元素,实际可能要分配 20 个元素的空间,以避免哈希冲突),同时不管是用链式存储还是用红黑树来处理冲突,都要存储指针;再加上冲突时需要在链表中比较字符串,性能上也是一个损耗。

  1. 布隆过滤器。相比散列表所需内存提升了很多。但是在实际实现过程中,布隆过滤器可能会存在误判的情况,即某个值经过布隆过滤器判断不存在,那这个值肯定不存在,但如果经布隆过滤器判断存在,那这个值不一定存在。针对这种情况我们可以通过调整布隆过滤器的哈希函数或其底层的位图大小来尽可能地降低误判的概率。

爬完网页,网页该如何存储呢,一般是把网页内容存储在一个文件(doc_raw.bin)中,一般的文件系统对单个文件的大小是有限制的,那么再多新建一个就好了。一个 url 对应一个网页 id,我们可以增加一个发号器,每爬取完一个网页,发号器给它分配一个 id,将网页 id 与 url 存储在一个文件里,可命名为 doc_id.bin。

2. 预处理

爬取完一个网页后我们对其进行预处理,我们拿到的是网页的 html 代码,需要把起始终止标签及其中的内容全部去掉即可。做完以上步骤后,我们也要把其它的 html 标签去掉(标签里的内容保留),因为我们最终要处理的是纯内容(内容里面包含用户要搜索的关键词)

3. 分词并创建倒排索引

拿到上述步骤处理过的内容后,需要将这些内容进行分词。

  • 英文分词:如 「I am a chinese」分词后,就有 「I」,「am」,「a」,「chinese」这四个词,从中也可以看到,英文分词相对比较简单,每个单词基本是用空格隔开的,只要以空格为分隔符切割字符串基本可达到分词效果;

  • 中文分词:词与词之类没有空格等字符串分割,比较难以分割。

    不同的模式产生的分词结果不一样,可以参考 jieba 分词开源库,它有如下几种分词模式:全模式、精确模式、新词识别、搜索引擎模式。分词一般是根据现成的词库来进行匹配,比如词库中有「中国」这个词,用处理过的网页文本进行匹配即可。当然在分词之前要把一些无意义的停止词如「的」,「地」,「得」先去掉。经过分词之后我们得到了每个分词与其文本的关系。 其中不同的网页内容有可能出现同样的分词,我们把具有相同分词的网页归在一起。 这样我们在搜「大学」的时候找到「大学」对应的行,就能找到所有包含有「大学」的文档 id 了。

ps. 根据某个词语获取得了一组网页的 id 之后,在结果展示上,哪些网页应该排在最前面呢?为啥我们在 Google 上搜索一般在第一页的前几条就能找到我们想要的答案。这就涉及到搜索引擎涉及到的另一个重要的算法: PageRank。

PageRank是 Google 对网页排名进行排名的一种算法,它以网页之间的超链接个数质量作为主要因素,粗略地分析网页重要性以便对其进行打分。我们一般在搜问题的时候,排名靠前的网页一般是因为 Google 认为这个网页的权重很高,比如说有无数个网页指向此网站的链接等等,根据 PageRank 算法,自然此网站权重就更高。

4. 查询

完成以上步骤,搜索引擎对网页的处理就完了,那么用户输入关键词搜索引擎又是怎么给我们展示出结果的呢。

用户输入关键词后,首先肯定是要经过分词器的处理。接下来就用输出的词去倒排索引里查相应的文档

得到网页 id 后,我们去对应文件中提取出网页的链接和内容,按权重从大到小排列即可。 这里的权重除了和上文说的 PageRank 算法有关外,还与另外一个TF-IDF算法有关。

此外我们在输入 worl 这四个字母后,底下会出现一列提示词。

image.png

提示词的实现基于一种树形结构:Trie 树,即字典树、前缀树(Prefix Tree)、单词查找树,是一种多叉树结构, Trie 树具有以下性质:

  1. 根节点不包含字符,除根节点外的每一个子节点都包含一个字符
  2. 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串
  3. 每个节点的所有子节点包含的字符互不相同
  • 通常在实现的时候,会在节点结构中设置一个标志,用来标记该结点处是否构成一个单词(关键字)。
  • 另外我们发现具有公共前缀的关键字(单词),它们前缀部分在 Trie 树中是相同的

一般搜索引擎会维护一个词库,假设这个词库由所有搜索次数大于某个阈值(如 10000)的字符串组成,我们就可以用这个词库构建一颗 Trie 树,这样当用户输入字母的时候,就可以以这个字母作为前缀去 Trie 树中查找。

Trie 树除了作为前缀树来实现搜索提示词的功能外,还可以用来辅助寻找热门搜索字符串,只要对 Trie 树稍加改造即可。假设我们要寻找最热门的 10 个搜索字符串,则具体实现思路如下: 一般搜索引擎都会有专门的日志来记录用户的搜索词,我们用用户的这些搜索词来构建一颗 Trie 树,但要稍微对 Trie 树进行一下改造,上文提到,Trie 树实现的时候,可以在节点中设置一个标志,用来标记该结点处是否构成一个单词,也可以把这个标志改成以节点为终止字符的搜索字符串个数,每个搜索字符串在 Trie 树遍历,在遍历的最后一个结点上把字符串个数加 1,即可统计出每个字符串被搜索了多少次(根节点到结点经过的路径即为搜索字符串),然后我们再维护一个有 10 个节点的小顶堆。依次遍历 Trie 树的节点,将节点(字符串+次数)传给小顶堆,根据搜索次数不断调整小顶堆,这样遍历完 Trie 树的节点后,小顶堆里的 10 个节点即是最热门的搜索字符串。