数据结构与算法笔记:实战篇 - 剖析搜索引擎背后的经典数据结构和算法

news2024/12/24 20:42:17

概述

像百度、Google 这样的搜索引擎,在我们平时的工作、生活中,几乎天天用到。如果我们把搜索引擎也当做一个互联网产品的话,那它跟社交、电商这些类型的产品相比,有一个非常大的区别,那就是,它是一个技术驱动的产品。所谓技术驱动是指,搜索引擎实现起来,技术难度非常大,技术的好坏直接决定了这个产品的核心竞争力。

在搜索引擎的设计与实现中,会用到大量的算法。有很多针对特定问题的算法,也有很多基础的算法。所以,百度、Google 这样的搜索引擎公司,在面试的时候,会格外注重考察候选人的算法能力。

本章我就借助搜搜引起,这样一个非常有技术含量的产品,来给你展示一下,数据结构和算法如何应用在其中。


整体系统介绍

像 Google 这样的大型商用搜索引擎,有成千上万的工程师,十年如一日地对它进行优化改进,所以,它所包含的技术细节非常多。很难通过一篇文章讲所有的细节都给讲清楚。所以接下来的讲解,主要给你展示,如何在一台机器上(假设这台机器的内存是 8GB,应用是 100 多 GB),通过少量的代码,实现一个小型搜索引擎。不过,麻雀虽小,五脏俱全。跟大型搜索引擎相比,实现这样一个小的搜索引擎的理论基础是相通的。

搜索引擎大致可以分为四个部分:搜集分析索引查询。其中,搜集,就是我们常说的爬虫爬取网页。分析,主要负责网页内容抽取、分词,构建临时索引,计算 PageRank 值这几部分工作。索引,主要负责通过分析阶段得到的临时索引,构建倒排索引。查询,主要负责响应用户的请求,根据倒排索引获取相关网页,计算网页排名,直接返回查询结果给用户。

接下来,我就按照网页处理的生命周期,从这四个阶段,依次来给你讲解,一个网页从北爬取到最终展示给用户,这样一个完整的过程。同时,我会穿插讲解,这个过程中需要用到哪些数据结构和算法。

搜集

现在互联网越来越发达,网站越来越多,对应的网页也就越来越多。对于搜索引擎来说,它事先并不知道网页在哪里。打个比方来说就是,我们只知道海里有很多鱼,但却并不知道鱼在哪里。那搜索引擎是如何搜索网页的呢?

搜索引擎把整个互联网看做数据结构中的有向图,把每个页面看做一个顶点。如果某个页面中包含另外一个页面的链接,那我们就在两个顶点之间连一条有向边。我们可以利用图的遍历搜索算法,来遍历互联网中的网页。

我们前面介绍过两种图的遍历算法,深度优先和广度优先。搜索引擎采用的是广度优先搜索策略。具体点讲的话,那就是,我们先找一些比较知名的网页(专业的叫法是权重比较高)的链接(比如新浪、腾讯主页网址登),作为种子网页链接,放入到队列中。爬虫按照广度优先的策略,不停地从队列中取出链接,然后去爬取对应的网页,解析出网页里包含的其他网页链接,再将解析出来的链接添加到队列中。

基本的原理就是这么简单,但落实到实现层面,还有很多技术细节。下面借助搜索阶段涉及的几个重要文件,来给你解释下搜集工程中有哪些关键技术细节。

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

在光谷优先搜索爬取页面的过程中,爬虫会不停地解析页面链接,将其放到队列中。于是,队列中的链接就会越来越多,可能会多到内存放不下。所以,我们用一个存储在磁盘中的文件(link.bin)来作为广度优先搜索中的队列。爬虫从 links.bin 文件中,取出链接去爬取对应的页面。等待爬取到网页之后,将解析出来的链接,直接存储到 links.bin 文件中。

这样用文件来存储网页链接的方式,还有其他好处。比如,支持断点续爬。也就是说,当机器断电之后,网页链接不会丢失;当机器重启之后,还可以从之前爬取到的位置继续爬取。

2.网页判重文件:bloom_filter.bin

如何避免重复爬取相同的网页呢?这个问题我们在位图那一节已经讲过了。使用布隆过滤器,我们就可以快速并且非常节省内存地实现网页的判重。

不过,还是刚刚那个问题,如果我们把布隆过滤器存储在内存中,那机器重启之后,布隆过滤器就被清空了。这样就可能导致大量已经爬取的网页会被重复爬取。

这个问题该怎么解决呢?我们可以定期地(比如每隔半小时)将布隆过滤器持久化到磁盘中,存储在 bloom_filter.bin 文件中。这样,即便出现机器宕机,也只会丢失布隆过滤器中的部分数据。当机器重启之后,我们就可以重新读取磁盘中的 bloom_filter.bin 文件,将其恢复到内存中。

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

爬取到网页之后,我们需要将其存储下来,以备后续离线分析、索引。那如何存储海量的原始网页数据呢?

如果我们把每个网页都存储在一个独立的文件,那磁盘中的文件就会非常多,数量可能会有几千万,甚至上亿。常用的文件系统显然不适合存储如此之多的文件。所以,我们可以把多个网页存储在一个文件中。每个网页之间,通过一定的标识进行分割,方便后续读取。具体的存储格式如下图所示。其中,doc_id 这个字段是网页中的编号,我们待会解释。

在这里插入图片描述

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

假设一台机器的硬盘大小是 100GB 左右,一个网页的平均大小是 64KB。那在一台机器上,我们可以存储 100 万到 200 万左右的网页。假设我们的机器带宽是 10MB,那下载 100GB 的网页,大约需要 10000 秒。也就是说,爬取 100 多万的网页,也就是只需要花费几小时的事件。

4.网页链接及其对应的编号的对应文件:doc_id.bin

刚刚提到了网页编号这个概念,现在解释一下。网页编号实际上就是给每个网页分配的一个唯一 ID,方便我们后续对网页进行分析、索引。那如何给网页编号呢?

我们可以按照网页被爬取的先后顺序,从小到大依次编号。具体是这样做的:我们维护一个中心的计数器,每爬取到一个网页之后,就从计数器中拿一个号码,分配给这个网页,然后计数器加一。在存储网页的同时,我们将网页链接跟编号之间的对应关系,存储在另一个 doc_id.bin 文件中。

爬虫在爬取网页的过程中,涉及的四个重要的文件,就介绍完了。其中,links.bin 和 bloom_filter.bin 这两个文件是爬虫自身所用的。另外两个(doc_raw.bin、doc_id.bin)是作为搜索阶段的成果,工后面的分析、索引、查询用的。

分析

网页爬取下来之后,我们需要对网页进行离线分析。分析阶段主要包括两个步骤,第一个是抽取网页文本信息,第二个是分词并创建临时索引。我们逐一来讲解。

1.抽取网页文本信息

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

我们之所以把网页叫做半结构化数据,是因为它本身是按照一定的规则来书写的。这个规则就是HTML 语法规范。我们依靠 HTML 标签来抽取网页中的文本信息。这个抽取的过程,大体可以分为两步。

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

第二步是去掉所有 HTML 标签。这一步也是通过字符串匹配算法来实现的。过程跟第一步类似,就不重复讲了。

2.分词并创建临时索引

经过上面的处理之后,我们就从网页中抽取了我们关心的文本信息。接下来,我们要对文本信息进行分词处理,并创建临时索引。对于英文网页来说,分词非常简单。我们只需要通过空格、标点符号等分隔符,将每个单词分割开来就可以了。但是,对于中文来书,分词就复杂太多了。这里介绍一种比较简单的思路,基于字典和规则的分词方法。

其中,字典也叫词库,里面包含大量常用的词库(可以从直接从网上下载别人整理好的)。我们借助词库,并采用最长匹配规则来对文本进行分词。所谓最长匹配,也就是匹配尽可能长的词语。我举个例子解释下。

比如要分词的文本是 “中国人民解放了”,我们词库中有 “中国” 、“中国人”、 “中国人民” 、“中国人民解放军” 这几个词,那我们就取最长匹配,也就是 “中国人民” 划为一个词,而不是把 “中国”、“中国人” 划为一个词。具体到实现层面,我们可以将词库中的单词,构建成 Trie 树,然后那网页文本在 Trie 树中匹配。

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

在这里插入图片描述

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

给单词编号的方式,跟给网页编号的方式类似。我们维护一个计数器,每当从网页文本信息中分割出一个新的单词时,我们就从计数器中取一个编号,分配给它,然后计数器加一。

在这个过程中,我们还需要使用散列表,记录已经编过号的单词。在对网页文本信息分词的过程中,我们拿分割出来的单词,先到散列表中查找,如果找到,那就直接使用已有的编号;如果没有找到,我们再去计数器中拿号码,并且将这个新单词以及编号添加到散列表中。

当所有的网页处理(分词及临时索引)完成之后,我们再将这个单词跟编号之间的对应关系写入到磁盘文件中,并命名为 term_id.bin。

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

索引

索引阶段主要负责将分析阶段产生的临时索引,构建成倒排索引。倒排索引(Inverted index)中记录了每个单词以及包含它的网页列表。文字描述比较难理解,我画了一张倒排索引的结构图,你一看就明白。

在这里插入图片描述
刚刚讲到,在临时索引文件中,记录的是单词跟每个包含它的文档之间的对应关系。那如何通过临时索引文件,构建出倒排索引文件呢?这是第一个非常典型的算法问题,你可以先自行思考下,再看兴下面的讲解。

解决这个问题的方法有很多。考虑到临时索引文件很大,无法一次性假造到内存中,搜索引擎一般会选择使用多路归并排序的方法来实现。

我们先对临时索引文件,按照单词编号的大小进行排序。因为临时索引很大,所以一般基于内存的排序算法就没法处理这个问题了。我们可以用之前讲的归并排序的处理思想,将器分割成多个小文件,先对每个小文件独立排序,最后再合并在一起。当然,实际的软件开发中,我们其实可以直接利用 MapReduce 来处理。

临时索引文件排序完成之后,相同的单词就背排列到一起了。我们只需要顺序地遍历排好序的临时索引文件,就能将每个单词对应的网页编号列表找出来,然后把它们存储在倒排索引文件中。具体的处理过程,如下图所示。

在这里插入图片描述

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

在这里插入图片描述

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

查询

前面三个阶段的处理,只是为了最后的查询做铺垫。因此,现在我们就要利用之前产生的几个文件,来实现最终的用户搜索功能。

  • doc_id.bin:记录网页链接和编号之间的关系
  • term_id.bin:记录单词和单词编号之间的关系
  • index.bin:倒排索引文件,记录每个单词编号以及对应包含它的网页编号列表
  • term_offset.bin:记录每个单词编号在倒排索引文件中的偏移位置

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

当用户在搜索框中,输入某个查询文本的时候,我们先对用户输入的文本进行分词处理。假设分词之后,我们得到 k 个单词。

我们拿这 k 个单词,去 term_id.bin 对应的散列表中,查找每个单词对应的单词编号。经过这个查找之后,我们得到了这 k 个单词对应的单词编号。

我们拿这 k 个单词编号,去 term_offset.bin 对应的散列表中,查找每个单词编号在倒排索引文件中的偏移位置。经过这个查询之后,我们得到了 k 个偏移位置。

我们拿着 k 个偏移位置,去倒排索引(index.bin)中,查找 k 个单词对应的包含它的网页编号列表。经过这一步骤查询之后,我们得到了 k 个网页编号列表。

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

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

总结

本章给你展示了一个小型搜索引擎的设计思路。这只是一个搜索引擎设计的基本原理,有很多优化、细节我们并未涉及,比如计算网页权重的 PageRank 算法、计算查询结果排名的 tf-id 模型等等。

在讲解的过程中,我们设计的数据结构和算法有:图、散列表、Trie 树、布隆过滤器、单模式字符串匹配算法、AC 自动机、广度优先遍历、归并排序等等。如果对其中的哪些内容不清楚,可以回到对应的章节进行复习。

最后,如果有时间的话,强烈建议你,按照文章的思路,自己写代码实现一个简单的搜索引擎。这样写出来的,即便只是一个 demo,但对于你深入理解数据结构和算法,也是很有帮助的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1884994.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

内容分发网络(CDN)学习记录

目录 静态内容动态内容CDN工作原理CDN缓存 CDN关键技术1.内容路由功能2.内容分发技术&#xff1a;内容分发技术主要是PUSH和PULL3.内容存储技术4.内容管理技术 全局负载均衡基于DNS的GSLB基于HTTP重定向的GSLB基于IP欺骗的GSLB服务器群选择策略 静态内容 静态内容是不会因用户…

隐私计算实训营第二期第七课:XGB算法与SGB算法开发实践

隐私计算实训营第二期-第七课 第七课&#xff1a;XGB算法与SGB算法开发实践1 决策树模型1.1 决策树的训练和预测过程1.2 决策树的发展过程 2 GBDT模型2.1 Boosting核心思想2.2 GBDT原理 3 XGB模型3.1 XGB核心思想3.2 XGB优点 3 隐语纵向树模型3.1 数据纵向分割3.2 隐私保护的树…

学习笔记——动态路由——IS-IS中间系统到中间系统(背景)

一、IS-IS技术背景 1、前言 IS-IS最初是国际标准化组织ISO(the International Organization for Standardization)为它的无连接网络协议CLNP(ConnectionLess Network Protocol)设计的一种动态路由协议。 和OSPF一样&#xff0c;IS-IS也是一种基于链路状态并使用最短路径优先…

医疗器械FDA认证中,如何准备SBOM文件?

在医疗器械FDA认证过程中&#xff0c;准备SBOM&#xff08;软件物料清单&#xff09;文件是确保医疗器械软件部分符合FDA要求的关键步骤。以下是准备SBOM文件的清晰指南&#xff1a; 一、了解SBOM文件的目的和重要性 SBOM文件用于详细列出医疗器械所使用的所有软件组件、版本…

程序员节视频创意盛宴,邀您共赴创意之旅!

&#x1f31f; 程序员节专属创意大赛震撼启幕&#xff01; &#x1f389; 亲爱的程序员及编程爱好者们&#xff0c;是时候拿起你们的镜头&#xff0c;捕捉那些日常中闪光的编程瞬间&#xff0c;向世界展示代码编织的奇迹与无限创意了&#xff01;&#x1f4bb;✨ &#x1f3c…

惠海 H6900B 2.7V3.7V4.2V5V9V升12V24V48VLED升压恒流芯片IC

惠海H6900B LED升压恒流芯片IC是一款功能丰富的LED驱动解决方案&#xff0c;为高亮度LED灯串设计。以下是针对该产品的进一步分析和解释&#xff1a; 产品特点 高效率&#xff1a;高达95%以上的效率意味着在驱动LED时&#xff0c;只有很少的能量转化为热量&#xff0c;从而提…

【Kaggle】Telco Customer Churn 数据编码与模型训练

&#x1f4ac;在上一部分中&#xff0c;我们已经完成了对数据集背景解读、数据预处理与探索性分析。在数据背景解读中&#xff0c;我们介绍了数据集来源、电信用户流失分析的基本业务背景&#xff0c;并详细解释了每个字段的基本含义&#xff1b;在数据预处理过程中&#xff0c…

全国WMS厂商大盘点,哪家未来能杀出重围?

导语 大家好&#xff0c;我是社长&#xff0c;老K。专注分享智能制造和智能仓储物流等内容。 新书《智能物流系统构成与技术实践》人俱乐部 一、引言 随着物流行业的快速发展&#xff0c;仓储管理作为其重要的一环&#xff0c;正逐步受到越来越多企业的重视。当前&#xff0c;市…

可燃气体报警器检测标准对比:不同标准的优缺点分析

在工业生产及家庭生活中&#xff0c;可燃气体报警器发挥着至关重要的作用。它能够实时监测空气中可燃气体浓度&#xff0c;一旦超过安全阈值&#xff0c;便会发出警报&#xff0c;避免火灾和爆炸事故的发生。 接下来&#xff0c;佰德将重点探讨可燃气体报警器的检测标准&#…

三层交换基础

一、什么是三层交换 三层交换是一种在OSI模型第三层&#xff0c;即网络层上工作的网络设备技术&#xff0c;它整合了二层交换机的功能和路由器的部分功能&#xff0c;以实现更高效的网络数据转发和路由选择。三层交换技术的核心在于结合了二层交换技术和三层转发技术&#xff…

Java服务器代码远程调试(IDEA版)

Java服务器代码远程调试 配置启动脚本参数配置IDEA远程调试工具操作步骤 注意&#xff1a;远程调试的代码需要与本地代码一致&#xff0c;远程调试目的是解决本地环境无法支持调试的情况下&#xff0c;解决线上&#xff08;测试&#xff09;环境调试问题。 配置启动脚本参数 n…

Java Lambda语法介绍

目录 一、概述 二、Lambda语法的历史 2.1 Lambda名字的含义 2.2 Lambda的历史 三、Lambda语法的核心接口 3.1 Lambda的四大核心接口 3.1.1 概述 3.1.2 Consumer 接口 3.1.3 Supplier 接口 3.1.4 Function 接口,> 3.1.5 Predicate 接口 四、Lambda的引用 4.1 概…

统计鸟:小而美的网站流量统计工具,免费好用

目前常见的网站流量统计平台有百度统计、Google Analytics、51.LA、友盟等&#xff0c;但Google Analytics在国内打不开、友盟已收费、百度统计限制较多、51.LA也很可能走向收费&#xff0c;无奈的我总算在网上搜到一款小众而好用的网站流量统计工具——统计鸟&#xff0c;现给…

探索Toshiba东芝TLP265J光耦合器

在当今的电子元件市场中&#xff0c;寻找高质量和高可靠性的光耦合器解决方案对许多工程师和设计师来说至关重要。TLP265J这款产品在性能、安全性和可靠性方面均表现卓越&#xff0c;适用于多种工业和商业应用。本文将深入探讨TLP265J的特点、应用领域以及其技术优势&#xff0…

探索PcapPlusPlus开源库:网络数据包处理与性能优化

文章目录 0. 本文概要1. PcapPlusPlus介绍1.1 概述1.2主要特性和功能1.3 PcapPlusPlus 主要模块关系和依赖1.4 网络协议层处理过程 2. 实例2.1 基于 PcapPlusPlus 的应用程序设计和封装流程&#xff1a;2.2 多线程示例代码2.3 代码说明&#xff1a; 3. 程序性能进一步优化3.1 避…

基于人脸识别的考勤系统(Qt+opencv+Arm)测试报告

目录 1、项目描述 2、服务器端测试 2.1、注册人脸 A、填写基本信息 B、点击打开摄像头采集信息 C、点击拍照&#xff0c;提示数据库保存人脸信息成功 D、点击注册&#xff0c;提示注册成功 至此&#xff0c;服务器启动测试完毕 3、客户端测试 3.1、人脸验证测试 3.2、服务器端点…

2024 年江西省研究生数学建模竞赛A题:交通信号灯管理问题分析、实现代码及参考论文

2024 年江西省研究生数学建模竞赛题目交通信号灯管理 1 题目 交通信号灯是指挥车辆通行的重要标志&#xff0c;由红灯、绿灯、 黄灯组成。红灯停、绿灯行&#xff0c;而黄灯则起到警示作用。交通 信号灯分为机动车信号灯、非机动车信号灯、人行横道信号 灯、方向指示灯等。 一…

生产环境 CentOS 7 k8s v1.28.0离线部署

背景描述&#xff1a;CentOS 7 Kubernetes 离线部署 随着云计算和微服务架构的普及&#xff0c;Kubernetes&#xff08;K8s&#xff09;已经成为容器编排的标准工具。它能够自动化应用的部署、扩展和管理&#xff0c;使得开发和运维的工作更加高效和可靠。然而&#xff0c;在一…

(亲测有效)2024代替电视家的app,电视家停了还有什么软件可以看电视?

嘿&#xff0c;大家好&#xff0c;我是阿星&#xff0c;今天又来跟大家聊聊那些让人眼前一亮的电视直播软件。咱们这回不聊那些老掉牙的&#xff0c;来点新鲜的&#xff0c;让咱们的电视屏幕也能跟上潮流&#xff0c;享受一下科技带来的便利和乐趣。 首先&#xff0c;得提一提…

【Sklearn-驯化】一文分析教你如何使用k-means进行数据聚类

【Sklearn-驯化】一文分析教你如何使用k-means进行数据聚类 本次修炼方法请往下查看 &#x1f308; 欢迎莅临我的个人主页 &#x1f448;这里是我工作、学习、实践 IT领域、真诚分享 踩坑集合&#xff0c;智慧小天地&#xff01; &#x1f387; 免费获取相关内容文档关注&#…