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

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: Elasticsearch:使用 Runtime fields 对索引字段进行覆盖处理以修复错误 - 7.11 发布

作者:刘晓国


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


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


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

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

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

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

你可以像其他任何字段一样从搜索 API 访问运行时字段,Elasticsearchimage.png看到的运行时字段没有任何不同。 你可以在 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 来展示数据时,它是这样的:

image.png

从上面,我们可以看出来,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 中进行数据的展示:

image.png

显然这次我们得到了正确的统计图。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 是不匹配的。


相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
相关文章
|
20天前
|
存储 自然语言处理 关系型数据库
ElasticSearch索引 和MySQL索引那个更高效实用那个更合适
ElasticSearch索引 和MySQL索引那个更高效实用那个更合适
35 0
|
2月前
|
Java API iOS开发
Elasticsearch 字段别名 field-alias
Elasticsearch 字段别名 field-alias
27 0
|
2月前
|
存储 算法 NoSQL
Elasticsearch拆分索引知多少
Elasticsearch拆分索引知多少
31 0
|
3月前
|
数据采集 存储 自然语言处理
elasticsearch 跨索引联合多条件查询
elasticsearch 跨索引联合多条件查询
|
4月前
|
API 索引
ElasticSearch索引模板
ElasticSearch索引模板
130 1
|
4月前
|
JSON 自然语言处理 数据库
数据库-ElasticSearch入门(索引、文档、查询)
数据库-ElasticSearch入门(索引、文档、查询)
285 0
|
1月前
|
JSON 监控 数据管理
【Elasticsearch专栏 12】深入探索:Elasticsearch使用索引生命周期管理(ILM)自动化删除旧数据
Elasticsearch的ILM功能允许用户定义策略,自动管理索引从创建到删除的生命周期。用户可以设置策略,根据索引年龄或大小自动删除旧数据,节省存储空间。通过应用ILM策略于索引模板,新索引将遵循预定义的生命周期。用户还可以监控ILM状态,确保策略按预期执行。使用ILM,用户可以高效地管理数据,确保旧数据及时删除,同时保持数据完整性和安全性。
|
3月前
|
索引
elasticsearch 创建索引模版template
elasticsearch 创建索引模版template
|
2月前
|
存储 自然语言处理 搜索推荐
【Elasticsearch专栏 01】深入探索:Elasticsearch的正向索引和倒排索引是什么?
正向索引根据文档ID直接查找文档内容,适用于精确匹配场景;而倒排索引则基于文档内容构建,通过关键词快速定位相关文档,适用于全文搜索,显著提高查询效率,是搜索引擎的核心技术。
|
2月前
|
存储 自然语言处理 搜索推荐
【Elasticsearch专栏 02】深入探索:Elasticsearch为什么使用倒排索引而不是正排索引
倒排索引在搜索引擎中更受欢迎,因为它直接关联文档内容,支持全文搜索和模糊搜索,提高查询效率。其紧凑的结构减少了存储空间,并方便支持多种查询操作。相比之下,正排索引在搜索效率、存储和灵活性方面存在局限。

相关产品

  • 检索分析服务 Elasticsearch版