Elasticsearch:由于映射冲突而重新索引数据流

2026年5月2日   |   by mebius

作者:来自 ElasticLisa Larribas

%title插图%num

了解如何通过重新索引数据流来修复 Elasticsearch 映射冲突。本博客解释了重新索引过程以及如何确保新数据被正确映射。

如果你是 Elasticsearch 新手?加入我们的 Elasticsearch 入门网络研讨会。你也可以现在开始免费的云试用,或者在你的本地机器上试用 Elastic。


当字段中出现映射冲突时,无论这些字段是 Elastic Common Schema – 标准(ECS 标准)还是特定于数据源,使用 Dev Tools 对数据进行重新索引就变得很有必要。这些冲突可能会对摄取之后的任何下游功能产生负面影响,可能导致结果不准确,或者阻止在可视化、仪表板、Security 应用以及聚合等功能中使用完整的数据集。本博客详细说明了该重新索引过程的步骤。

本博客内容基于 Elastic 版本 9.2.8 和 8.19.14,以及 Filestream Integration 版本 2.3.0 和 1.2.0 开发并验证。

重要说明:根据你的环境,某些步骤可能需要进行特定修改。此外,请注意,从 Filestream Integration 版本 2.3.3 开始,动态模板已从 @package 组件模板中移除。

在开始重新索引过程之前,考虑你当前环境中的存储分配非常重要。下面概述的步骤涉及创建现有 backing index 的副本,该副本将临时驻留在 hot 层

Elasticsearch 数据分层

  • Hot:Hot 层是时间序列数据进入 Elasticsearch 的入口,存储最新、最常被搜索的数据。Hot 层节点需要快速读写,因此需要更多资源和更快的存储(SSD)。该层是必需的,新数据流索引会自动分配到这里。
  • Warm:当时间序列数据的查询频率低于最近在 hot 层中索引的数据时,可以移动到 warm 层。Warm 层通常存储最近几周的数据。仍然允许更新,但通常不频繁。Warm 层节点通常不需要像 hot 层那样快。为了提高弹性,warm 层中的索引应配置一个或多个副本。
  • Cold:不常被搜索的数据可以从 warm 层移动到 cold 层。Cold 层仍然可搜索,但更侧重于降低存储成本而不是搜索速度。或者,cold 层可以存储带副本的常规索引,而不是可搜索快照,从而在不减少磁盘空间需求的情况下,使用更便宜的硬件存储较旧的数据。
  • Frozen:很少被查询或不再被查询的数据会从 cold 层移动到 frozen 层,直到生命周期结束。该层使用快照仓库和部分挂载的索引来存储和加载数据,从而减少本地存储和成本,同时仍然允许搜索。由于 Elasticsearch 可能需要从快照仓库获取冻结数据,因此在 frozen 层上的搜索通常比 cold 层更慢。我们建议使用专用的 frozen 层节点。

更多有关数据分层的知识,请详细阅读文章 “Elasticsearch:Searchable snapshot – 可搜索的快照”。

前提条件:确定哪些字段存在冲突

要确定哪些字段存在映射冲突,请导航到 Stack Management -> Data Views -> logs-(使用 logs- 数据视图可以查看所有以 logs- 前缀开头的最高层级数据)。如果存在冲突,会显示一个黄色提示框。你可以点击 “View conflicts”,或者在搜索框旁边的 Field type 下拉框中选择 “conflict”。

%title插图%num

点击黄色的 Conflict 按钮将显示哪些索引关联了哪些映射类型。

这种情况(字段同时被映射为 keyword 和 long)通常是因为在为相关数据流组件模板定义特定映射类型之前,数据就已经被摄取。在这种情况下,Elasticsearch 会尝试根据其动态模板来设置映射。

%title插图%num

为了确定该字段应使用哪种映射类型,以及该字段是否属于 ECS 字段,需要参考 ECS 字段参考文档进行验证。如果该字段不是 ECS 字段,则需要检查其值以确定正确的映射类型。

%title插图%num

如果某个字段(例如本例中的 log.offset)没有在 ECS 中记录,接下来的步骤是:分析该字段的值,确定哪种冲突的映射类型对应的 backing index 数量最多,并检查其他索引的组件模板。

通常,与最多索引关联的映射类型是正确的,但我们仍建议你验证该字段的实际值以确认这一点。要确认某种映射类型(例如 long)是否有效,还必须验证该字段的值是否符合该类型的要求。可以通过使用 Discover 搜索该字段来完成此验证。同时,查看包含相同字段的其他数据流也可以提供额外的确认。

要查看存在映射问题字段的取值,请返回之前提到的黄色 Conflict 按钮,点击该按钮,选中其中一个 backing index,并将其粘贴到 Discover 会话中。你的 Kibana Query Language(KQL)语句应类似如下截图所示,包含 _index: 字段限定符。

%title插图%num

%title插图%num

准备新的 backing index 自定义组件模板

为了解决数据流中的映射冲突,首先检查相关的 @package 组件模板。你可以在 Stack Management -> Index Management -> Component Template 中找到它。搜索对应的数据流,并选择相应的 @package 链接。该模板开箱即包含字段映射,虽然映射不匹配的情况并不常见,但仍有可能忽略更合适的类型。

检查模板以确认其中包含目标字段所需的字段嵌套和映射。例如,如果模板错误地将 log.offset 定义为 keyword,这就是问题的根源。

重要说明:由于不建议修改 @package/managed 模板,你必须使用或创建一个 @custom 组件模板来修正映射类型(例如 log.offset),以确保未来的数据使用正确的映射。

  • 我们不建议修改 @package/managed 模板,因为当你将集成升级到较新版本时,你对 @package 模板所做的任何更改都会被覆盖。这也是我们推荐使用 @custom 模板的原因。
  • 如果数据流存在映射冲突,你需要在该数据流的 @custom 组件模板中添加所有缺失的字段(包括 ECS 和非 ECS)的嵌套或映射。如果该模板尚未创建,请先创建,并确保为相关字段指定正确的映射类型。
  • 如果你的数据视图中存在多个冲突,建议一次性为数据流补齐所有必要的映射,这样只需执行一次 reindex,而不是多次执行。在 @custom 组件模板中正确配置数据类型,将确保未来的数据摄取遵循相同的映射规范。
  • 要创建 @custom 组件模板(或验证其已被使用且已配置),请导航到 Index Templates,输入目标数据流的名称,然后点击该数据流正在使用的 @custom 模板。如果模板尚未创建,会出现一个黄色提示框,允许你通过 UI 创建该模板。

%title插图%num

%title插图%num

下面的截图显示了选择 Create component template 后进入的下一页。在第一页保持默认设置不变,点击 “Mappings” 或 “Next”,直到进入 Mappings 页面。

%title插图%num

%title插图%num

为了显式为新进入的字段设置映射,或者更新存在映射冲突的字段,当由于 index lifecycle policy 中的配置导致数据流发生 rollover 时,需要为发生冲突的字段添加一条映射配置。

下面的操作将会在 filestream 数据流的 @custom 组件模板中为 log.offset 字段设置映射。如果需要,可以重复这些步骤,为该数据集添加其他自定义字段,或使用适当的映射类型更新 @package 中需要调整的字段。

在本例中,当将 offset 设置为 Long 时,字段类型应选择 Numeric,而 Numeric 的具体类型选择 Long。点击 “Add field”,然后点击该区域外部以继续操作。

%title插图%num

%title插图%num

当所有需要的字段都添加完成后,点击继续进入 review 页面进行检查,确认无误后,选择 “Create component template” 以完成创建。从此步骤之后,所有新摄取的数据中,log.offset 将被设置为 long 类型。

%title插图%num

创建新的 backing index 结构

新的 backing index 需要包含数据流组件模板中的现有映射,以及 ECS 的 ecs@mappings 组件模板。ecs@mappings 组件模板会在数据流组件之后应用,作为“兜底(catchall)”机制,用于补充之前组件模板可能未覆盖到的其他字段映射。

请打开数据流的 @package mappings 对应浏览器标签页。(路径:Stack Management -> Index Management -> Component Template -> logs-filestream.generic@package -> Manage -> Edit。)进入后,点击 Review 部分,然后选择 Request,最后点击右侧的 Copy 按钮。复制得到的组件模板 JSON 内容将确保在更新 log.offset 字段映射的同时,其余字段映射和设置得以保留。该 JSON 将作为新 reindex 后 backing index 的基础结构。

重要提示:如果没有复制模板 JSON 就继续进行 reindex 操作,虽然 log.offset 冲突会被解决,但由于未保持当前映射的完整性,可能会与集成产生新的冲突,从而导致需要重复处理原本的问题,增加额外工作量。

%title插图%num

打开第二个浏览器标签页,进入 Dev Tools,并粘贴刚刚复制的内容。现在需要对粘贴内容进行清理与调整:

对请求的修改

1. 索引名称:
_component_template/logs-filestream.generic@package 替换为你计划重新索引的 backing index 名称,并在末尾加上 -1。例如:PUT -1

这里的 -1 表示这是一个 reindex 操作,不会与默认基于创建时间的 ILM rollover 设置冲突。

2. Settings:
删除 "template"(第 3 行),以及整个 JSON payload 最后的闭合大括号。

第 3 行应从 "settings": { 开始。

settings 内部内容替换为:

  • "index.codec": "best_compression"(应用最佳压缩)

  • "index.lifecycle.name": "logs"(应用 logs ILM 策略;如果你使用不同策略,请相应修改)

  • "index.lifecycle.rollover_alias": ""(留空,但必须保留该字段,否则 ILM 在 hot 之后阶段可能报错)

3. 结构调整:
当前请求应包含两个主要部分:

  • settings

  • mappings

"mappings": { 中,应包含:

  • "dynamic_templates"

  • "properties"(包含字段及其映射)

4. dynamic_templates 修改:
当前 dynamic templates 中包含一些与后续 ecs@mappings 重复的字段,这会造成冗余。

需要:

  • 删除 dynamic_templates 中除第二部分 _embedded_ecs-data_stream_to_constant 之外的所有内容

  • 然后按上述方式,将 ecs@mappings component template 中的 dynamic mappings 一并加入

  • 建议直接从 UI 复制 ecs@mappings 的完整 mappings 内容,然后粘贴到 Dev Tools 的 dynamic_templates 中,再去重整理

最终 dynamic_templates 应保留 _embedded_ecs-data_stream_to_constant,并在其后追加整理后的 ecs@mappings 内容。


⚠️ 重要说明:
如果不移除或正确整理 dynamic_templates,会导致字段出现重复映射(例如 text + keyword 同时存在),从而造成新的 mapping 冲突,并在 data view 中引发字段解析问题。

最终剩余结构应主要为 "mappings" 下的 "properties" 部分。

%title插图%num

%title插图%num

%title插图%num

5. 元数据删除:删除最后一个名为 “_meta” 的部分,以及名为 “version” 的部分(如果存在)。

6. 格式化:对剩余部分进行自动缩进,并调整或删除任何不必要的花括号,以避免执行失败。

%title插图%num

7. 映射更改:导航到 “properties” 部分,找到 “log”,然后定位其下的 “offset”。将类型从 keyword 更改为 long,并删除标记为 “ignore_above”: 1024, 的行(包括逗号)。如果之前创建的 @custom 组件模板中添加了多个条目,也需要在此处一并包含。

你的 Dev Tools 控制台视图现在应类似于下方提供的示例。

PUT .ds-logs-filestream.generic-default-2026.04.14-000001-1
{
  "settings": {
    "index.codec": "best_compression",
    "index.lifecycle.name": "logs",
    "index.lifecycle.rollover_alias": ""
  },
  "mappings": {
    "dynamic_templates": [
      {
        "_embedded_ecs-data_stream_to_constant": {
          "path_match": "data_stream.*",
          "mapping": {
            "type": "constant_keyword"
          }
        }
      },
      {
        "ecs_timestamp": {
          "mapping": {
            "ignore_malformed": false,
            "type": "date"
          },
          "match": "@timestamp"
        }
      },
      {
        "ecs_message_match_only_text": {
          "path_match": [
            "message",
            "*.message"
          ],
          "mapping": {
            "type": "match_only_text"
          },
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_non_indexed_keyword": {
          "path_match": [
            "*event.original"
          ],
          "mapping": {
            "index": false,
            "type": "keyword",
            "doc_values": false
          }
    tgcode    }
      },
      {
        "ecs_non_indexed_long": {
          "path_match": [
            "*.x509.public_key_exponent"
          ],
          "mapping": {
            "index": false,
            "type": "long",
            "doc_values": false
          }
        }
      },
      {
        "ecs_ip": {
          "path_match": [
            "ip",
            "*.ip",
            "*_ip"
          ],
          "mapping": {
            "type": "ip"
          },
          "match_mapping_type": "string"
        }
      },
      {
        "ecs_wildcard": {
          "path_match": [
            "*.io.text",
            "*.message_id",
            "*registry.data.strings",
            "*url.path"
          ],
          "mapping": {
            "type": "wildcard"
          },
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_path_match_wildcard_and_match_only_text": {
          "path_match": [
            "*.body.content",
            "*url.full",
            "*url.original"
          ],
          "mapping": {
            "fields": {
              "text": {
                "type": "match_only_text"
              }
            },
            "type": "wildcard"
          },
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_match_wildcard_and_match_only_text": {
          "mapping": {
            "fields": {
              "text": {
                "type": "match_only_text"
              }
            },
            "type": "wildcard"
          },
          "unmatch_mapping_type": "object",
          "match": [
            "*command_line",
            "*stack_trace"
          ]
        }
      },
      {
        "ecs_path_match_keyword_and_match_only_text": {
          "path_match": [
            "*.title",
            "*.executable",
            "*.name",
            "*.working_directory",
            "*.full_name",
            "*file.path",
            "*file.target_path",
            "*os.full",
            "*email.subject",
            "*vulnerability.description",
            "*user_agent.original"
          ],
          "mapping": {
            "fields": {
              "text": {
                "type": "match_only_text"
              }
            },
            "type": "keyword"
          },
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_date": {
          "path_match": [
            "*.timestamp",
            "*_timestamp",
            "*.not_after",
            "*.not_before",
            "*.accessed",
            "created",
            "*.created",
            "*.installed",
            "*.creation_date",
            "*.ctime",
            "*.mtime",
            "ingested",
            "*.ingested",
            "*.start",
            "*.end",
            "*.indicator.first_seen",
            "*.indicator.last_seen",
            "*.indicator.modified_at",
            "*threat.enrichments.matched.occurred"
          ],
          "mapping": {
            "type": "date"
          },
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_path_match_float": {
          "path_match": [
            "*.score.*",
            "*_score*"
          ],
          "mapping": {
            "type": "float"
          },
          "path_unmatch": "*.version",
          "unmatch_mapping_type": "object"
        }
      },
      {
        "ecs_usage_double_scaled_float": {
          "path_match": "*.usage",
          "mapping": {
            "scaling_factor": 1000,
            "type": "scaled_float"
          },
          "match_mapping_type": [
            "double",
            "long",
            "string"
          ]
        }
      },
      {
        "ecs_geo_point": {
          "path_match": [
            "*.geo.location"
          ],
          "mapping": {
            "type": "geo_point"
          }
        }
      },
      {
        "ecs_flattened": {
          "path_match": [
            "*structured_data",
            "*exports",
            "*imports"
          ],
          "mapping": {
            "type": "flattened"
          },
          "match_mapping_type": "object"
        }
      },
      {
        "all_strings_to_keywords": {
          "mapping": {
            "ignore_above": 1024,
            "type": "keyword"
          },
          "match_mapping_type": "string"
        }
      }
    ],
    "properties": {
      "input": {
        "properties": {
          "type": {
            "ignore_above": 1024,
            "type": "keyword"
          }
        }
      },
      "@timestamp": {
        "ignore_malformed": false,
        "type": "date"
      },
      "ecs": {
        "properties": {
          "version": {
            "ignore_above": 1024,
            "type": "keyword"
          }
        }
      },
      "log": {
        "properties": {
          "file": {
            "properties": {
              "inode": {
                "ignore_above": 1024,
                "type": "keyword"
              },
              "path": {
                "ignore_above": 1024,
                "type": "keyword"
              },
              "device_id": {
                "ignore_above": 1024,
                "type": "keyword"
              },
              "fingerprint": {
                "index": false,
                "type": "keyword"
              }
            }
          },
          "offset": {
            "type": "long"
          },
          "level": {
            "ignore_above": 1024,
            "type": "keyword"
          }
        }
      },
      "data_stream": {
        "properties": {
          "namespace": {
            "type": "constant_keyword"
          },
          "type": {
            "type": "constant_keyword"
          },
          "dataset": {
            "type": "constant_keyword"
          }
        }
      },
      "event": {
        "properties": {
          "original": {
            "index": false,
            "type": "keyword",
            "doc_values": false
          },
          "module": {
            "type": "constant_keyword",
            "value": "filestream"
          },
          "dataset": {
            "type": "constant_keyword",
            "value": "filestream.generic"
          }
        }
      },
      "message": {
        "type": "match_only_text"
      },
      "tags": {
        "ignore_above": 1024,
        "type": "keyword"
      }
    }
  }
}

当你的控制台视图与示例一致(并已包含任何额外的自定义字段以及你环境中的自定义值)后,执行命令以创建新 backing index 的“空壳”,并在过程中暂停处理出现的任何错误。

开始 reindex 过程

当新 backing index 的空壳成功创建后,下一步是执行 reindex 并解决映射冲突。

⚠️ 重要说明:
如果存在映射冲突的 backing index 是最新的索引,并且它是当前的写入索引(例如 backing index 结尾为 -000001),则需要对数据流执行 rollover。

这是因为当前写入索引正在接收实时数据,属于 live backing index,无法直接修改。

在通过之前创建的 @custom 组件模板将正确字段映射应用到新的写入索引之后,所有新写入的文档都会自动使用新的映射配置。

接下来通过执行以下操作完成该步骤:

POST /_rollover

例如:

POST logs-filestream.generic-default/_rollover

%title插图%num

重新索引(Reindexing)是指在相同命名规范下,将数据从现有的 backing index 复制到一个新的 index 中,通常用于应用必要的变更。这些修改可能包括更新 component template,或为数据添加新的 ingest pipeline 以进行处理。

接下来,数据将从存在映射错误的 backing index 复制到一个新的 backing index 中。tgcode原始 backing index 已经完成 rollover,因此不会再接收新的文档。新的 backing index 将遵循相同的命名规范,以保证数据可见性和完整性,并应用正确的 ILM 策略,但会增加 -1 后缀,以表示它已被重新索引。

请根据需要调整 index 名称,并将以下代码粘贴到 console 中。通过设置 wait_for_completion=false,你可以跟踪文档复制进度,从而估算剩余 reindex 时间。如果不使用该设置,则无法通过 GET _tasks 命令查看任务状态,只能通过 GET -1/_count 查看新 backing index 中的文档数量。

重要说明:如果 reindex 过程中出现问题,不要重新运行 reindex 命令,因为这样会重新开始整个过程,并在以 -1 结尾的 index 中产生重复数据。如果需要重试,应先删除带 -1 后缀的 index,然后重新执行之前的 PUT 命令以重新创建新的 backing index 空壳。

POST _reindex?wait_for_completion=false
{
  "source": {
    "index": ""
  },
  "dest": {
    "index": "-1"
  }
}

i.e.
POST _reindex?wait_for_completion=false
{
  "source": {
    "index": ".ds-logs-filestream.generic-default-2026.04.13-000001"
  },
  "dest": {
    "index": ".ds-logs-filestream.generic-default-2026.04.13-000001-1"
  }
}

%title插图%num

执行后,响应中会包含一个 task ID。你可以使用该 ID,通过以下命令监控 reindex 进度:GET _tasks/。

reindex 的持续时间取决于原始 index 中的数据量。可以通过执行 GET 命令来跟踪完成情况,当看到 “completed”: true 时,表示任务已完成,其输出应类似如下所示。

%title插图%num

随着 reindex 过程中文档数量部分已完成,下一步是验证新 backing index 以及相关字段的映射是否正确。

GET -1/_mapping

例如:

GET .ds-logs-filestream.generic-default-2026.04.13-000001-1/_mapping

你可以验证 log.offset 的映射如下所示。为了确认其他字段只有单一映射条目(而不是同时存在 text 和 keyword),可以将它们与之前 PUT 命令中未包含在 dynamic template 部分的字段进行对比。

%title插图%num

如果正在重新索引的 backing index 包含大量文档,检查这些文档复制到新 backing index 的状态会很有帮助;可以通过以下两个 Dev Tools 命令来对比文档数量。

%title插图%num

一旦确认文档数量一致且正确的映射已存在,就需要更新数据流以包含新的 backing index,避免在 index management 中出现孤立的 backing index,否则 ILM 策略将不会在该 backing index 上执行。

  • 如果操作成功,返回结果应为 true 的确认响应。
POST _data_stream/_modify
{
  "actions": [
    {
      "add_backing_index": {
        "data_stream": "logs-filestream.generic-default",
        "index": ".ds-logs-filestream.generic-default-2026.04.14-000001-1"
      }
    }
  ]
}

%title插图%num

使用以下命令验证新的 backing index 是否已被正确添加,并确保 ilm_policy 配置正确:

GET _data_stream/logs-filestream.generic-default

%title插图%num

接下来使用以下命令检查该 backing index 的 ILM 状态:

  • 通常情况下看到该 index 处于 hot 阶段是正常的,因为它是刚刚创建的(请参考第 8 行或第 10 行)。

%title插图%num

GET .ds-logs-filestream.generic-default-2026.04.14-000001-1/_ilm/etgcodexplain

%title插图%num

%title插图%num

执行以下操作,将 backing index 从 hot 层迁移到该数据流 ILM 策略中 hot 阶段之后的下一个合适层级。current_step 中的 phase、action 和 name 的具体值可以分别参考上方截图中的第 11、13、15 行。

next_step 的值表示该 index 将要进入的下一个 ILM 阶段或数据分层。

例如:

POST _ilm/move/.ds-logs-filestream.generic-default-2026.04.14-000001-1
{
  "current_step": {
    "phase": "hot",
    "action": "rollover", 
    "name": "check-rollover-ready"
  },
  "next_step": {
    "phase": "warm" 
  }
}

%title插图%num

  • 虽然不是必须步骤,但作为安全检查,你可以再次执行 _ilm/explain 命令,以确认该 backing index 已经进入下一个阶段,并且不再处于 hot 状态。

%title插图%num

当满足以下条件时,你可以安全删除最初存在映射冲突的 backing index:

  • 已成功创建新的 backing index
  • 文档已迁移到新 index,并且文档数量一致
  • 映射已修正(包括数据流特定映射和 ECS 映射)
  • 数据流已包含新的 backing index
  • ILM 策略已生效,并且 index 已从 hot 阶段迁移出去

重要说明:或者,在删除原始 index 之前,你可以检查 Data Views 页面。选择 logs-*,并确认重新索引后的 backing index(以 -1 结尾)已出现在 long 类型部分。而原始 backing index 应仍然出现在 keyword 类型下。如果 reindexed 的 backing index 未出现在 long 部分,请返回前面的步骤并进行必要的修正。

例如:

DELETE .ds-logs-filestream.generic-default-2026.04.14-000001

%title插图%num

在解决冲突之后,返回 Data Views 页面并选择 logs-*。如果冲突仅与 log.offset 相关,那么你将不再看到任何冲突列表。如果还存在其他冲突,原始 backing index 应不再出现在冲突列表中;相反,新的 backing index 应已出现在 long 部分。

你也可以在 Discover 中进行验证,确认 log.offset 字段现在显示的是正确的图标。

%title插图%num

%title插图%num

继续重复上述流程,对每一个存在映射冲突的 backing index 执行相同步骤,直到所有冲突都成功解决。

参考资料

最终说明

通过遵循本文中的步骤,你可以解决映射冲突,并确保所有新数据都被正确映射。这是通过将必要的组件模板关联到你的数据源来实现的。该工作流不仅修复了当前问题,还建立了一套安全且可重复的流程,以便在数据和需求不断演进时管理 schema 变更。

原文:https://www.elastic.co/search-labs/blog/elasticsearch-mapping-conflicts-reindex-data-streams

文章来源于互联网:Elasticsearch:由于映射冲突而重新索引数据流

Tags: