blog-post

关于 Manticore 搜索中的列式存储

引言

在本文中,我们将探讨 Manticore 列式存储的目的,它与行式存储的不同之处,以及在何种情况下使用它是有意义的。我们还将了解存储格式的基本结构及其在搜索守护进程的查询处理工作流中的集成细节。

默认属性存储(行式)

在 Manticore 中,有两个不同的实体:仅支持全文查询的全文字段,以及可用于分组、排序和过滤的各种类型的属性。默认存储引擎(engine='rowwise')将所有文档的所有属性存储在内存中。

为了将属性加载到内存中,使用 mmap,可以通过选项 access_plain_attrsaccess_blob_attrs 进行配置。第一个选项负责加载 .spa 文件,这些文件包含所有固定长度的属性(整数、长整型、浮点型等)。第二个选项用于加载 .spb 文件,这些文件包含可变长度的属性(字符串、mva、浮点向量等)。由于 mmap 仅在访问时将数据加载到内存中,因此使用属性的初始查询可能会较慢。为了解决这个问题,默认情况下将 'access_plain_attrs' 和 'access_blob_attrs' 设置为 'mmap_preread' 模式,在该模式下,Manticore 会在启动时让后台线程读取 .spa.spb 文件。这通常意味着(但不保证)属性文件将保留在内存中,从而避免在查询期间从磁盘读取数据的需要。然而,如果系统决定内存不足,属性可能会部分或完全从内存中卸载,从而再次减慢查询速度。为了避免这种情况,可以在 access_plain_attrs / access_blob_attrs 中指定 'mlock'。如果内存充足,属性将被保证保留在内存中,系统不会卸载它们。然而,如果内存不足,则没有保证。

我们为什么需要列式存储?

在可用内存足以存储所有属性的情况下,传统的 '行式' 存储工作效率高。当内存不足以容纳所有属性时,问题就出现了。
实际上,mmap 可以根据需要自动卸载和重新加载属性文件的部分内容,即使在内存有限的情况下也能进行操作。然而,在实践中,这种方法可能会显著降低性能到不可接受的水平。问题部分在于行式存储架构,它将一个文档的所有属性顺序存储,接着是下一个文档的属性,依此类推。以下是这样的数据表的示例:

.spa 文件具有以下结构:

.spb 文件看起来是这样的:

当查询包括按特定属性分组时,只需要检索每个文档该属性的值。然而,mmap 的工作方式是它无法读取单个字节;相反,它加载一个或多个内存页,每个页通常为 4 KB。这意味着在尝试读取一个属性的值时,系统通常会加载所有附近属性的值,即使它们在查询处理时并不需要。

rowwise 存储的另一个细节是缺乏数据压缩。由于它通过直接内存访问操作,代码假设随时可用的数据是立即可用的。此外,同一文档中不同属性的数据是异构的,这使得有效压缩单个文档变得困难。此外,在这种存储格式中没有可以压缩的文档块的概念。

列式存储(engine='columnar')正是为内存不足以加载所有属性的情况而开发的。这种存储格式提供了以下优点:

  • 每个属性的数据单独存储,可以在不影响其他属性的情况下读取。
  • 由于单个属性中的数据通常是同质的,因此可以应用压缩。
  • 属性值可以分成多个文档的块进行压缩。
  • 解压缩块后,可以应用流处理的优化。
  • 在 RAM 中存储非常少量的元数据,其余数据存储在磁盘上。
  • 为了快速访问热数据,而不是通过 mmap 将页面加载到内存中,使用文件系统缓存。

从示意图上看,列式存储中的数据结构可以表示如下:

列式存储以名为 MCL(Manticore 列式库)的单独库提供。该库负责创建存储、打包和读取数据。
此外,搜索守护进程本身还添加了大量代码以处理属性,考虑到列式存储的特性。

最初,守护进程实现了 rowwise 存储,旨在随机数据访问。可以简单地用列式存储替换内存中的数据访问,但如果不考虑列式存储是为流处理设计并以压缩块存储数据,这样做会严重降低性能。

以下是为使守护进程能够与列式存储一起工作而添加的一些示例:

  • 列式排序器
    在 Manticore 的架构中,通过全文搜索找到的文档会立即发送到所谓的排序器。排序器可以简单地排序,或者可以对文档进行分组、计算聚合等。然而,在列式存储中访问属性的速度较慢,因为值是逐个检索的。
    如果查询相对较轻——意味着全文搜索快速或根本不存在——一次从存储中检索属性可能会显著影响性能。因此,在某些情况下,使用标准排序器之上的额外排序器是有益的。这个额外的排序器并不进行排序,而是累积文档并批量检索列属性,然后将文档传递给主排序器进行最终处理,这样速度显著更快。

  • 列分组器
    Manticore中的分组器负责将传入的文档转换为一个或多个分组键,以便后续传递给排序器。列分组器的主要目标是通过减少 虚拟调用 的数量来提高性能。例如,当常规分组器操作时,它会请求从存储中检索数据的表达式的数据。这个表达式在守护进程端实现,并提供对不同类型属性存储的透明访问。在内部,它使用一个列迭代器(下面讨论),该迭代器在MCL库端实现,直接访问存储。然而,专门的列分组器知道它只会与列存储一起工作,并去除了一些抽象,直接与列迭代器一起工作。
    当按字符串分组时,使用不同的机制。当字符串被添加到列存储时,会自动计算每个字符串的哈希(考虑当前的 排序规则 )并作为单独的整数属性存储。列分组器从列存储中检索这些哈希,而不是读取字符串本身并计算哈希以用作分组键。

  • 同样,通过移除列表达式,列迭代器通过该表达式检索当前文档的属性值,列过滤器和列聚合在守护进程中实现。

  • 查询优化器( 基于成本的优化器 ,CBO)也需要了解列存储。CBO确定查询的执行路径。例如,它可以用通过相应的二级索引进行搜索来替换其中一个过滤器,从查询中移除该过滤器。因此,二级索引将返回文档编号(行ID),其余过滤器将在其上操作。
    当使用列存储时,优化器必须比较二级索引和列分析器的性能(列分析器在列存储上执行快速搜索,下面会详细介绍)。根据数据和可用线程的数量,有时一种方法的速度比另一种更快。例如,列分析器的并行化效果更好。

MCL中包含什么?

在直接属于MCL库的组件中,可以区分出两个主要组:

1.构建器
负责从守护进程接收原始数据并构建列存储。

2.访问器
负责数据访问。主要有两种类型:

  • 迭代器
    一种较慢的访问存储的方法。迭代器可以移动到指定文档并提取属性值。它的工作速度较慢,因为它不适合流处理。然而,在某些情况下,守护进程无法以流的方式处理数据,这时使用列迭代器。

  • 分析器
    一种显著更快的访问数据的方法。分析器将一组过滤器作为输入,并输出满足这些过滤器的文档编号(行ID)。它们工作迅速,因为在数据处理过程中使用了列存储的所有可用元数据,并且可以立即在一次调用中解包和处理许多文档。例如,如果块的最小值和最大值表明所需值不存在,它们可以完全跳过数据块(而不解包它们)。
    在守护进程中,这种类型的访问会根据CBO分析自动启用,但也可以通过索引提示手动启用;这种提示称为ColumnarScan。

结论

在本文中,我们广泛介绍了Manticore列存储的原理以及其与Manticore搜索守护进程集成的一些细节。

安装Manticore Search

安装Manticore Search