1. 啥是映射

  ES中的映射(Mapping)实质上就是对文档对象结构的定义,也即对文档中各元素的描述。在ES中定义映射,就如同定义XML文档的XML Schema。
  ES中的映射定义了文档模式(就如同在关系数据库中定义了关系模式),文档模式确定了存在ES中的文档的格式,结构和字段的数据类型。通过查看某个索引的映射可以了解文档的结构,以便使用查询语言(Query DSL)构建更符合我们要求的查询命令。

2. 从一个示例开始

  让我们首先看一下如下关于银行账号的文档示例:

{
    "account_number": 1,
    "balance": 39225,
    "firstname": "Amber",
    "lastname": "Duke",
    "age": 32,
    "gender": "M",
    "address": "880 Holmes Lane",
    "employer": "Pyrami",
    "email": "[email protected]",
    "city": "Brogan",
    "state": "IL"
}

  ES对该文档的自动生成的映射是下面这个样子的:

{
    "bank": {
        "mappings": {
            "account": {
                "properties": {
                    "account_number": {
                        "type": "long"
                    },
                    "address": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "age": {
                        "type": "long"
                    },
                    "balance": {
                        "type": "long"
                    },
                    "city": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "email": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "employer": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "firstname": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "gender": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "lastname": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    },
                    "state": {
                        "type": "text",
                        "fields": {
                            "keyword": {
                                "type": "keyword",
                                "ignore_above": 256
                            }
                        }
                    }
                }
            }
        }
    }
}

  由这个自动生成的映射可以看到:ES自动将account_number、balance、age这些属性映射为long类型,其它的属性都映射为text类型。text类型的属性常用于全文搜索,但是并不进入内存中索引,因此text类型并不可用于聚合和排序(系统会报错:"Fielddata is disabled on text fields by default. Set fielddata=true on [address] in order to load fielddata in memory by uninverting the inverted index. Note that this can however use significant memory. Alternatively use a keyword field instead.")。
  ES允许为一个对象属性定义多个域(fields),每个域是该属性的一个facet(我思考很久,还是觉得这个词最合适),如“address”属性类型为text,为它定义 一个域为keyword,该域的类型为“keyword”,不会被分析器(analyzer)分析,可用于排序、聚合和精确查找(请注意ignore_above这个属性,限制了用于keyword的有效字符数目)。
  在DSL查询语言中查询时,使用“address”时,经分析器分析后,"880 Holmes Lane"可能被分解为“880”,“Holmes”,“Lane”进入全文搜索。看看下面的两个查询命令:

curl -iXGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json'  -d'
{"query":
    {"match":
       {"address.keyword":"880 Holmes Lane"}
    }
}'

  查询出来只有一个结果,精确匹配“880 Holmes Lane”。

curl -iXGET 'localhost:9200/bank/_search?pretty' -H 'Content-Type: application/json'  -d'
{"query":
    {"match":
       {"address":"880 Holmes Lane"}
    }
}'

  查询出来多个结果,查询条件“880 Holmes Lane”被分析器分析后检索,"591 Nolans Lane"也被检索出来(其中包含了一个分析器分解后的Lane)。
  以一张图总结相关的知识点:
编程随笔-ElasticSearch知识导图(3):映射_第1张图片

3. 域的数据类型

  ES中域的主要数据类型如下表所示,还可由一些插件扩展数据类型(这里不赘述了):

数据类型 分类
text , keyword 字符串
long , integer , short , byte , double , float , half_float , scaled_float 数字
date 日期
boolean 布尔
binary 二进制
integer_range , float_range , long_range , double_range , date_range 区间类型
Array, object, nested 复杂数据类型
geo_point, geo_shape 地理数据类型
binary 二进制
ip, completion,token_count,percolator,join, alias 特殊数据类型

3.1. 核心数据类型

  核心数据类型与我们常使用的强类型语言中的数据类型类似,可分为以下几类:

  • 字符串型:包含text 、keyword。
  • 数字类型:包含long , integer , short , byte , double , float , half_float , scaled_float。这里偷个懒,各数字类型参见参考文献1中的截图描述。
    编程随笔-ElasticSearch知识导图(3):映射_第2张图片
  • 日期类型:date。可表示为字符串类型,如"2015-01-01" or "2015/01/01 12:10:30",也可表示为秒或毫秒的整数。
  • 布尔类型:boolean。真值可用true(对应json中的布尔值),“true”表示。
  • 二进制类型:binary。值用以base64编码的字符串表示。
  • 区间类型:包含integer_range , float_range , long_range , double_range , date_range。
      对于数字区间类型,示例如下:
    //类型为integer_range
    "expected_attendees" : { 
    "gte" : 10,
    "lte" : 20
    }

      对于日期区间类型,示例如下:

    //类型为date_range
    "time_frame" : { 
    "gte" : "2015-10-31 12:00:00", 
    "lte" : "2015-11-01"
    }

    3.2. 复杂数据类型

      复杂数据类型可用于表达对象之间的语义,包含Array, object, nested等类型。

  • Array类型:在ES中,没有专门的“array”类型用于显式定义,任何域都可以包含零个或多个值,即任何域都可以是一个数组,不过不同于JSON中的数组,ES中的数组各元素必须为相同类型。
  • Object类型:JSON对象可以是一个嵌套结构,在现实应用中我们常常使用这种嵌套结构来表达对象之间的层次关系。
    我们看看下面这个映射的定义:
    "manager": { 
          "properties": {
            "age":  { "type": "integer" },
            "name": { 
              "properties": {
                "first": { "type": "text" },
                "last":  { "type": "text" }
              }
            }
          }
        }

      其对应的对象为:

    "manager": { 
    "age":     30,
    "name": { 
      "first": "John",
      "last":  "Smith"
    }
    }

      其中域manager就是一个对象类型,其中的name是它的子对象。对于对象类型,缺省设置“type”为”object”,因此不用显式定义“type”。
      对于上面的对象类型,ES在索引时将其转换为"manager.age", "manager.name.first" 这样扁平的key,因此查询时也可以使用这样的扁平key作为域来进行查询。

  • nested类型:是object类型的延伸,主要用于对象数组。仍以参考文献1中的示例:
    对于对象:
    "user" : [ 
    {
      "first" : "John",
      "last" :  "Smith"
    },
    {
      "first" : "Alice",
      "last" :  "White"
    }
    ]

      如果使用动态映射,会被ES索引为如下形式:

    "user.first" : [ "alice", "john" ],
    "user.last" :  [ "smith", "white" ]

      这样的索引形式在查询时会丢失对象中”first”与“last”之间的关联关系。
      如果将user映射为如下形式:

    "user": {
          "type": "nested" 
        }

      ES在索引时会保留对象域之间的关联关系,在查询时找对正确的对象。
      如使用如下查询则找不到任何命中对象(不存在“Alice Smith”这个对象):

    {
    "query": {
    "nested": {
      "path": "user",
      "query": {
        "bool": {
          "must": [
            { "match": { "user.first": "Alice" }},
            { "match": { "user.last":  "Smith" }} 
          ]
        }
      }
    }
    }
    }

    3.3. 地理数据类型

      地理数据类型可用于LBS的应用,包括:

  • geo_point类型:可用于存储某个地理坐标的经纬度。示例如下:
       // location为geo_point类型
    "location": { 
    "lat": 41.12,
    "lon": -71.34
    }
  • geo_shape类型:用于存储地理多边形形状,有兴趣的读者可以参考文献1。

    3.4. 特殊数据类型

      特殊数据类型包括:

  • ip类型:用于表示IPv4与IPv6的地址
  • completion类型:提供自动输入关联完成功能,如常见的baidu收缩框。
  • token_count类型:用于计算字符串token的长度,使用时需提供"analyzer"定义。
  • percolate类型:定义为percolate类型的字段会被ES分析为一个查询并保存下来,并可用在后继对文档的查询中。Percolate可以理解为一个预置的查询。
  • alias类型:定义一个已存在域的别名。
  • join类型:该类型定义了文档对象之间的父子关系(可定义多层,形成一颗层次树),即同一索引中多个文档对象可以存在依赖关系,如互联网应用常见的博客文章与回复,问题与回答之间的关系。参看文献1中的如下示例:
    定义映射字段:
    {
    "my_join_field": {
        "type": "join",
        "relations": {
            "question": "answer"
        }
    }
    }

      my_join_field定义了"question"与"answer"之间关系为父子关系。
      观察对于该映射的一个文档实例,路径为“my_index/_doc/1”:

    {
    "text": "This is a question",
    "my_join_field": "question" 
    }

      该文档的一个子文档对象示例如下,在my_join_field需要定义父亲的ID(这里根据上面的父实例,为1):

    {
    "text": "This is an answer",
    "my_join_field": {
    "name": "answer", 
    "parent": "1" 
    }
    }

      需要注意的是,一个父文档可以有多个子文档,父子文档应部署在同一个分片上。因而在向ES提交父子文档时,应在URI中使用相同的routing参数。
      join类型定义了文档之间的父子依赖关系,在查询和聚合操作中可使用这种依赖关系。

    4. 映射参数

      JSON是JS对象序列化的字符串,ES接收一个JSON字符串形式的文档对象,本质上是存入一个JS对象,JS定义了对象,数组,字符串,数字,布尔型和null等数据类型。
      ES中的域数据类型可视为对JS对象数据类型的扩展,如join,区间类型等都表示为js对象。
      在定义域映射时,ES定义了相关的映射参数,这里简单列举并描述,详细信息可以查看文献1。

参数 描述
analyzer 定义对文本数据的分析器
normalizer 对文本数据规范化
boost 用于提升字段搜索的权重
coerce 当为false时,强制输入值必须符合映射的域数据类型
copy_to 将当前域的值复制到另一个域中
doc_values 当该域不参与排序域聚合操作时,可设置为false使得不在磁盘上存储Doc value(以列式存储的文档值)以节约磁盘空间。缺省为true
dynamic 该参数控制在对象中检测到的新的域(未在映射中定义)是否加入到域中,当为false或strict时,新域不会加入到映射中。缺省为true
enabled 主要应用于object类型的域,当设置为false,该域不被索引。缺省为true
fielddata 对于text类型的域,如果该参数设置为true,该域的数据在第一次使用时会载入常驻于内存中。缺省为false
format 定义域数据的格式,用于日期类型
ignore_above 定义字符串的有效长度
ignore_malformed 如果设置为true,当字段值与映射定义不一致时,不会抛出错误。缺省为false
index 缺省为true,当设置为false时,该域不被索引,不可被搜索
null_value 定义该域为空值时的格式,如使用“NULL”这样的字符串
search_analyzer 定义搜索时的analyzer,可与定义映射时使用的analyzer不同
store 该值设置为true时,当前域的原始值也存储下来(在_source之外)。默认为false

  总结一下:

  • 与域数据格式及约束相关的参数:normalizer,format,ignore_above,ignore_malformed,coerce
  • 与索引相关的参数:index,dynamic,enabled
  • 与存储策略相关的参数:store, fielddata,doc_values
  • 分析器相关参数:analyzer,search_analyzer
  • 其它参数:boost,copy_to,null_value
      对于这些参数的描述主要基于笔者的理解,可能有不准确之处。实际上这些参数与ES的实现机制(如存储结构,索引结构密切有关),只能在实际应用中去慢慢体会。

    5. 一个设计实例

      在ES中设计一个索引的映射和在关系数据库中设计关系模式,ER模型,在XML中设计XML Schema一样。需要完整包含领域知识并满足数据之间的约束。
      在这一节中我们探讨一个使用ES构建视频图像信息数据库的实例。

    5.1. GA/T 1400.3的数据模型分析

      视频图像信息数据库(以下简称视图库)基于GA/T 1400.3 标准定义,用于存储视频、图像等基本对象(二进制数据)和由这些基本对象分析(可自动)出的属性对象。
      在GA/T 1400.4中,定义了访问视频图像信息数据库的接口,这些接口以基于HTTP的restful形式定义,以JSON格式传输数据。因而使用ES作为视频图像信息数据库的存储容器可以利用ES的JSON文档对象存储和。
      在GA/T 1400.3中定义了视频图像信息数据库的数据模型,该数据模型中定义了三十多个领域对象,对象之间具有关联关系。视图库中的对象定义主要包含以下特征:

  • 视图库中每个对象都定义了唯一ID用于标识。
  • 对象之间存在父子(或因果)关系,关联方式为:子对象中包含父对象的ID。如车道对象包含对应卡口的ID;告警对象包含布控对象的ID;人、车、物对象包含其对应的来源图像ID和采集设备ID。
  • 对象之中嵌套子对象:如人、车、物对象中都包含子图像列表对象。
      总的来说,视图库模型中的数据对象间的关联关系比较简单,也相对独立。
    视图库中的对象的属性(字段)被约束为三种类型:R(必选),R/O(条件可选,当满足某个条件时该属性必须存在),O(可选)。
      不同于RDBMS,ES在映射对象的属性时并没有标明值是否不为空的选项,因而需要在提交对象数据到ES前进行数据的有效性检验。在设计基于ES的视图库架构设计时,这样的数据有效性检验服务(模块)可放在API网关(或负载均衡网关)与ES之间,并根据实际场景的效率和完整性要求决定是否启用。

    5.2. GA/T 1400.3的数据类型分析

      视图库规范定义对象字段的数据类型可为:

  • 基础数据类型:包含字符串,整型数,长整型数,浮点数,日期时间,数组,对象。可与ES的数据类型对应。
  • 扩展数据类型:定义了领域知识,对基础数据类型做了一些约束形成扩展数据类型。如道路类型(SceneType)定义为使用最大长度为2的字符串表示的枚举类型。

    5.3. File对象的映射分析

      以视图库中的File对象(GA/T 1400.3附录A.7)为例,我们看看如何定义它的映射。
      在GA/T 1400.3中,它的XML Schema是这样定义的:

    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

      一个文件对象的对象实例如下所示。

    {
    "FileObject": {
        "FileID": "31000000001190000138022019021416121100001",
        "InfoKind": 1, 
        "Source": "3",  
        "FileName": "tollgate_3_lane_4_20190214161211.jpg",
        "StoragePath": "/tollgate/3/lane/4/images",
        "FileHash": "38b8c2c1093dd0fec383a9d9ac940515",
        "FileFormat": "Jpeg",
        "Title": "tollgate_3_lane_4_20190214161211",
        "SecurityLevel": "3",
        "SubmiterName": "zhangkai",
        "SubmiterOrg": "pudong",
        "EntryTime": "20190214161214",
        "FileSize": 94208
    }
    }

      分析该对象中的各属性字段,整理出下表:

字段名称 标准中的数据类型定义 ES中对应类型 备注
FileID string(41) type:keyword
doc_values:false
ignore_above : 41
不参与排序与聚合
InfoKind int type: integer
coerce: false
Source string(2) type:keyword
ignore_above : 2
FileName string(0..256) type:keyword
ignore_above : 256
StoragePath string(256) type:keyword
doc_values:false
ignore_above : 256
不参与排序与聚合
FileHash string(32) type:keyword
doc_values:false
ignore_above : 32
不参与排序与聚合
FileFormat string(32) type:keyword
ignore_above : 32
Title string(128) type:keyword
ignore_above : 128
SecurityLevel String(1) type:keyword
ignore_above : 1
SubmiterName string(0..50) type:keyword
ignore_above : 50
SubmiterOrg string(0..100) type:keyword
ignore_above : 100
EntryTime dateTime type: date
format:yyyyMMddHHmmss
格式为:YYYYMMDDhhmmss
FileSize int type: integer
coerce: false

5.4. File对象的映射定义

  我们使用如下命令在ES中创建索引file(注意这里的index.mapping.coerce被设置为false):

curl -iXPUT 'localhost:9200/file?pretty' -H "Content-type: application/json" -d' 
{
  "settings": {
     "number_of_shards":3,
     "number_of_replicas":1,
     "index.mapping.coerce": false
  }
}
'

  使用如下命令修改file索引的映射:

curl -iXPUT 'localhost:9200/file/_mapping/object?pretty' -H "Content-type: application/json" -d'
{
    "properties": {
        "FileObject": {
            "properties": {
                "FileID": {
                    "type": "keyword",
                    "doc_values": false,
                    "ignore_above": 41
                },
                "InfoKind": {
                    "type": "integer",
                    "coerce": false
                },
                "Source": {
                    "type": "keyword",
                    "ignore_above": 2
                },
                "FileName": {
                    "type": "keyword",
                    "ignore_above": 256
                },
                "StoragePath": {
                    "type": "keyword",
                    "doc_values": false,
                    "ignore_above": 256
                },
                "FileHash": {
                    "type": "keyword",
                    "doc_values": false,
                    "ignore_above": 32
                },
                "FileFormat": {
                    "type": "keyword",
                    "ignore_above": 32
                },
                "Title": {
                    "type": "keyword",
                    "ignore_above": 128
                },
                "SecurityLevel": {
                    "type": "keyword",
                    "ignore_above": 1
                },
                "SubmiterName": {
                    "type": "keyword",
                    "ignore_above": 50
                },
                "SubmiterOrg": {
                    "type": "keyword",
                    "ignore_above": 100
                },
                "EntryTime": {
                    "type": "date",
                    "format": "yyyyMMddHHmmss"
                },
                "FileSize": {
                    "type": "integer",
                    "coerce": false
                }
            }
        }
    }
}
'

  使用如下命令查看file的映射信息:

curl -iXGET 'localhost:9200/file/_mapping?pretty'

  可以看到返回的映射信息:

{
  "file" : {
    "mappings" : {
      "object" : {
        "properties" : {
          "FileObject" : {
            "properties" : {
              "EntryTime" : {
                "type" : "date",
                "format" : "yyyyMMddHHmmss"
              },
              "FileFormat" : {
                "type" : "keyword",
                "ignore_above" : 32
              },
              "FileHash" : {
                "type" : "keyword",
                "doc_values" : false,
                "ignore_above" : 32
              },
              "FileID" : {
                "type" : "keyword",
                "doc_values" : false,
                "ignore_above" : 41
              },
              "FileName" : {
                "type" : "keyword",
                "ignore_above" : 256
              },
              "FileSize" : {
                "type" : "integer",
                "coerce" : false
              },
              "InfoKind" : {
                "type" : "integer",
                "coerce" : false
              },
              "SecurityLevel" : {
                "type" : "keyword",
                "ignore_above" : 1
              },
              "Source" : {
                "type" : "keyword",
                "ignore_above" : 2
              },
              "StoragePath" : {
                "type" : "keyword",
                "doc_values" : false,
                "ignore_above" : 256
              },
              "SubmiterName" : {
                "type" : "keyword",
                "ignore_above" : 50
              },
              "SubmiterOrg" : {
                "type" : "keyword",
                "ignore_above" : 100
              },
              "Title" : {
                "type" : "keyword",
                "ignore_above" : 128
              }
            }
          }
        }
      }
    }
  }
}

  现在我们可以向file索引提交数据对象了,使用如下命令:

curl -iXPOST 'localhost:9200/file/object/31000000001190000138022019021416121100001?pretty'  -H "Content-type: application/json" -d'
{
    "FileObject": {
        "FileID": "31000000001190000138022019021416121100001",
        "InfoKind": 1, 
        "Source": "3",  
        "FileName": "tollgate_3_lane_4_20190214161211.jpg",
        "StoragePath": "/tollgate/3/lane/4/images",
        "FileHash": "38b8c2c1093dd0fec383a9d9ac940515",
        "FileFormat": "Jpeg",
        "Title": "tollgate_3_lane_4_20190214161211",
        "SecurityLevel": "3",
        "SubmiterName": "zhangkai",
        "SubmiterOrg": "pudong",
        "EntryTime": "20190214161214",
        "FileSize": 94208
    }
}
'

5.5. 小结

  视图库中对象的字段不用进行全文检索,也可以使用关系数据库作为存储容器,但需要对JSON数据进行反序列化解析相应字段入库,查询出库时需要将多个字段序列化为JSON数据。固然在编程时可以使用ORM和JSON序列化中间件来完成工作,但在海量请求下,效率会有影响。使用ES可以利用ES的restful接口和JSON存储格式的天然特性以契合规范要求。

  在视图库规范中有一些自定义的约束,这些涉及数据有效性检验的服务应该部署在ES入库之前。在本实例中,更多的是把ES作为一个Nosql数据库使用。

6. 参考文献

  1. https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
  2. Clinton Gormley &Zachary Tong, Elasticsearch: The Definitive Guide,2015
  3. GA/T 1400.3 公安视频图像信息应用系统 第3部分:数据库技术要求,2017