一文带你掌握Elasticsearch复合字段类型:Array、Flattened、Nested在业务系统中最佳实践

开发 前端
elasticsearch 是一个分布式、实时的搜索和分析引擎,通常用于处理和查询大规模的结构化与非结构化数据。它基于 Apache Lucene 构建,具有高效的全文搜索、数据存储、数据分析和数据可视化功能。elasticsearch 主要应用于日志分析、监控、数据检索、实时分析等场景。

1.概述

elasticsearch在当下互联网系统中使用非常广泛,是一个非常热门的组件框架。那elasticsearch到底是什么呢?

elasticsearch 是一个分布式、实时的搜索和分析引擎,通常用于处理和查询大规模的结构化与非结构化数据。它基于 Apache Lucene 构建,具有高效的全文搜索、数据存储、数据分析和数据可视化功能。elasticsearch 主要应用于日志分析、监控、数据检索、实时分析等场景。

官方文档地址:https://www.elastic.co/

2.映射mapping

在 elasticsearch 中,mapping(映射)是指为索引中的字段定义结构、类型和规则。类似于关系型数据库中的表结构,映射决定了 elasticsearch 如何存储和索引数据,其作用和关系型数据库定义表字段差不多:

定义字段类型:例如字符串、数字、日期等。

控制字段行为:决定字段是否进行分词、是否索引、如何排序等。

优化查询性能:正确的映射能提高查询效率,减少不必要的存储。

数据验证:映射可以确保写入的文档符合预期的数据结构和类型。

映射mapping分为:动态映射和静态映射

2.1 动态映射

顾名思义,就是自动创建出来的映射。es 根据存入的文档,自动分析出来文档中字段的类型以及存储方式,这种就是动态映射。示例如下所示:

首先,先定义一个索引,名为user_info

PUT user_info

一个简单的DSL语句就建立了一个索引,不太清楚的先跳转上面的链接入门学习下哈。

紧接着我们直接插入一条es文档数据

PUT user_info/_doc/1
{
  "id":1,
  "name":"张三",
  "amount":88.99
}

插入成功,再来看看索引user_info的mapping定义:

GET user_info/_mapping

查询结果如下:

{
  "user_info" : {
    "mappings" : {
      "properties" : {
        "amount" : {
          "type" : "float"
        },
        "id" : {
          "type" : "long"
        },
        "name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        }
      }
    }
  }
}

可以看到,es在我们插入文档数据时自动检测字段类型动态定义了mapping结构。

2.2 静态映射

静态映射就和关系型数据库表结构加字段一样,在插入es文档数据之前,先定义字段mapping再插入数据,比如我们往user_info添加一个身份证号idCard:

PUT user_info/_mapping
{
  "properties": {
     "idCard": {
      "type": "keyword"
    }
  }
}

keyword关键字类型代表不分词,elasticsearch会对text字段进行分词实现全文检索,但是身份证并不需要分词,它是一个整体属性字段,如果是通过动态映射生成mapping定义,那么会和上面的name字段一样:

"idCard" : {
      "type" : "text",
      "fields" : {
        "keyword" : {
          "type" : "keyword",
          "ignore_above" : 256
        }
      }
    }

这里我解释一下这个mapping的定义:其中既包含 text 类型字段用于全文搜索,也包含 keyword 字段用于精确匹配和聚合。这种设计方式可以在同一个字段上实现不同类型的查询

text 类型用于全文搜索(如 matchQuery),Elasticsearch 会对 text 类型字段进行分词(默认使用标准分词器)。

keyword是这是 idCard 字段的子字段类型,用于精确匹配和聚合。

keyword 类型的字段不会被分词,适合存储如ID、状态、标签等字符串数据。

**ignore_above: 256**:如果字段的长度超过 256 个字符,将会被忽略,不会被索引

这种组合让你可以在同一个字段上支持全文搜索和精确匹配,从而满足不同查询需求。但实际上name字段也是并不需要分词的,直接定义keyword更加合理,性能更好。分词是一个相对比较耗费性能的操作。

由此可见动态映射也并不是万能可行的,我个人建议是:

我们在创建真正索引之前,可以先随便创建一个测试索引,然后写入测试的es文档数据,然后查看测试索引生成的mapping,这样后面我们在创建真正索引设置mapping时,就可以在前面得到的mapping基础上修改,不需要从无到有一个一个字段地开始写mapping,极大提高效率。如果是往索引添加新字段时,我觉得还是严格按照和关系型数据库表结构加字段一样,采用静态映射插入数据前,先手动添加字段mapping定义,杜绝动态映射带来的类型不匹配、性能降低等问题。

3.使用数组Array实现一对多查询

在平时业务系统表单列表搜索中,一对多查询是非常常见的,比如说,一个用户有多个标签,有多个角色,这时候我们会搜索某些标签或者某些角色下有哪些用户,是不是很常见呀???话不多说开干,我这里以标签展示,首先我们按照建议先通过静态映射在索引user_info中添加一个新字段tag存储标签id:

PUT user_info/_mapping
{
  "properties": {
     "tag": {
      "type": "long"
    }
  }
}

接下来我们就可以插入数据了。这时候你可能会问,你这个定义tag字段是long类型,并没有定义array类型呀???这就可以插入数组多个数据了?是的,elasticsearch关于mapping字段介绍时,明确提到你无需专门声明某字段为数组类型,任何字段都可以隐式地存储为数组。但是要求数组中的所有值必须具有相同的数据类型。不支持混合数据类型的数组: [10,“ some string”],插入数据示例:

PUT user_info/_doc/10
{
  "id":10,
  "name":"张三2",
  "tag":[1,2,3] 
}

PUT user_info/_doc/11
{
  "id":11,
  "name":"张三3",
  "tag":[3,4] 
}
PUT user_info/_doc/12
{
  "id":12,
  "name":"张三4",
  "tag":[4,8,9] 
}

PUT user_info/_doc/13
{
  "id":13,
  "name":"张三5",
  "tag":[1,6] 
}

然后我们来搜索试试吧,搜索有标签id为3或者4的用户:

GET user_info/_search
{
  "query": {
    "terms": {
      "tag": [3,4]
    }
  }
}

查询结果id=10,11,12都命中了。

Java查询实现代码:

boolQueryBuilder.must(QueryBuilders.termsQuery("tag", tagIds));

关于Spring Boot整合restHighLevelClient实现es相关操作,请查看上面的入门篇

如果要查询同时包含标签id为3和4的员工:

GET user_info/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "tag": {
              "value": 3
            }
          }
        },
        {
          "term": {
            "tag": {
              "value": 4
            }
          }
        }
      ]
    }
  }
}

只命中id=11这一条数据

Java实现代码:

for(tagId : tagIds) {
  boolQueryBuilder.must(QueryBuilders.termQuery("tag", tagId));
}

数组Array的限制:无法建立数组元素之间的关系

PUT orders/_doc/1
{
  "products": ["laptop", "mouse"],
  "quantities": [1, 2]
}

假设你想查询购买了 "laptop" 且数量为 1 的订单。这在 Elasticsearch 的数组查询中无法实现,因为 Elasticsearch 无法判断 "laptop" 和 1 是相关联的(它们只被视为两个独立的数组字段)。

解决方案:如果你需要这种关联关系,应使用 nested 类型来代替数组字段。后面会讲。

总的来说,数组(Arrays)字段类型适用于简单的多值字段,如标签、兴趣、角色等场景。使用简单、查询高效、

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。

4.使用Flattened存储动态字段

Flattened 类型是 Elasticsearch 7.3 引入的一种特殊的字段类型,主要用于处理层级结构(嵌套结构)数据,但以扁平化方式存储,以节省内存并提高查询效率。它特别适用于存储动态的或高可变的键值对数据,例如日志、自定义字段等。

该类型会将嵌套 JSON 对象的键值扁平化为单一层级字段。开搞!!!先定一个字段存储用户附加信息:

PUT user_info/_mapping
{
  "properties": {
      "extendInfo": {
        "type": "flattened"
      }
    }
}

插入数据,我们在上面的数据更新字段extendInfo信息:

POST user_info/_update/10
{
  "doc":{
    "extendInfo":{
      "age":18,
      "gender":"女",
      "height":180,
      "weight":"68kg"
    }
  }
}

POST user_info/_update/11
{
  "doc":{
    "extendInfo":{
      "age":30,
      "gender":"女"
    }
  }
}

POST user_info/_update/12
{
  "doc":{
    "extendInfo":{
      "height":170,
      "weight":"58kg",
      "hobby":"篮球"
    }
  }
}

POST user_info/_update/13
{
  "doc":{
    "extendInfo":{
      "address":"杭州",
      "phone":"12334464"
    }
  }
}

可以看到我们插入的extendInfo的信息都不太一样,主打就是一个动态字段信息。接下来查询看看:

GET user_info/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "extendInfo.gender": {
              "value": "女"
            }
          }
        },
        {
          "term": {
            "extendInfo.age": {
              "value": 18
            }
          }
        }
      ]
    }
  }
}

查询结果会命中id=10这条数据

flattened 类型是一种折中的选择,适合存储动态键值对数据,如日志、标签和指标等。它提供了比 object 类型更好的性能,并且比 nested 类型占用更少的资源。然而,它无法进行复杂的嵌套关联查询和深层聚合,因此需要根据业务需求进行合理选择。

5.使用Nested存储对象数组

Nested:嵌套类型,有点千呼万唤始出来的感觉,他到底能干嘛呢?

nested 类型允许 Elasticsearch 以“独立文档”方式存储每个嵌套对象,并维持每个对象内字段之间的关系。这样可以确保查询到的数据和文档结构一致,避免出现跨对象匹配的情况。

上面我们提到数据Array类型的限制是无法建立两个数组字段直接的关系,而Flattened是存储一个对象动态字段,Nested就是可以存储数据对象,并能维护数组对象之间关系,来看看示例,一个用户有多个家人family:

PUT user_info/_mapping
{
  "properties": {
      "family": {
        "type": "nested"
      }
    }
}

写入数据,基于上面的数据写入family即可:

POST user_info/_update/10
{
  "doc":{
    "family":[
      {
        "name":"李四",
        "relation":"父子",
        "amount":10000
      },
      {
        "name":"王婆",
        "relation":"母子",
        "amount":6000
      }
    ]
  }
}

POST user_info/_update/11
{
  "doc":{
    "family":[
      {
        "name":"李四",
        "relation":"兄弟",
        "amount":10000
      },
      {
        "name":"小红",
        "relation":"夫妻",
        "amount":1000
      }
    ]
  }
}

POST user_info/_update/12
{
  "doc":{
    "family":[
      {
        "name":"王五",
        "relation":"父子",
        "amount":10000
      },
      {
        "name":"小兰",
        "relation":"兄妹",
        "amount":1000
      }
    ]
  }
}

接下来我们查询 relation=父子 and amount=10000的数据:

GET user_info/_search
{
  "query": {
    "nested": {
      "path": "family",
      "query": {
        "bool": {
          "must": [
            {
              "term": {
                "family.relation.keyword": {
                  "value": "父子"
                }
              }
            },
            {
              "term": {
                "family.amount": {
                  "value": 10000
                }
              }
            }
          ]
        }
      }
    }
  }
}

查询结果命中了id=10,12两条数据。你可能注意到查询family.relation后面接了keyword,这是因为我们嵌套字段对象属性是动态生成的,family.relation自动映射成text,对text字段进行term精确匹配,必须接上关键字keyword

nested 类型在存储和查询时比普通 Flattened 类型更耗资源,因为每个嵌套对象会被当作单独文档存储和索引,并且文档不能直接更新嵌套对象的部分字段,通常需要重建整个嵌套对象。

nested 类型适合嵌套对象数组的结构化数据,支持精确的对象内匹配查询和聚合。当嵌套对象只需要简单查询、没有严格的对象内关联需求时,使用 Flattened 类型更高效。

6.总结

上面我们讲了各自的定义、使用、局限性,最后这里总结概括下:

字段类型

适用场景

优点

缺点

Array

适合简单的多值字段(如标签、爱好)

易用、性能好

无法维护元素之间的关系

Flattened

动态键值对结构(如可变的配置、元数据)

占用存储空间少、动态字段支持好

不支持嵌套关系查询

Nested

多层级结构对象,且需要查询对象内部关系

支持复杂的关系查询

性能和存储开销大

简单来说:

  • Array 简单快捷,适合不需要关系的多值字段。
  • Flattened 提供了一种高效存储动态键值对的方式,但不适合复杂查询。
  • Nested 适合复杂的嵌套关系查询,但会增加存储和查询的开销。

根据你的业务需求,合理选择字段类型可以显著提升系统的查询性能和数据管理效率。

责任编辑:武晓燕 来源: Shepherd进阶笔记
相关推荐

2022-12-20 07:39:46

2023-12-21 17:11:21

Containerd管理工具命令行

2023-12-15 09:45:21

阻塞接口

2022-02-18 10:13:07

SolrElasticSea开源

2021-02-22 09:05:59

Linux字符设备架构

2020-12-18 11:54:22

Linux系统架构

2021-06-04 09:35:05

Linux字符设备架构

2022-03-21 17:30:04

JetpackGoogle开发者

2020-10-09 07:56:52

Linux

2020-12-19 16:12:58

操作系统计算机科学

2023-09-11 06:32:30

VPAHPA容量

2022-10-21 17:24:34

契约测试定位

2021-04-28 08:05:30

SpringCloudEureka服务注册

2023-11-20 08:18:49

Netty服务器

2023-05-15 08:44:15

Redis数据库

2021-11-20 10:27:43

Python数据类型

2021-11-22 06:21:31

Python数据类型Python基础

2023-07-31 08:18:50

Docker参数容器

2021-05-29 10:11:00

Kafa数据业务

2023-11-06 08:16:19

APM系统运维
点赞
收藏

51CTO技术栈公众号