这篇文章和之前的 “Elasticsearch:Dynamic mapping” 有重复的地方。那篇文章是在很久以前创作的。由于有了 runtime fields 的新功能,我在这里再次重新写一遍以补充之前的内容。Runtime fields 是在 Elastic Stack 7.11 所引入的新功能。
动态模板(dynamic templates)让你可以更好地控制 Elasticsearch 如何在默认动态字段映射规则之外映射你的数据。默认的动态字段映射规则如下:
JSON data type | "dynamic": "true" | "dynamic": "runtime" |
---|---|---|
null | 不添加任何字段 | 不添加任何字段 |
true 或者 false | boolean | boolean |
double | float | double |
long | long | long |
object | object | 不添加任何字段 |
array | 依赖于数组里的第一个非 null 值 | 依赖于数组里的第一个非 null 值 |
通过 date detection 的字符串 | date | date |
通过 numeric detection 的字符串 | float 或者 long | double 或者 long |
不通过 date detection 或者 numeric detection 的字符串 | 含有 .keyword 子字段的 text 类型 | keyword |
你可以通过将 dynamic 参数设置为 true 或 runtime 来启用动态映射。 如果在 mapping 中没有明确定义 dynamic,那么它的默认值为 true。然后,你可以使用动态模板来定义可应用于基于匹配条件动态添加的字段的自定义映射:
使用映射规范中的 {name} 和 {dynamic_type} 模板变量作为占位符。
重要:仅当字段包含具体值时,才会添加动态字段映射。 当字段包含 null 或空数组时,Elasticsearch 不会添加动态字段映射。 如果在 dynamic_template 中使用了 null_value 选项,它只会在第一个具有字段具体值的文档被索引后应用。
动态模板被指定为命名对象的数组:
- "dynamic_templates": [
- {
- "my_template_name": { #1
- ... match conditions ... #2
- "mapping": { ... } #3
- }
- },
- ...
- ]
如果提供的映射包含无效的映射片段,则会返回验证错误。 在索引时应用动态模板时进行验证,并且在大多数情况下,在更新动态模板时进行。 提供无效的映射片段可能会导致动态模板的更新或验证在某些情况下失败:
模板按顺序处理 — 第一个匹配的模板获胜。 通过更新映射 API放置新的动态模板时,所有现有模板都会被覆盖。 这允许在最初添加动态模板后重新排序或删除它们。
如果你希望 Elasticsearch 将某种类型的新字段动态映射为运行时字段,请在索引映射中设置 "dynamic":"runtime"。这些字段未编入索引,并在查询时从 _source 加载。
或者,你可以使用默认的动态映射规则,然后创建动态模板以将特定字段映射为运行时字段。你在索引映射中设置 "dynamic":"true",然后创建一个动态模板以将某种类型的新字段映射为运行时字段。
假设你有每个字段以 ip_ 开头的数据。基于动态映射规则,Elasticsearch 将任何通过数值检测的字符串映射为浮点数或长整数。但是,吗可以创建一个动态模板,将新字符串映射为 ip 类型的运行时字段。
以下请求定义了一个名为 strings_as_ip 的动态模板。当 Elasticsearch 检测到与 ip* 模式匹配的新字符串字段时,它会将这些字段映射为 ip 类型的运行时字段。因为 ip 字段不是动态映射的,所以你可以将此模板与 "dynamic":"true" 或 "dynamic":"runtime" 一起使用。
- PUT my-index-000001/
- {
- "mappings": {
- "dynamic_templates": [
- {
- "strings_as_ip": {
- "match_mapping_type": "string",
- "match": "ip*",
- "runtime": {
- "type": "ip"
- }
- }
- }
- ]
- }
- }
上面的命令是这样的:
我们可以使用如下的命令来进行测试:
- PUT my-index-000001/_doc/1
- {
- "ip1": "1.1.1.1",
- "ip": 10
- }
我们执行如下的命令来查看 my-index-00001 的 mapping:
GET my-index-000001/_mapping
上面的命令返回的值为:
- {
- "my-index-000001" : {
- "mappings" : {
- "dynamic_templates" : [
- {
- "strings_as_ip" : {
- "match" : "ip*",
- "match_mapping_type" : "string",
- "runtime" : {
- "type" : "ip"
- }
- }
- }
- ],
- "runtime" : {
- "ip1" : {
- "type" : "ip"
- }
- },
- "properties" : {
- "ip" : {
- "type" : "long"
- }
- }
- }
- }
- }
从上面,我们可以看出来,ip1 字段是一个 runtime fields,并且它的类型为 ip,而 ip 字段的类型为 long。ip 被映射为 long 类型,这是因为它的 match_mapping_type 为整型值,而不是 string。
match_mapping_type 是 JSON 解析器检测到的数据类型。 因为 JSON 不区分 long 和 integer 或 double 和 float,所以任何解析的浮点数都被认为是 double JSON 数据类型,而任何解析的整数都被认为是 long。
注意:使用动态映射,Elasticsearch 将始终选择更广泛的数据类型。 一个例外是 float,它需要的存储空间比 double 少,并且对于大多数应用程序来说足够精确。 运行时字段不支持浮点数,这就是 "dynamic":"runtime" 使用双精度的原因。
Elasticsearch 会自动检测按照文章开始部分的表格来进行检测数据类型。我们可以在 match_mapping_type 中使用通配符 * 来匹配所有的数据类型。
例如,如果我们想将所有整数字段映射为整数(integer)而不是长整数(long),并将所有字符串字段同时映射为文本和关键字,我们可以使用以下模板:
- PUT my-index-000001
- {
- "mappings": {
- "date_detection": true,
- "numeric_detection": true,
- "dynamic_templates": [
- {
- "integers": {
- "match_mapping_type": "long",
- "mapping": {
- "type": "integer"
- }
- }
- },
- {
- "strings": {
- "match_mapping_type": "string",
- "mapping": {
- "type": "text",
- "fields": {
- "raw": {
- "type": "keyword",
- "ignore_above": 256
- }
- }
- }
- }
- }
- ]
- }
- }
-
- PUT my-index-000001/_doc/1
- {
- "my_integer": 5,
- "my_string": "Some string"
- }
在上面的索引中,my_integer 会被映射为 integer,而 my_string 将被映射为 text 及 keyword 的 multi-field。我们执行如下的命令来查看字段的类型:
GET my-index-000001/_mapping
上面命令的返回结果为:
- {
- "my-index-000001" : {
- "mappings" : {
- "dynamic_templates" : [
- {
- "integers" : {
- "match_mapping_type" : "long",
- "mapping" : {
- "type" : "integer"
- }
- }
- },
- {
- "strings" : {
- "match_mapping_type" : "string",
- "mapping" : {
- "fields" : {
- "raw" : {
- "ignore_above" : 256,
- "type" : "keyword"
- }
- },
- "type" : "text"
- }
- }
- }
- ],
- "properties" : {
- "my_integer" : {
- "type" : "integer"
- },
- "my_string" : {
- "type" : "text",
- "fields" : {
- "raw" : {
- "type" : "keyword",
- "ignore_above" : 256
- }
- }
- }
- }
- }
- }
- }
match 参数使用模式匹配字段名称,而 unmatch 使用模式排除匹配匹配的字段。
match_pattern 参数调整 match 参数的行为,以支持在字段名称上匹配完整的 Java 正则表达式,而不是简单的通配符。 例如:
- "match_pattern": "regex",
- "match": "^profit_\d+$"
以下示例匹配名称以 long_ 开头的所有字符串字段(以 _text 结尾的字符串字段除外)并将它们映射为 long 字段:
- PUT my-index-000001
- {
- "mappings": {
- "dynamic_templates": [
- {
- "longs_as_strings": {
- "match_mapping_type": "string",
- "match": "long_*",
- "unmatch": "*_text",
- "mapping": {
- "type": "long"
- }
- }
- }
- ]
- }
- }
-
- PUT my-index-000001/_doc/1
- {
- "long_num": "5",
- "long_text": "foo"
- }
在上面,long_num 被映射为 long 类型的字段,这是因为它的 match_mapping_type 为 string 类型,并且它的字段名是以 long_ 为开头的。而 long_text 虽然是以 long_ 开头的,但是它符合 unmatch 的条件,也就是它的字段名是以 _text 为结尾的。这样的话,long_text 就按照默认的规则 string 进行映射。它将被映射为 text 及 keyword 的 multi-field 字段。我们通过如下的命令来进行查看:
GET my-index-000001/_mapping
上面的命令显示的结果为:
- {
- "my-index-000001" : {
- "mappings" : {
- "dynamic_templates" : [
- {
- "longs_as_strings" : {
- "match" : "long_*",
- "unmatch" : "*_text",
- "match_mapping_type" : "string",
- "mapping" : {
- "type" : "long"
- }
- }
- }
- ],
- "properties" : {
- "long_num" : {
- "type" : "long"
- },
- "long_text" : {
- "type" : "text",
- "fields" : {
- "keyword" : {
- "type" : "keyword",
- "ignore_above" : 256
- }
- }
- }
- }
- }
- }
- }
path_match 和 path_unmatch 参数的工作方式与 match 和 unmatch 相同,但对字段的完整虚线路径进行操作,而不仅仅是最终名称,例如 some_object.*.some_field。
此示例将 name 对象中任何字段的值复制到顶级 full_name 字段,middle 字段除外:
- PUT my-index-000001
- {
- "mappings": {
- "dynamic_templates": [
- {
- "full_name": {
- "path_match": "name.*",
- "path_unmatch": "*.middle",
- "mapping": {
- "type": "text",
- "copy_to": "full_name"
- }
- }
- }
- ]
- }
- }
-
- PUT my-index-000001/_doc/1
- {
- "name": {
- "first": "John",
- "middle": "Winston",
- "last": "Lennon"
- }
- }
如上所示,凡是在 name 之下的任何字段,除了是以 middle 为结尾的字段除外,都会被映射为 text 字段,并 copy_to 带 full_name 字段。如果你想了解更多 copy_to 的知识,请参考我之前的文章 “如何使用 Elasticsearch 中的 copy_to 来提高搜索效率”。
执行上面的命令,并使用如下的命令来进行查看:
GET my-index-000001/_mapping
上面的命令显示的结果为:
- {
- "my-index-000001" : {
- "mappings" : {
- "dynamic_templates" : [
- {
- "full_name" : {
- "path_match" : "name.*",
- "path_unmatch" : "*.middle",
- "mapping" : {
- "copy_to" : "full_name",
- "type" : "text"
- }
- }
- }
- ],
- "properties" : {
- "full_name" : {
- "type" : "text",
- "fields" : {
- "keyword" : {
- "type" : "keyword",
- "ignore_above" : 256
- }
- }
- },
- "name" : {
- "properties" : {
- "first" : {
- "type" : "text",
- "copy_to" : [
- "full_name"
- ]
- },
- "last" : {
- "type" : "text",
- "copy_to" : [
- "full_name"
- ]
- },
- "middle" : {
- "type" : "text",
- "fields" : {
- "keyword" : {
- "type" : "keyword",
- "ignore_above" : 256
- }
- }
- }
- }
- }
- }
- }
- }
- }
从上面的结果中,我们可以看出来,name 之下的 middle 字段没有 copy_to,而它的映射是遵循默认的 string 的映射规则:text 及 keyword 的 multi-field 字段。
请注意,除了叶(leaf)字段之外,path_match 和 path_unmatch 参数还匹配对象路径。 例如,索引以下文档将导致错误,因为 path_match 设置也匹配对象字段 name.title,它不能映射为文本:
- PUT my-index-000001/_doc/2
- {
- "name": {
- "first": "Paul",
- "last": "McCartney",
- "title": {
- "value": "Sir",
- "category": "order of chivalry"
- }
- }
- }
执行上面的命令将导致如下的错误信息:
- {
- "error" : {
- "root_cause" : [
- {
- "type" : "mapper_parsing_exception",
- "reason" : "failed to parse field [name.title] of type [text] in document with id '2'. Preview of field's value: '{category=order of chivalry, value=Sir}'"
- }
- ],
- "type" : "mapper_parsing_exception",
- "reason" : "failed to parse field [name.title] of type [text] in document with id '2'. Preview of field's value: '{category=order of chivalry, value=Sir}'",
- "caused_by" : {
- "type" : "illegal_state_exception",
- "reason" : "Can't get text on a START_OBJECT at 5:14"
- }
- },
- "status" : 400
- }
{name} 和 {dynamic_type} 占位符在映射中被替换为字段名称和检测到的动态类型。 以下示例将所有字符串字段设置为使用与字段同名的分析器(analyzer),并禁用所有非字符串字段的 doc_values:
- PUT my-index-000001
- {
- "mappings": {
- "dynamic_templates": [
- {
- "named_analyzers": {
- "match_mapping_type": "string",
- "match": "*",
- "mapping": {
- "type": "text",
- "analyzer": "{name}"
- }
- }
- },
- {
- "no_doc_values": {
- "match_mapping_type":"*",
- "mapping": {
- "type": "{dynamic_type}",
- "doc_values": false
- }
- }
- }
- ]
- }
- }
-
- PUT my-index-000001/_doc/1
- {
- "english": "Some English text",
- "count": 5
- }
在上面,{name} 被替换为 field name,而 {dynamic_type} 被替换为由 JSON 解析器检测的数据类型。在上面,english 字段的解析器被设置为 english,而 count 被检测为 long 类型,并且它的 doc_values 被设置为 false。
执行完上面的命令,我们可以通过如下的命令来查看:
GET my-index-000001/_mapping
上面的命令返回的结果为:
- {
- "my-index-000001" : {
- "mappings" : {
- "dynamic_templates" : [
- {
- "named_analyzers" : {
- "match" : "*",
- "match_mapping_type" : "string",
- "mapping" : {
- "analyzer" : "{name}",
- "type" : "text"
- }
- }
- },
- {
- "no_doc_values" : {
- "match_mapping_type" : "*",
- "mapping" : {
- "doc_values" : false,
- "type" : "{dynamic_type}"
- }
- }
- }
- ],
- "properties" : {
- "count" : {
- "type" : "long",
- "doc_values" : false
- },
- "english" : {
- "type" : "text",
- "analyzer" : "english"
- }
- }
- }
- }
- }
以下是一些可能有用的动态模板的示例:
当你设置 "dynamic":"true" 时,Elasticsearch 会将字符串字段映射为带有 keyword 子字段的 text 字段。 如果你只是索引结构化内容并且对全文搜索不感兴趣,你可以让 Elasticsearch 仅将你的字段映射为 keyword 字段。 但是,你必须搜索与索引完全相同的值才能搜索这些字段,也即精确匹配。
- PUT my-index-000001
- {
- "mappings": {
- "dynamic_templates": [
- {
- "strings_as_keywords": {
- "match_mapping_type": "string",
- "mapping": {
- "type": "keyword"
- }
- }
- }
- ]
- }
- }
与前面的示例相反,如果你只关心字符串字段的全文搜索并且不打算运行聚合、排序或精确搜索,你可以告诉 Elasticsearch 将字符串映射为 text:
- PUT my-index-000001
- {
- "mappings": {
- "dynamic_templates": [
- {
- "strings_as_text": {
- "match_mapping_type": "string",
- "mapping": {
- "type": "text"
- }
- }
- }
- ]
- }
- }
或者,你可以创建一个动态模板来将你的字符串字段映射为映射的运行时部分中的 keyword 字段。 当 Elasticsearch 检测到字符串类型的新字段时,这些字段将被创建为 keyword 类型的运行时字段。 尽管你的字符串字段不会被索引,但它们的值存储在 _source 中,可用于搜索请求、聚合、过滤和排序。 例如,以下请求创建一个动态模板来将 string 字段映射为 keyword 类型的运行时字段。 尽管运行时定义为空白,但新的 string 字段将根据 Elasticsearch 用于向映射添加字段类型的动态映射规则(在本文开始部分的表格中定义)映射为 keyword 运行时字段。 任何未通过日期检测或数字检测的字符串都会自动映射为 keyword:
- PUT my-index-000001
- {
- "mappings": {
- "dynamic_templates": [
- {
- "strings_as_keywords": {
- "match_mapping_type": "string",
- "runtime": {}
- }
- }
- ]
- }
- }
你索引一个简单的文档:
- PUT my-index-000001/_doc/1
- {
- "english": "Some English text",
- "count": 5
- }
查看映射时,你会看到 english 字段是 keyword 类型的运行时字段:
GET my-index-000001/_mapping
- {
- "my-index-000001" : {
- "mappings" : {
- "dynamic_templates" : [
- {
- "strings_as_keywords" : {
- "match_mapping_type" : "string",
- "runtime" : { }
- }
- }
- ],
- "runtime" : {
- "english" : {
- "type" : "keyword"
- }
- },
- "properties" : {
- "count" : {
- "type" : "long"
- }
- }
- }
- }
- }
归一化是索引时间的评分因素。 如果你不关心评分,例如,如果你从不按分数对文档进行排序,你可以禁用这些评分因素在索引中的存储并节省一些空间。
- PUT my-index-000001
- {
- "mappings": {
- "dynamic_templates": [
- {
- "strings_as_keywords": {
- "match_mapping_type": "string",
- "mapping": {
- "type": "text",
- "norms": false,
- "fields": {
- "keyword": {
- "type": "keyword",
- "ignore_above": 256
- }
- }
- }
- }
- }
- ]
- }
- }
该模板中出现的子 keyword 字段与动态映射的默认规则保持一致。 当然,如果你不需要它们,比如你不需要对该字段执行精确搜索或聚合,你可以按照上一节中的说明将其删除。
使用 Elasticsearch 进行时间序列分析时,通常会有许多数字字段,你经常会在这些字段上进行聚合,但从不进行过滤。 在这种情况下,你可以禁用这些字段的索引以节省磁盘空间并可能获得一些索引速度:
- PUT my-index-000001
- {
- "mappings": {
- "dynamic_templates": [
- {
- "unindexed_longs": {
- "match_mapping_type": "long",
- "mapping": {
- "type": "long",
- "index": false
- }
- }
- },
- {
- "unindexed_doubles": {
- "match_mapping_type": "double",
- "mapping": {
- "type": "float",
- "index": false
- }
- }
- }
- ]
- }
- }
在上面,如同在 "dynamic" 设置为 true 一样的情况,double 被映射为 float,因为它可以满足绝大部分的精度要求。在上面,我们可以通过 dynamic_templates 的使用,强制把 double 类型的数据转换为 float 以节省空间。