派生字段类型
2.15 版引入
派生字段允许您通过对现有字段执行脚本来动态创建新字段。现有字段可以从包含原始文档的 _source
字段中检索,也可以从字段的文档值中检索。一旦您在索引映射中或在搜索请求中定义了派生字段,您就可以像使用常规字段一样在查询中使用该字段。
何时使用派生字段
派生字段在字段操作中提供了灵活性,并优先考虑存储效率。但是,由于它们在查询时计算,可能会降低查询性能。派生字段在需要实时数据转换的场景中特别有用,例如:
- 日志分析:从日志消息中提取时间戳和日志级别。
- 性能指标:根据开始和结束时间戳计算响应时间。
- 安全分析:用于威胁检测的实时 IP 地理定位和用户代理解析。
- 实验用例:测试新的数据转换、为 A/B 测试创建临时字段,或生成一次性报告,而无需更改映射或重新索引数据。
尽管查询时计算可能会对性能产生影响,但派生字段的灵活性和存储效率使其成为这些应用场景中的宝贵工具。
当前限制
目前,派生字段有以下限制:
- 评分和排序:尚未支持。
- 聚合:从 OpenSearch 2.17 开始,派生字段支持大多数聚合类型。以下聚合类型不受支持:地理(地理距离、地理哈希网格、地理十六进制网格、地理瓦片网格、地理边界、地理中心点)、重要词项、重要文本和脚本化指标。
- Dashboards 支持:这些字段不会显示在 OpenSearch Dashboards 的可用字段列表中。但是,如果您知道派生字段名称,仍然可以使用它们进行筛选。
- 链式派生字段:一个派生字段不能用于定义另一个派生字段。
- 连接字段类型:派生字段不支持 连接字段类型。
我们计划在未来的版本中解决这些限制。
先决条件
在使用派生字段之前,请确保满足以下先决条件:
- 启用
_source
或doc_values
:确保您的脚本中使用的字段已启用_source
字段或文档值。 - 启用高开销查询:确保
search.allow_expensive_queries
设置为true
。 - 功能控制:派生字段默认启用。您可以通过以下设置启用或禁用派生字段:
- 索引级别:更新
index.query.derived_field.enabled
设置。 - 集群级别:更新
search.derived_field.enabled
设置。这两个设置都是动态的,因此可以在不重新索引或重启节点的情况下进行更改。
- 索引级别:更新
- 性能考量:在使用派生字段之前,请评估性能影响,以确保派生字段满足您的扩展性要求。
定义派生字段
您可以在索引映射中定义派生字段,也可以直接在搜索请求中定义和搜索派生字段。
示例设置
要尝试本页上的示例,请首先创建以下 logs
索引:
PUT logs
{
"mappings": {
"properties": {
"request": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"clientip": {
"type": "keyword"
}
}
}
}
向索引添加示例文档
POST _bulk
{ "index" : { "_index" : "logs", "_id" : "1" } }
{ "request": "894030400 GET /english/images/france98_venues.gif HTTP/1.0 200 778", "clientip": "61.177.2.0" }
{ "index" : { "_index" : "logs", "_id" : "2" } }
{ "request": "894140400 GET /french/playing/mascot/mascot.html HTTP/1.1 200 5474", "clientip": "185.92.2.0" }
{ "index" : { "_index" : "logs", "_id" : "3" } }
{ "request": "894250400 POST /english/venues/images/venue_header.gif HTTP/1.0 200 711", "clientip": "61.177.2.0" }
{ "index" : { "_index" : "logs", "_id" : "4" } }
{ "request": "894360400 POST /images/home_fr_button.gif HTTP/1.1 200 2140", "clientip": "129.178.2.0" }
{ "index" : { "_index" : "logs", "_id" : "5" } }
{ "request": "894470400 DELETE /images/102384s.gif HTTP/1.0 200 785", "clientip": "227.177.2.0" }
在索引映射中定义派生字段
要从 logs
索引中已索引的 request
字段派生出 timestamp
、method
和 size
字段,请配置以下映射:
PUT /logs/_mapping
{
"derived": {
"timestamp": {
"type": "date",
"format": "MM/dd/yyyy",
"script": {
"source": """
emit(Long.parseLong(doc["request.keyword"].value.splitOnToken(" ")[0]))
"""
}
},
"method": {
"type": "keyword",
"script": {
"source": """
emit(doc["request.keyword"].value.splitOnToken(" ")[1])
"""
}
},
"size": {
"type": "long",
"script": {
"source": """
emit(Long.parseLong(doc["request.keyword"].value.splitOnToken(" ")[5]))
"""
}
}
}
}
请注意,timestamp
字段有一个额外的 format
参数,用于指定 date
字段的显示格式。如果您不包含 format
参数,则格式默认为 strict_date_time_no_millis
。有关支持的日期格式的更多信息,请参阅参数。
参数
下表列出了 derived
字段类型接受的参数。所有参数都是动态的,无需重新索引文档即可修改。
参数 | 必需/可选 | 描述 |
---|---|---|
type | 必需 | 派生字段的类型。支持的类型包括 boolean 、date 、geo_point 、ip 、keyword 、text 、long 、double 、float 和 object 。 |
script(脚本) | 必需 | 与派生字段关联的脚本。从脚本发出的任何值都必须使用 emit() 发出。发出值的类型必须与派生字段的 type 匹配。如果已启用 doc_values 和 _source 字段,则脚本可以访问它们。字段的文档值可以使用 doc['field_name'].value 访问,源可以使用 params._source["field_name"] 访问。 |
format | 可选 | 用于解析日期的格式。仅适用于 date 字段。有效值包括 strict_date_time_no_millis 、strict_date_optional_time 和 epoch_millis 。有关更多信息,请参阅格式。 |
忽略畸形值 (ignore_malformed) | 可选 | 一个布尔值,指定在对派生字段运行查询时是否忽略格式错误的值。默认值为 false (遇到格式错误的值时抛出异常)。 |
prefilter_field | 可选 | 一个用于提升派生字段性能的已索引文本字段。指定一个现有的已索引字段,在对派生字段进行筛选之前先在该字段上进行筛选。有关更多信息,请参阅预筛选字段。 |
在脚本中发出值
emit()
函数仅在派生字段脚本上下文中可用。它用于为脚本运行的文档发出一个或多个(对于多值字段)脚本值。
下表列出了支持的字段类型的 emit()
函数格式。
类型 | 发出格式 | 支持多值字段 |
---|---|---|
布尔值 | emit(boolean) | 否 |
双精度浮点型 | emit(double) | 是 |
日期 | emit(long timeInMilis) | 是 |
浮点型 | emit(float) | 是 |
geo_point | emit(double lat, double lon) | 是 |
ip | emit(String ip) | 是 |
keyword | emit(String) | 是 |
长整型 (long) | emit(long) | 是 |
object | emit(String json) (有效 JSON) | 是 |
text | emit(String) | 是 |
默认情况下,派生字段与其发出值之间的类型不匹配将导致搜索请求失败并出现错误。如果 ignore_malformed
设置为 true
,则跳过失败的文档,搜索请求成功。
发出值的大小限制为每个文档 1 MB。
搜索索引映射中定义的派生字段
要搜索派生字段,请使用与搜索常规字段相同的语法。例如,以下请求在指定范围内搜索具有派生 timestamp
字段的文档:
POST /logs/_search
{
"query": {
"range": {
"timestamp": {
"gte": "1970-01-11T08:20:30.400Z",
"lte": "1970-01-11T08:26:00.400Z"
}
}
},
"fields": ["timestamp"]
}
响应包含匹配文档:
响应
{
"took": 315,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "logs",
"_id": "1",
"_score": 1,
"_source": {
"request": "894030400 GET /english/images/france98_venues.gif HTTP/1.0 200 778",
"clientip": "61.177.2.0"
},
"fields": {
"timestamp": [
"1970-01-11T08:20:30.400Z"
]
}
},
{
"_index": "logs",
"_id": "2",
"_score": 1,
"_source": {
"request": "894140400 GET /french/playing/mascot/mascot.html HTTP/1.1 200 5474",
"clientip": "185.92.2.0"
},
"fields": {
"timestamp": [
"1970-01-11T08:22:20.400Z"
]
}
},
{
"_index": "logs",
"_id": "3",
"_score": 1,
"_source": {
"request": "894250400 POST /english/venues/images/venue_header.gif HTTP/1.0 200 711",
"clientip": "61.177.2.0"
},
"fields": {
"timestamp": [
"1970-01-11T08:24:10.400Z"
]
}
},
{
"_index": "logs",
"_id": "4",
"_score": 1,
"_source": {
"request": "894360400 POST /images/home_fr_button.gif HTTP/1.1 200 2140",
"clientip": "129.178.2.0"
},
"fields": {
"timestamp": [
"1970-01-11T08:26:00.400Z"
]
}
}
]
}
}
在搜索请求中定义和搜索派生字段
您也可以直接在搜索请求中定义派生字段,并与常规索引字段一起查询它们。例如,以下请求创建了 url
和 status
派生字段,并与常规的 request
和 clientip
字段一起搜索这些字段
POST /logs/_search
{
"derived": {
"url": {
"type": "text",
"script": {
"source": """
emit(doc["request"].value.splitOnToken(" ")[2])
"""
}
},
"status": {
"type": "keyword",
"script": {
"source": """
emit(doc["request"].value.splitOnToken(" ")[4])
"""
}
}
},
"query": {
"bool": {
"must": [
{
"term": {
"clientip": "61.177.2.0"
}
},
{
"match": {
"url": "images"
}
},
{
"term": {
"status": "200"
}
}
]
}
},
"fields": ["request", "clientip", "url", "status"]
}
响应包含匹配文档:
响应
{
"took": 6,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 2.8754687,
"hits": [
{
"_index": "logs",
"_id": "1",
"_score": 2.8754687,
"_source": {
"request": "894030400 GET /english/images/france98_venues.gif HTTP/1.0 200 778",
"clientip": "61.177.2.0"
},
"fields": {
"request": [
"894030400 GET /english/images/france98_venues.gif HTTP/1.0 200 778"
],
"clientip": [
"61.177.2.0"
],
"url": [
"/english/images/france98_venues.gif"
],
"status": [
"200"
]
}
},
{
"_index": "logs",
"_id": "3",
"_score": 2.8754687,
"_source": {
"request": "894250400 POST /english/venues/images/venue_header.gif HTTP/1.0 200 711",
"clientip": "61.177.2.0"
},
"fields": {
"request": [
"894250400 POST /english/venues/images/venue_header.gif HTTP/1.0 200 711"
],
"clientip": [
"61.177.2.0"
],
"url": [
"/english/venues/images/venue_header.gif"
],
"status": [
"200"
]
}
}
]
}
}
派生字段在搜索期间使用索引分析设置中指定的默认分析器。您可以像对待常规字段一样,在搜索请求中覆盖默认分析器或指定搜索分析器。更多信息,请参阅 分析器。
当一个字段同时存在索引映射和搜索定义时,搜索定义优先。
检索字段
您可以像前面示例中所示的常规字段一样,在搜索请求中使用 fields
参数检索派生字段。您还可以使用通配符检索所有匹配给定模式的派生字段。
高亮显示
类型为 text
的派生字段支持使用 统一高亮器 进行高亮显示。例如,以下请求指定高亮显示派生 url
字段
POST /logs/_search
{
"derived": {
"url": {
"type": "text",
"script": {
"source": """
emit(doc["request"].value.splitOnToken(" " )[2])
"""
}
}
},
"query": {
"bool": {
"must": [
{
"term": {
"clientip": "61.177.2.0"
}
},
{
"match": {
"url": "images"
}
}
]
}
},
"fields": ["request", "clientip", "url"],
"highlight": {
"fields": {
"url": {}
}
}
}
响应在 url
字段中指定了高亮显示
响应
{
"took": 45,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1.8754687,
"hits": [
{
"_index": "logs",
"_id": "1",
"_score": 1.8754687,
"_source": {
"request": "894030400 GET /english/images/france98_venues.gif HTTP/1.0 200 778",
"clientip": "61.177.2.0"
},
"fields": {
"request": [
"894030400 GET /english/images/france98_venues.gif HTTP/1.0 200 778"
],
"clientip": [
"61.177.2.0"
],
"url": [
"/english/images/france98_venues.gif"
]
},
"highlight": {
"url": [
"/english/<em>images</em>/france98_venues.gif"
]
}
},
{
"_index": "logs",
"_id": "3",
"_score": 1.8754687,
"_source": {
"request": "894250400 POST /english/venues/images/venue_header.gif HTTP/1.0 200 711",
"clientip": "61.177.2.0"
},
"fields": {
"request": [
"894250400 POST /english/venues/images/venue_header.gif HTTP/1.0 200 711"
],
"clientip": [
"61.177.2.0"
],
"url": [
"/english/venues/images/venue_header.gif"
]
},
"highlight": {
"url": [
"/english/venues/<em>images</em>/venue_header.gif"
]
}
}
]
}
}
聚合
从 OpenSearch 2.17 开始,派生字段支持大多数聚合类型。
不支持地理、重要术语、重要文本和脚本指标聚合。
例如,以下请求在 method
派生字段上创建一个简单的 terms
聚合
POST /logs/_search
{
"size": 0,
"aggs": {
"methods": {
"terms": {
"field": "method"
}
}
}
}
响应包含以下存储桶
响应
{
"took" : 12,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 5,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"methods" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "GET",
"doc_count" : 2
},
{
"key" : "POST",
"doc_count" : 2
},
{
"key" : "DELETE",
"doc_count" : 1
}
]
}
}
}
性能
派生字段未被索引,而是通过从 _source
字段或文档值中检索值动态计算的。因此,它们的运行速度较慢。为了提高性能,请尝试以下操作
- 通过在索引字段上添加查询过滤器并结合派生字段来修剪搜索空间。
- 在脚本中尽可能使用文档值而不是
_source
以实现更快访问。 - 考虑使用
prefilter_field
来自动修剪搜索空间,而无需在搜索请求中添加显式过滤器。
预过滤字段
指定预过滤字段有助于修剪搜索空间,而无需在搜索请求中添加显式过滤器。预过滤字段指定一个现有的索引字段(prefilter_field
),在构建查询时自动对其进行过滤。prefilter_field
必须是文本字段(可以是 text
或 match_only_text
)。
例如,您可以将 prefilter_field
添加到 method
派生字段。更新索引映射,指定对 request
字段进行预过滤
PUT /logs/_mapping
{
"derived": {
"method": {
"type": "keyword",
"script": {
"source": """
emit(doc["request.keyword"].value.splitOnToken(" ")[1])
"""
},
"prefilter_field": "request"
}
}
}
现在使用对 method
派生字段的查询进行搜索
POST /logs/_search
{
"profile": true,
"query": {
"term": {
"method": {
"value": "GET"
}
}
},
"fields": ["method"]
}
OpenSearch 会自动向您的查询添加一个针对 request
字段的过滤器
"#request:GET #DerivedFieldQuery (Query: [ method:GET])"
您可以使用 profile
选项分析派生字段的性能,如前面的示例所示。
派生对象字段
脚本可以发出一个有效的 JSON 对象,这样您就可以像处理常规字段一样查询子字段而无需索引它们。这对于需要偶尔搜索某些子字段的大型 JSON 对象非常有用。在这种情况下,索引子字段的成本很高,而为每个子字段定义派生字段也会增加大量的资源开销。如果您不明确提供子字段类型,则子字段类型将被推断。
例如,以下请求将 derived_request_object
派生字段定义为 object
类型
PUT logs_object
{
"mappings": {
"properties": {
"request_object": { "type": "text" }
},
"derived": {
"derived_request_object": {
"type": "object",
"script": {
"source": "emit(params._source[\"request_object\"])"
}
}
}
}
}
考虑以下文档,其中 request_object
是 JSON 对象的字符串表示
POST _bulk
{ "index" : { "_index" : "logs_object", "_id" : "1" } }
{ "request_object": "{\"@timestamp\": 894030400, \"clientip\":\"61.177.2.0\", \"request\": \"GET /english/venues/images/venue_header.gif HTTP/1.0\", \"status\": 200, \"size\": 711}" }
{ "index" : { "_index" : "logs_object", "_id" : "2" } }
{ "request_object": "{\"@timestamp\": 894140400, \"clientip\":\"129.178.2.0\", \"request\": \"GET /images/home_fr_button.gif HTTP/1.1\", \"status\": 200, \"size\": 2140}" }
{ "index" : { "_index" : "logs_object", "_id" : "3" } }
{ "request_object": "{\"@timestamp\": 894240400, \"clientip\":\"227.177.2.0\", \"request\": \"GET /images/102384s.gif HTTP/1.0\", \"status\": 400, \"size\": 785}" }
{ "index" : { "_index" : "logs_object", "_id" : "4" } }
{ "request_object": "{\"@timestamp\": 894340400, \"clientip\":\"61.177.2.0\", \"request\": \"GET /english/images/venue_bu_city_on.gif HTTP/1.0\", \"status\": 400, \"size\": 1397}\n" }
{ "index" : { "_index" : "logs_object", "_id" : "5" } }
{ "request_object": "{\"@timestamp\": 894440400, \"clientip\":\"132.176.2.0\", \"request\": \"GET /french/news/11354.htm HTTP/1.0\", \"status\": 200, \"size\": 3460, \"is_active\": true}" }
以下查询搜索 derived_request_object
的 @timestamp
子字段
POST /logs_object/_search
{
"query": {
"range": {
"derived_request_object.@timestamp": {
"gte": "894030400",
"lte": "894140400"
}
}
},
"fields": ["derived_request_object.@timestamp"]
}
响应包含匹配文档:
响应
{
"took": 26,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "logs_object",
"_id": "1",
"_score": 1,
"_source": {
"request_object": """{"@timestamp": 894030400, "clientip":"61.177.2.0", "request": "GET /english/venues/images/venue_header.gif HTTP/1.0", "status": 200, "size": 711}"""
},
"fields": {
"derived_request_object.@timestamp": [
894030400
]
}
},
{
"_index": "logs_object",
"_id": "2",
"_score": 1,
"_source": {
"request_object": """{"@timestamp": 894140400, "clientip":"129.178.2.0", "request": "GET /images/home_fr_button.gif HTTP/1.1", "status": 200, "size": 2140}"""
},
"fields": {
"derived_request_object.@timestamp": [
894140400
]
}
}
]
}
}
您还可以指定高亮显示派生对象字段
POST /logs_object/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"derived_request_object.clientip": "61.177.2.0"
}
},
{
"match": {
"derived_request_object.request": "images"
}
}
]
}
},
"fields": ["derived_request_object.*"],
"highlight": {
"fields": {
"derived_request_object.request": {}
}
}
}
响应将高亮显示添加到 derived_request_object.request
字段
响应
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 2,
"relation": "eq"
},
"max_score": 2,
"hits": [
{
"_index": "logs_object",
"_id": "1",
"_score": 2,
"_source": {
"request_object": """{"@timestamp": 894030400, "clientip":"61.177.2.0", "request": "GET /english/venues/images/venue_header.gif HTTP/1.0", "status": 200, "size": 711}"""
},
"fields": {
"derived_request_object.request": [
"GET /english/venues/images/venue_header.gif HTTP/1.0"
],
"derived_request_object.clientip": [
"61.177.2.0"
]
},
"highlight": {
"derived_request_object.request": [
"GET /english/venues/<em>images</em>/venue_header.gif HTTP/1.0"
]
}
},
{
"_index": "logs_object",
"_id": "4",
"_score": 2,
"_source": {
"request_object": """{"@timestamp": 894340400, "clientip":"61.177.2.0", "request": "GET /english/images/venue_bu_city_on.gif HTTP/1.0", "status": 400, "size": 1397}
"""
},
"fields": {
"derived_request_object.request": [
"GET /english/images/venue_bu_city_on.gif HTTP/1.0"
],
"derived_request_object.clientip": [
"61.177.2.0"
]
},
"highlight": {
"derived_request_object.request": [
"GET /english/<em>images</em>/venue_bu_city_on.gif HTTP/1.0"
]
}
}
]
}
}
推断子字段类型
类型推断基于与 动态映射 相同的逻辑。它不是从第一个文档推断子字段类型,而是使用文档的随机样本来推断类型。如果随机样本中的任何文档中未找到子字段,则类型推断将失败并记录警告。对于很少在文档中出现的子字段,请考虑定义显式字段类型。对此类子字段使用动态类型推断可能会导致查询不返回结果,就像缺少字段一样。
显式子字段类型
要定义显式子字段类型,请在 properties
对象中提供 type
参数。在以下示例中,derived_logs_object.is_active
字段定义为 boolean
。由于此字段仅出现在其中一个文档中,其类型推断可能会失败,因此定义显式类型很重要
POST /logs_object/_search
{
"derived": {
"derived_request_object": {
"type": "object",
"script": {
"source": "emit(params._source[\"request_object\"])"
},
"properties": {
"is_active": "boolean"
}
}
},
"query": {
"term": {
"derived_request_object.is_active": true
}
},
"fields": ["derived_request_object.is_active"]
}
响应包含匹配文档:
响应
{
"took": 13,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "logs_object",
"_id": "5",
"_score": 1,
"_source": {
"request_object": """{"@timestamp": 894440400, "clientip":"132.176.2.0", "request": "GET /french/news/11354.htm HTTP/1.0", "status": 200, "size": 3460, "is_active": true}"""
},
"fields": {
"derived_request_object.is_active": [
true
]
}
}
]
}
}