一字段,一份副本:Elasticsearch 列式存储如何丢弃倒排索引

作者:来自 Elastic Martijn van Groningen

每个字段只存储一份副本,意味着不再需要倒排索引,因此 doc values 现在可以批量读取,而跳过器(skippers)则允许查询跳过整个文档范围;与此同时,新增的映射属性可以控制每个字段允许包含哪些内容。

想获得 Elastic 认证?了解下一期 Elasticsearch Engineer 培训 什么时候开始!你现在就可以开始 免费云试用,或者立即在你的 本地机器 上试用 Elastic。

作为 9.5.0 版本发布的一部分,Elasticsearch 引入了处于技术预览阶段的 columnar 和 logsdb_columnar 索引模式。从 1.0.0 版本开始,Elasticsearch 就已经通过 Lucene 的 doc values 使用列式存储。Lucene 的 doc values 为分析和搜索功能提供支持,例如按字段进行分组和排序。那么,columnar 索引模式发生了什么变化?

这些变化涉及存储和性能,以及开箱即用(out-of-the-box - OOTB)的体验。在 9.5.0 之前,Elasticsearch 默认作为基于文档的搜索引擎运行。从存储方式来看,它可以被配置为表现得像一个列式系统,但这并不是开箱即用的体验。columnar 索引模式 进行了多项根本性的改变,使 Elasticsearch 能够针对列式分析和搜索场景进行优化:

  • 字段只作为 doc values 存储一次,默认情况下不再建立索引。

  • 新的多值语义。默认情况下,会保留每个文档中每个字段多个值的原始顺序(例如数组中的值顺序)。

  • 映射始终是扁平的,并且映射中的 object 字段和 passthrough 字段始终会自动扁平化。

Columnar 索引模式如何融入 Elasticsearch

Columnar 索引模式中的许多变化都源自时间序列数据流(TSDS)。作为让 TSDB 成为具有竞争力的指标解决方案的一部分,我们改进了磁盘上的 doc values 格式,并且只将维度和指标字段作为 doc values 存储一次。我们还改进了查询性能。TSDB 如今已经是列式的。本质上,这使 TSDB 的存储模式变成了列式存储。现在,我们正在将从 TSDB 中获得的经验更广泛地应用于 Elasticsearch。

需要注意的是,columnar 索引模式是选择性启用的,columnar 索引和基于文档的索引可以在同一个集群中共存。企业搜索场景可以使用面向文档的索引模式,而日志场景则可以使用 logsdb_columnar 索引模式并完全采用列式存储,这些索引模式都可以存在于同一个集群中。事实上,目前共有 7 种索引模式,并且这些索引模式都可以在同一个集群中使用。

没有倒排索引时,列式存储如何保持快速

已建立索引的字段 ------ 字符串字段使用倒排索引,数值字段使用块 K 维(BKD)树 ------ 可以让 Elasticsearch 非常高效地根据字段进行查询或过滤。然而,这需要额外的数据结构,而这些数据结构不仅会占用大量磁盘空间,在索引阶段和 合并 阶段构建它们的成本也很高。对于 columnar 索引模式,字段默认不再建立索引。那么,对于这些不再建立索引的字段,我们是如何让查询性能仍然保持在可接受水平的呢?

一个重要的变化是改进了 doc values 的扫描性能。这一点至关重要,也是任何列式系统所依赖的基础。过去,doc values 的扫描本质上是逐个文档进行的。Lucene 的 doc values API 只允许一次查找一个值。从历史上看,这与搜索引擎的执行模型非常匹配。在我们自己的 doc value 格式中,我们构建了支持批量读取值的能力,以用于 Elasticsearch Query Language(ES|QL)查询。此外,在最近几个 Lucene 小版本中,Lucene doc values API 也增加了对批量读取的支持。如果没有这一能力,就不可能实现快速的列式扫描。

其次,我们全面采用了doc value skippers,这是一种建立在 doc values 之上的分层跳表。与倒排索引或 BKD 树不同,doc value skippers 是轻量级的数据结构。从本质上来说,一个 skipper 允许查询跳过一段不符合查询条件的文档范围。之所以能够做到这一点,是因为它存储了诸如最小值和最大值之 类 的信息。

例如,当执行范围查询时,可以根据中间结果以及 doc value skipper 中记录的最小值和最大值,跳过一段文档。doc value skippers 的有效性取决于文档在磁盘上的排列顺序。因此,应该启用或调整索引排序,使其与具体使用场景相匹配。

通过显著改进列式扫描,并进一步利用 doc value skippers,我们能够默认避免为字段建立索引。需要注意的是,字段仍然可以建立索引;index 映射属性 在 columnar 模式下只是默认设置为 false

这一规则的例外是基于文本的字段,它们默认仍然会建立索引。这是因为 text 字段提供全文搜索能力,其中包括文本分析,以及短语匹配和通配符匹配。这与单纯的过滤有所不同。

Elasticsearch 如何处理高基数和低基数字段

在使用字符串字段设置 schema 时,一个重要的配置参数通常是_基数(cardinality)_;也就是说,需要判断预期会出现许多唯一值,还是只有少量唯一的字符串值。许多系统都有专门的字段类型或列类型,用于针对低基数和高基数字符串字段进行优化。

低基数字段通常使用字典进行存储,其中包含所有唯一值。然后,对于每一个行偏移量,存储一个指向字典的偏移量,该偏移量对应于这一行所拥有的术语。这通常称为_序数(ordinal)_。对于低基数字段,这种方式效果很好,因为每个文档只存储一个序数,占用的空间要小得多。增量编码、偏移量编码和位打包(bitpacking)等编码技术非常适合序数,可以将每个文档的存储压缩到只有几位。

然而,对于高基数字段,字典和序数这种方式可能会产生反直觉的结果。如果更大比例的文档都拥有唯一值,那么构建字典的成本就会变高,同时存储节省也会减少。此时,字典还会成为读取值时的另一级间接寻址。这就是为什么大多数系统在这种情况下会使用基于列的方式,通过基于块的压缩来存储值。例如,将多行的值存储在 128KB 的数据块中,并使用基于滑动窗口的字典压缩算法(例如 zstandard 或 lz4)进行压缩。总体来说,这是一种简单而有效的高基数字段存储方式,同时避免了构建和维护字典。

对于基于文档的 Elasticsearch,有两种方式可以映射字符串:使用 keyword 字段映射,或者使用其中一种基于 text 的字段映射。前者存储倒排索引以及基于字典的 doc values。后者只存储倒排索引。这就是为什么通常会将 text 字段映射器与 keyword 映射器结合使用,作为一个多字段(multi-field)

此外,keyword 字段映射器(正如其名称所暗示的)面向关键词或低基数字段,并使用基于字典的 doc values 实现。不过在实际使用中,keyword 字段映射器也经常用于高基数字段。

在 columnar 模式下,默认情况下每个字段只存储一次。对于 keyword 字段,这意味着只存储 doc values,而不再存储倒排索引。基于 text 的字段现在也默认同时存储 doc values 和倒排索引。基于 text 的字段映射器与 keyword 字段映射器有所不同,因为对于 columnar 索引,Elasticsearch 的动态映射逻辑不会自动使用这些映射器,因此 text 字段默认仍然会存储倒排索引。

对于 keyword 和基于 text 的字段,我们没有选择公开一个 cardinality 映射属性。因为并不总能提前知道一个字段究竟是低基数还是高基数。当 Elasticsearch 将 segment 刷新并合并到磁盘时,它可以看到所有值,并确定字段的基数。这就是为什么我们选择通过一个简单的基数阈值,自动判断使用字典和基于序数的编码是否比基于块的压缩更有优势。

如果字段的基数低于这个阈值,就使用基于字典和序数的编码;否则就使用基于块的压缩。这简化了映射配置,并使 Elasticsearch 能够随着数据的变化自动优化存储,因为对于同一个字段,不同的 segment 可以分别使用字典或数据块进行存储。

不过,目前这一功能还没有准备好。因此,在 9.5.0 版本中,对于 columnar 模式,keyword 和基于 text 的字段映射器都会在磁盘上使用基于块压缩的布局,将值存储在 doc values 中。

这两种方式的比较如下:

基于字典和序数的编码 基于块的压缩
适用场景 低基数字段 高基数字段
存储内容 唯一值组成的字典,以及每个文档对应的一个序数 将多个文档的值压缩在一起的数据块
压缩方式 增量编码、偏移量编码和位打包将每个序数压缩到几位 基于滑动窗口的字典压缩,例如 zstandard 或 lz4,通常以 128KB 数据块为单位
读取路径 解析序数,然后在字典中查找对应的值 解压数据块,然后直接读取值
随着基数增加的成本 字典变大,节省的空间减少,同时额外的间接寻址仍然存在 保持稳定,无需构建或维护字典
在 columnar 模式技术预览中的使用情况 尚未使用 是,keyword 和 text 字段均使用

Columnar 映射属性:multi_value、nullability、on_failure

Columnar 索引模式可以更好地控制数据如何作为 doc values 进行存储。默认情况下,Elasticsearch 比较宽松,会接受所有格式正确的值(例如 null 值,以及每个文档中每个字段包含多个值的情况)。如果文档中的字段每个文档包含多个值,或者没有值,那么 doc values 就需要存储额外的数据结构来处理这些情况,因此会隐式增加存储成本。

使用 columnar 模式时,新的映射属性提供了额外的控制能力。需要注意的是,这些新的映射属性目前仅适用于 columnar 索引模式,但最终也会适用于所有索引模式。

这里涉及 3 个新的映射属性:

属性 默认值 强制要求 违反要求时的行为 可用版本
multi_value true 每个文档的每个字段只能有一个值 文档索引失败 9.5.0
nullability true 字段必须有值 文档索引失败 9.5.0
on_failure fail 如何处理上述失败 设置 failignore 行为 下一个小版本

multi_value:强制字段只能有单个值

默认情况下,Elasticsearch 接受每个文档包含多个值的字段。为了理解这一点所带来的影响,我们首先来看一下 Elasticsearch(使用 Lucene 的 doc values)如何存储一个所有文档都只有单个值的稠密数值字段:

采用这种布局时,所有值都会存储在数据块中。每个数据块包含的值数量取决于索引模式,但通常为 128 个值,并且在同一个索引中始终保持一致。数据块中的所有值都会使用各种编码技术进行编码,例如增量编码和位打包,因此每个数据块的大小可能不同,具体取决于这些编码技术对值的压缩效果。这就是为什么需要一个数据块索引。

Lucene 有一个 docid 的概念(文档的内部编号),它本质上就是一个行标识符。

  1. 查询会产生匹配的 docid。

  2. 对于稠密字段,可以直接根据 docid 确定数据块 ID。

  3. 可以通过数据块索引确定数据块的偏移量。

  4. 找到偏移量后,就可以对目标数据块进行解码,此时所有值都可用。

  5. 最后,可以根据 docid 确定已解码值数组中的序数,从而得到最终的值。

现在,让我们来看一下,当文档包含多个值时,数据布局会发生怎样的变化:

要确定单个 docid 对应多少个值,需要进行偏移量查找。

在这种情况下,一个 docid 会有一个或多个偏移量。每个偏移量都指向一个数据块索引。单个文档的值彼此相邻,但可能跨越多个数据块。通常情况下,在数据块中对值进行压缩效果很好,因为这些值彼此相似。然而,如果每个字段、每个文档包含的值数量较多,并且这些值彼此并不相似,那么多值字段可能会导致值的压缩效率降低。此外,偏移量本身也需要额外的存储空间,因此,多值字段通常会在磁盘上占用更多存储空间。

如果一个字段确实是单值字段,那么你可能希望强制执行这一属性。现在,multi_value 映射属性可以实现这一点。一个例子是日志级别字段。日志通常只有一个日志级别(例如 debug、info 或 error)。在 mapping 中强制该字段只能有一个值,可以帮助避免意外使用超出预期的存储空间。下面是一个 mapping 片段示例,它禁止字段 log.level 在每个文档中包含多个值:

json 复制代码
`

1.  {
2.    "properties": {
3.       "log.level": {
4.          "type": "keyword",
5.          "multi_value": false
6.       }
7.    }
8.  }

`Lobster AI

请注意,即使一个字段允许包含多个值,也并不意味着一定会存储偏移量查找表。只有当一个 Lucene segment 中至少有一个文档包含两个或更多值时,才会发生这种情况。multi_value 映射属性仅用于强制执行这一规则。

nullability:要求每个文档都具有值

默认情况下,Elasticsearch 接受字段没有值或值为 null 的文档。正如每个文档包含多个值需要额外的记录信息一样,没有值的文档也需要额外的记录信息,以确定哪些文档至少包含一个值。

如果一个 segment 中并非所有文档都有值,doc values 会存储一个从 docid 到偏移量的查找表(在 Lucene 中称为 IndexedDISI)。对于单值字段,该偏移量直接指向数据块索引;对于多值字段,该偏移量则指向偏移量查找表。与所存储的数据块相比,这个查找表非常紧凑。然而,如果对于本应每个文档至少具有一个值的字段也创建这个查找表,就会造成浪费。

nullability 映射属性允许你控制文档是否可以没有值。与 multi_value 映射属性一样,nullability 属性用于强制执行这一规则。下面是一个 mapping 片段示例,它要求 log.level 字段必须具有值:

markdown 复制代码
`

1.  {
2.      "properties": {
3.          "log.level": {
4.              "type": "keyword",
5.              "nullability": false
6.       }
7.    }
8.  }

`Lobster AI

on_failure:验证失败时会发生什么

如果一个文档的某个字段包含多个值,而 multi_value 映射属性设置为 false,或者某个字段的 mapping 将 nullability 设置为 false,而一个文档又没有该字段,会发生什么?目前,对这些文档进行索引会失败,并返回 bad request 错误。

作为下一个小版本的一部分,on_failure 映射属性将会提供。它允许你针对每个已映射的字段,指定如何处理这些验证失败。它将支持两个值:

  1. Fail:使用客户端错误使整个文档的索引操作失败。这是 Elasticsearch 9.5.0 中当前的行为。

  2. Ignore:忽略验证错误,将该字段标记为被忽略,并将该字段的值存储在一个隐藏字段中,以便在请求 source 时进行检查。

尝试使用列式索引模式

列式索引模式仍在积极开发中,但我们鼓励你亲自试用。在我们准备列式索引模式正式发布(GA)的过程中,我们还会加入更多性能和效率方面的改进。我们相信,通过采用列式思维,许多使用场景都将受益于更具成本效益的方案,或者获得更好的性能特征。

原文:Columnar storage in Elasticsearch: Storing every field once | Elasticsearch Labs

相关推荐
LIXIUPING20141 小时前
微服务分布式日志环境完整搭建实战|从服务改造到 ELK 集群部署、全流程可落地
elk·elasticsearch·logstash·filebeat·分布式日志
OsDepK14 小时前
项目快速Git至仓库(完整版)
大数据·git·elasticsearch·搜索引擎
Elastic 中国社区官方博客20 小时前
如何通过一条 ES|QL 查询为 Elasticsearch 中的每个指标构建指标图表
大数据·运维·数据库·elasticsearch·搜索引擎·全文检索·kibana
程序员梅雨1 天前
微服务学习最终篇: Elasticsearch
学习·elasticsearch·微服务
Elasticsearch1 天前
教程:使用 ES|QL 进行威胁狩猎
elasticsearch
智搜广告1 天前
GEO优化公司怎么选?智搜广告从三个维度帮你判断
大数据·人工智能·python·elasticsearch·microsoft·geo
Elastic 中国社区官方博客1 天前
Elasticsearch:ES|QL 搜索教程
大数据·数据库·人工智能·sql·elasticsearch·搜索引擎·全文检索
Elasticsearch1 天前
Elasticsearch 中的查询重写规则:通配符扫描速度提升 2.3 倍
elasticsearch
Elasticsearch1 天前
隐藏在可观测性数据中的安全攻击
elasticsearch