检索用的好,下班走的早!值得收藏

IT可乐

共 3321字,需浏览 7分钟

 ·

2022-03-06 13:50

这篇文章是可乐花了很长时间总结出来的,注意:本篇文章不是教大家如何检索,而是教大家如何设计检索系统。

我从宏观层面介绍了各种数据结构和算法,并在结尾介绍了如何设计一个搜索系统,下面是本文目录截图,相信大家看完一定会有收获的。


1、什么是检索?

指从用户特定的信息需求出发,对特定的信息集合采用一定的方法、技术手段,根据一定的线索与规则从中找出相关信息

对应到我们实际工作中,检索其实就是:

如何用最小的内存(物理成本),最快(时间成本)的取出我们需要的数据。

2、检索体系架构

3、存储介质层

3.1 磁盘为什么能存储数据

机械硬盘的磁盘主体是一块金属薄片(也有用其他材料的),上面涂覆一层磁性材料,可以理解为一层小磁针。

硬盘工作时,磁盘在马达的驱动下高速旋转,转速高达数千转每分钟,磁头则在磁头驱动系统的的控制下,在高速旋转的磁盘表面飞行。

①、写数据

磁头线圈上通电,在其周围产生磁场,磁化磁盘表面的磁性材料,不同方向的电流产生的磁场方向不同,磁盘表面的磁性材料被磁化的极性也不同,不同极性便代表0与1;

②、读数据

磁头线圈切割磁盘表面的磁性材料的磁场,产生电信号,不同极性的磁性材料产生的感应电流方向不同,因此可以读出0与1。

注意:断电并不会影响磁盘表面的磁性材料的极性,因此断电后数据仍然不会消失,但剧烈的碰撞或加热则有可能导致数据丢失。

3.2 磁盘和内存的区别

①、持久性

磁盘能永久存储(HDD10年,SDD5年),断电不丢失数据;

内存断电即丢失数据。

②、容量

磁盘通常是几百G到几个T;

内存通常是几个G到几十个G。

③、价格

内存 > 磁盘

④、读写速度

内存 > SDD > HDD

4、数据结构层

4.1 数组

数组结构

1.数组是相同数据类型的元素的集合。

2.数组各元素是按照先后顺序连续存储的。

3.插入慢:无序数组末尾插入快,其余情况需要维护数组地址连续效率都是比较差。

4.查找:支持下标随机查找快,有序数组也可以用诸如二分法加快查找速度。

5.删除慢:和插入类似,除了末尾插入快。其余情况需要维护数组地址连续都比较慢。

4.2 链表

链表结构

1.链表物理存储单元上非连续(可以充分利用计算机内存)、非顺序的存储结构。

2.不支持随机读取。

3.存储空间会增大,比如单向链表每个节点都会存储下一个节点的引用。

4.插入快。

5.删除快。

6.查找慢。

其余比如栈、队列、二叉树,红黑树,B+树等等都是这两种数据结构的单独变化或组合变化。

4.3 栈

栈只支持两个基本操作:入栈 push()和出栈 pop()。

典型应用:

①、实现字符串逆序;

②、判断标签是否匹配;

③、计算机中的函数调用;

4.4 队列

和栈类似,也只支持两个操作:入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。

①、单向队列(Queue):只能在一端插入数据,另一端删除数据。

②、双向队列(Deque):每一端都可以进行插入数据和删除数据操作。

③、优先级队列(Priority Queue):数据项按照关键字进行排序,关键字最小(或者最大)的数据项往往在队列的最前面,而数据项在插入的时候都会插入到合适的位置以确保队列的有序。

④、阻塞队列(Block Queue):在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。

典型的生产者-消费者模型。

⑤、并发队列

典型应用:

①、线程池

②、数据库连接池

对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过“队列”这种数据结构来实现请求排队。

4.5 树

①、二叉树

②、红黑树

③、B+树

典型应用:关系型数据库存储数据结构。

1.数据很大,不可能全部存储在内存中,还要持久化,故要存储到磁盘上。

2.减少查找过程中磁盘I/O的存取次数。

局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。

与磁盘预读,预读的长度一般为页(page)的整倍数,(在许多操作系统中,页得大小通常为4k)

叶子节点数据多。

3.支持范围查找

④、LSM 树

Log Structured Merge Trees

一个日志系统有如下特征:

一、数据量大且持续生成

二、写入操作会特别频繁

三、查询快,且查询一般不是全范围的随机搜索,而是近期检索。

B+ 树为什么不行?(叶子节点存储在磁盘中,需要随机写磁盘,数据量大会导致性能急剧下降)

LSM 树:

⑤、Trie 树

字典树、前缀树、单词查找树。

典型应用:

字符串检索

百度谷歌搜索框

拼写检查

4.6 跳表

链表的基础上增加了多级索引。

Redis 中的有序集合(Sorted Set)就是用跳表来实现的。

4.7 散列表

通过把关键值映射到表中一个位置来访问记录,这个映射函数叫做散列函数,存放记录的数组叫做散列表。

解决哈希冲突:

①、开放寻址法:线性探测、双重散列

②、链表法

散列表设计原则:

①、散列函数

②、初始容量;

③、装载因子;

④、散列冲突解决办法;

典型应用:

①、有限的数据集合中快速查询数据

比如:Word 文档中单词拼写检查功能是如何实现的?

常用的英文单词有 20 万个左右,假设单词的平均长度是 10 个字母,平均一个单词占用 10 个字节的内存空间,那 20 万英文单词大约占 2MB 的存储空间,就算放大 10 倍也就是 20MB。

所以可以将全部英文单词放到散列表,用户输入单词直接去散列表里面查,没有就报错。

②、词频统计、访问统计等等。

4.8 布隆过滤器

简单来说就是一个二进制数组。

典型应用:数据海量,不要求一定准确的场景。

①、判断ID是否已经注册,即使误判也能容忍。

②、爬虫判断网页是否已经爬过。

4.9 图

存储:

①、邻接矩阵

②、邻接表

DFS(Deep First Search)深度优先搜索算法

BFS(Breath First Search)广度优先搜索算法

img

飞机航线

电子线路

城市地图

好友关系

5、算法层

比较好用的查找算法是二分法O(logn),在有序的数据结构中是特别bug的,但是如何进行快速的排序,有如下常用的排序算法:

实际应用:

①、如何根据年龄给100W用户排序?

利用桶排序,从1岁到150岁(有人会说超过150岁,这里超过三界之外的人不算),建立150个桶,然后遍历这100W个用户,依次放入150个桶中,遍历完,边排好序了。

②、如何快速查询每个考生的高考排名?

同样也是桶排序,高考分数0-750,也就是顶多 750 个桶。

6、业务设计层

6.1 爬虫系统

通过高性能的爬虫系统来完成网页的持续抓取,然后将抓取到的网页存入存储平台中。

一般来说是是将抓取到的网页存放在基于 LSM 的 HBase 中,以便支持数据的高效读写。

①、爬取网页

首先找到权重较高的网页,比如新浪、腾讯,通过广度优先搜索算法放入爬取队列中;

计算网页权重算法:PageRank

网页太多,持久化队列,便于断点爬取。

如何爬取网页链接:可以获取到网页的 HTML 文件,看成一个大的字符串,然后利用字符串匹配算法,获取 或者这样的标签内容。

image-20211129223812752

②、网页去重

利用布隆过滤器。

需要注意的是:布隆过滤器是在内存中的,如果机器重启,布隆过滤器就会被清空,防止网页重复爬取,需要持久化布隆过滤器,比如定时每半小时持久化一次。

③、原始网页存储

便于后面的离线分析,索引构建,需要将海量的原始网页存储。

网页很多,通常的文件系统不适合存储这么多的文件,而是将多个网页存储在一个文件中。

④、网页编号和链接存储

上一步给每个网页分配了一个id,在存储网页的同时,也将网页编号和网页链接存储在一个文件中。

6.2 分析索引系统

①、抽取网页文本信息

网页都是遵循 HTML 规范的,只需要去掉JavaScript代码、CSS代码,还有比如下拉框的代码。

在网页这个大字符串中,一次性查找 , ,

②、网页质量分析

去掉低质量的垃圾网页

③、反作弊

避免一些作弊网页来干扰搜索结果

④、分词创建临时索引

抽取到网页文本信息之后,对文本信息进行分词,并创建临时索引文件。

英文网页:只需要通过空格、标点符号等分隔符,将每个单词分割开来就可以了。

中文网页:借助词库并采用最长匹配规则,来对文本进行分词。

临时索引文件如下:

注意这里存的是单词编号,因为单词很多,为了节省内存,用一个散列表存储:单词编号-单词。

⑤、通过临时索引创建倒排索引

⑥、记录单词编号在倒排索引文件的偏移位置

帮助我们快速地查找某个单词编号在倒排索引中存储的位置,进而快速地从倒排索引中读取单词编号对应的网页编号列表。

6.3 查询

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

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

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

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

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

然后对这 k 个单词进行纠错模型判断:

image-20211202232035291

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

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

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

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

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

10、总结

检索核心思路:通过合理的组织数据,尽可能的快速减少查询范围。

①、合理选择存储介质、存储数据结构;

②、合理创建索引,使得索引和数据分离;

③、减少磁盘IO,将频繁读取的数据加载到内存中;

④、读写分离;

⑤、分层处理;


参考文档:极客时间《数据结构和算法之美》

关于我

可乐是一个热爱技术的Java程序猿,公众号「IT可乐」定期分享有趣有料的精品原创文章!
非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!
愿你我人生尽量没有遗憾的事,愿你我都能奔赴在各自想去的路上。
浏览 64
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报