注意:这个目前在 Elastic Stack 9.5 版本中是 Preview。后期版本有可能会发生变化。
列式索引模式将 Elasticsearch 转变为一种分析型和搜索型列式存储,用于启用了该模式的索引。列式模式不会针对不同的查询路径,为每个字段保留多个副本,而是默认将字段作为 doc values 存储一次。这种策略可以降低高数据量、以分析为主的数据的存储成本,同时保持相同的 API、仪表板和集成。
列式模式与现有的索引模式并存,例如standard、logsdb 和 time_series。你可以在创建索引时针对每个索引(或在模板中)选择该模式;索引创建后无法更改模式。
本页面介绍什么是列式索引模式、何时使用它,以及它如何与 Elasticsearch 的其他数据存储模式配合使用。有关启用步骤、排序、_source模式和限制,请参阅 Elasticsearch 参考中的列式索引模式。
更多阅读:
为什么 Elasticsearch 正在成为列式数据库
快 17% 的搜索,零配置:Elasticsearch 中的自动校准向量量化
何时使用列式索引模式何时使用列式索引模式
当你的数据以大量写入、以分析型方式进行查询(过滤、聚合和仪表板),并且需要长期保留时,可以选择列式索引模式。典型适用场景包括:
大规模日志、追踪和可观测性:数据写入速率高,存储和聚合成本占主导,同时仍然需要对消息字段进行全文搜索。
安全遥测和威胁狩猎:在大型事件存储中进行分面探索和历史查询,同时保留搜索行为,以支持数据透视和查找工作流。
运营和业务分析:针对应用事件、事务或 IoT 读数构建仪表板和执行聚合,否则这些工作负载可能会促使你采用独立的分析型存储。
列式模式并不是通用的默认选择。
当你的工作负载以文档搜索为主,需要保留提交时的原始 JSON_source,或者默认依赖大多数字段上的倒排索引及相关结构时,优先使用standard索引(或其他专用模式)。
对于需要时间序列维度和指标字段语义的指标数据,应使用时间序列数据流。
对于希望保留当前logsdb默认设置、但又不想切换到完全列式存储的日志数据,应使用日志数据流。
列式模式
index.mode 的两个值可以启用列式存储:
columnar- 通用型列式存储,不包含针对特定使用场景的默认设置。
- 适用于非日志导向的普通索引和数据流。
logsdb_columnar- 采用相同的列式存储行为,同时提供面向日志的默认设置,包括默认的
@timestamp映射,以及当存在@timestamp和host.name时进行索引排序。当你希望日志数据使用列式存储,而不是默认的 logsdb 模式时使用它。 如果存在
@timestamp和host.name映射,则启用索引排序。
- 采用相同的列式存储行为,同时提供面向日志的默认设置,包括默认的
这两种模式都适用于索引,也适用于通过模板配置的数据流后备索引。两种模式都严格采用列式存储:它们会拒绝映射级别的 runtime 字段,并禁止关闭_source。
工作原理
从较高层面来看,列式索引模式改变了存储默认设置,同时保留了熟悉的文档和查询模型:
按字段只存储一次:非文本字段以 doc values 的形式存储,默认不会建立索引,从而避免为分析型工作负载可能不需要的数据结构付出额外成本。
在需要的地方保留搜索能力:文本字段默认仍会建立索引,因此全文搜索仍然可以在日志消息等字段上正常工作。
扁平字段布局:Object 和 passthrough 映射会被扁平化为叶子字段,这与列式系统组织数据以实现高效扫描和压缩的方式相匹配。
索引排序对于压缩和查询性能仍然非常重要。logsdb_columnar会为日志设置合理的默认排序;对于columnar,你可以根据自己的访问模式选择排序字段。详细信息请参阅参考文档。
列式模式并不会取代核心数据存储概念:
你仍然会在索引(或数据流)中存储 JSON 文档、定义映射,并通过名称、别名或数据流指定目标索引。
分片、副本以及近实时搜索的行为与其他索引模式一致。
现有的查询语言、Kibana 可视化、告警和数据摄取集成仍然可以针对列式索引运行。
改变的是 Elasticsearch 在磁盘上的数据布局方式,以及默认构建哪些数据结构,而不是你日常与集群交互的方式。
启用和配置列式索引模式
创建使用columnar或logsdb_columnar的索引或模板,配置索引排序,并查看_source模式和限制。
启用列式模式后,Elasticsearch 会成为一个完整的分析型和搜索型列式存储。我们将在下面介绍如何启用列式模式、配置索引排序,以及进一步介绍_source模式和限制。
你启用的是一组整体性的变更,这些变更共同使 Elasticsearch 的存储模型与专用列式存储保持一致:
字段只存储一次,并且仅作为 doc values 存储。非文本字段默认不会建立索引,从而消除了维护冗余索引结构所产生的存储成本。文本字段默认仍会建立索引,以支持全文搜索。
对于未建立索引的字段,默认启用 doc values skippers。doc values skippers 是带有元数据(例如最小值和最大值)的紧凑型跳过列表,可以在执行查询时避免扫描大量数据块。
映射始终是扁平的,映射中的 object 和 passthrough 字段始终会自动扁平化。Nested 字段不会自动扁平化。
根据你的许可证,你可以在两种 _source 模式之间进行选择。如果使用 synthetic source,那么在查询时请求
_source时,会自动生成扁平化或列式表示。或者,这种列式 source 可以在索引时生成,并作为 doc values 存储到磁盘。新的多值语义:默认保留每个文档中每个字段多个值的原始顺序(例如数组中的值)。也可以在映射中配置字段,使其每个文档只允许一个值。
可以在映射中配置字段,拒绝那些没有该字段值的文档。
_routing和_id等元数据字段也会使用 doc values 存储。默认使用经过优化的 doc values 格式,进一步降低存储占用,尤其是在与索引排序结合使用时。
结合索引排序,列式模式可以让 Elasticsearch 的存储占用和列式访问方式达到专用列式存储的水平,同时保留完整的搜索和聚合能力。
启用列式模式
在创建索引时设置mode索引设置。索引创建后无法更改该设置。
PUT my-index { "settings": { "mode": "columnar" } }对于日志数据,使用组件模板为所有logs-*-*数据流启用logsdb_columnar模式:
PUT _component_template/logs@custom { "template": { "settings": { "mode": "logsdb_columnar" } } }默认情况下,所有匹配logs-*-*的数据流都会使用logsdb,但添加自定义组件模板后,所有匹配logs-*-*的数据流都会使用logsdb_columnar。
或者在创建索引时直接设置:
PUT my-logs-index { "settings": { "mode": "logsdb_columnar" } }索引排序
设置列式索引模式时,你必须确定索引排序字段。合理的索引排序字段可以提高数据存储效率并缩短查询响应时间。合适的索引排序字段取决于使用场景和数据。
logsdb_columnar索引模式与logsdb索引模式一样,默认使用升序排列的host.name字段和降序排列的@timestamp字段作为索引排序字段。主机通常会生成相似的日志,因此将同一主机的日志条目按顺序存储在磁盘上,可以提高运行长度编码和增量编码等压缩技术的效率。@timestamp字段也会被用作索引排序字段,并让最近的日志条目排在最前面,从而提高针对近期数据的查询性能。
columnar索引模式默认不会启用索引排序。如果你从不同的 agent 收集日志,那么按照 agent ID 和时间戳进行排序可能是一个不错的选择:
PUT my-index { "settings": { "mode": "columnar", "sort.field": [ "agent.id", "@timestamp" ], "sort.order": [ "asc", "desc" ] } }如果查询延迟比存储效率更重要,那么仅按@timestamp排序可能会提高查询响应时间:
PUT my-logs-index { "settings": { "mode": "logsdb_columnar", "sort.field": [ "@timestamp" ], "sort.order": [ "desc" ] } }动态映射
在列式模式下,静态映射中未引用的字段会按照以下方式进行动态映射:
整数映射为
long字段类型。小数映射为
double类型。字符串映射为
keyword字段类型。对象和数组会映射为一个或多个叶子字段(具体取决于未映射字段路径的数量)。列式模式中的映射会被扁平化,这同样适用于未映射的对象。未映射对象下的每个叶子字段都会被映射为独立的叶子字段。映射中不会添加任何对象字段。
动态映射的字段默认启用 doc values,并关闭索引,这符合列式存储通过使用 doc values 为每个字段仅存储一次的设计理念。
动态映射行为由 dynamic 配置参数控制,该参数可以设置为:
true(默认):启用前述段落中描述的动态映射行为。false:未映射字段不会被映射或存储。未映射字段中的数据会丢失。strict:包含未映射字段的文档不会被建立索引,而是产生索引错误。
请注意,
runtime选项在列式模式下不受支持。
警告
如果你将
"dynamic": false配置为false,那么只有在映射中明确配置的字段才会被存储。映射中未明确配置的字段不会被存储,因此会丢失。
自动扁平化
如果使用列式模式,映射始终会被扁平化。在定义映射时,object 和 passthrough 字段映射器会被移除,并为每个字段路径创建叶子字段映射。索引过程中发生的动态映射更新也采用相同的方式。
在进行object扁平化时,enabled和dynamic设置会被保留并分别进行跟踪。对于passthrough字段也是如此,同时还会保留其priority设置。
例如,给定一个包含attributes对象(dynamic: false)和labelspassthrough 字段(priority: 10)的映射:
PUT my-index { "settings": { "mode": "columnar" }, "mappings": { "properties": { "attributes": { "type": "object", "dynamic": false, "properties": { "host": { "type": "keyword" }, "ip": { "type": "ip" } } }, "labels": { "type": "passthrough", "priority": 10, "properties": { "env": { "type": "keyword" } } } } } }处理后的映射显示,object 映射器已被移除,其设置被记录在prefix_properties下。例如,GET my-index/_mapping返回:
{ "my-index": { "mappings": { "prefix_properties": { "attributes": { "dynamic": "false" }, "labels": { "passthrough": 10 } }, "properties": { "attributes.host": { "type": "keyword" }, "attributes.ip": { "type": "ip" }, "labels.env": { "type": "keyword" } } } } }attributes和labelsobject 映射器已从properties中移除;其中只保留扁平化的点号路径叶子字段。attributes中的dynamic: false被保留在prefix_properties.attributes.dynamic下,从而阻止索引时自动为attributes.*下的新字段创建映射。labels中的priority: 10被保留在prefix_properties.labels.passthrough下。
列式 _source
列式索引模式不会在磁盘上存储原始 JSON_source。支持两种_source模式:
合成列式_source
在查询时从 doc values 重建扁平化的_source表示。合成列式_source需要相应的许可证。更多信息请参阅合成 _source。
列式存储_source
在索引时生成列式_source表示,并将其作为 doc values 存储在磁盘上。当未获得合成列式_source的许可证时会自动使用该模式,也可以显式配置它,以加快_source的检索速度。更多信息请参阅列式 source。
限制
以下功能在列式索引模式下不受支持:
Nested 字段类型:列式索引模式以有限的方式支持 nested 字段类型。不支持嵌套 nested 字段类型。
映射级别的 runtime 字段:不允许在索引映射中定义 runtime 字段。仍然可以在单独的搜索请求中定义 runtime 字段。
关闭 doc values:映射字段无法关闭 doc values。将
doc_values设置为false会导致映射错误。唯一的例外是多字段(multi-fields),因为通常希望只为其中一个字段存储 doc values,并为其他字段使用不同的索引配置。不兼容的字段类型:不支持不支持 doc values 的字段类型,例如
search_as_you_type。关闭
_source:不允许设置"_source": {"enabled": false}。存储型 source 模式:不支持传统的
storedsource 模式;仅支持 synthetic columnar 和 columnar stored 模式。请参阅列式 _source。dynamic: false和enabled: false:这两种设置都会导致数据丢失。在对象上设置dynamic: false会阻止存储未映射的子字段;这些字段的数据将永久丢失。设置enabled: false会忽略整个对象子树;其中的数据将永久丢失。默认查询字段:在列式模式下,
index.query.default_field索引设置默认只包含已建立索引的字段(默认情况下,基于文本的字段会建立索引)。
列式 source
列式索引模式使用列式 source。默认情况下,这些内容会在查询时根据 doc values 动态生成。但也可以通过使用columnar_storedsource 模式,在索引时将这些内容存储在磁盘上。
对于需要获取索引中全部或大部分字段的查询,columnar_storedsource 模式可能很有用。
要使用列式存储的 source:
PUT my-columnar-index { "settings": { "index": { "mode": "columnar" } }, "mappings": { "_source": { "mode": "columnar_stored" } } }在列式模式下,完全关闭_source("_source": {"enabled": false})不被允许。
举例 一
下面我们来使用一个例子来进行说明:
创建一个 columnar 索引
PUT products { "settings": { "mode": "columnar" }, "mappings": { "properties": { "name": { "type": "keyword" }, "category": { "type": "keyword" }, "price": { "type": "double" }, "in_stock": { "type": "boolean" } } } }重要的是:
"settings": { "mode": "columnar" }mode必须在创建索引时指定;之后无法更改。
另外请注意,这些字段都是显式映射的。在columnar模式下,非文本字段会作为 doc values 存储,并且默认不会建立索引,这是列式设计的一部分。
写入两个文档
PUT products/_doc/1 { "name": "MacBook Pro", "category": "laptop", "price": 2499.99, "in_stock": true } PUT products/_doc/2 { "name": "iPhone 17", "category": "phone", "price": 999.99, "in_stock": true }此时,Elasticsearch 中的字段值已经以列式 / doc values 表示形式存在,而不是像传统的_source那样仅保留原始 JSON 文档。
搜索索引
例如:
GET products/_search { "query": { "match_all": {} } }响应结果在概念上如下:
{ "took": 46, "timed_out": false, "_shards": { "total": 1, "successful": 1, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 1, "hits": [ { "_index": "products", "_id": "1", "_score": 1, "_source": { "category": "laptop", "in_stock": true, "name": "MacBook Pro", "price": 2499.99 } }, { "_index": "products", "_id": "2", "_score": 1, "_source": { "category": "phone", "in_stock": true, "name": "iPhone 17", "price": 999.99 } } ] } }那么,这个_source是从哪里来的?
这正是有意思的地方。
对于普通的 Elasticsearch 索引,_source本质上就是 Elasticsearch 存储的原始 JSON 文档。
在columnar模式下,Elasticsearch 不会在磁盘上存储原始 JSON_source。相反,在使用合成列式_source时,当你请求_source,Elasticsearch 会根据 doc values 中的字段值重新构建_source。
所以从概念上来说:
Indexing ────────────── JSON document │ ├── name → doc values ├── category → doc values ├── price → doc values └── in_stock → doc values然后当你进行搜索时:
Search ────────────── doc values │ ├── name → "MacBook Pro" ├── category → "laptop" ├── price → 2499.99 └── in_stock → true │ ▼ synthetic _source │ ▼ { "name": "MacBook Pro", "category": "laptop", "price": 2499.99, "in_stock": true }所以,对用户来说,搜索响应中显示的_source看起来就像普通的_source,但在内部,它是根据列式数据重新构建的。
你也可以只请求特定字段
例如:
GET products/_search { "_source": [ "name", "price" ], "query": { "match_all": {} } }你会得到:
{ "hits": { "hits": [ { "_id": "1", "_source": { "name": "MacBook Pro", "price": 2499.99 } }, { "_id": "2", "_source": { "name": "iPhone 17", "price": 999.99 } } ] } }同样,这些值可以根据列式表示形式重新构建。
那么columnar_stored呢?
你可以显式选择另一种_source模式:
PUT products_stored { "settings": { "mode": "columnar", "index.mapping.source.mode": "columnar_stored" }, "mappings": { "properties": { "name": { "type": "keyword" }, "category": { "type": "keyword" }, "price": { "type": "double" } } } }区别在于:
| 模式 | _source的获取方式 |
|---|---|
columnar+ synthetic source | 在查询时根据 doc values 重新构建 |
columnar+columnar_stored | 列式_source在索引时生成并存储在磁盘上 |
| 传统索引 | 存储原始 JSON_source |
Elastic 的文档说明,columnar模式不会存储原始 JSON_source;它支持合成列式_source和列式存储_source。
这也是为什么在上面的限制中写道:
Turning off _source: "_source": { "enabled": false }在列式模式下,不被允许。同样,传统的storedsource 模式也不受支持。
举例 二
一个 nested/object 示例可以更清楚地说明这种差异。一个重要细节是,合成_source可能无法逐字节还原原始 JSON:对象数组可以根据列式字段值重新构建,而最终生成的结构或顺序可能与最初建立索引时有所不同。
下面是一个具体示例。
创建一个 columnar 索引
PUT products { "settings": { "mode": "columnar" }, "mappings": { "properties": { "name": { "type": "keyword" }, "tags": { "type": "keyword" }, "specs": { "properties": { "color": { "type": "keyword" }, "weight": { "type": "double" } } }, "offers": { "properties": { "seller": { "type": "keyword" }, "price": { "type": "double" } } } } } }这里值得关注的字段是:
specs.color specs.weight offers.seller offers.price它们会被表示为独立的列式字段,而不是存储原始 JSON 结构。
使用 object 和对象数组索引一个文档
假设原始文档是:
PUT products/_doc/1 { "name": "MacBook Pro", "tags": [ "laptop", "apple", "premium" ], "specs": { "color": "silver", "weight": 1.6 }, "offers": [ { "seller": "Amazon", "price": 2399 }, { "seller": "BestBuy", "price": 2449 } ] }原始 JSON 如下:
products ├── name: "MacBook Pro" ├── tags │ ├── "laptop" │ ├── "apple" │ └── "premium" ├── specs │ ├── color: "silver" │ └── weight: 1.6 └── offers ├── { seller: "Amazon", price: 2399 } └── { seller: "BestBuy", price: 2449 }列式存储看到的是什么?
从概念上来说,这些值会被扁平化为列:
name → "MacBook Pro" tags → ["laptop", "apple", "premium"] specs.color → "silver" specs.weight → 1.6 offers.seller → ["Amazon", "BestBuy"] offers.price → [2399, 2449]这正是关键区别。
列式表示从根本上处理的是字段 / 值列,例如:
offers.seller offers.price而不是保留原始的 JSON 树结构:
"offers": [ { "seller": "Amazon", "price": 2399 }, { "seller": "BestBuy", "price": 2449 } ]对于普通的object字段,Elasticsearch 会将该对象视为字段层级结构。它不是nested字段,因此,在同一个数组元素中不同字段的值之间的关联关系不会被保留用于查询。
例如:
"offers": [ { "seller": "Amazon", "price": 2399 }, { "seller": "BestBuy", "price": 2449 } ]在搜索层面,它实际上会被表示为:
offers.seller = ["Amazon", "BestBuy"] offers.price = [2399, 2449]检索文档
现在运行:
GET products/_doc/1{ "_index": "products", "_id": "1", "_version": 1, "found": true, "_source": { "name": "MacBook Pro", "offers.price": [ 2399, 2449 ], "offers.seller": [ "Amazon", "BestBuy" ], "specs.color": "silver", "specs.weight": 1.6, "tags": [ "laptop", "apple", "premium" ] } }这看起来可能与原始文档完全一样。
但在内部,对于columnar模式来说,存储并随后返回的并不是原始 JSON。Elasticsearch 会根据列式表示重新构建_source。
差异会在这里变得明显
考虑一个有趣得多的文档:
PUT products/_doc/2 { "name": "Phone", "tags": [ "android", "5g", "phone" ], "attributes": [ { "name": "color", "value": "black" }, { "name": "memory", "value": "256GB" } ] }原始结构是:
{ "attributes": [ { "name": "color", "value": "black" }, { "name": "memory", "value": "256GB" } ] }但从概念上来说,列式字段是:
attributes.name → ["color", "memory"] attributes.value → ["black", "256GB"]请注意,列式表示从根本上并不是如下形式:
attribute #1 name = color value = black attribute #2 name = memory value = 256GB相反,它会为每个字段分别存储独立的值。
当 Elasticsearch 重建合成_source时,可以根据这些值重新构建对象 / 数组结构。
因此,返回的_source是合成的,而不是原始 JSON 序列化结果。
GET products/_doc/2{ "_index": "products", "_id": "2", "_version": 1, "found": true, "_source": { "attributes.name": [ "color", "memory" ], "attributes.value": [ "black", "256GB" ], "name": "Phone", "tags": [ "android", "5g", "phone" ] } }一个更直观的示例:数组可能会被重新排序
考虑以下示例:
PUT products/_doc/3 { "tags": [ "zebra", "apple", "banana" ] }原始_source中的顺序是:
zebra apple banana因为tags是keyword字段,而合成_source是根据 doc values 重新构建的,所以重新构建的数组可能会按照该字段的 doc values 表示进行排序,而不是保留原始数组的顺序。
因此,你可能会看到类似这样的结果:
{ "tags": [ "apple", "banana", "zebra" ] }而不是:
{ "tags": [ "zebra", "apple", "banana" ] }这是传统_source与合成_source之间最重要的区别之一。
值仍然存在,但原始 JSON 表示形式不一定会被保留。
对象数组会发生什么?
这正是这种区别特别有意义的地方。
假设:
{ "user": { "name": "Alice", "roles": [ { "name": "admin", "level": 10 }, { "name": "developer", "level": 5 } ] } }列式字段从概念上来说是:
user.name → "Alice" user.roles.name → ["admin", "developer"] user.roles.level → [10, 5]合成_source必须重新构建:
{ "user": { "name": "Alice", "roles": [ { "name": "admin", "level": 10 }, { "name": "developer", "level": 5 } ] } }而不是简单地返回一份存储的原始 JSON 副本。
这也是为什么普通object和nested对象之间的区别非常重要。如果roles被映射为普通的object,Elasticsearch 会将其中的字段扁平化用于索引。如果将其映射为nested,Elasticsearch 则会保留属于数组中每个元素的字段之间的关联关系。
一个有用的可视化方式
传统_source:
Original JSON │ ▼ ┌───────────────────────────┐ │ { │ │ "offers": [ │ │ { │ │ "seller": "Amazon", │ │ "price": 2399 │ │ }, │ │ { │ │ "seller": "BestBuy",│ │ "price": 2449 │ │ } │ │ ] │ │ } │ └───────────────────────────┘ │ ▼ Stored _source使用合成列式_source:
Original JSON │ ▼ ┌────────────────────────────┐ │ offers.seller → Amazon │ │ offers.seller → BestBuy │ │ offers.price → 2399 │ │ offers.price → 2449 │ └────────────────────────────┘ │ │ reconstruct ▼ ┌───────────────────────────┐ │ Synthetic _source │ │ │ │ "offers": [ │ │ {...}, │ │ {...} │ │ ] │ └───────────────────────────┘关键要点
最容易记住的方法是:
columnar模式以列的形式存储数据,而不是存储原始 JSON 文档。需要时再重新构建_source。
因此:
Original JSON ↓ field extraction / columnar representation ↓ doc values ↓ synthetic _source ↓ JSON returned to the client由于 JSON 是重新构建的,因此不能假定数组顺序以及原始 JSON 的确切表示形式一定会被保留。
需要对前面的示例做一个重要修正:如果你的目标是专门演示Elastic 文档中记录的合成_source在数组 / 对象方面的限制,那么最好直接使用 Elastic 文档中的确切字段映射和示例,而不要假定每一种对象数组的重建行为都完全符合前面所示的概念性扁平化过程。具体行为取决于字段类型和映射方式(keyword、数值类型、object、nested等)。