【Elastic Engineering】Elasticsearch:使用 Runtime fields 对索引字段进行覆盖处理以修复错误 - 7.11 发布

作者:刘晓国


警告:此功能处于 beta 版本,可能会更改。 该设计和代码不如正式的 GA 功能成熟,并且按原样提供,不提供任何担保。 Beta 功能不受官方 GA 功能的支持 SLA 约束。


运行时字段(runtime fields)是在查询时评估的字段。 运行时字段使你能够:


将字段添加到现有文档中,而无需重新索引数据

在不了解数据结构的情况下开始使用数据

覆盖查询时从索引字段返回的值

为特定用途定义字段,而无需修改基础架构

你可以像其他任何字段一样从搜索 API 访问运行时字段,Elasticsearch 【Elastic Engineering】Elasticsearch:使用 Runtime fields 对索引字段进行覆盖处理以修复错误 - 7.11 发布看到的运行时字段没有任何不同。 你可以在 index mappingsearch request 中定义 runtime fields。 这个完全由你来进行选择,这是运行时字段固有的灵活性的一部分。


当使用日志数据时,运行时字段很有用(请参见示例),尤其是在不确定数据结构时。 你的搜索速度会降低,但是索引的大小要小得多,你可以更快地处理日志而不必对它们进行索引。


你也可以阅读如下的其它两篇 runtime fields 文章:


Elasticsearch:创建 Runtime field 并在 Kibana 中使用它 - 7.11 发布


Elasticsearch:动态创建 Runtime fields - 7.11 发行版


Runtime fields 的好处


由于未对 runtime fields 进行索引,因此添加运行时字段不会增加索引的大小。你可以直接在索引映射中定义运行时字段,从而节省存储成本并提高提取速度。你可以更快地将数据提取到 Elastic Stack 中并立即访问。定义运行时字段时,可以立即将其用于搜索请求,聚合,过滤和排序。


如果你将 runtime fields 设为索引字段,则无需修改任何引用该 runtime fields 的查询。更好的是,你可以引用某些索引,其中该字段是 runtime fields,而可以引用其他索引,其中该字段是索引字段(他们可以共用一个索引字段的名字)。你可以灵活选择要索引的字段以及要保留为运行时字段的字段。


从本质上讲,运行时字段的最重要优点是能够在提取文档后将其添加到文档中。此功能简化了映射决策,因为你不必预先决定如何解析数据,并且可以随时使用运行时字段来修改映射。使用运行时字段允许使用较小的索引和更快的提取时间,从而减少了资源消耗并降低了运营成本。


折衷


Runtime fields 使用较少的磁盘空间,并在访问数据时提供了灵活性,但根据运行时脚本中定义的计算,可能会影响搜索性能。


为了平衡搜索效果和灵活性,你通常会搜索并过滤索引字段,例如时间戳。运行查询时,Elasticsearch 首先自动使用这些索引字段,从而缩短了响应时间。然后,你可以使用  runtime fields 来限制 Elasticsearch 计算其值所需的字段数。将索引字段与 runtime fields 一起使用可为你索引的数据以及如何定义其他字段的查询提供灵活性。


使用异步搜索 API 来运行包含 runtime fields 的搜索。这种搜索方法有助于抵消包含该字段的每个文档中运行时字段的计算值对性能的影响。如果查询无法同步返回结果集,则结果可用时,你将异步获得结果。


重要:对 runtime fields 的查询被认为是耗时的。 如果将 search.allow_expensive_queries 设置为 false,则不允许耗时的查询,并且 Elasticsearch 将拒绝针对 runtime field 的任何查询。


例子


在下面的例子演示了如何使用 runtime fields 修复索引数据中的错误。 我们特意为有一些错误的文档建立索引,然后使用 runtime fields 来隐藏索引字段。 该例子展示了用户在Kibana Lens 中查询数据或创建可视化效果时将如何看到正确的信息,该信息是在 runtime fields 中计算的。 这种情况下,可以通过在 runtime fields 中添加阴影数据(而不是重新编制索引)来立即修复索引数据中的错误。 Runtime filed 是在 Elasticsearch 中读取时为 schema 的实现提供的名称。


首先我们在 Kibana 的 console 中打入如下的命令来创建一个叫做 dur_log 的 index template:

# Create an index template which we will use to create multiple indices
PUT _index_template/dur_log
{
  "index_patterns": [
    "dur_log-*"
  ],
  "template": {
    "mappings": {
    "properties": {
      "timestamp": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss"
      },
      "browser": {
        "type": "keyword"
      },
      "duration": {
        "type": "double"
      }
    }
  }
  }
}

上面表明任何一个以 dur_log- 为开头的索引将具有的索引属性。它定义了三个字段:


timestamp

type

duration


接着我们使用 bulk API 来创建一个叫做 dur_log-1 的索引:

# Laod a few documents, Firefox erroneously entered in ms instead of sec
POST dur_log-1/_bulk
{"index":{}}
{"timestamp": "2021-01-25 10:01:12", "browser": "Chrome", "duration": 1.176}
{"index":{}}
{"timestamp": "2021-01-25 10:01:13", "browser": "Safari", "duration": 1.246}
{"index":{}}
{"timestamp": "2021-01-26 10:02:11", "browser": "Edge", "duration": 0.993}
{"index":{}}
{"timestamp": "2021-01-26 10:02:15", "browser": "Firefox", "duration": 1342}
{"index":{}}
{"timestamp": "2021-01-26 10:01:23", "browser": "Chrome", "duration": 1.151}
{"index":{}}
{"timestamp": "2021-01-27 10:01:54", "browser": "Chrome", "duration": 1.141}
{"index":{}}
{"timestamp": "2021-01-28 10:01:32", "browser": "Firefox", "duration": 984}
{"index":{}}
{"timestamp": "2021-01-29 10:01:21", "browser": "Edge", "duration": 1.233}
{"index":{}}
{"timestamp": "2021-01-30 10:02:07", "browser": "Safari", "duration": 1.312}
{"index":{}}
{"timestamp": "2021-01-30 10:01:19", "browser": "Chrome", "duration": 1.231}

在上面我们导入一些数据到 Elasticsearch 中去。从上面的数据中我们可以看到一些问题:其中 Firefox 浏览器的 duration 显示值为 984,1342,而其它的浏览器的这个值在 1 附近。我们可以使用如下的 aggregation 来轻松地发现这个问题:

# Aggregate for average duration per browser
GET dur_log-1/_search
{
  "size": 0,
  "aggs": {
    "terms": {
      "terms": {
        "field": "browser"
      },
      "aggs": {
        "average duration": {
          "avg": {
            "field": "duration"
          }
        }
      }
    }
  }
}

上面命令显示的结果为:

{
  "took" : 1,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 20,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "terms" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "Chrome",
          "doc_count" : 8,
          "average duration" : {
            "value" : 1.17475
          }
        },
        {
          "key" : "Edge",
          "doc_count" : 4,
          "average duration" : {
            "value" : 1.113
          }
        },
        {
          "key" : "Firefox",
          "doc_count" : 4,
          "average duration" : {
            "value" : 1163.0
          }
        },
        {
          "key" : "Safari",
          "doc_count" : 4,
          "average duration" : {
            "value" : 1.279
          }
        }
      ]
    }
  }
}

上面显示 Firefox 的平均 duration 值为 1163。这显然远远高于其它浏览器的值。


这是什么原因呢?这个原因可能是在最初导入数据的时候 duration 的时间单位搞错了。其它的浏览器的 duration 单位为秒,而 Firefox 的单位为毫秒。这样当我们使用 Lens 来展示数据时,它是这样的:

【Elastic Engineering】Elasticsearch:使用 Runtime fields 对索引字段进行覆盖处理以修复错误 - 7.11 发布

从上面,我们可以看出来,Firefox 的值比其它的浏览器的值显然高的太多了。明显显示它的单位是不对的。那么我们如何修改这个错误呢,一种办法是我们重新修改我们的数据源,并把 Firefox 的 duration 的值除以 1000,并重新导入,或者使用 ingest pipeline 在导入的过程中进行处理。那么有没有一种办法在不重新导入的情况下能展示正确的数据呢?


答案是使用 runtime field。我们使用如下的方式来定义一个 runtime field:

# Create a runtime field to shadow the indexed field and have the Firefox duration divided by 1000
GET dur_log-1/_search
{
  "runtime_mappings": {
    "duration": {
      "type": "double",
      "script": {
        "source": """if(doc['browser'].value == "Firefox")
        {emit(params._source['duration'] / 1000.0)}
        else {emit(params._source['duration'])}"""
      }
    }
  },
  "size": 0,
  "aggs": {
    "terms": {
      "terms": {
        "field": "browser"
      },
      "aggs": {
        "average duration": {
          "avg": {
            "field": "duration"
          }
        }
      }
    }
  }
}

请注意上面的 runtime_mappings 这个部分:

 "runtime_mappings": {
    "duration": {
      "type": "double",
      "script": {
        "source": """if(doc['browser'].value == "Firefox")
        {emit(params._source['duration'] / 1000.0)}
        else {emit(params._source['duration'])}"""
      }
    }
  },

在上面,我们通过 painless 脚本有针对性对 Firefox 这个浏览器的 duration 做了特殊的处理。如果是 Firefox 浏览器,那么它的 duration 的值被除以 1000。尽管这个字段和 source 中的 duration 是同样一个字段名字,但是在搜索的时候,它将首先采纳在 runtime_mappings 中定义的这个 duration。这个结果将被显示在搜索的结果中,但是它不影响存放在 Elasticsearch 中的源数据。上面的命令显示结果为:

{
  "took" : 21,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "terms" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "Chrome",
          "doc_count" : 4,
          "average duration" : {
            "value" : 1.17475
          }
        },
        {
          "key" : "Edge",
          "doc_count" : 2,
          "average duration" : {
            "value" : 1.113
          }
        },
        {
          "key" : "Firefox",
          "doc_count" : 2,
          "average duration" : {
            "value" : 1.163
          }
        },
        {
          "key" : "Safari",
          "doc_count" : 2,
          "average duration" : {
            "value" : 1.279
          }
        }
      ]
    }
  }
}

从上面的结果中可以看出来,Firefox 这次的 duration 的平均值为 1.163。显然它和其它的浏览器的  duration 的值是相近的,也就是非常合理的一个值。


上面在搜索的时候定义 runtime_mappings 解决了我们的搜索问题,但是它不便于在 Kibana 中进行可视化。我们可以在 mapping 中加上这个 runtime field 的定义:

# Add the runtime field to the mapping so all can use it
PUT dur_log-1/_mapping
{
  "runtime": {
    "duration": {
      "type": "double",
      "script": {
        "source": """if(doc['browser'].value == "Firefox")
        {emit(params._source['duration'] / 1000.0)}
        else {emit(params._source['duration'])}"""
      }
    }
  }
}

通过上面的定义,我们将对 dur_log-1 的索引进行特殊的处理。我们可以通过如下的命令来查询 dur_log-1 的 mapping:

GET dur_log-1/_mapping

上面的命令显示:

{
  "dur_log-1" : {
    "mappings" : {
      "runtime" : {
        "duration" : {
          "type" : "double",
          "script" : {
            "source" : """if(doc['browser'].value == "Firefox")
        {emit(params._source['duration'] / 1000.0)}
        else {emit(params._source['duration'])}""",
            "lang" : "painless"
          }
        }
      },
      "properties" : {
        "browser" : {
          "type" : "keyword"
        },
        "duration" : {
          "type" : "double"
        },
        "timestamp" : {
          "type" : "date",
          "format" : "yyyy-MM-dd HH:mm:ss"
        }
      }
    }
  }
}

我们再次使用如下的命令来进行聚合:

# Aggregate on duration and return all fields
GET dur_log-*/_search
{
  "size": 0, 
  "aggs": {
    "terms": {
      "terms": {
        "field": "browser"
      },
      "aggs": {
        "average duration": {
          "avg": {
            "field": "duration"
          }
        }
      }
    }
  }
}

上面的命令显示的结果为:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 10,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "terms" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "Chrome",
          "doc_count" : 4,
          "average duration" : {
            "value" : 1.17475
          }
        },
        {
          "key" : "Edge",
          "doc_count" : 2,
          "average duration" : {
            "value" : 1.113
          }
        },
        {
          "key" : "Firefox",
          "doc_count" : 2,
          "average duration" : {
            "value" : 1.163
          }
        },
        {
          "key" : "Safari",
          "doc_count" : 2,
          "average duration" : {
            "value" : 1.279
          }
        }
      ]
    }
  }
}

在上面,它显示了正确的结果。


同样我们直接在 Lens 中进行数据的展示:

【Elastic Engineering】Elasticsearch:使用 Runtime fields 对索引字段进行覆盖处理以修复错误 - 7.11 发布

显然这次我们得到了正确的统计图。Firefox 的值和其它的浏览器的值相近,而不是相差非常之大。


修正字段的类型


运行时字段的另一个方便用途是临时访问索引期间禁用(disabled)的字段。 (此处不需要 Painless scripting!)。假如我们有如下的一个索引:

PUT twitter
{
  "mappings": {
    "properties": {
      "uid": {
        "enabled": false
      }
    }
  }
}
 
POST twitter/_doc
{
  "uid": "1"
}
 
GET twitter/_search
{
  "size": 0,
  "aggs": {
    "NAME": {
      "terms": {
        "field": "uid",
        "size": 10
      }
    }
  }
}

在上面,我们已经 disable 了字段 uid,也就是说我们不能对它进行搜索和聚合。上面的查询返回的结果为:

{
  "took" : 3,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "NAME" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [ ]
    }
  }
}

从上面,我们可以看出来没有任何结果。但是我们可以通过 runtime field 的办法来重新启动这个字段:

GET twitter/_search
{
  "size": 0,
  "runtime_mappings": {
    "uid": {
      "type": "keyword"
    }
  },
  "aggs": {
    "NAME": {
      "terms": {
        "field": "uid",
        "size": 10
      }
    }
  }
}

上面查询的结果为:

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [ ]
  },
  "aggregations" : {
    "NAME" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "1",
          "doc_count" : 1
        }
      ]
    }
  }
}

我们甚至可以修改一个字段的 type,比如我们针对 twitter 索引:

DELETE twitter
 
PUT twitter
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      }
    }
  }
}
 
POST twitter/_doc
{
  "name": "Liu Xiaoguo"
}
 
GET twitter/_search
{
  "runtime_mappings": {
    "name": {
      "type": "keyword"
    }
  },
  "query": {
    "match": {
      "name": "Liu Xiaoguo"
    }
  }
}

在上面,我们把 name 字段的属性修改为 keyword,而它最早的属性为 text。上面搜索的结果是:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "twitter",
        "_type" : "_doc",
        "_id" : "rCAQn3sBm1H0vkAi3-do",
        "_score" : 1.0,
        "_source" : {
          "name" : "Liu Xiaoguo"
        }
      }
    ]
  }
}

但是如果我们进行如下的搜索:

GET twitter/_search
{
  "runtime_mappings": {
    "name": {
      "type": "keyword"
    }
  },
  "query": {
    "match": {
      "name": "Liu xiaoguo"
    }
  }
}

我们将得不到任何的搜索结果,因为这个不是完全匹配。其中的 Xiaoguo 和 xiaoguo 是不匹配的。


上一篇:黑马程序员 六、线程技术


下一篇:使用NSClassFromString