3. 映射(1)

195 阅读12分钟

1. 映射的定义

映射类似于关系型数据库中的Schema(模式)。Schema指表包含的字段及字段存储类型等基础信息。 映射描述了文档可能具有的字段、属性、每个字段的数据类型,以及Lucene是如何索引和存储这些字段的。

映射定义由两部分组成:元字段、数据类型字段

1.1 元字段

元字段用于自定义关于处理文档的相关元数据。元字段包括文档的index、id和source等。各种元字段都以一个下划线开头,例如_id和_source。

常见元字段类型如下。

  • 标识元字段
    • _index:文档所属索引。
    • _id:文档ID。
  • 文档源元字段
    • _source:文档正文的原始JSON对象。
    • _size:source字段的大小(字节为单位)。
  • 索引元字段
    • _field_names:给定文档中包含非空值的所有字段。
    • _ignored:由于设置ignore_malformed而在索引时被忽略的字段。
  • 路由元字段
    • _routing:用于将给定文档路由到指定的分片。
  • 其他元字段
    • _meta:应用程序特定的元数据,可用于给索引加必要注释信息。
    • _tier:指定文档所属索引的数据层级别,如在查询文档时可以指定data_hot、data_warm、data_cold等。

1.2 数据类型

  • binary:编码为Base64字符串的二进制类型。
  • boolean:仅支持true和false的布尔类型。
  • keyword:支持精准匹配的keyword类型、const_keyword类型和wildcard类型。
  • number:数值类型,如integer、long、float、double等。
  • date:日期类型,包括date和date_nanos。
  • alias:别名类型,区别于索引别名,此处的别名是字段级别的别名。
  • text:全文检索类型。
  • 数组类型:Array。
  • JSON对象类型:Object。
  • 嵌套数据类型:Nested。
  • 父子关联类型:Join。
  • Flattened类型:将原来一个复杂的Object或者Nested嵌套多字段类型统一映射为扁平的单字段类型

注意:

  1. 严格来讲,es无专门的数组类型。
  2. 任何类型都可以包含一个或者多个元素,当数据包含多个元素时,它就是数组类型。
  3. 数组类型要求一个组内的数据类型一致。
PUT lwy_index
{
  "mappings": {
    "properties": {
      "title":{
        "type":"text"
      }
    }
  }
}
POST lwy_index/_doc/1
{
  "title": ["标题1","标题2"]
}
  • 多字段类型:cont类型有俩扩展字段:key,stand
PUT my_index 
{
    "mappings": {
        "properties": {
            "cont": {
                "type": "text",
                "fields": {
                    "key" : { // cont原本支持全文索引(text) 加上这个符合字段后支持聚合和排序
                        "type": "keyword"                    
                    },
                    "stand":{
                        "type": "text"                    
                    }                
                }            
            }        
        }    
    }
}

多字段类型允许用户对单个文档字段设定多种不同的数据类型,使其能够根据不同场景为相同字段生成多种数据类型。

1.3 映射类型

动态映射

核心是在自动检测字段类型后添加新字段。

支持动态检测:boolean类型、float类型、long类型、Object类型、Array类型、date类型、字符串类型。除此之外的类型是不支持动态检测匹配的,会适配为text类型。

弊端:

  1. 字段匹配不准确,如将date类型匹配为keyword类型。可以提前设置匹配规则
PUT my_index
{
    "mappings": {
        "dynamic_date_formates": ["yyyy-MM-dd HH:mm:ss"] // 设置后下面的create_time就会匹配为时间了    
    }
}
PUT my_index/_doc/1
{
    "create_time": "2020-12-26 12:00:00"
}
  1. 字段匹配不精准,可能不是用户期望的。

举例:用户期望text类型支持ik中文分词,但默认的是standard标准分词器。对此当然也有解决方案,可借助动态模板实现。

  1. 占据多余的存储空间。

举例:string类型匹配为text和keyword两种类型,但实际用户极有可能只期望排序和聚合的keyword类型,或者只需要存储text类型,如网页正文内容只需要全文检索,不需要排序和聚合操作。

  1. 映射可能错误泛滥。

不小心写错查询语句,由于使用了PUT操作,导致映射变得非常混乱。

静态映射

也称作显式映射,类似于关系型数据库MySQL。在数据建模前,需要明确文档中各字段的类型。如何限制动态添加字段,这是静态映射要解决的问题。

对于该场景,可以将dynamic参数设置为false(忽略新字段),或者将dynamic参数设置为strict(如果遇到未知字段,则引发异常)。

例如,在"dynamic":false后,cont字段可以写入,但不能被检索。

PUT lwy_index
{
    "mappings": {
        "dynamic": false,
        "properties": {
          "user": {
            "properties": {
              "name": {
                "type": "text"
              }
            }
          }
        }
    }
}

GET lwy_index/_mapping

PUT lwy_index/_doc/1
{
  "cont": "hihihi"
}
# 可以写入但检索不出来,因为cont是未映射字段
POST lwy_index/_search  
{
  "profile": true,
  "query": {
    "match": {
      "cont": "hi"
    }
  }
}
# 可以返回数据
GET lwy_index/_doc/1

代码中"profile":true辅助我们看到底层的检索逻辑,而不能召回数据的核心原因在于cont是未映射的字段

"description" : """MatchNoDocsQuery("unmapped fields \[cont]")""",

如果"dynamic":"strict",写入就直接报错了不允许写入未定义过的字段的。

1.4 映射创建后还能更新吗

官方文档强调已经定义的字段在大多数情况下不能更新,除非通过reindex操作来更新映射。但以下3种情况例外。

  • Object对象可以添加新的属性。
  • 在已经存在的字段里面可以添加fields,以构成一个字段多种类型。
  • 字段增加ignore_above字段。

2. Nested类型及应用

Nested类型也被称作嵌套数据类型

在es中,可以将密切相关的实体存储在单个文档中。例如,博客文章和评论可以写在一个文档中。

PUT lwy_index00/_bulk
{"index":{"_id":1}}
{"title":"my blog","body": "welcome to my blog","comments":[{"name": "wiliam","age":34},{"name": "dily","age": 31},{"name": "hewin","age":33}]}

POST lwy_index00/_search
{
  "query": {
    "bool":{
      "must": [
        {
          "match": {
            "comments.name": "dily"
          }
        },
        {
          "match": {
            "comments.age": 33
          }
        }
        ]
    }
  }
}

查询发现召回了数据,原因在于如果没有特殊的字段类型说明,默认写入的嵌套数据映射为Object类型,其嵌套的字段部分被扁平化为一个简单的字段名称和值列表。上面写入的嵌套文档的内部存储结构如下所示。

image.png

comments.name和comments.age之间的关系已丢失。这就是检索dily和33依然有结果文档召回的原因。

要解决这个问题,需要将默认的Object类型修改为Nested类型。实操如下。

PUT lwy_index01
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text"
      },
      "body":{
        "type": "text"
      },
      "comments":{
        "type": "nested",
        "properties": {
          "name":{
            "type":"text"
          },
          "age":{
            "type":"text"
          }
        }
      }
    }
  }
}

POST lwy_index01/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "nested": {
            "path": "comments",
            "query": {
              "bool": {
                "must": [
                  {
                    "match": {
                      "comments.name": "dily"
                    }
                  },
                  {
                    "match": {
                      "comments.age": 33
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

由于用户{name:dily,age:33}没有匹配,上面的查询将不会召回任何文档。因为Nested嵌套对象将数组中的每个对象索引为单独的隐藏文档,这意味着可以独立于其他对象查询每个嵌套对象。 image.png

简单来说,Nested类型是Object数据类型的升级版本,它允许对象以彼此独立的方式进行索引。

2.1 Nested类型的操作

以上一个mapping为例

# 增
POST lwy_index01/_doc/2
{
  "title":"helloInsert",
  "body":"insert",
  "comments": [
    {
      "name":"steve",
      "age":24
    }]
}

# 序号1的评论原先有三条,删除一条
POST lwy_index01/_update/1
{
  "script": {
    "lang":"painless",
    "source": "ctx._source.comments.removeIf(it->it.name == 'dily');"
  }
}
# 改:将steve的年龄改为25
POST lwy_index01/_update/2
{
  "script": {
    "source": "for(e in ctx._source.comments) {if (e.name == 'steve') {e.age = 25;}}"
  }
}
# 查, 如上

3. Join类型

在传统的关系型数据库中,多表关联是一种常见操作。允许多个表中的数据关联,以便查询和分析。而在es这种分布式搜索和分析引擎中,实现这种多表关联操作并不简单,因为它的底层数据结构和存储方式与关系型数据库有很大的不同。为了解决这个问题,在6.X版本之后引入了一种名为Join的数据类型,旨在模拟关系型数据库中的多表关联操作。

Join类型允许在同一个索引下通过父子关系来实现类似于MySQL中多表关联的操作。使用Join类型时,我们需要在映射中定义一个“join”类型的字段,并为其分配一个或多个关系名称。这些关系名称定义了父子文档之间的层次结构,使得我们能够在查询时进行关联操作。

PUT lwy_index02 
{
  "mappings": {
    "properties": {
      "my_join_field":{ // 字段名
        "type": "join",
        "relations": { // 定义字段间的关系
          "question": [ // 父类,括号内answer1,2为子类
            "answer1",
            "answer2"
            ]
        }
      }
    }
  }
}
# 写入父文档
POST lwy_index02/_doc/1
{
  "text": "this is a question",
  "my_join_field": "question"
}
POST lwy_index02/_doc/2
{
  "texts": "this is another question",
  "my_join_field": "question"
}
# 写入子文档。路由值是强制的,父子文件需在相同分片建立索引
POST lwy_index02/_doc/3?routing=1&refresh
{
  "text": "this is a answer 1",
  "my_join_field":{
    "name": "answer1",
    "parent": "1"  // 指向其父文档id
  }
}

POST lwy_index02/_doc/3?routing=1&refresh
{
  "text": "this is a answer 2",
  "my_join_field":{
    "name": "answer2",
    "parent": "1"
  }
}

# 通过父文档找子文档
POST lwy_index02/_search
{
  "query": {
    "has_parent": {
      "parent_type": "question",
      "query": {
        "match": {
          "text": "This is"
        }
      }
    }
  }
}
# 通过子文档找父文档
POST lwy_index02/_search
{
  "query": {
    "has_child": {
      "type": "answer1",
      "query": {
        "match": {
          "text": "This is"
        }
      }
    }
  }
}

3.1 Join类型多层关系

image.png

PUT lwy_index03
{
  "mappings": {
    "properties": {
      "my_join_field":{
        "type": "join",
        "relations":{
          "question":[
              "answer",
              "comment"
            ],
            "answer": "vote"
        }
      }
    }
  }
}

3.2 注意点

  1. 对于每个索引,仅允许定义一个与Join类型关联的映射
  2. 父子文档必须在同一个分片上写入索引。这意味着当进行删除、更新、查找子文档等操作时需要提供相同的路由值。
  3. 一个文档可以有多个子文档,但一个子文档只能有一个父文档。
  4. 可以为已经存在的Join类型添加新的关系。
  5. 当一个文档已经成为父文档后,就可以为该文档添加子文档。

4. Flattened类型

es映射如果不进行特殊设置,则默认为dynamic:true,支持不加约束地动态添加字段。某些场景可能会产生大量的未知字段,很快达到es映射的上限,对应设置和默认大小为index. mapping.total_fields.limit:1000。这种非预期字段激增的现象称为字段膨胀。

在业务中混淆检索和写入的语法,则会将检索语句动态地认定为新增映射字段。如果是非常复杂的大型bool检索语句写入映射,会导致映射变得非常复杂,甚至会出现字段膨胀的情况。

例如,若误将检索语句写成插入文档语句,依然可以写入成功

image.png

可行的解决方案就是将dynamic设置为false或strict

4.1 Flattened类型的产生背景

将dynamic设置为false或strict不是普适的解决方案。在日志场景中期望动态添加字段,但strict会导致新字段数据拒绝写入,而dynamic:true过于松散会导致字段膨胀。这就导致Flattened字段的诞生。

中文释义为“字段扁平化”。当面临处理包含大量不可预测字段的文档时,使用Flattened类型可以将整个JSON对象及其Nested字段索引为单个keyword类型字段,以此减少字段总数。

映射字段数有时是无法预知的。随着新写入数据的激增,如果字段也激增,后果会如何?

es必须为每个新字段更新集群状态,并将此集群状态传递给所有节点。由于跨节点的集群状态传输是单线程操作,这种延迟通常大大降低集群性能,有时会导致整个集群宕机。这被称为mapping爆炸或字段膨胀。

这也是5.X版本开始将索引中的字段数限制为1000的原因之一。如果实战中字段数超过1000,那么必须手动更改默认索引字段限制或者考虑架构重构。

PUT lwy_index
{
    "settings": {
        "index.mapping.total_fields.limit": 2000    
    }
}

4.2 flattened类型实战

PUT lwy_index04
{
  "mappings": {
    "properties": {
      "host": {
        "type": "flattened"
      }
    }
  }
}

Flattened的本质是将一个复杂的Object或者Nested嵌套多字段类型统一映射为扁平的单字段类型。这里要强调的是:不管原来内嵌多少个字段、内嵌多少层,利用Flattened类型都能直接“拉平”。

PUT lwy_index04/_doc/1
{
  "message": "[dwafjnaio.conse/32323",
  "process":{
    "name":"org.com",
    "pid":3383
  },
  "host": {
    "hostname": "bionic",
    "name": "bionic"
  }
}

写入一条数据后查看映射

image.png

由于将host字段设置为flattened,所以hostname和name字段都不再映射为特定嵌套子字段。

POST lwy_index04/_update/1
{
  "doc": {
    "host":{
      "osVersion": "bionic beaver",
      "osArchitecture": "x86_64"
    }
  }
}

更新完发现 image.png

再次查看映射结构,它依然是Flattened,既没有字段扩增,也不会有mapping爆炸出现。

POST lwy_index04/_search
{
  "query": {
    "term": {
      "host": "bionic beaver"
    }
  }
}


POST lwy_index04/_search
{
  "query": {
    "term": { // 改为match也行
      "host.osVersion": "bionic beaver"
    }
  }
}
# 上面两种检索方法能召回数据,将term改为match则只有第二个形式可以召回

由于使用Flattened类型,es未对该字段进行分词等处理,因此它只会返回匹配字母大小写且完全一致的结果。所以,如上检索结果和keyword类型检索结果一致。这也初步暴露出Flattened类型的部分缺陷.

4.3 flattened类型的不足

面对Flattened对象,在进行es扁平化数据类型的选型时,我们需要考虑以下几个关键限制。

1)Flattened类型支持的查询类型目前仅限于以下几种:term、terms、terms_set、prefix、range、match、multi_match、query_string、simple_query_string、exists。

2)Flattened不支持的查询类型如下。

  • 无法执行涉及数字计算的查询,例如range检索。
  • 无法支持高亮查询。
  • 尽管支持诸如term聚合之类的聚合,但不支持处理诸如histograms或date_histograms之类的数值数据的聚合。

总之,Flattened类型的出现解决了字段膨胀引起的mapping爆炸问题。如果业务生产环境高于

7.3版本,那么我们遇到类似问题时可以小心求证,然后大胆尝试使用Flattened这种新类型。