Elasticsearch实战(三)---复杂数据结构及映射 Mapping操作

367 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

Elasticsearch实战-复杂数据结构及映射 Mapping操作

1.ElasticSearch 映射操作

基于文章 Elasticsearch实战(一)---安装及基本语法使用 前面的文章,我们已经基本使用了ES,但是我们对于底层知识还是不了解的,现在我们讲一下 底层结构

1.1 结构
  • 1.索引
  • 2.类型
  • 3.文档
MYSQLElasticsearch
数据库DB索引 Index
表table类型type _doc
行 记录 record文档 Documents
列 字段column字段 Fields

逻辑设计:索引一篇文档时,通过顺序找到它:索引 -> 类型 -> 文档ID

1.2 映射

映射可以分为动态映射和显式映射

  • 动态映射 (dynamic mapping) 在关系数据库中,需要事先创建数据库,然后在该数据库实例下创建数据表,然后才能在该数据表中插入数据。而 ElasticSearch中 不需要事先定义映射(Mapping),文档写入 ElasticSearch 时,会根据文档字段自动识别类型,这种机制称之为动态映射, 像我们前几篇文章中 直接操作 put /test/-doc/1 插入数据就是 动态映射,并没有提前建立映射,根据自动插入的内容去映射信息
#  插入数据
PUT /jzj/_doc/1
{
  "name":"jzj",
  "age":10,
  "address":"湖北省武汉市"
}

查看映射

GET /jzj/_mapping

可以看到 address:type:text 类型文本, age:type:long 整数, name:type:text 文本 除 text 外其他类型不会进行分词 , 所以其他类型必须要进行精确查询 在这里插入图片描述

1.3 映射-显式映射

映射可以分为动态映射和显式映射

  • 显式映射(explicit mappings) 在 ElasticSearch 中也可以事先定义好映射,包含文档的各个字段及其类型等,这种方式称之为显式映射。显示映射适用于事先直到结构,然后部分字段要定义使用其它分词器、是否分词、是否存储等
# 新建索引
PUT /jzjshow
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
    "mappings" : {
      "properties" : {
        "address" : {
          "type" : "text",
          "analyzer": "standard",
          "search_analyzer": "standard"
        },
        "addtime" : {
          "type" : "date"
        },
        "age" : {
          "type" : "long"
        },
        "name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
        }
      }
    }

新建显式索引 在这里插入图片描述 然后 插入数据

PUT /jzjshow/_doc/1
{
  "address":"湖北省武汉市",
  "addtime": "2022-05-01",
  "age":20,
  "name":"测试人员"
}

查询结果 在这里插入图片描述

2.ElasticSearch 数据类型

2.1 基本数据类型
  • 字符串:text 。 text:当一个字段是要被全文搜索的,比如 Email 内容、产品描述,应该使用 text 类型。设置 text 类型以后,字段内容会被分析,在生成倒排索引以前,字符串会被分析器分成一个一个词项。text 类型的字段不能排序,很少用于聚合

  • 字符串:keyword keyword:该类型不需要进行分词,可以被用来检索过滤、排序和聚合,keyword 类型的字段只能通过精确匹配

  • 整数:byte, short, integer, long

  • 浮点数:float, double

  • 布尔:boolean

  • 日期:date,JSON 本身并没有日期数据类型 ES 中的日期类型可以是: 2015-01-01 or 2015/01/01 12:10:30 的字符串 long 类型的毫秒级别的时间戳 int 类型的秒级别的时间戳

  • 二进制型:`binary``,二进制类型以 Base64 编码方式接收一个二进制值,二进制类型字段默认不存储,也不可搜索。

简单操作 插入 日期,boolean,整数 类型的字段, 依次插入四条数据

PUT /jzjshow/_doc/1
{
  "address":"湖北省武汉市",
  "addtime": "2022-05-01",
  "age":20,
  "name":"测试人员"
}

PUT /jzjshow/_doc/2
{
  "address":"湖北省武汉市",
  "addtime": "2021-04-14T21:16:42.000Z",
  "age":20,
  "name":"测试人员"
}
PUT /jzjshow/_doc/3
{
  "address":"湖北省武汉市",
  "addtime": "2021-04-14",
  "age":20,
  "name":"测试人员"
}


PUT /jzjshow/_doc/4
{
  "address":"湖北省武汉市",
  "addtime": "1654413037",
  "age":20,
  "name":"测试人员"
}

PUT /jzjshow/_doc/4
{
  "address":"湖北省武汉市",
  "addtime": "1654413037000",
  "age":20,
  "name":"测试人员"
}

可以看到查询结果, 数据成功插入, 查询结果如下 第一条 yyyy-mm-dd, 数据存储也是 yyyy-mm-dd 第二条 UTC时间 , 数据存储也是 UTC时间 第三条 是Integer 时间戳 秒, 数据存储转换成了 yyyy-mm-dd 第四条是 Long 毫秒, 数据存储依旧是 Long毫秒

在这里插入图片描述

2.2 复杂数据类型 - 数组

在 ElasticSearch 中,没有专门的数组(Array)数据类型, 不需要进行任何配置,就可以直接使用。

在默认情况下,任意一个字段都可以包含 0 或多个值,这意味着每个字段默认都是数组类型,只不过,数组类型的各个元素值的数据类型必须相同:

  • 字符串数组: [ "one", "two" ]
  • 整型数组: [ 1, 2 ]
  • 数组数组: [ 1, [ 2, 3 ]] which is the equivalent of [ 1, 2, 3 ]
  • 对象数组: [ { "name": "Mary", "age": 12 }, { "name": "John", "age": 10 }]

注意: 动态添加数据时,数组的第一个值的类型决定整个数组的类型 混合数组类型是不支持的,比如:[1,”abc”] 数组可以包含 null 值,空数组 [ ] 会被当做 missing field 对待

数组存储结构会被扁平化处理

put /jzjarr/_doc/1
{
    "title":"人员列表"
  ,
  "person":[
    {
    "name":"joy",
    "age":20,
    "address":"湖北省"
    },
    {
    "name":"zhangsan",
    "age":40,
    "address":"河北省"
    },
    {
    "name":"lisi",
    "age":30,
    "address":"河南省"
    }
    ]
}

对于ES来说 会把数组扁平化处理成 , 查询的时候 对于array数组内部的元素就要用 person.name来处理

title:[“人”,“员”,“列“,”表”]
person.name:["joy", "zhangsan", "lisi"]
peson.age:[20,40,30]
person.address:[“湖北省”,“河北省”,“河南省”]

这样的数据, 就会导致 {"name":"joy", "age":20, "address":"湖北省" } 没了关联性 比如要查询 age=20 且 address=“河南省” 的信息 , 期望是查不到的,因为 person中不包含这样的信息

get /jzjarr/_search
{
  "query":{
    "bool": {
      "must": [
        {
          "match": {
            "person.age": "20"
          }
        },
         {
          "match": {
            "person.address": "河南省"
          }
        }
      ]
    }
  }
}

查询结果, 出现了不匹配的情况, 明明是查询 age=20 且 address=河南省, 没有这种记录,但是搜到了其他的记录 在这里插入图片描述 这种问题 后面用 复杂结构Nested来处理 即可

2.3 复杂数据类型 -对象Object

JSON 文档是有层次结构的,一个文档可能包含其他文档,如果一个文档包含其他文档,那么该文档值是对象类型,其数据类型是对象,ES 默认把文档的属性 type设置为 object,即 "type":"object"。

比如我现在有个嵌套文档 say 下面有三种语言, 可以看到 name 及 say 字段都是嵌套的 Json对象,say下面还有子对象 {chinese:你好 }, 如果没有明确给出 type类型,默认类型就是 type:Object对象类型

# 插入数据
put /jzjsay/_doc/1
{
  "name":{
    "first":"joy",
    "last":"boy"
  },
  "age":20,
  "say":{
    "chinese":"你好",
    "english":"hello",
    "japan":"hai"
  }
}

对于Object对象 我们要如何进行查询? 通过对象类型Object来存储二级文档,因为 Lucene 并没有内部对象的概念,ES 会将原 JSON 文档扁平化 上面的对象类型,ES在索引时将其转换为"name.first", "say.chinese" 这样扁平的key,用这样的扁平key作为域来进行查询

如果采用 name 或者 first 单独查询是无法查询的 在这里插入图片描述 必须使用 扁平化索引 eg name.first 或者 say.chinese 来实现

#扁平化索引 name.first
GET /jzjsay/_search
{
  "query":{
    "match": {
      "name.first": "joy"
    }
  }
}


#扁平化索引 say.chinese
get /jzjsay/_search
{
  "query":{
    "match": {
      "say.chinese": "你"
    }
  }
}

在这里插入图片描述

2.4 Object 对象类型 存在的问题

我们上面看了Object 存储这样的结构,看似没问题,可以通过索引去查询出来想要的结构 因为 底层是扁平化存储的数据

  • name.first, name.last
  • say.chinese, say.english, say.japan

但是我们看 下下面的场景, 先插入1 条比较复杂的数据 有一个 张三的同学, 三个人给了他评论 评论人 one, age=20 ,评价 good job 评论人 two, age=30, 评价 bad job 评论人 three,age=40, 评价 normal job

put /jzjcomment/_doc/1
{
 "name":"张三",
 "tags":["优秀", "好"],
 "comments":[
   {
     "name":"one",
     "age":20,
     "say":"good job"
   },
   {
     "name":"two",
     "age":30,
     "say":"bad job"
   }
   ,
   {
     "name":"three",
     "age":40,
     "say":"normal job"
   }
   ]
}

我现在想 查询 评价人是 one 的, 且 One的 年龄是30岁的 记录信息 按道理说是 找不到的, 因为 第一个评价人 One的年龄是 20, 查询语句应该查不到,我们试一下

# 查询 评价人年龄是30岁  且 评价人名字是 one的 那条记录
# bool 过滤, must And操作   "comments.age": 30 且  "comments.name": "one" 
GET /jzjcomment/_search
{
 "query": {
   "bool": {
     "must": [
       {
         "match": {
           "comments.age": 30
         }
       },
       {
         "match": {
           "comments.name": "one"
         }
       }
     ]
   }
 }
 
}

查询结果, 出现一条,明明 评价人 One的年龄是20 岁, 我的 must And操作是 姓名=one 且 年龄是30岁, 为什么能把这条记录查出来? 在这里插入图片描述

因为Object 存储结构的问题
Object结构 的 JSON 文档在 Lucene 底层会被打平成一个简单的键值格式,就像这样: { "name": ["张", "三"], "tags": ["优秀", "好"], "comments.name": ["one","two","three"], "comments.age": [20, 30,40], "comments.say": ["good job", "bad job", "normal job"] } 这样 底层查询就是 one in comments.name , age=30 in commemts.age 已经没有顺序性了,没有了 one 打了 20分, 评语是 goodjob 这一条记录的关联性丢失了 所以 Object结构是有缺陷的,所以 才有了Nested结构

2.5 复杂结构类型 -Nested

针对上面的 记录 Nested 是如何存储的?每一个 comments 都是一个单独的Document来处理, 如下 { "name": ["张", "三"], "tags": ["优秀", "好"], { "comments.name": ["one"], "comments.age": [20], "comments.say": ["good" ,"job"] }, { "comments.name": ["two"], "comments.age": [30], "comments.say": ["bad" ,"job"] }, { "comments.name": ["three"], "comments.age": [40], "comments.say": ["normal" ,"job"] } }

在查询的时候将根文档 和 nested 文档拼接是很快的。 这些额外的 nested objects 文档是隐藏的,为了更新、增加或者移除一个 nested 对象,必须重新插入整个文档。 查询请求返回的结果不仅仅包括 nested 对象,而是整个文档

重新插入 Nested结构的文档

# 插入 Nested结构的文档 comments type:nested
put /jzjcommentnested
{
      "mappings" : {
      "properties" : {
        "comments" : {
          "type":"nested",
          "properties" : {
            "age" : {
              "type" : "long"
            },
            "name" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword",
                  "ignore_above" : 256
                }
              }
            },
            "say" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword",
                  "ignore_above" : 256
                }
              }
            }
          }
        },
        "name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "tags" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}

查看一下 mapping 映射结构

get /jzjcommentnested/_mapping

在这里插入图片描述

再用刚才的 查询语句 查一下, 查询 评论人 one 且 年龄是30 岁 的 记录

 # 查询 评价人年龄是30岁  且 评价人名字是 one的 那条记录
 # bool 过滤, must And操作   "comments.age": 30 且  "comments.name": "one" 
GET /jzjcommentnested/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "comments.age": 30
          }
        },
        {
          "match": {
            "comments.name": "one"
          }
        }
      ]
    }
  }
}

同样的语句, 不一样的结果, 查不到 评论人 one 且年龄是30岁 的记录, 这才是我们预期结果 在这里插入图片描述

2.6 Nested 类型搜索 nested:{path:""}

nested 作为一个独立隐藏文档单独建索引,因此,我们不能直接查询它们。所以 我们必须使用 nested 查询或者 nested filter 来获取 整个文档信息

对于刚才结构 jzjcommentnested, 再插入3条数据

put /jzjcommentnested/_doc/1
{
  "name":"张三",
  "tags":["优秀", "好"],
  "comments":[
    {
      "name":"one",
      "age":20,
      "say":"good job"
    },
    {
      "name":"two",
      "age":30,
      "say":"bad job"
    }
    ,
    {
      "name":"three",
      "age":40,
      "say":"normal job"
    }
    ]
}

put /jzjcommentnested/_doc/2
{
  "name":"李四",
  "tags":["完美", "好"],
  "comments":[
    {
      "name":"one",
      "age":20,
      "say":"bad job"
    },
    {
      "name":"two",
      "age":30,
      "say":"bad job"
    }
    ,
    {
      "name":"three",
      "age":40,
      "say":"good job"
    }
    ]
}

put /jzjcommentnested/_doc/3
{
  "name":"王五",
  "tags":["一般", "良好"],
  "comments":[
    {
      "name":"one",
      "age":60,
      "say":"normal job"
    },
    {
      "name":"two",
      "age":30,
      "say":"bad job"
    }
    ,
    {
      "name":"three",
      "age":40,
      "say":"good job"
    }
    ]
}

查询结果 3条

get /jzjcommentnested/_search

在这里插入图片描述

如何用Nested 来进行查询? 需要指定 path

# 原来的方式 是查不到 隐藏文档的  错误的查询方式
get /jzjcommentnested/_search
{
  "query":{
    "bool": {
      "must": [
        {
          "match": {
            "comments.age": 60
          }
        },
                {
          "match": {
            "comments.name": "one"
          }
        }
      ]
    }
  }
}

必须使用 嵌套 Nested path 路径

# 查询 nested 嵌套 文档 comments 下面的 age 及 name信息
get /jzjcommentnested/_search
{
  "query":{
    "bool": {
      "must": [
        {
          "nested": {
            "path": "comments",
            "query": {
              "bool": {
                "must": [
                  {
                    "match": {
                      "comments.age": "20"
                    }
                  },
                  {
                    "match": {
                      "comments.name": "one"
                    }
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

查询结果 也是正确的, 把嵌套的nested文档 张三 和 李四 两条结果查询出来 在这里插入图片描述

如果 需要关联 父 层的 结构 该如何查询, 比如 我要查 name=张三的 且 comments 内层 age=20, name = one的 记录

# 关联 父结构的 字段 name=张三
get /jzjcommentnested/_search
{
  "query":{
    "bool": {
      "must": [
        {
          "match": {
            "name": "张三"
          }
        },
        //nested 文档查询方式
        {
          "nested": {
            "path": "comments",
            "query": {
              "bool": {
                "must": [
                  {
                    "match": {
                      "comments.age": "20"
                    }
                  },
                  {
                    "match": {
                      "comments.name": "one"
                    }
                  }
                ]
              }
            } 
           
          }
        }
      ]
    }
  }
}

查询结果 只有1条 张三这一条 且 匹配 嵌套 comments 内层 age=20, name = one的 记录 在这里插入图片描述

2.7 Nested 嵌套查询 sort排序

如果要基于Nested 中的文档排序,该如何实现? 我们看下官网的例子

先创建索引

    "mappings" : {
      "properties" : {
        "body" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "comments" : {
          "properties" : {
            "age" : {
              "type" : "long"
            },
            "comment" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword",
                  "ignore_above" : 256
                }
              }
            },
            "date" : {
              "type" : "date"
            },
            "name" : {
              "type" : "text",
              "fields" : {
                "keyword" : {
                  "type" : "keyword",
                  "ignore_above" : 256
                }
              }
            },
            "stars" : {
              "type" : "long"
            }
          }
        },
        "tags" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
        "title" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }

然后 插入 文档 1, 文档 2

PUT /blog/_doc/1
{
    "title":"Nest eggs",
    "body":"Making your money work...",
    "tags":[
        "cash",
        "shares"
    ],
    "comments":[
        {
            "name":"John Smith",
            "comment":"Great article",
            "age":28,
            "stars":4,
            "date":"2014-09-01"
        },
        {
            "name":"Alice White",
            "comment":"More like this please",
            "age":31,
            "stars":5,
            "date":"2014-10-22"
        }
    ]
}
PUT /blog/_doc/2
{
    "title":"Nest eggs2",
    "body":"2Making your money work...",
    "tags":[
        "cash2",
        "shares2"
    ],
    "comments":[
        {
            "name":"John Smith2",
            "comment":"Great article",
            "age":28,
            "stars":3,
            "date":"2014-09-01"
        },
        {
            "name":"Alice White2",
            "comment":"More like this please",
            "age":31,
            "stars":2,
            "date":"2014-10-22"
        }
    ]
}

然后 执行 sort语句

GET /blog/_search
{
  "query": {
    "nested": {
      "path": "comments",
      "query": {
        "range": {
          "comments.date": {
            "gte": "2014-10-01",
            "lt": "2014-11-01"
          }
        }
      }
    }
  },
  "sort": {
    "comments.stars": {
      "order": "asc",
      "mode": "min"
    }
  }
}

不管是 order asc 还是desc 都是这个顺序 这个让我疑问很大,不知道哪里出了问题


至此 我们复杂的结构已经介绍完毕, 并且基本的查询也已经介绍完毕, 下一篇 ,我们介绍下Nested聚合的使用场景