运行时字段

edit

一个运行时字段是在查询时进行计算的字段。运行时字段使您能够:

  • 无需重新索引数据即可向现有文档添加字段
  • 无需了解数据结构即可开始处理数据
  • 在查询时覆盖从索引字段返回的值
  • 为特定用途定义字段,而无需修改底层模式

您可以通过搜索API访问运行时字段,就像访问其他字段一样,Elasticsearch对运行时字段的处理也没有什么不同。您可以在索引映射搜索请求中定义运行时字段。您的选择,这是运行时字段固有的灵活性的一部分。

使用 fields 参数在 _search API 上 检索运行时字段的值。运行时字段不会显示在 _source 中,但 fields API 适用于所有字段, 即使是那些未作为原始 _source 的一部分发送的字段。

运行时字段在处理日志数据时非常有用(参见示例),特别是当您不确定数据结构时。您的搜索速度会降低,但您的索引大小会小得多,并且您可以更快地处理日志,而无需对它们进行索引。

好处

edit

因为运行时字段未被索引,添加运行时字段不会增加索引大小。您可以直接在索引映射中定义运行时字段,从而节省存储成本并提高摄取速度。您可以更快地将数据摄取到Elastic Stack中,并立即访问它。当您定义一个运行时字段时,您可以立即在搜索请求、聚合、过滤和排序中使用它。

如果你将一个运行时字段更改为索引字段,你不需要修改任何引用该运行时字段的查询。更好的是,你可以引用一些字段是运行时字段的其他索引,以及字段是索引字段的其他索引。你可以灵活地选择哪些字段进行索引,哪些字段保持为运行时字段。

其核心是,运行时字段最重要的好处是能够在文档被索引后向其中添加字段。这一功能简化了映射决策,因为你不必事先决定如何解析数据,并且可以随时使用运行时字段来修正映射。使用运行时字段可以减少索引大小和加快索引时间,从而减少资源使用并降低运营成本。

激励

edit

运行时字段可以替代许多使用_search API进行脚本编写的方式。您如何使用运行时字段受包含的脚本所运行的文档数量的影响。例如,如果您在使用_search API的fields参数来检索运行时字段的值,脚本仅针对顶部命中运行,就像脚本字段一样。

您可以使用脚本字段来访问_source中的值,并基于脚本估值返回计算值。运行时字段具有相同的功能,但提供了更大的灵活性,因为您可以在搜索请求中查询和聚合运行时字段。脚本字段只能获取值。

同样地,你可以编写一个脚本查询,该查询根据脚本在搜索请求中过滤文档。运行时字段提供了一个非常相似的功能,但更加灵活。你可以编写一个脚本来创建字段值,并且这些字段值可以在任何地方使用,例如字段所有查询聚合

您还可以使用脚本来对搜索结果进行排序,但该脚本在运行时字段中的工作方式完全相同。

如果你将一个脚本从搜索请求中的这些部分移动到一个运行时字段,该字段正在计算来自相同数量文档的值,性能应该大致相同。这些功能的性能在很大程度上取决于所包含脚本运行的计算以及脚本运行的文档数量。

妥协

edit

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

为了平衡搜索性能和灵活性,索引您将频繁搜索和过滤的字段,例如时间戳。Elasticsearch在运行查询时会自动首先使用这些索引字段,从而实现快速的响应时间。然后,您可以使用运行时字段来限制Elasticsearch需要计算值的字段数量。结合使用索引字段和运行时字段,可以在索引数据和定义其他字段的查询时提供灵活性。

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

针对运行时字段的查询被认为是昂贵的。如果 search.allow_expensive_queries 设置为 false,则不允许昂贵的查询,Elasticsearch 将拒绝任何针对运行时字段的查询。

映射一个运行时字段

edit

您可以通过在映射定义下添加一个runtime部分并定义一个Painless脚本来映射运行时字段。该脚本可以访问文档的整个上下文,包括通过params._source访问的原始_source以及任何映射字段及其值。在查询时,脚本运行并为查询所需的每个脚本化字段生成值。

例如,以下请求中的脚本从@timestamp字段计算星期几,该字段被定义为date类型。脚本根据timestamp的值计算星期几,并使用emit返回计算的值。

PUT my-index-000001/
{
  "mappings": {
    "runtime": {
      "day_of_week": {
        "type": "keyword",
        "script": {
          "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))"
        }
      }
    },
    "properties": {
      "@timestamp": {"type": "date"}
    }
  }
}

The runtime 部分可以是以下任意一种数据类型:

  • 布尔值
  • 复合
  • 日期
  • 双精度
  • 地理点
  • IP地址
  • 关键字
  • 长整型
  • 查找

具有 typedate 的运行时字段可以接受与 date 字段类型完全相同的 format 参数。

具有 lookup 类型的运行时字段允许从相关索引中检索字段。请参阅 从相关索引中检索字段

如果启用了动态字段映射,其中dynamic参数设置为runtime,新字段将自动作为运行时字段添加到索引映射中:

PUT my-index-000001
{
  "mappings": {
    "dynamic": "runtime",
    "properties": {
      "@timestamp": {
        "type": "date"
      }
    }
  }
}

定义无脚本的运行时字段

edit

运行时字段通常包括一个Painless脚本,用于以某种方式操作数据。然而,在某些情况下,您可能定义一个带脚本的运行时字段。例如,如果您想从_source中检索单个字段而不进行更改,则不需要脚本。您可以直接创建一个不带脚本的运行时字段,例如day_of_week

PUT my-index-000001/
{
  "mappings": {
    "runtime": {
      "day_of_week": {
        "type": "keyword"
      }
    }
  }
}

当未提供脚本时,Elasticsearch 在查询时会隐式地在 _source 中查找与运行时字段同名的字段,并返回一个值(如果存在)。如果同名的字段不存在,响应中不会包含该运行时字段的任何值。

在大多数情况下,尽可能通过 doc_values 获取字段值。使用运行时字段访问 doc_values 比从 _source 中检索值更快,因为数据是从 Lucene 加载的方式。

然而,在某些情况下,从_source中检索字段是必要的。 例如,text字段默认情况下没有doc_values可用,因此您必须从_source中检索值。在其他情况下,您可能会选择在特定字段上禁用doc_values

您可以另外在要检索值的字段前加上params._source(例如params._source.day_of_week)。为了简单起见,在映射定义中定义一个运行时字段而不使用脚本是推荐的选择,只要有可能。

忽略运行时字段上的脚本错误

edit

脚本在运行时可能会抛出错误,例如在访问文档中缺失或无效的值时,或者由于执行无效操作。可以使用on_script_error参数来控制在这种情况下错误的行为。将此参数设置为continue将导致在此运行时字段上静默忽略所有错误。默认的fail值将导致分片失败,并在搜索响应中报告。

更新和移除运行时字段

edit

您可以随时更新或删除运行时字段。要替换现有的运行时字段,请在映射中添加一个具有相同名称的新运行时字段。要从映射中删除运行时字段,请将运行时字段的值设置为null

PUT my-index-000001/_mapping
{
 "runtime": {
   "day_of_week": null
 }
}

在搜索请求中定义运行时字段

edit

您可以在搜索请求中指定一个runtime_mappings部分,以创建仅作为查询一部分存在的运行时字段。您可以在runtime_mappings部分中指定一个脚本,就像在向映射添加运行时字段时一样。

在搜索请求中定义运行时字段使用与在索引映射中定义运行时字段相同的格式。只需从索引映射中的runtime复制字段定义到搜索请求的runtime_mappings部分。

以下搜索请求在runtime_mappings部分中添加了一个day_of_week字段。该字段的值将动态计算,并且仅在此搜索请求的上下文中有效:

GET my-index-000001/_search
{
  "runtime_mappings": {
    "day_of_week": {
      "type": "keyword",
      "script": {
        "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))"
      }
    }
  },
  "aggs": {
    "day_of_week": {
      "terms": {
        "field": "day_of_week"
      }
    }
  }
}

创建使用其他运行时字段的运行时字段

edit

您甚至可以在搜索请求中定义运行时字段,这些字段从其他运行时字段返回值。例如,假设您批量索引了一些传感器数据:

POST my-index-000001/_bulk?refresh=true
{"index":{}}
{"@timestamp":1516729294000,"model_number":"QVKC92Q","measures":{"voltage":"5.2","start": "300","end":"8675309"}}
{"index":{}}
{"@timestamp":1516642894000,"model_number":"QVKC92Q","measures":{"voltage":"5.8","start": "300","end":"8675309"}}
{"index":{}}
{"@timestamp":1516556494000,"model_number":"QVKC92Q","measures":{"voltage":"5.1","start": "300","end":"8675309"}}
{"index":{}}
{"@timestamp":1516470094000,"model_number":"QVKC92Q","measures":{"voltage":"5.6","start": "300","end":"8675309"}}
{"index":{}}
{"@timestamp":1516383694000,"model_number":"HG537PU","measures":{"voltage":"4.2","start": "400","end":"8625309"}}
{"index":{}}
{"@timestamp":1516297294000,"model_number":"HG537PU","measures":{"voltage":"4.0","start": "400","end":"8625309"}}

您在索引后意识到您的数值数据被映射为类型 text。 您希望对 measures.startmeasures.end 字段进行聚合,但由于无法对类型为 text 的字段进行聚合,聚合失败了。 运行时字段来拯救!您可以添加与索引字段同名的运行时字段并修改数据类型:

PUT my-index-000001/_mapping
{
  "runtime": {
    "measures.start": {
      "type": "long"
    },
    "measures.end": {
      "type": "long"
    }
  }
}

运行时字段优先于索引映射中定义的同名字段。这种灵活性允许您遮蔽现有字段并计算不同的值,而无需修改字段本身。如果您在索引映射中犯了错误,您可以使用运行时字段来计算值,这些值可以在搜索请求期间覆盖映射中的值

现在,您可以轻松地在 measures.startmeasures.end 字段上运行 平均聚合

GET my-index-000001/_search
{
  "aggs": {
    "avg_start": {
      "avg": {
        "field": "measures.start"
      }
    },
    "avg_end": {
      "avg": {
        "field": "measures.end"
      }
    }
  }
}

响应包括聚合结果,而不改变底层数据的值:

{
  "aggregations" : {
    "avg_start" : {
      "value" : 333.3333333333333
    },
    "avg_end" : {
      "value" : 8658642.333333334
    }
  }
}

此外,您可以在搜索查询中定义一个运行时字段,该字段计算一个值,然后在该字段上运行统计聚合 在同一查询中

索引映射中不存在duration运行时字段,但我们仍然可以对该字段进行搜索和聚合。以下查询返回duration字段的计算值,并运行统计聚合以计算从聚合文档中提取的数值的统计信息。

GET my-index-000001/_search
{
  "runtime_mappings": {
    "duration": {
      "type": "long",
      "script": {
        "source": """
          emit(doc['measures.end'].value - doc['measures.start'].value);
          """
      }
    }
  },
  "aggs": {
    "duration_stats": {
      "stats": {
        "field": "duration"
      }
    }
  }
}

尽管duration运行时字段仅存在于搜索查询的上下文中,但您可以对该字段进行搜索和聚合。这种灵活性非常强大,使您能够在单个搜索请求中纠正索引映射中的错误并动态完成计算。

{
  "aggregations" : {
    "duration_stats" : {
      "count" : 6,
      "min" : 8624909.0,
      "max" : 8675009.0,
      "avg" : 8658309.0,
      "sum" : 5.1949854E7
    }
  }
}

在查询时覆盖字段值

edit

如果你创建一个与映射中已存在的字段同名的运行时字段,该运行时字段会遮蔽映射字段。在查询时,Elasticsearch会评估运行时字段,根据脚本计算一个值,并将其作为查询的一部分返回。由于运行时字段遮蔽了映射字段,你可以在不修改映射字段的情况下覆盖搜索中返回的值。

例如,假设您将以下文档索引到 my-index-000001

POST my-index-000001/_bulk?refresh=true
{"index":{}}
{"@timestamp":1516729294000,"model_number":"QVKC92Q","measures":{"voltage":5.2}}
{"index":{}}
{"@timestamp":1516642894000,"model_number":"QVKC92Q","measures":{"voltage":5.8}}
{"index":{}}
{"@timestamp":1516556494000,"model_number":"QVKC92Q","measures":{"voltage":5.1}}
{"index":{}}
{"@timestamp":1516470094000,"model_number":"QVKC92Q","measures":{"voltage":5.6}}
{"index":{}}
{"@timestamp":1516383694000,"model_number":"HG537PU","measures":{"voltage":4.2}}
{"index":{}}
{"@timestamp":1516297294000,"model_number":"HG537PU","measures":{"voltage":4.0}}

你后来意识到HG537PU传感器没有报告它们的实际电压。索引值应该是报告值的1.7倍!与其重新索引你的数据,你可以在_search请求的runtime_mappings部分定义一个脚本,来隐藏voltage字段并在查询时计算一个新值。

如果您搜索型号匹配 HG537PU 的文档:

GET my-index-000001/_search
{
  "query": {
    "match": {
      "model_number": "HG537PU"
    }
  }
}

响应包括与型号HG537PU匹配的文档的索引值:

{
  ...
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0296195,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_id" : "F1BeSXYBg_szTodcYCmk",
        "_score" : 1.0296195,
        "_source" : {
          "@timestamp" : 1516383694000,
          "model_number" : "HG537PU",
          "measures" : {
            "voltage" : 4.2
          }
        }
      },
      {
        "_index" : "my-index-000001",
        "_id" : "l02aSXYBkpNf6QRDO62Q",
        "_score" : 1.0296195,
        "_source" : {
          "@timestamp" : 1516297294000,
          "model_number" : "HG537PU",
          "measures" : {
            "voltage" : 4.0
          }
        }
      }
    ]
  }
}

以下请求定义了一个运行时字段,其中脚本评估model_number字段,其值为HG537PU。对于每个匹配项,脚本将voltage字段的值乘以1.7

使用fields参数在_search API上,您可以检索脚本为匹配搜索请求的文档计算的measures.voltage字段的值:

POST my-index-000001/_search
{
  "runtime_mappings": {
    "measures.voltage": {
      "type": "double",
      "script": {
        "source":
        """if (doc['model_number.keyword'].value.equals('HG537PU'))
        {emit(1.7 * params._source['measures']['voltage']);}
        else{emit(params._source['measures']['voltage']);}"""
      }
    }
  },
  "query": {
    "match": {
      "model_number": "HG537PU"
    }
  },
  "fields": ["measures.voltage"]
}

查看响应,每个结果中measures.voltage的计算值分别为7.146.8。这才对嘛!运行时字段在搜索请求中计算了这个值,而没有修改映射值,映射值仍然在响应中返回:

{
  ...
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0296195,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_id" : "F1BeSXYBg_szTodcYCmk",
        "_score" : 1.0296195,
        "_source" : {
          "@timestamp" : 1516383694000,
          "model_number" : "HG537PU",
          "measures" : {
            "voltage" : 4.2
          }
        },
        "fields" : {
          "measures.voltage" : [
            7.14
          ]
        }
      },
      {
        "_index" : "my-index-000001",
        "_id" : "l02aSXYBkpNf6QRDO62Q",
        "_score" : 1.0296195,
        "_source" : {
          "@timestamp" : 1516297294000,
          "model_number" : "HG537PU",
          "measures" : {
            "voltage" : 4.0
          }
        },
        "fields" : {
          "measures.voltage" : [
            6.8
          ]
        }
      }
    ]
  }
}

检索运行时字段

edit

使用 fields 参数在 _search API 上检索运行时字段的值。运行时字段不会显示在 _source 中,但 fields API 适用于所有字段,即使是那些未作为原始 _source 的一部分发送的字段。

定义一个运行时字段以计算星期几

edit

例如,以下请求添加了一个名为 day_of_week 的运行时字段。 该运行时字段包含一个脚本,该脚本根据 @timestamp 字段的值计算星期几。我们将在请求中包含 "dynamic":"runtime",以便将新字段作为运行时字段添加到映射中。

PUT my-index-000001/
{
  "mappings": {
    "dynamic": "runtime",
    "runtime": {
      "day_of_week": {
        "type": "keyword",
        "script": {
          "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ENGLISH))"
        }
      }
    },
    "properties": {
      "@timestamp": {"type": "date"}
    }
  }
}

摄取一些数据

edit

让我们导入一些示例数据,这将生成两个索引字段: @timestampmessage

POST /my-index-000001/_bulk?refresh
{ "index": {}}
{ "@timestamp": "2020-06-21T15:00:01-05:00", "message" : "211.11.9.0 - - [2020-06-21T15:00:01-05:00] \"GET /english/index.html HTTP/1.0\" 304 0"}
{ "index": {}}
{ "@timestamp": "2020-06-21T15:00:01-05:00", "message" : "211.11.9.0 - - [2020-06-21T15:00:01-05:00] \"GET /english/index.html HTTP/1.0\" 304 0"}
{ "index": {}}
{ "@timestamp": "2020-04-30T14:30:17-05:00", "message" : "40.135.0.0 - - [2020-04-30T14:30:17-05:00] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{ "index": {}}
{ "@timestamp": "2020-04-30T14:30:53-05:00", "message" : "232.0.0.0 - - [2020-04-30T14:30:53-05:00] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{ "index": {}}
{ "@timestamp": "2020-04-30T14:31:12-05:00", "message" : "26.1.0.0 - - [2020-04-30T14:31:12-05:00] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{ "index": {}}
{ "@timestamp": "2020-04-30T14:31:19-05:00", "message" : "247.37.0.0 - - [2020-04-30T14:31:19-05:00] \"GET /french/splash_inet.html HTTP/1.0\" 200 3781"}
{ "index": {}}
{ "@timestamp": "2020-04-30T14:31:27-05:00", "message" : "252.0.0.0 - - [2020-04-30T14:31:27-05:00] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{ "index": {}}
{ "@timestamp": "2020-04-30T14:31:29-05:00", "message" : "247.37.0.0 - - [2020-04-30T14:31:29-05:00] \"GET /images/hm_brdl.gif HTTP/1.0\" 304 0"}
{ "index": {}}
{ "@timestamp": "2020-04-30T14:31:29-05:00", "message" : "247.37.0.0 - - [2020-04-30T14:31:29-05:00] \"GET /images/hm_arw.gif HTTP/1.0\" 304 0"}
{ "index": {}}
{ "@timestamp": "2020-04-30T14:31:32-05:00", "message" : "247.37.0.0 - - [2020-04-30T14:31:32-05:00] \"GET /images/nav_bg_top.gif HTTP/1.0\" 200 929"}
{ "index": {}}
{ "@timestamp": "2020-04-30T14:31:43-05:00", "message" : "247.37.0.0 - - [2020-04-30T14:31:43-05:00] \"GET /french/images/nav_venue_off.gif HTTP/1.0\" 304 0"}

搜索计算的星期几

edit

以下请求使用搜索 API 来检索原始请求在映射中定义为运行时字段的 day_of_week 字段。该字段的值是在查询时动态计算的,无需重新索引文档或索引 day_of_week 字段。这种灵活性允许您修改映射而不改变任何字段值。

GET my-index-000001/_search
{
  "fields": [
    "@timestamp",
    "day_of_week"
  ],
  "_source": false
}

之前的请求返回了所有匹配文档的day_of_week字段。 我们可以定义另一个名为client_ip的运行时字段,它也作用于 message字段,并将进一步优化查询:

PUT /my-index-000001/_mapping
{
  "runtime": {
    "client_ip": {
      "type": "ip",
      "script" : {
      "source" : "String m = doc[\"message\"].value; int end = m.indexOf(\" \"); emit(m.substring(0, end));"
      }
    }
  }
}

运行另一个查询,但使用 client_ip 运行时字段搜索特定的IP地址:

GET my-index-000001/_search
{
  "size": 1,
  "query": {
    "match": {
      "client_ip": "211.11.9.0"
    }
  },
  "fields" : ["*"]
}

这次,响应只包含两个匹配项。day_of_week的值(Sunday)是在查询时使用映射中定义的运行时脚本计算的,结果只包含与211.11.9.0 IP地址匹配的文档。

{
  ...
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_id" : "oWs5KXYB-XyJbifr9mrz",
        "_score" : 1.0,
        "_source" : {
          "@timestamp" : "2020-06-21T15:00:01-05:00",
          "message" : "211.11.9.0 - - [2020-06-21T15:00:01-05:00] \"GET /english/index.html HTTP/1.0\" 304 0"
        },
        "fields" : {
          "@timestamp" : [
            "2020-06-21T20:00:01.000Z"
          ],
          "client_ip" : [
            "211.11.9.0"
          ],
          "message" : [
            "211.11.9.0 - - [2020-06-21T15:00:01-05:00] \"GET /english/index.html HTTP/1.0\" 304 0"
          ],
          "day_of_week" : [
            "Sunday"
          ]
        }
      }
    ]
  }
}

从相关索引中检索字段

edit

_search API 上的 fields 参数也可以用于通过类型为 lookup 的运行时字段从相关索引中检索字段。

由类型为 lookup 的运行时字段检索的字段可以用于丰富搜索响应中的命中结果。无法对这些字段进行查询或聚合。

POST ip_location/_doc?refresh
{
  "ip": "192.168.1.1",
  "country": "Canada",
  "city": "Montreal"
}

PUT logs/_doc/1?refresh
{
  "host": "192.168.1.1",
  "message": "the first message"
}

PUT logs/_doc/2?refresh
{
  "host": "192.168.1.2",
  "message": "the second message"
}

POST logs/_search
{
  "runtime_mappings": {
    "location": {
        "type": "lookup", 
        "target_index": "ip_location", 
        "input_field": "host", 
        "target_field": "ip", 
        "fetch_fields": ["country", "city"] 
    }
  },
  "fields": [
    "host",
    "message",
    "location"
  ],
  "_source": false
}

在主搜索请求中定义一个类型为 lookup 的运行时字段,该字段使用 term 查询从目标索引中检索字段。

查找查询执行的目标索引

主索引上的一个字段,其值用作查找词查询的输入值

查找索引上的一个字段,查找查询将针对该字段进行搜索

要从查找索引中检索的字段列表。请参阅搜索请求中的fields参数。

上述搜索从 ip_location 索引中返回每个IP地址的国家和城市。

{
  "took": 3,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1.0,
    "hits": [
      {
        "_index": "logs",
        "_id": "1",
        "_score": 1.0,
        "fields": {
          "host": [ "192.168.1.1" ],
          "location": [
            {
              "city": [ "Montreal" ],
              "country": [ "Canada" ]
            }
          ],
          "message": [ "the first message" ]
        }
      },
      {
        "_index": "logs",
        "_id": "2",
        "_score": 1.0,
        "fields": {
          "host": [ "192.168.1.2" ],
          "message": [ "the second message" ]
        }
      }
    ]
  }
}

查找字段的响应被分组以保持每个文档与查找索引的独立性。每个输入值的查找查询预计最多匹配查找索引中的一个文档。如果查找查询匹配多个文档,则将随机选择一个文档。

索引运行时字段

edit

运行时字段由它们运行的上下文定义。例如,您可以在 搜索查询的上下文中或在索引映射的 runtime 部分中定义运行时字段。如果您决定为提高性能而索引运行时字段,只需将完整的运行时字段定义(包括脚本)移动到索引映射的上下文中。Elasticsearch 会自动使用这些索引字段来驱动查询,从而实现快速响应时间。这一功能意味着您只需编写一次脚本,并将其应用于任何支持运行时字段的上下文。

目前不支持索引一个复合运行时字段。

然后,您可以使用运行时字段来限制 Elasticsearch 需要计算值的字段数量。结合使用索引字段和运行时字段,可以灵活地索引数据以及定义其他字段的查询方式。

在索引运行时字段后,您无法更新包含的脚本。如果需要更改脚本,请使用更新后的脚本创建一个新字段。

例如,假设您的公司想要更换一些旧的压力阀门。连接的传感器只能报告真实读数的一小部分。与其为压力阀门配备新传感器,您决定根据报告的读数来计算这些值。基于报告的数据,您在映射中为my-index-000001定义了以下字段:

PUT my-index-000001/
{
  "mappings": {
    "properties": {
      "timestamp": {
        "type": "date"
      },
      "temperature": {
        "type": "long"
      },
      "voltage": {
        "type": "double"
      },
      "node": {
        "type": "keyword"
      }
    }
  }
}

然后,您从传感器中批量索引一些样本数据。这些数据包括每个传感器的电压读数:

POST my-index-000001/_bulk?refresh=true
{"index":{}}
{"timestamp": 1516729294000, "temperature": 200, "voltage": 5.2, "node": "a"}
{"index":{}}
{"timestamp": 1516642894000, "temperature": 201, "voltage": 5.8, "node": "b"}
{"index":{}}
{"timestamp": 1516556494000, "temperature": 202, "voltage": 5.1, "node": "a"}
{"index":{}}
{"timestamp": 1516470094000, "temperature": 198, "voltage": 5.6, "node": "b"}
{"index":{}}
{"timestamp": 1516383694000, "temperature": 200, "voltage": 4.2, "node": "c"}
{"index":{}}
{"timestamp": 1516297294000, "temperature": 202, "voltage": 4.0, "node": "c"}

在与几位现场工程师交谈后,你意识到传感器应该报告至少两倍于当前值的数据,但可能更高。你创建了一个名为voltage_corrected的运行时字段,用于检索当前电压并将其乘以2

PUT my-index-000001/_mapping
{
  "runtime": {
    "voltage_corrected": {
      "type": "double",
      "script": {
        "source": """
        emit(doc['voltage'].value * params['multiplier'])
        """,
        "params": {
          "multiplier": 2
        }
      }
    }
  }
}

您使用 fields 参数在 _search API 上检索计算的值:

GET my-index-000001/_search
{
  "fields": [
    "voltage_corrected",
    "node"
  ],
  "size": 2
}

在审查传感器数据并运行一些测试后,您确定报告的传感器数据乘数应为4。为了获得更高的性能,您决定使用新的multiplier参数对voltage_corrected运行时字段进行索引。

在一个名为 my-index-000001 的新索引中,将 voltage_corrected 运行时字段定义复制到新索引的映射中。就是这么简单!你可以添加一个名为 on_script_error 的可选参数,该参数决定在索引时如果脚本抛出错误是否拒绝整个文档(默认)。

PUT my-index-000001/
{
  "mappings": {
    "properties": {
      "timestamp": {
        "type": "date"
      },
      "temperature": {
        "type": "long"
      },
      "voltage": {
        "type": "double"
      },
      "node": {
        "type": "keyword"
      },
      "voltage_corrected": {
        "type": "double",
        "on_script_error": "fail", 
        "script": {
          "source": """
        emit(doc['voltage'].value * params['multiplier'])
        """,
          "params": {
            "multiplier": 4
          }
        }
      }
    }
  }
}

如果在索引时脚本抛出错误,会导致整个文档被拒绝。将值设置为 ignore 将在文档的 _ignored 元数据字段中注册该字段并继续索引。

将一些来自传感器的样本数据批量索引到 my-index-000001 索引中:

POST my-index-000001/_bulk?refresh=true
{ "index": {}}
{ "timestamp": 1516729294000, "temperature": 200, "voltage": 5.2, "node": "a"}
{ "index": {}}
{ "timestamp": 1516642894000, "temperature": 201, "voltage": 5.8, "node": "b"}
{ "index": {}}
{ "timestamp": 1516556494000, "temperature": 202, "voltage": 5.1, "node": "a"}
{ "index": {}}
{ "timestamp": 1516470094000, "temperature": 198, "voltage": 5.6, "node": "b"}
{ "index": {}}
{ "timestamp": 1516383694000, "temperature": 200, "voltage": 4.2, "node": "c"}
{ "index": {}}
{ "timestamp": 1516297294000, "temperature": 202, "voltage": 4.0, "node": "c"}

您现在可以在搜索查询中检索计算值,并根据精确值查找文档。以下范围查询返回所有计算的voltage_corrected大于或等于16,但小于或等于20的文档。同样,使用fields参数在_search API上检索您想要的字段:

POST my-index-000001/_search
{
  "query": {
    "range": {
      "voltage_corrected": {
        "gte": 16,
        "lte": 20,
        "boost": 1.0
      }
    }
  },
  "fields": ["voltage_corrected", "node"]
}

响应包括与范围查询匹配的文档的 voltage_corrected 字段,基于所包含脚本的计算值:

{
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_id" : "yoSLrHgBdg9xpPrUZz_P",
        "_score" : 1.0,
        "_source" : {
          "timestamp" : 1516383694000,
          "temperature" : 200,
          "voltage" : 4.2,
          "node" : "c"
        },
        "fields" : {
          "voltage_corrected" : [
            16.8
          ],
          "node" : [
            "c"
          ]
        }
      },
      {
        "_index" : "my-index-000001",
        "_id" : "y4SLrHgBdg9xpPrUZz_P",
        "_score" : 1.0,
        "_source" : {
          "timestamp" : 1516297294000,
          "temperature" : 202,
          "voltage" : 4.0,
          "node" : "c"
        },
        "fields" : {
          "voltage_corrected" : [
            16.0
          ],
          "node" : [
            "c"
          ]
        }
      }
    ]
  }
}

使用运行时字段探索您的数据

edit

考虑一个您想要从中提取字段的大量日志数据集。 索引数据耗时且占用大量磁盘空间,而您只是想在不预先承诺架构的情况下探索数据结构。

你知道你的日志数据包含你想要提取的特定字段。 在这种情况下,我们想要关注 @timestampmessage 字段。通过 使用运行时字段,你可以定义脚本在搜索时计算这些字段的值。

将索引字段定义为起点

edit

您可以通过将 @timestampmessage 字段添加到 my-index-000001 映射中作为索引字段来开始一个简单的示例。为了保持灵活性,请将 wildcard 作为 message 字段的类型:

PUT /my-index-000001/
{
  "mappings": {
    "properties": {
      "@timestamp": {
        "format": "strict_date_optional_time||epoch_second",
        "type": "date"
      },
      "message": {
        "type": "wildcard"
      }
    }
  }
}

导入一些数据

edit

在映射了您想要检索的字段后,将一些日志数据记录索引到 Elasticsearch 中。以下请求使用 bulk API 将原始日志数据索引到 my-index-000001 中。您不需要索引所有的日志数据,可以使用一个小样本来实验运行时字段。

最终文档不是有效的Apache日志格式,但我们可以在脚本中处理这种情况。

POST /my-index-000001/_bulk?refresh
{"index":{}}
{"timestamp":"2020-04-30T14:30:17-05:00","message":"40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:30:53-05:00","message":"232.0.0.0 - - [30/Apr/2020:14:30:53 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:12-05:00","message":"26.1.0.0 - - [30/Apr/2020:14:31:12 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:19-05:00","message":"247.37.0.0 - - [30/Apr/2020:14:31:19 -0500] \"GET /french/splash_inet.html HTTP/1.0\" 200 3781"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:22-05:00","message":"247.37.0.0 - - [30/Apr/2020:14:31:22 -0500] \"GET /images/hm_nbg.jpg HTTP/1.0\" 304 0"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:27-05:00","message":"252.0.0.0 - - [30/Apr/2020:14:31:27 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"}
{"index":{}}
{"timestamp":"2020-04-30T14:31:28-05:00","message":"not a valid apache log"}

此时,您可以查看 Elasticsearch 如何存储您的原始数据。

GET /my-index-000001

映射包含两个字段:@timestampmessage

{
  "my-index-000001" : {
    "aliases" : { },
    "mappings" : {
      "properties" : {
        "@timestamp" : {
          "type" : "date",
          "format" : "strict_date_optional_time||epoch_second"
        },
        "message" : {
          "type" : "wildcard"
        },
        "timestamp" : {
          "type" : "date"
        }
      }
    },
    ...
  }
}

使用grok模式定义运行时字段

edit

如果您想检索包含clientip的结果,您可以在映射中将该字段添加为运行时字段。以下运行时脚本定义了一个grok模式,该模式从文档中的单个文本字段中提取结构化字段。grok模式类似于支持可重用别名表达式的正则表达式。

脚本匹配 %{COMMONAPACHELOG} 日志模式,该模式理解 Apache 日志的结构。如果模式匹配(clientip != null),脚本会发出匹配的 IP 地址的值。如果模式不匹配,脚本只会返回字段值而不崩溃。

PUT my-index-000001/_mappings
{
  "runtime": {
    "http.client_ip": {
      "type": "ip",
      "script": """
        String clientip=grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.clientip;
        if (clientip != null) emit(clientip); 
      """
    }
  }
}

此条件确保即使消息的模式不匹配,脚本也不会崩溃。

或者,您可以在搜索请求的上下文中定义相同的运行时字段。运行时定义和脚本与之前在索引映射中定义的完全相同。只需将该定义复制到搜索请求的 runtime_mappings 部分,并包含一个与运行时字段匹配的查询。此查询将返回与在索引映射中为 http.clientip 运行时字段定义搜索查询时相同的结果,但仅限于此特定搜索的上下文中:

GET my-index-000001/_search
{
  "runtime_mappings": {
    "http.clientip": {
      "type": "ip",
      "script": """
        String clientip=grok('%{COMMONAPACHELOG}').extract(doc["message"].value)?.clientip;
        if (clientip != null) emit(clientip);
      """
    }
  },
  "query": {
    "match": {
      "http.clientip": "40.135.0.0"
    }
  },
  "fields" : ["http.clientip"]
}

定义一个复合运行时字段

edit

您还可以定义一个复合运行时字段,以从单个脚本中发出多个字段。您可以定义一组类型化的子字段,并发出一个值的映射。在搜索时,每个子字段检索与其名称在映射中关联的值。这意味着您只需指定一次您的grok模式,并可以返回多个值:

PUT my-index-000001/_mappings
{
  "runtime": {
    "http": {
      "type": "composite",
      "script": "emit(grok(\"%{COMMONAPACHELOG}\").extract(doc[\"message\"].value))",
      "fields": {
        "clientip": {
          "type": "ip"
        },
        "verb": {
          "type": "keyword"
        },
        "response": {
          "type": "long"
        }
      }
    }
  }
}

搜索特定IP地址

edit

使用 http.clientip 运行时字段,您可以定义一个简单的查询来搜索特定的 IP 地址并返回所有相关字段。

GET my-index-000001/_search
{
  "query": {
    "match": {
      "http.clientip": "40.135.0.0"
    }
  },
  "fields" : ["*"]
}

API返回以下结果。因为http是一个复合运行时字段,响应包括字段下的每个子字段,包括与查询匹配的任何相关值。无需提前构建数据结构,您可以以有意义的方式搜索和探索数据,以进行实验并确定要索引的字段。

{
  ...
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_id" : "sRVHBnwBB-qjgFni7h_O",
        "_score" : 1.0,
        "_source" : {
          "timestamp" : "2020-04-30T14:30:17-05:00",
          "message" : "40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"
        },
        "fields" : {
          "http.verb" : [
            "GET"
          ],
          "http.clientip" : [
            "40.135.0.0"
          ],
          "http.response" : [
            200
          ],
          "message" : [
            "40.135.0.0 - - [30/Apr/2020:14:30:17 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"
          ],
          "http.client_ip" : [
            "40.135.0.0"
          ],
          "timestamp" : [
            "2020-04-30T19:30:17.000Z"
          ]
        }
      }
    ]
  }
}

另外,还记得脚本中的if语句吗?

if (clientip != null) emit(clientip);

如果脚本不包含此条件,查询将在任何不匹配模式的分片上失败。通过包含此条件,查询将跳过不匹配grok模式的数据。

在特定范围内搜索文档

edit

您还可以运行一个范围查询,该查询作用于timestamp字段。以下查询返回任何timestamp大于或等于2020-04-30T14:31:27-05:00的文档:

GET my-index-000001/_search
{
  "query": {
    "range": {
      "timestamp": {
        "gte": "2020-04-30T14:31:27-05:00"
      }
    }
  }
}

响应包括日志格式不匹配的文档,但时间戳落在定义范围内的文档。

{
  ...
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_id" : "hdEhyncBRSB6iD-PoBqe",
        "_score" : 1.0,
        "_source" : {
          "timestamp" : "2020-04-30T14:31:27-05:00",
          "message" : "252.0.0.0 - - [30/Apr/2020:14:31:27 -0500] \"GET /images/hm_bg.jpg HTTP/1.0\" 200 24736"
        }
      },
      {
        "_index" : "my-index-000001",
        "_id" : "htEhyncBRSB6iD-PoBqe",
        "_score" : 1.0,
        "_source" : {
          "timestamp" : "2020-04-30T14:31:28-05:00",
          "message" : "not a valid apache log"
        }
      }
    ]
  }
}

使用dissect模式定义一个运行时字段

edit

如果你不需要正则表达式的强大功能,你可以使用 解剖模式 而不是 grok 模式。解剖模式匹配固定分隔符,但通常比 grok 更快。

您可以使用dissect来实现与使用grok模式解析Apache日志相同的结果。与匹配日志模式不同,您可以包含您想要丢弃的字符串部分。特别注意您想要丢弃的字符串部分将有助于构建成功的dissect模式。

PUT my-index-000001/_mappings
{
  "runtime": {
    "http.client.ip": {
      "type": "ip",
      "script": """
        String clientip=dissect('%{clientip} %{ident} %{auth} [%{@timestamp}] "%{verb} %{request} HTTP/%{httpversion}" %{status} %{size}').extract(doc["message"].value)?.clientip;
        if (clientip != null) emit(clientip);
      """
    }
  }
}

同样地,您可以定义一个dissect模式来提取HTTP响应代码

PUT my-index-000001/_mappings
{
  "runtime": {
    "http.responses": {
      "type": "long",
      "script": """
        String response=dissect('%{clientip} %{ident} %{auth} [%{@timestamp}] "%{verb} %{request} HTTP/%{httpversion}" %{response} %{size}').extract(doc["message"].value)?.response;
        if (response != null) emit(Integer.parseInt(response));
      """
    }
  }
}

然后,您可以运行查询以使用http.responses运行时字段检索特定的HTTP响应。使用_search请求的fields参数来指示您想要检索的字段:

GET my-index-000001/_search
{
  "query": {
    "match": {
      "http.responses": "304"
    }
  },
  "fields" : ["http.client_ip","timestamp","http.verb"]
}

响应包括一个文档,其中HTTP响应为304

{
  ...
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "my-index-000001",
        "_id" : "A2qDy3cBWRMvVAuI7F8M",
        "_score" : 1.0,
        "_source" : {
          "timestamp" : "2020-04-30T14:31:22-05:00",
          "message" : "247.37.0.0 - - [30/Apr/2020:14:31:22 -0500] \"GET /images/hm_nbg.jpg HTTP/1.0\" 304 0"
        },
        "fields" : {
          "http.verb" : [
            "GET"
          ],
          "http.client_ip" : [
            "247.37.0.0"
          ],
          "timestamp" : [
            "2020-04-30T19:31:22.000Z"
          ]
        }
      }
    ]
  }
}