08 | 索引构建:搜索引擎如何为万亿级别网站生成索引?

简介: 针对超大规模数据场景,如搜索引擎对万亿网页索引时,倒排索引远超内存容量。为此,工业界采用分治与多路归并思想:先将文档分批,在内存中为每批构建小型倒排索引,并写入磁盘临时文件;随后通过多路归并技术合并有序临时文件,生成全局有序的最终倒排文件。该过程类似MapReduce模型,支持分布式并行处理,大幅提升效率。检索时,优先将词典加载至内存(如用哈希表或FST压缩结构),结合B+树或跳表等技术高效访问磁盘中的posting list,辅以缓存机制优化频繁查询,实现高效检索。

对基于内容或者属性的检索场景,我们可以使用倒排索引完成高效的检索。但是,在一些超大规模的数据应用场景中,比如搜索引擎,它会对万亿级别的网站进行索引,生成的倒排索引会非常庞大,根本无法存储在内存中。这种情况下,我们能否像 B+ 树或者 LSM 树那样,将数据存入磁盘呢?
如何生成大于内存容量的倒排索引?
我们先来回顾一下,对于能够在内存中处理的小规模的文档集合,我们是如何生成基于哈希表的倒排索引的。步骤如下:

  1. 给每个文档编号,作为它们的唯一标识,并且排好序;
  2. 顺序扫描每一个文档,将当前扫描的文档中的所有内容生成 < 关键字,文档 ID,关键字位置 > 数据对,并将所有的 < 关键字,文档 ID,关键字位置 > 这样的数据对,都以关键字为 key 存入倒排表(位置信息如果不需要可以省略);
  3. 重复第 2 步,直到处理完所有文档。这样就生成一个基于内存的倒排索引。

对于大规模的文档集合,如果我们能将它分割成多个小规模文档集合,是不是就可以在内存中建立倒排索引了呢?这些存储在内存中的小规模文档的倒排索引,最终又是怎样变成一个完整的大规模的倒排索引存储在磁盘中的呢?这两个问题,你可以先思考一下,然后我们一起来看工业界是怎么做的。

首先,搜索引擎这种工业级的倒排索引表的实现,会比我们之前学习过的更复杂一些。比如说,如果文档中出现了「极客时间」四个字,那除了这四个字本身可能被作为关键词加入词典以外,「极客」和「时间」还有「极客时间」这三个词也可能会被加入词典。因此,完整的词典中词的数量会非常大,可能会达到几百万甚至是几千万的级别。并且,每个词因为长度不一样,所占据的存储空间也会不同。

所以,为了方便后续的处理,我们不仅会为词典中的每个词编号,还会把每个词对应的字符串存储在词典中。此外,在 posting list 中,除了记录文档 ID,我们还会记录该词在该文档中出现的每个位置、出现次数等信息。因此,posting list 中的每一个节点都是一个复杂的结构体,每个结构体以文档 ID 为唯一标识。完整的倒排索引表结构如下图所示:

那么,我们怎样才能生成这样一个工业级的倒排索引呢?

首先,我们可以将大规模文档均匀划分为多个小的文档集合,并按照之前的方法,为每个小的文档集合在内存中生成倒排索引。

接下来,我们需要将内存中的倒排索引存入磁盘,生成一个临时倒排文件。我们先将内存中的文档列表按照关键词的字符串大小进行排序,然后从小到大,将关键词以及对应的文档列表作为一条记录写入临时倒排文件。这样一来,临时文件中的每条记录就都是有序的了。

而且,在临时文件中,我们并不需要存储关键词的编号。原因在于每个临时文件的编号都是局部的,并不是全局唯一的,不能作为最终的唯一编号,所以无需保存。

我们依次处理每一批小规模的文档集合,为每一批小规模文档集合生成一份对应的临时文件。等文档全部处理完以后,我们就得到了磁盘上的多个临时文件。

那磁盘上的多个临时文件该如何合并呢?这又要用到我们熟悉的 多路归并技术 了。每个临时文件里的每一条记录都是根据关键词有序排列的,因此我们在做多路归并的时候,需要先将所有临时文件当前记录的关键词取出。如果关键词相同的,我们就可以将对应的 posting list 读出,并且合并了。

如果 posting list 可以完全读入内存,那我们就可以直接在内存中完成合并,然后把合并结果作为一条完整的记录写入最终的倒排文件中;如果 posting list 过大无法装入内存,但 posting list 里面的元素本身又是有序的,我们也可以将 posting list 从前往后分段读入内存进行处理,直到处理完所有分段。这样我们就完成了一条完整记录的归并。

每完成一条完整记录的归并,我们就可以为这一条记录的关键词赋上一个编号,这样每个关键词就有了全局唯一的编号。重复这个过程,直到多个临时文件归并结束,这样我们就可以得到最终完整的倒排文件。

这种 将大任务分解为多个小任务,最终根据 key 来归并 的思路,其实和分布式计算 Map Reduce 的思路是十分相似的。因此,这种将大规模文档拆分成多个小规模文档集合,再生成倒排文件的方案,可以非常方便地迁移到 Map Reduce 的框架上,在多台机器上同时运行,大幅度提升倒排文件的生成效率。那如果你想了解更多的内容,你可以看看 Google 在 2004 年发表的经典的 map reduce 论文,论文里面就说了使用 map reduce 来构建倒排索引是当时最成功的一个应用。

如何使用磁盘上的倒排文件进行检索?

那对于这样一个大规模的倒排文件,我们在检索的时候是怎么使用的呢?其实,使用的时候有一条核心原则,那就是 内存的检索效率比磁盘高许多,因此,能加载到内存中的数据,我们要尽可能加载到内存中。

我们知道,一个倒排索引由两部分构成,一部分是 key 集合的词典,另一部分是 key 对应的文档列表。在许多应用中,词典这一部分数据量不会很大,可以在内存中加载。因此,我们完全可以将倒排文件中的所有 key 读出,在内存中使用哈希表建立词典。

那么,当有查询发生时,通过检索内存中的哈希表,我们就能找到对应的 key,然后将磁盘中 key 对应的 postling list 读到内存中进行处理了。

说到这里,你可能会有疑问,如果词典本身也很大,只能存储在磁盘,无法加载到内存中该怎么办呢?其实,你可以试着将词典看作一个有序的 key 的序列,那这个场景是不是就变得很熟悉了?是的,我们完全可以用 B+ 树来完成词典的检索。

这样一来,我们就可以把检索过程总结成两个步骤。
● 第一步,我们使用 B+ 树或类似的技术,查询到对应的词典中的关键字。
● 第二步,我们将这个关键字对应的 posting list 读出,在内存中进行处理。

到这里,检索过程我们就说完了。不过,还有一种情况你需要考虑,那就是 如果 posting list 非常长,它是很有可能无法加载到内存中进行处理的。比如说,在搜索引擎中,一些热门的关键词可能会出现在上亿个页面中,这些热门关键词对应的 posting list 就会非常大。那这样的情况下,我们该怎么办呢?

其实,这个问题在本质上和词典无法加载到内存中是一样的。而且,posting list 中的数据也是有序的。因此,我们完全可以对长度过大的 posting list 也进行类似 B+ 树的索引,只读取有用的数据块到内存中,从而降低磁盘访问次数。包括在 Lucene 中,也是使用类似的思想,用分层跳表来实现 posting list,从而能将 posting list 分层加载到内存中。而对于长度不大的 posting list,我们仍然可以直接加载到内存中。

此外,如果内存空间足够大,我们还能使用缓存技术,比如 LRU 缓存,它会将频繁使用的 posting list 长期保存在内存中。这样一来,当需要频繁使用该 posting list 的时候,我们可以直接从内存中获取,而不需要重复读取磁盘,也就减少了磁盘 IO,从而提升了系统的检索效率。

总之,对于大规模倒排索引文件的使用,本质上还是我们之前学过的检索技术之间的组合应用。因为倒排文件分为词典和文档列表两部分,所以,检索过程其实就是分别对词典和文档列表的访问过程。因此,只要你知道如何对磁盘上的词典和文档列表进行索引和检索,你就能很好地掌握大规模倒排文件的检索过程。

重点回顾

今天,我们学习了使用多文件归并的方式对万亿级别的网页生成倒排索引,还学习了针对这样大规模倒排索引文件的检索,可以通过查询词典和查询文档列表这两个阶段来实现。

除此之外,我们接触了两个 很基础但也很重要的设计思想。

一个是 尽可能地将数据加载到内存中,因为内存的检索效率大大高于磁盘。那为了将数据更多地加载到内存中,索引压缩是一个重要的研究方向,目前有很多成熟的技术可以实现对词典和对文档列表的压缩。比如说在 Lucene 中,就使用了类似于前缀树的技术 FST,来对词典进行前后缀的压缩,使得词典可以加载到内存中。

另一个是将大数据集合拆成多个小数据集合来处理。这其实就是分布式系统的核心思想。在大规模系统中,使用分布式技术进行加速是很重要的一个方向。不过,今天我们只是学习了利用分布式的思想来构建索引,在后面的课程中,我们还会进一步地学习,如何利用分布式技术优化检索效率。

讨论

词典如果能加载在内存中,就会大幅提升检索效率。在哈希表过大无法存入内存的情况下,我们是否还有可能使用其他占用内存空间更小的数据结构,来将词典完全加载在内存中?有序数组和二叉树是否可行?为什么?

拓展阅读/问题

● 有序数组存储
首先,词典的查询,是以字符串为 key 的(因为应用入口并不知道每个字符串对应的 ID 是什么,它只能以字符串为 key 来查询,看看这个 key 是否存在)。那如果是将字符串按照字典序在 数组 中排序好,并且是紧凑存储的(存string|length),那么就可以使用遍历的方式查找。但是遍历的效率不高,因此我们需要加上一个索引数组来进行二分查找。索引数组很简单,就一个元素,存每个词项在字符串数组中的偏移量。比如 [0,5,18] 这样。二分查找时,从数组中间开始,读出偏移量,然后从 str 数组中取出这个词项,和查询的词对比,看看是否相等。如果不等,那么就继续二分查找,往左或往右,取出下一个字符串比较。
因此,我们使用两个数组,就能实现所有数据的紧凑存储。从而提升了内存的使用率。
● 使用数组存储的方案
使用数组存每个词项,这个需要解决每个词项长度不同的问题,一个思路是使用最长的词项作为数组每个元素的大小(比如说每个元素都是 20 个字节)。这样就可以用数组存储和查找了。
这种方法空间会浪费,因此,改进方案可以另外开一个 char 数组,将所有字符串挨个紧凑存入;然后索引数组每个元素都是 int 32 类型,指向 char 数组中对应词项的初始位置。这样空间就都是紧凑的了。
其实对于 JAVA 来说,不太能明白为什么要解决每个词长度不同的问题。

相关文章
|
13天前
|
JSON 缓存 API
小红书笔记评论API开发指南
小红书笔记评论API支持获取评论列表、详情及发布新评论,提供点赞、回复等互动数据,适用于内容分析与用户运营。基于Bearer Token认证,返回JSON格式数据,建议结合分页、异步请求与缓存机制提升效率,遵守调用频率限制,确保采集稳定可靠。
|
4天前
|
机器学习/深度学习 搜索推荐 算法
20 | 推荐引擎:没有搜索词,「头条」怎么找到你感兴趣的文章?
本文深入解析了资讯类App推荐引擎的检索技术。通过“下拉刷新”这一简单操作,系统能在无搜索词情况下精准推荐内容,核心在于个性化召回机制。文章详细讲解了基于内容的召回与协同过滤(用户/物品)两大算法,揭示其如何通过用户行为数据、画像构建及向量相似度计算实现高效推荐,并探讨多策略混合与分层排序的工程实践,展现推荐系统的智能与灵活性。
|
4天前
|
存储 NoSQL 定位技术
13 | 空间检索(上):如何用 Geohash 实现「查找附近的人」功能?
本文介绍了如何高效实现“查找附近的人”功能,针对大规模系统提出基于区域划分与Geohash编码的检索方案。通过将二维空间划分为带编号的区域,并利用一维编码(如Geohash)建立索引,可大幅提升查询效率。支持非精准与精准两种模式:前者直接查所在区域,后者结合邻近8区域扩大候选集以保证准确性。Geohash将经纬度转为字符串编码,便于存储与比较,广泛应用于Redis等系统。适用于社交、餐饮、出行等LBS场景。
|
4天前
|
搜索推荐 算法 UED
15 | 最近邻检索(上):如何用局部敏感哈希快速过滤相似文章?
在搜索引擎与推荐系统中,相似文章去重至关重要。通过向量空间模型将文档转化为高维向量,利用SimHash等局部敏感哈希技术生成紧凑指纹,结合海明距离与抽屉原理分段索引,可高效检索近似重复内容,在百亿网页中快速过滤雷同结果,提升用户体验。该方法适用于文本、图像等多种对象的相似性检测。
|
4天前
|
机器学习/深度学习 搜索推荐 算法
12 | 非精准 Top K 检索:如何给检索结果的排序过程装上加速器
本文介绍了非精准 Top K 检索的优化思路与三种实现方法:基于静态质量得分排序截断、胜者表利用词频打分、分层索引两阶段检索。核心思想是将复杂计算移至离线,在线快速截断,降低打分开销。结合精准检索的两阶段架构,可显著提升检索效率,广泛应用于搜索与推荐系统中。
|
4天前
|
存储 算法 关系型数据库
06丨数据库检索:如何使用 B+ 树对海量磁盘数据建立索引?
本节深入探讨磁盘环境下大规模数据检索的挑战与解决方案,重点解析B+树如何通过减少磁盘访问次数、实现索引与数据分离,支持高效查找、范围查询及动态调整,成为数据库索引的核心技术。
|
4天前
|
存储 数据采集 缓存
04 | 状态检索:如何快速判断一个用户是否存在?
本文探讨高效判断对象是否存在的技术方案,对比有序数组、二叉树、哈希表的查询性能,引出位图与布隆过滤器。位图利用bit级存储,节省空间;布隆过滤器通过多哈希函数压缩数组长度,实现O(1)查询,适用于允许误判的场景,如用户注册、网页抓取去重。二者在时间与空间效率上优于传统结构,广泛应用于缓存、搜索引擎等系统中。
|
4天前
|
机器学习/深度学习 搜索推荐 算法
19 | 广告系统:广告引擎如何做到在 0.1s 内返回广告信息
广告系统是互联网核心营收支柱,支撑Google、Facebook等巨头超80%收入。它需在0.1秒内完成百万级广告实时检索,属高并发、低延迟典型。本文以展示广告为例,解析其引擎架构:通过标签构建倒排索引,结合树形分片、向量检索与非精准打分预筛,优化召回效率;再用深度学习精准排序,提升匹配度。同时,在索引构建时前置过滤无效广告,压缩检索空间,并依赖全量+增量机制实现实时更新。整体设计兼顾性能与效果,实现千人千面的高效投放。
|
4天前
|
存储 缓存 NoSQL
17 | 存储系统:从检索技术角度剖析 LevelDB 的架构设计思想
LevelDB是Google开源的高性能键值存储系统,基于LSM树优化,采用跳表、读写分离、SSTable分层与滚动合并等技术,结合BloomFilter、缓存机制与二分查找,显著提升读写效率,广泛应用于工业级系统中。(239字)
|
4天前
|
机器学习/深度学习 数据采集 自然语言处理
18 | 搜索引擎:输入搜索词以后,搜索引擎是怎么工作的?
搜索引擎通过爬虫、索引与检索三大系统,实现从万亿网页中快速精准查找信息。本文详解其工作原理,包括分词、纠错、短语检索及位置索引等核心技术,揭示搜索背后的智能机制。(239字)