# About Columnar storage in Manticore Search

In this article, we will examine the purpose of Manticore Columnar storage, how it differs from the row-wise storage, and in which cases it makes sense to use it. We will also get acquainted with the basic structure of the storage format and the specifics of its integration into the query processing workflow of the search daemon.

### 引言

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

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

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

### 为什么我们需要列存储？

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

`.spa` 文件具有以下结构：

![](./mcl/spa_table.png)

`.spb` 文件如下所示：

![](./mcl/spb_table.png)


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

`rowwise` 存储的另一个特点是缺乏数据压缩。由于它通过直接内存访问工作，代码假设准备好的数据立即可用。此外，一个文档中不同属性的数据是异构的，使得有效压缩单个文档变得困难。此外，这种存储格式中没有文档块的概念，这可能被压缩。

列存储（`engine='columnar'`）正是为了解决内存不足以加载所有属性的情况而开发的。这种存储格式提供了以下优势：

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

从概念上讲，列存储中的数据结构可以表示如下：

![](./mcl/data_structure.png)

列存储通过一个名为 MCL（Manticore Columnar Library）的独立库提供。该库负责创建存储、打包和读取数据。
此外，为了处理属性并考虑列存储的特性，搜索守护进程本身添加了大量代码。

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

以下是为与列存储配合使用而添加到守护进程的一些示例：

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

* **列式分组器**  
  Manticore 中的 Grouper 负责将传入的文档转换为一个或多个分组键，以便后续传递给 Sorter。列式分组器的主要目标是通过减少 [虚拟调用](https://en.wikipedia.org/wiki/Virtual_function) 的数量来提高性能。例如，当常规分组器运行时，它会从一个表达式请求数据，该表达式从存储中检索数据。此表达式在守护进程端实现，并提供对不同类型的属性存储的透明访问。内部使用了列式迭代器（下文将讨论），该迭代器在 MCL 库端实现，可直接访问存储。然而，专门的列式分组器知道它仅与列式存储一起工作，移除了某些抽象，直接与列式迭代器交互。  
  在按字符串分组时，使用了不同的机制。当字符串添加到列式存储时，会自动计算每个字符串的哈希值（考虑当前 [排序规则](https://manual.manticoresearch.com/Searching/Collations)），并将其存储为单独的整数属性。列式分组器从列式存储中检索这些哈希值，而不是读取字符串本身并计算哈希值作为分组键。  

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

* 查询优化器（[基于成本的优化器](https://manual.manticoresearch.com/Searching/Cost_based_optimizer)，CBO）也必须了解列式存储。CBO 确定查询的执行路径。例如，它可以将其中一个过滤器替换为通过相应的二级索引进行搜索，从而从查询中移除该过滤器。因此，二级索引将返回文档编号（行 ID），剩余的过滤器将在这些编号上操作。  
  当使用列式存储时，优化器必须比较二级索引和列式分析器（在列式存储上执行快速搜索，下文将更详细说明）的性能。根据数据和可用线程的数量，有时一种方法比另一种更快。例如，列式分析器在并行化方面表现更好。  


### MCL 中包含哪些内容？  

在 MCL 库中直接包含的组件中，可以区分出两个主要组：  

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

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

  * **迭代器**  
  访问存储的较慢方法。迭代器可以移动到指定文档并提取属性值。它速度较慢，因为未针对流式处理进行优化。然而，在某些情况下，守护进程无法以流式处理数据，这时会使用列式迭代器。  

  * **分析器**  
  访问数据的显著更快方法。分析器以一组过滤器作为输入，并输出满足这些过滤器的文档编号（行 ID）。它们速度快，因为在数据处理过程中使用了列式存储的所有可用元数据，并且可以在单次调用中立即解包和处理许多文档。例如，如果块的 min-max 值表明所需值不存在，它们可以完全跳过数据块（无需解包）。  
  在守护进程中，这种类型的访问基于 CBO 分析自动启用，但也可以通过索引提示手动启用；这种提示称为 ColumnarScan。  

### 结论  

在本文中，我们广泛介绍了 Manticore 列式存储的原理及其在 Manticore Search 守护进程中的集成细节。
