我们知道在文档摄入到 Elasticsearch 时,如果文档的字段在 mapping 中已经有定义,而当前的文档的字段的类型和之前的类型是不一样的情况下,那么我们该如何处理呢?通常由如下的几种方法:
- 使用 coerce 属性。在这种情况下,即便不同类型的数据被写入到相应的字段,在能够相互转换的情况下,它的写入也可以是成功的。请详细阅读文章 “Elasticsearch:Elasticsearch 中的数据强制匹配” 及 “Elasticsearch:如何将浮点值存储到整型字段中”。
- 通过 ingest pipeline 或者 Logstash 进行数据转换再写入到 Elasticsearch。
上面的方法只适用于在能够转换的条件下才可以实现,比如 “1” => 1 的转换。但是,在有些情况下,我们的这种转换是根本不可行的,比如 "one" => 1。首先这种数据的类型是根本不一样,而且即便强制转换,也会失败。
那么出现这种情况,一种是直接丢弃该文档,这会造成文档的丢失,即使是一个字段的类型不匹配。那么我们是否有其它的方式呢来处理这个文档呢?比如丢失这个不符合的字段,而摄入其它的字段。
忽略不符合索引映射的字段,并避免在使用 Elasticsearch 摄取期间丢弃文档。一个几乎不为人知的名为 ignore_malformed 的设置如何在因为单个字段格式错误而完全删除文档或忽略该字段并无论如何摄取文档之间产生差异。
ignore_malformed
有时你对收到的数据没有太多控制权。 一个用户可以发送一个 login 字段,它是一个 date,另一个用户可以发送一个 login 字段,它是一个电子邮件地址。
尝试将错误的数据类型索引到字段中会默认引发异常,并拒绝整个文档。 ignore_malformed 参数,如果设置为 true,则允许忽略异常。 格式错误的字段没有被索引,但文档中的其他字段被正常处理。
在本文中,我将解释设置 ignore_malformed 如何影响 100% 的丢弃率和 100% 的成功率(即使只是忽略一些格式错误的字段)。
映射是如何工作的
我喜欢将 Elasticsearch 视为一个基于文档的 NoSQL 数据库,其中不需要预先定义索引的模式。 Elasticsearch 将从第一个文档或包含新字段的任何后续文档推断 schema。
或者,你可以预先提供一个 schema(在 Elasticsearch 术语中称为映射 mapping),并且你的所有文档都需要遵循该 schema。
实际上,情况并非非黑即白。 你还可以为每个文档提供仅涵盖部分字段(可能是最常见的)的部分模式,并让 Elasticsearch 找出更多动态字段的 schema。
当数据格式错误时会发生什么?
无论你是预先指定映射还是 Elasticsearch 自动推断映射。 如果文档只显示一个与索引映射不匹配的字段,Elasticsearch 将删除整个文档并在客户端日志中返回警告。
如果客户不查看这些日志而错过了警告,就会出现大问题。 他们可能永远不会发现哪里出了问题,或者更糟的是,如果所有后续文档的格式都错误,Elasticsearch 甚至可能会完全停止摄取数据。
上述情况听起来非常灾难性,但完全有可能发生,尤其是在你无法完全控制数据质量的情况下。 想想用户生成的文档。
Elasticsearch 中可能有一个非常未知的设置可以准确解决上述问题。 这个字段从 Elasticsearch 2.0 开始就有了。 我们在这里谈论古老的历史,因为在撰写本文时最新版本是 8.8。
示例用例
为了更方便地与 Elasticsearch 交互,我将在这里使用 Kibana(Elasticsearch 的前端工具)和 Dev Tools 控制台。
以下示例摘自 Elasticsearch 官方文档。
我在这里通过提供一些关于幕后发生的事情的更多细节来扩展这个例子。
首先,我们要定义 2 个字段,它们都是整数类型,但只有一个字段定义了 ignore_malformed 。
PUT my-index-000001
{
"mappings": {
"properties": {
"number_one": {
"type": "integer",
"ignore_malformed": true
},
"number_two": {
"type": "integer"
}
}
}
}
如果你尝试使用以下命令获取生成的映射:
GET my-index-000001/_mapping
你讲得到如下的结果:
{
"my-index-000001": {
"mappings": {
"properties": {
"number_one": {
"type": "integer",
"ignore_malformed": true
},
"number_two": {
"type": "integer"
}
}
}
}
}
然后我们提取两个示例文档:
PUT my-index-000001/_doc/1
{
"text": "Some text value",
"number_one": "foo"
}
PUT my-index-000001/_doc/2
{
"text": "Some text value",
"number_two": "foo"
}
文档 1 被正确摄取,而文档 2 反而返回以下错误消息。
{
"error": {
"root_cause": [
{
"type": "document_parsing_exception",
"reason": "[3:17] failed to parse field [number_two] of type [integer] in document with id '2'. Preview of field's value: 'foo'"
}
],
"type": "document_parsing_exception",
"reason": "[3:17] failed to parse field [number_two] of type [integer] in document with id '2'. Preview of field's value: 'foo'",
"caused_by": {
"type": "number_format_exception",
"reason": "For input string: \"foo\""
}
},
"status": 400
}
如果你随后使用以下查询搜索相同的索引:
GET my-index-000001/_search
{
"fields": [
"*"
],
"_source": true
}
你会看到只有第一个文档(id=1)被正确摄取。
{
"took": 659,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "my-index-000001",
"_id": "1",
"_score": 1,
"_ignored": [
"number_one"
],
"_source": {
"text": "Some text value",
"number_one": "foo"
},
"fields": {
"text.keyword": [
"Some text value"
],
"text": [
"Some text value"
]
},
"ignored_field_values": {
"number_one": [
"foo"
]
}
}
]
}
}
从上面的 JSON 响应中,你可以注意到几件事:
- 现在有一个 _ignored 类型的字段,其中包含在摄取此文档时被忽略的所有字段的列表。
- 有一个字段 ignored_field_values 带有忽略字段及其值的字典。
- 字段 source 包含未修改的原始文档。 如果你想稍后修复映射问题,这将特别有用。
结论
你可以从今天开始在你的索引上使用 ignore_malformed ,只需添加到你的索引设置,创建映射时添加到单个字段,或者添加到索引模板以使具有特定索引模式的所有索引成为默认选项。 为了简洁起见,我不会在这里展示如何将此设置用于索引、索引模板或组件模板。 请参阅官方文档或继续关注有关该主题的更多文章。
Elastic 目前正在努力使此设置成为 Elasticsearch 8.9 的默认设置。