注意这个目前在 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/logscustom { 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 模式可能很有用。要使用列式存储的 sourcePUT 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时当你请求_sourceElasticsearch 会根据 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 重新构建columnarcolumnar_stored列式_source在索引时生成并存储在磁盘上传统索引存储原始 JSON_sourceElastic 的文档说明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被映射为普通的objectElasticsearch 会将其中的字段扁平化用于索引。如果将其映射为nestedElasticsearch 则会保留属于数组中每个元素的字段之间的关联关系。一个有用的可视化方式传统_sourceOriginal JSON │ ▼ ┌───────────────────────────┐ │ { │ │ offers: [ │ │ { │ │ seller: Amazon, │ │ price: 2399 │ │ }, │ │ { │ │ seller: BestBuy,│ │ price: 2449 │ │ } │ │ ] │ │ } │ └───────────────────────────┘ │ ▼ Stored _source使用合成列式_sourceOriginal 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等。