什么是Mapping
Mapping类似数据库中的schema的定义,作用如下
- 定义索引中的字段的名称
- 定义字段的数据类型,例如字符串、数字、布尔......
- 字段,倒排索引的相关配置,(Analyzed or Not Analyzed,Analyzer)
Mapping会把JSON文档映射成Lucene所需要的扁平格式
一个Mapping属于一个索引的Type
- 每个文档都属于一个Type
- 一个Type有一个Mapping定义
- 7.0开始,不需要再Mapping定义中指定type信息
字段的数据类型
简单类型
- Text / Keyword
- Date
- Interger / Floating
- Boolean
- IPv4 & IPv6
复杂类型 - 对象和嵌套对象
特殊类型
- geo_point & geo_shape / percolator
什么是Dynamic Mapping
- 在写入文档的时候,如果索引不存在,会自动创建索引
- Dynamic Mapping的机制,使得我们无需手动定义Mappings。ElasticSearch会自动根据文档信息,推算出字段的类型
- 当时有时候会推算的不对,例如地理位置信息
- 当类型如果设置的不对时,会导致一些功能无法正常使用,例如Range查询
类型推断错误:
#插入数据
PUT b_test/_doc/1
{
"startLocation": {
"lat": 32.004287,
"lon": 118.779369
}
}
#查看mapping
GET b_test/_mapping
{
"b_test" : {
"mappings" : {
"properties" : {
"startLocation" : {
"properties" : {
"lat" : {
"type" : "float"
},
"lon" : {
"type" : "float"
}
}
}
}
}
}
}
正确做法
#先创建索引
PUT b_test
{
"mappings": {
"properties": {
"location": {
"type": "geo_point"
}
}
}
}
#再插入数据
PUT b_test/_doc/1
{
"location": {
"lat": 32.004287,
"lon": 118.779369
}
}
类型的自动识别
JSON类型 | ElasticSearch 类型 |
---|
JSON类型 | ElasticSearch 类型 |
---|
字符串 |
- 匹配日期格式
- 设置数字设置为float 或者long ,该选项默认关闭
- 设置为Text,并且增加 keyword 子字段
| 布尔值 | boolean | 浮点数 | float | 整数 | long | 对象 | Object | 数组 | 由第一个非空数值的类型所决定 | 空值 | 忽略 |
添加一个文档
// 写入文档,查看Mapping
PUT mapping_test/_doc/1
{
"firstName": "Chan",
"lastName": "Jackie",
"loginDate": "2018-07-24T10:29:48.103Z"
}// 查看Mapping 文件
GET mapping_test/_mapping
{
"mapping_test" : {
"mappings" : {
"properties" : {
"firstName" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"lastName" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"loginDate" : {
"type" : "date"
}
}
}
}
}
** 我们看到这里。es 将 loginDate 字符串自动处理成了 date 类型的**
#删除index
DELETE mapping_test
# dynamic mapping 推断字段的类型
PUT mapping_test/_doc/1
{
"uid":"123",
"isVip": false,
"isAdmin":"true",
"age":19,
"heigh":180
}
// 查看Mapping 文件
GET mapping_test/_mapping
{
"mapping_test" : {
"mappings" : {
"properties" : {
"age" : {
"type" : "long"
},
"heigh" : {
"type" : "long"
},
"isAdmin" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"isVip" : {
"type" : "boolean"
},
"uid" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
能否更改Mapping的字段类型
两种情况:
1、新增加字段
- Dynamic 设置为true 时, 一旦有新增字段的文档写入, Mapping 也同时被更新
- Dynamic 设置为false 时,Mapping 不会被更新,新增字段的数据无法被索引,但是信息会出现在_source 中
- Dynamic 设置成Strict, 文档写入失败
2、对已有字段,一旦已经有数据写入,就不再支持修改字段定义
- Lucene 实现的倒排索引,一旦生成后,就不允许修改
- 如果希望改变字段类型,必须Reindex API, 重建索引
原因:
1、因为如果修改了字段的数据类型,会导致已被索引的属性无法被搜索,
2、但是如果是增加新的字段,就不会有这样的影响。
控制Dynamic Mappings
| true | false | strict |
---|
文档可索引 | √ | √ | x | 字段可索引 | √ | x | x | Mapping被更新 | √ | x | x |
- 当 dynamic 被设置为 false 的时候,存在新增字段的数据写入,该数据可以被索引。但是新增字段被丢弃
- 当设置为 Strict 模式的时候,数据写入直接报错
#写入的文档加入新的字段,默认Mapping支持dynamic
PUT mapping_test/_doc/1
{
"dynamicTest":"test"
}
#查询
GET /mapping_test/_search
{
"version": true,
"query": {
"match": {
"dynamicTest": "test"
}
}
}
#修改为dynamic false
PUT mapping_test/_mapping
{
"dynamic": false
}
#新增anotherField
PUT mapping_test/_doc/10
{
"anotherField":"test"
}
#查看数据
GET b_test/_doc/10
#该字段无法被索引,应为dynamic为false
GET /mapping_test/_search
{
"version": true,
"query": {
"match": {
"anotherField": "test"
}
}
}
#修改为dynamic strict
PUT mapping_test/_mapping
{
"dynamic": "strict"
}
#写入数据出错, HTTP CODE 400
PUT mapping_test/_doc/10
{
"lastField":"test"
}
如何显示定义一个Mapping
PUT /your_index
{
"mappings": {
"properties": {
// 字段
}
}
}
#e.g.
PUT /scheduler_driver_intention_info
{
"mappings": {
"properties": {
"intentionId": {
"type": "long"
},
"productId": {
"type": "long"
},
"driverId": {
"type": "long"
},
"cooperationType": {
"type": "integer"
},
"startLocation": {
"type": "geo_point"
},
"truckType": {
"type": "long"
},
"truckLength": {
"type": "double"
},
"startCityList": {
"type": "nested",
"properties": {
"districtId": {
"type": "integer"
},
"districtName": {
"type": "keyword"
},
"cityId": {
"type": "integer"
},
"cityName": {
"type": "keyword"
},
"generalizationFlag": {
"type": "boolean"
}
}
},
"endCityList": {
"type": "nested",
"properties": {
"districtId": {
"type": "integer"
},
"districtName": {
"type": "keyword"
},
"cityId": {
"type": "integer"
},
"cityName": {
"type": "keyword"
},
"generalizationFlag": {
"type": "boolean"
}
}
},
"startExcludeCityList": {
"type": "nested",
"properties": {
"districtId": {
"type": "integer"
},
"districtName": {
"type": "keyword"
}
}
},
"endExcludeCityList": {
"type": "nested",
"properties": {
"districtId": {
"type": "integer"
},
"districtName": {
"type": "keyword"
}
}
},
"createTime": {
"type": "long"
},
"createTimeStr": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"updateTime": {
"type": "long"
},
"updateTimeStr": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"valid": {
"type": "boolean"
}
}
}
}
#索引新增字段
PUT your_index/_mapping
{
"properties": {
"aaa": {
"type": "text"
}
}
}
为了减少输入工作量,减少出错概率,可以依照以下步骤
- 创建一个临时的index,写入一些样本数据
- 通过访问Mapping API获取该临时文件的动态Mapping定义
- 修改后,使用该配置创建你的索引
- 删除临时索引
为了减少输入工作量,减少出错概率,可以依照以下步骤
- 创建一个临时的index,写入一些样本数据
- 通过访问Mapping API获取该临时文件的动态Mapping定义
- 修改后,使用该配置创建你的索引
- 删除临时索引
控制当前字段是否被索引
Index - 控制当前字段是否被索引,默认为true,如果设置成false,该字段不可被搜索
# 设置index为false
PUT b_test
{
"mappings": {
"properties": {
"firstName":{
"type": "text"
},
"lastName":{
"type": "text"
},
"mobile":{
"type": "text",
"index": false
}
}
}
}
PUT /b_test/_doc/1
{
"firstName": "zhang",
"lastName": "san",
"mobile": "123"
}
#报错
GET /b_test/_search
{
"query": {
"match": {
"mobile": "123"
}
}
}
Index Options
1、四种不同的级别的Index Options 设置,可以控制倒排索引记录的内容
- docs - 记录 doc id
- freqs - 记录 doc id 和 term frequencies
- positions - 记录 doc id 和 term frequencies / term position
- offsets - doc id / term frequencies / term position /character offects
2、Text 类型默认记录 positions,其他默认为 docs 3、记录内容越多,占用存储空间越大
# 显示的创建Mapping
PUT b_test
{
"mappings": {
"properties": {
"name":{
"type": "text"
},
"address":{
"type": "text"
},
"phone_num":{
"type": "text",
"index": false
},
"bio":{
"type": "text",
"index_options": "offsets"
}
}
}
}
null_value
- 需要对null 值进行搜索
- 只有 KeyWord类型支持设定 null_Value
# 显示的创建Mapping
PUT b_test
{
"mappings": {
"properties": {
"firstName":{
"type": "text"
},
"lastName":{
"type": "text"
},
"mobile":{
"type": "keyword",
"null_value": "NULL"
}
}
}
}
PUT /b_test/_doc/1
{
"firstName": "zhang",
"lastName": "san",
"mobile": null
}
# 查询 注意NULL为大写
GET b_test/_search?q=mobile:NULL
GET b_test/_search
{
"query": {
"match": {
"mobile": "NULL"
}
}
}
copy_to设置
- _all 在 7 中被 copy_to 所替代
- 满意一些特定的搜索要求
- copy_to 将字段的数值拷贝到 目标字段, 实现类似 _all 的作用
- copy_to 的目标字段不出现在 _source 中
# 显示的创建Mapping
PUT b_test
{
"mappings": {
"properties": {
"firstName":{
"type": "text",
"copy_to": "fullName"
},
"lastName":{
"type": "text",
"copy_to": "fullName"
}
}
}
}
PUT /b_test/_doc/1
{
"firstName": "zhang",
"lastName": "san"
}
# 查询
GET b_test/_search?q=fullName:(zhang san)
数组类型
ES中不提供专门的数组类型,但是任何字段,都可以包含多个相同类型的数值
PUT b_test/_doc/1
{
"firstName": "Chan",
"lastName": "zhangsan"
}
PUT b_test/_doc/2
{
"firstName": "Chan",
"lastName": ["zhangsan","lisi"]
}
# 查询
GET /b_test/_search
{
"query": {
"term": {
"lastName.keyword": "zhangsan"
}
}
}
对象类型和嵌套对象(Nested Object)
关系数据库的范式设计
1NF - 消除非主属性对键的部分函数依赖
2NF - 消除非主属性对键的传递函数依赖
3NF - 消除主属性对键的传递函数依赖
BCNF - 主属性不依赖于主属性
- 范式化设计(Normalization)的主要目标时”减少不必要的更新“
- 副作用:一个完全范式化设计的数据库会经常面临”查询缓慢“的问题(数据库越范式化,就需要Jion越多的表)
- 范式化节省了存储空间,但是存储空间却越来越便宜
- 范式化简化了更新,但是数据”读“取操作可能更多
Denormalization
- 反范式化设计
- 数据“Flattening”,不使用关联关系,而是在文档中保存冗余的数据拷贝
- 优点:无需处理Joins操作,数据读取性能好
- Elasticsearch 通过压缩_source字段,减少磁盘空间的开销
- 缺点:不适合在数据频繁修改的场景
- 一条数据(用户名)的改动,可能会引起很多数据的更新
ES中处理关联关系
- 关系型数据库,一般会考虑 Normalize 数据;在Elasticsearch,往往考虑 Denormalize 数据
- Denormalize 的好处:读的速度变快、无需表连接、无需行锁
- Elasticsearch 并不擅长处理关联关系,我们一般采用以下四种方法处理关联
- 对象类型
- 嵌套对象(Nested Object)
- 父子关联关系(Parent 、Child)
- 应用端关联
对象类型:
PUT /blog
{
"mappings": {
"properties": {
"content": {
"type": "text"
},
"time": {
"type": "date"
},
"user": {
"properties": {
"city": {
"type": "text"
},
"userid": {
"type": "long"
},
"username": {
"type": "keyword"
}
}
}
}
}
}
# 插入一条 Blog 信息
PUT blog/_doc/1
{
"content":"I like Elasticsearch",
"time":"2021-10-19T00:00:00",
"user":{
"userid":1,
"username":"Jack",
"city":"Shanghai"
}
}
# 查询 Blog 信息
POST blog/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "Elasticsearch"
}
},
{
"match": {
"user.username": "Jack"
}
}
]
}
}
}
对象数组类型:
# 电影的Mapping信息
PUT my_movies
{
"mappings" : {
"properties" : {
"actors" : {
"properties" : {
"first_name" : {
"type" : "keyword"
},
"last_name" : {
"type" : "keyword"
}
}
},
"title" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
# 写入一条电影信息
POST my_movies/_doc/1
{
"title":"Speed",
"actors":[
{
"first_name":"Keanu",
"last_name":"Reeves"
},
{
"first_name":"Dennis",
"last_name":"Hopper"
}
]
}
# 查询电影信息
POST my_movies/_search
{
"query": {
"bool": {
"must": [
{"match": {"actors.first_name": "Keanu"}},
{"match": {"actors.last_name": "Hopper"}}
]
}
}
}
问题:为什么会搜索到不需要的结果?
- 储存时,内部对象的边界没有在考虑在内,JSON格式被处理成扁平键值对的结构
- 当对多个字段进行查询时,导致了意外的搜索结果
- 可以用 Nested Data Type 解决这个问题
Nested Data Type
- Nested 数据类型:允许对象数组中的对象呗独立索引
- 使用 Nested 和 Properties 关键词,将所有 actors 索引到对个分隔的文档
- 在内部,Nested 文档会被保存在两个 Lucene 文档中,查询时做join处理
PUT my_movies
{
"mappings" : {
"properties" : {
"actors" : {
"type": "nested",
"properties" : {
"first_name" : {"type" : "keyword"},
"last_name" : {"type" : "keyword"}
}},
"title" : {
"type" : "text",
"fields" : {"keyword":{"type":"keyword","ignore_above":256}}
}
}
}
}
# 写入一条电影信息
POST my_movies/_doc/1
{
"title":"Speed",
"actors":[
{
"first_name":"Keanu",
"last_name":"Reeves"
},
{
"first_name":"Dennis",
"last_name":"Hopper"
}
]
}
# Nested 查询
POST my_movies/_search
{
"query": {
"bool": {
"must": [
{"match": {"title": "Speed"}},
{
"nested": {
"path": "actors",
"query": {
"bool": {
"must": [
{"match": {
"actors.first_name": "Keanu"
}},
{"match": {
"actors.last_name": "Hopper"
}}
]
}
}
}
}
]
}
}
}
倒排索引
倒排索引,是适合用于进行搜索的
倒排索引的结构
(1)包含这个关键词的document list (2)包含这个关键词的所有document的数量:IDF(inverse document frequency) (3)这个关键词在每个document中出现的次数:TF(term frequency) (4)这个关键词在这个document中的次序 (5)每个document的长度:length norm (6)包含这个关键词的所有document的平均长度
word doc1 doc2
dog? ? ?*? ? ? ?* hello? ? * you? ? ? *
倒排索引不可变的好处
(1)不需要锁,提升并发能力,避免锁的问题 (2)数据不变,一直保存在os cache中,只要cache内存足够 (3)filter cache一直驻留在内存,因为数据不变 (4)可以压缩,节省cpu和io开销
倒排索引不可变的坏处:每次都要重新构建整个索引
注意事项
1、在ES5.x里,一定要注意数值类型是否需要做范围查询,看似数值,但其实只用于Term或者Terms这类精确匹配的,应该定义为keyword类型。典型的例子就是索引web日志时常见的HTTP Status code。
2、如果RangeQuery的结果集很大,并且还需要和其他结果集更小的查询条件做AND的,应该升级到ES5.4+,该版本在底层引入的indexOrDocValuesQuery ,可以极大提升该场景下RangeQuery的查询速度。
3、reindex重建索引
一个field的设置是不能被修改的,如果要修改一个Field或者primary shard那么应该重新按照新的mapping,建立一个index,然后将数据批量查询出来,重新用bulk api写入index中批量查询的时候,建议采用scroll api,并且采用多线程并发的方式来reindex数据,每次scoll就查询指定日期的一段数据,交给一个线程即可。
如果说旧索引的名字,是old_index,新索引的名字是new_index,终端java应用,已经在使用old_index在操作了,难道还要去停止java应用,修改使用的index为new_index,才重新启动java应用吗?这个过程中,就会导致java应用停机,可用性降低,所以给java应用一个别名,这个别名是指向旧索引的,java应用先用index alias来操作,此时实际指向的是旧的my_index
①PUT /{my_index}/_alias/{alias_index}
②新建一个index,调整其mapping
③使用scroll api将数据批量查询出来
GET /my_index/_search?scroll=1m { "query": { "match_all": {} }, "sort": ["_doc"], "size": 1 }
④采用bulk api将scoll查出来的一批数据,批量写入新索引
POST /_bulk { "index": { "_index": "my_index_new", "_type": "my_type", "_id": "2" }} { "content": "xxx" }
⑤反复循环,查询一批又一批的数据出来,采取bulk api将每一批数据批量写入新索引
⑥将alias_index切换到my_index_new上去,java应用会直接通过index别名使用新的索引中的数据,java应用程序不需要停机,零提交,高可用
POST /_aliases { "actions": [ { "remove": { "index": "my_index", "alias": "alias_index" }}, { "add": { "index": "my_index_new", "alias": "alias_index" }} ] }
相关文档
官网文档 Mapping | Elasticsearch Guide [7.5] | Elastic
一文搞懂 Elasticsearch 之 Mapping 一文搞懂 Elasticsearch 之 Mapping - 云+社区 - 腾讯云
理解 Percolator 数据类型及 Percolate 查询 Elasticsearch:理解 Percolator 数据类型及 Percolate 查询-阿里云开发者社区
使用 ignore_above 限制字符串长度 Elasticsearch 7 : 使用 ignore_above 限制字符串长度 - 乐天笔记
|