当我们使用数据库时需要先建表,确定一张表一行数据存储哪些字段,每个字段的数据类型、约束是怎样的。ElasticSearch 也一样,
在使用时其每个索引每个文档要确定存储哪些字段,这些字段是何种数据类型,数据怎样转换,是否需要索引等。在使用 ES 索引之前我们也需要了解要存储哪些数据,然后对数据进行建模。
本篇笔记将简要介绍下 ES Mapping 使用的相关内容,包括数据类型、自定义 Mapping 以及 ES 数据建模相关的知识。
一. Mapping 简介
Mapping 是用来定义 ES 中索引存储的数据字段和相关配置的,类比于数据库的表结构。但不同于数据库需要先建表结构然后在插入数据。ES 可以直接创建索引文档,ES 会根据创建的文档为索引自动创建 Mapping。下面看一个例子
# 1. 创建文档
PUT test_index/doc/1
{
"username": "zyj",
"age": 12
}
# 2. 查询 Mapping, 其 endpoint 为 _mapping
GET test_index/_mapping
# 3查询结果如下
{
"test_index": { # 索引名
"mappings": { # 包含的即为索引的 mapping
"doc": {
"properties": { # 以下为索引中每个字段的设置
"age": {
"type": "long" # 数据类型,ES 自动将 age 转为了 long 长整型数据
},
"name": {
"type": "keyword"
},
"username": {
"type": "text", # 数据类型为 text
# 子字段,可以对博客标题进行完全匹配
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
}
二. 自定义 mapping
上面是一个简单的实例,我们直接将数据写入文档,对应的索引自动生成了 Mapping,但更多的时候我们需要根据业务需求和数据需求来确定需要哪些字段,各个字段有怎样的要求约束以及关系等。
下面是一个创建 mapping 的示例,
# 1. 创建一个索引
PUT /test_index
{
"mappings": {
# 2. 指明 mapping type 为 doc,
"doc":{
# 3. 定义需要的各个字段
"properties": {
"username":{
# 4. 指明字段类型为 txt
"type": "text"
},
"age":{
# 指明数据类型为整型
"type": "integer"
}
}
}
}
}
在上面的建模语句中,我们定义了 username 以及 age 字段,其类型分别为字符串和整型。我们使用 type 指明字段的数据类型。除此之外,还有很多字段定义相关的配置会经常用到,下面对这些进行简要的介绍。
1. Mapping 的数据类型
【1】 字符串类型
字符串类型主要分文 text 和 keyword 两种类型,区别如下:
- text 类型的字段会被分词器分词,被存入倒排索后可以进行全文检索,但很少用于排序和聚合分析
- keyword 类型的字符串不会被分词,只能精确查询,无法全文检索。常用来做过滤,排序和聚合分析
【2】 数字类型
常用的数字类型有
- 整型: long、integer、short、byte
- 浮点型: double、float、half_float 和 scaled_float
对于 scaled_float 类型,其会以整数的形式表示小数,需要与 scaling_factor 一起使用,该字段用来指明精度,示例如下:
PUT my_index
{
"mappings": {
"doc": {
"properties": {
"price": {
"type": "scaled_float",
"scaling_factor": 10
}
}
}
}
}
PUT my_index/doc/1
{
"price": 2.34
}
price 字段被索引时,会将乘上 scaling_factor 也就是 10,然后进行四舍五入后得到 23,在倒排索引中以 23 进行存储。
当进行搜索,分析排序等操作时,都认为文档中存储的值为 2.3。
【3】日期类型 date
ES 中并没有确定的日期类型,只要符合日期的格式,都可以被视作日期类型。比如格式化的日期字符串,表示秒和毫秒的整数都可以视作 date,ElasticSearch 内部是使用 long 类型作为内部存储的。官方示例示例如下:
PUT my_index
{
"mappings": {
"doc": {
"properties": {
"date": {
"type": "date"
# 可以通过 format 指定多种类型的日志格式
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
}
}
}
}
}
# 纯日期格式
PUT my_index/_doc/1
{ "date": "2015-01-01" }
# 字符串文本格式
PUT my_index/_doc/2
{ "date": "2015-01-01T12:10:30Z" }
# 毫秒计数
PUT my_index/_doc/3
{ "date": 1420070400001 }
【4】 boolean 类型
true 取值: true 或者 "true"
false 取值: false 或者 "false"
【5】 二进制数据 binary
binary 二进制类型的字段用于存储 Base64 编码的字符串,但要保证其字符中不含换行符。
PUT my_index
{
"mappings": {
"doc": {
"properties": {
"blob": {
"type": "binary"
}
}
}
}
}
PUT my_index/_doc/1
{
"blob": "U29tZSBiaW5hcnkgYmxvYg=="
}
【6】数组类型
ElasticSearch 中没有明确的数组类型定义,但是每个字段都可以含有多个值,只要保证同一字段中所有值的数据类型一致即可。
ES 的数组表现形式其实还是 string,ES 是倒排索引,数组的倒排索引只是多了几个单词而已。
官方实例如下
PUT my_index/_doc/1
{
"message": "some arrays in this document...",
"tags": [ "elasticsearch", "wow" ],
"lists": [
{
"name": "prog_list",
"description": "programming list"
},
{
"name": "cool_list",
"description": "cool stuff list"
}
]
}
【7】多字段特性
允许对同一个字段采取不同的配置,可以使用根据实际情况添加多个子字段。
示例如下:
"title": {
"type": "text",
"fields": {
# keyword 表示子字段名,可以任意设置
"keyword": {
"type": "keyword"
}
}
}
上面 mapping 设置后,索引除了会对 title 字段进行分词存储外,还会存储一个 title.keyword 的类型为 keyword 的不分词子字段。keyword 表示子字段名称,这里可以任意定义。这样我们就可以根据不同的场景来查询 title 或者 title.keyword 子字段。
除上面一些类型之外,ElasticSearch 还提供了 geo 地址位置相关的、IP 类型的字段等,具体的使用可以查询 官方文档-ES 数据类型。
2. Mapping 字段的相关设置
了解了数据类型之后,在看一下定义时对各个字段的限定,通过限定可以确定哪些字段需要索引,哪些字段需要额外存储以及聚合分析等。
【1】enabled
- true | false
默认值 true
当某个字段只存储,不需要做搜索或者聚合分析的时候,可以将 enabled 设置为 false,这样可以节省空间。
【2】index 与 index_options
- true | false
默认是 true
用来确定是否索引字段,我们知道 ES 默认会对文档的每个可分词字段作分词后使用倒排索引存储。如果某个字段不需要被索引就可以设置为 false,可以减少空间的占用。
既然需要索引,就需要倒排索引需要存储哪些信息,这是由 index_options 指定的,其有以下四个选项:
- docs: 只记录文档的 ID
- freqs: 记录文档 ID 和 term frequienices, 即词频
- position: 记录 文档 ID 、词频以及 term position 单词出现的位置
- offset: 记录文档 ID、词频、单词出现的位置以及 character offset (单词在文档的开始和结束位置,可以用于高亮显示)
text 类型默认为 position ,其他类型均为 docs。 要注意记录内容越多则消耗的空间越大,因此在数据建模时要根据实际情况尽量只记录需要的数据。
【3】 store
- true | false
默认 false,是否单独存储该字段。默认是不存储的,因为 ES 会将某个文档的原始 json 字符串存储到 _source 中。但如果想对该字段进行单独存储和检索可以设置为 true。
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"title": {
"type": "text",
"store": true
},
"date": {
"type": "date",
"store": true
},
"content": {
"type": "text"
}
}
}
}
}
这样在查询数据的时候可以指定要返回哪些字段。
GET my_index/_search
{
"stored_fields": [ "title", "date" ]
}
返回结果如下:
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [
{
"_index": "my_index",
"_type": "_doc",
"_id": "1",
"_score": 1,
"fields": {
"date": [
"2015-01-01T00:00:00.000Z"
],
"title": [
"Some short title"
]
}
}
]
}
}
注意其返回的都是数组,stroe_fields 返回的每个字段都是数组,因为 ES 不知道字段传入的值是单个、多值还是空值,因此默认传递的全是数组。
关于 store 的一个用途是如果文档中某个字段包含的数据量非常大,全部存在 _source 中的话对非常影响查询性能,此时可以关闭 _source,然后对各个字段进行单独 store。
【4】coerce
- true | false
是否开启自动数据类型转换,比如字符串转数字,默认是 true。可以设置为 false 来找出不符合要求的数据字段。
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"number_one": {
"type": "integer"
},
"number_two": {
"type": "integer",
"coerce": false
}
}
}
}
}
# 传字符串 “10” 不会有问题,会自动转为 interger 类型
PUT my_index/_doc/1
{
"number_one": "10"
}
# number_two 关闭了自动类型转换,必须传整数 10,传下面的字符串 "10" 会报错
PUT my_index/_doc/2
{
"number_two": “10”
}
【5】 null_value
空值替换,ElasticSearch 遇到空值时默认为 null,可以通过该选项来对空值指定默认值。
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"status_code": {
"type": "keyword",
"null_value": "NULL"
}
}
}
}
}
【6】 copy_to
可以将某字段的值复制到某个字段,被拷贝字段默认不会出现在 _source 中,只能用来搜索,一般为了使用特殊的搜索才会用到该特性。
下面是官方文档中的实例
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"first_name": {
"type": "text",
"copy_to": "full_name"
},
"last_name": {
"type": "text",
"copy_to": "full_name"
},
"full_name": {
"type": "text"
}
}
}
}
}
PUT my_index/_doc/1
{
"first_name": "John",
"last_name": "Smith"
}
GET my_index/_search
{
"query": {
"match": {
"full_name": {
"query": "John Smith",
"operator": "and"
}
}
}
}
使用 copy_to 有几点注意:
- 被拷贝的字段可以同时拷贝到多个字段中,“copy_to”: [ “field_1”, “field_2” ]
- 被拷贝字段不会出现在 _source 中,
- 仅仅是值拷贝
Mapping 的字段选项参数还有很多,这里就不一一赘述,不清楚的可以参考官方文档对各个参数的讲解 Mapping 字段参数官方文档。
3. 性能相关的参数配置
【1】norms
- norms: 是否存储归一化相关参数,如果不用于算分排序可关闭。
【2】doc_values
该配置主要影响排序和聚合分析操作的性能。我们知道 ES 是采用倒排索引的形式存储数据,通过某个单词、条件查询时,会先通过查询条件查倒排索引,然后在去查文档。这种查询模式并不适合排序、聚合分析的场景,此时我们不是通过字段查文档,而是拿到文档,然后对其字段进行排序或者聚合分析。
doc_values 存储和 _source 相同的数据,在索引建立时就已经存在,其采用磁盘数据结构和以列为导向的结构比倒排索引更适合进行排序和聚合分析。大多数数据类型字段都支持该参数的存储,除了 text 类型,ES 提供了额外的 filed_data 选项进行配置。
该配置默认开启, 如果某个字段仅用于搜索的可以关闭该选项以节省空间。
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"session_id": {
"type": "keyword",
"doc_values": false
}
}
}
}
}
【3】 filed_data
大多数数据类型开启 doc_values 以支持排序、聚合分析以及 script 操作。而 text 类型由于进行分词无法采用该结果,ES 提供了 filed_data 来对 text 类型进行单独处理。
该选项默认是关闭的,需要手动开启。
PUT my_index
{
"mappings": {
"_doc": {
"properties": {
"my_field": {
"type": "text",
"fielddata": true,
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
}
该选项会占用内存空间,在开启之前需要想好是否真的需要通过 text 类型的字段进行排序、聚合。更推荐的方式是提供一个 keyword 类型的子字段, 然后使用该字段进行排序和聚合分析。
三. Dynamic 设置
1. 新增字段设置
当我们定义好一个索引的 mapping 后,如果新增了一个字段,会对索引造成影响,可能会导致整个倒排索引的重建。因此我们需要控制是否允许字段的随意添加。ES 中提供了 dynamic 来控制字段的新增,其值有三个选择:
- true: 可以写入新字段,可以查询
- false: 可以写入字段,但不能查询
- strict: 严格模式,文档不能写入新字段
实例如下
PUT my_index
{
"mappings": {
"doc": {
# 使用 dynamic 指明为 false,表示可以加入但是不能查询
"dynamic": false,
"properties": {
"title": {
"type": "text"
},
}
}
}
}
# 设置了 dynamic 为 false, 写入 desc 时可以成功但不会被查询,mapping 设置也不会变
# 如果是 strict, 则在 post 时就会报错
POST my_index/doc/1
{
"title": "hello world",
"desc": "nothing here"
}
# 查询不会返回结果
GET my_index/doc/_search
{
"query": {
"match": {
"desc": "here"
}
}
}
建议使用 False, 可以写入,但不会被随意修改。比如类似于 cookid 类型的字段。该配置项可以在 type 下指定,也可以在 Object 中进行设置。
2. 数字日期识别设置
创建 Mapping 时可以对日期和数字识别进行自定义的设置,主要通过下面三个参数选项设置:
- date_detection: 开启日期自动识别, 默认开启,可关闭
- dynamic_date_formats: 指定要识别的日期格式
- numeric_detection: 开启对字符串数字的识别
这三个都是在 type 下面设置,示例如下:
PUT my_index4
{
"mappings": {
"doc":{
"date_detection": true
"dynamic_date_formats": ["MM/dd/yyyy"],
"numeric_detection": true
}
}
}
# create_data 会被识别为 date 类型,而 age 会被识别为 long
PUT my_index4/doc/2
{
"create_date": "02/25/2015",
# 默认是不会识别为整型的,开启 numeric_detection 后即可识别
"age": "12"
}
GET my_index4/_mapping
{
"my_index4": {
"mappings": {
"doc": {
"dynamic_date_formats": [
"MM/dd/yyyy"
],
"date_detection": true,
"numeric_detection": true,
"properties": {
"age": {
"type": "long"
},
"create_date": {
"type": "date",
"format": "MM/dd/yyyy"
}
}
}
}
}
}
3. Dynamic Template 动态模板
Dynamic Template 允许根据 ES 自动识别的数据类型、字段名来动态设定字段名。比如:
- 可以将字符串类型都设置为 keyword, 不分词,节省空间
- 将以 message 开头的字段设置为 text 类型
简单来说,就是自定义 ES 的自动类型转换, 将指定的字段设置为指定的数据类型。
示例如下:
# 将字符串类型都设置为 keyword
PUT test_index
{
"mappings": {
"doc": {
"dynamic_templates":[ # 数组,可以设定多个配置
{
"message_as_text": {
"match_mapping_type": "string",
# match、unmatch 可以针对字段名进行匹配
# 这里表示匹配 message 开头的字段,设置为 text
"match": "message*"
"mapping":{
"type": "text"
}
}
},
{
"strings_as_keyword": { # template 的自定义名称
# 指定匹配规则。这里表示匹配所有字符串类型的字段
"match_mapping_type": "string",
# 设定 mapping 信息,这里表示将其类型设置为 keyword
"mapping":{
"type":"keyword"
}
}
},
{
"age": {
"match_mapping_type": "long",
"mapping":{
"type": "integer"
}
}
}
]
}
}
}
在匹配的时候是由上到下进行匹配,一般来说会将匹配范围较小的放在前面,比如上面的设置中,我们将匹配 string 类型 message 开头的字段放在前面,将只匹配 string 类型的设置放在后面, 灵活设置的话可以减少很多 mapping 设置.
四. 索引模板,Index Template
索引模板其实就是预先定义好的索引设置,在索引模板中设置副本与分片、mapping 等,然后可以在新建索引时自动的应用。
# 创建一个 test_template
PUT _template/test_template
{
# 匹配 te 开头和 bar 开头的索引
"index_patterns":["te*", "bar*"],
# 执行顺序,order 大的设置覆盖掉 order 小的
"order":0,
# 设置分片副本
"settings": {
"number_of_shards": 5
},
# 设置 mapping
"mappings": {
"doc": {
"_source": {
"enabled": false
},
"properties": {
"name":{
"type": "keyword"
}
}
}
}
}
# 直接创建索引,就会应用到已经定义的索引模板。根据索引模板创建 mapping
PUT bar_index
使用索引模板需要注意几点:
- 可以通过 Index Template 预先定义,节省后续的工作
- 要注意已有的 Index Template 对新建索引的影响
在实际中索引模板有很多应用,比如 MetricBeat 就是预先在 ES 中创建索引模板,然后在收集系统等监控数据时会根据索引模板来创建索引、导入数据,然后供 Kibana 进行可视化分析,如果没有创建索引模板的话会导致创建的索引有出入令 kibana 可视化分析出现问题。
五. Reindex 索引重建
当旧的 Mapping 不合适或者需要迁移数据的时候,ES 提供了两种 API 来让我们完成任务。
- _update_by_query 在现有索引重建
POST blog_index/_update_by_query?conflicts=proceed& wait_for_completion=false
# 部分重建
POST blog_index/_update_by_query
{
"script": {
"source": "ctx._source.likes++",
"lang": "painless"
},
"query": {
"term": {
"author": "alfred"
}
}
}
- _reindex 在其他索引上重建
POST _reindex
{
"conflicts": "proceed",
"source": {
# 源索引
"index": "blog_index",
# 可以根据查询条件重建部分数据
"query": {
"term": {
"user": {
"value": "tom"
}
}
}
},
"dest": {
# 目的索引
"index": "blog_new_index"
}
}
将原目标上的 source 中的数据重建到目的索引中。
如果重建索引涉及到的数据量较大,可以通过设置参数 wait_for_completion 为 false 来异步执行。
六. 自定义 mapping 的建议
1. 数据建模步骤清单
- 哪些字段
- 何种类型
- 是否需要检索
- 是否需要排序、聚合分析
- 是否需要另行存储
- 确认合适的分片与副本
2. 建模建议
- 对 Mapping 进行单独存储,进行版本维护,做好注释。可以对每次更新加一个版本号字段,可以很清晰的看到哪些文档使用的是哪个版本的 Mapping,但这种做法会占用一些空间
- 防止字段过多,使用 index.mapping.total_fields.limit 限定索引最大字段数,默认 1000
- 一种推荐的做法是先预先导入一条数据自动创建 Mapping,然后基于自动创建的 Mapping 进行定制改进,然后重新创建索引