ES-Document数据建模详解
序号 | 内容 | 链接地址 |
---|---|---|
1 | SpringBoot整合Elasticsearch7.6.1 | https://blog.csdn.net/miaomiao19971215/article/details/105106783 |
2 | Elasticsearch Filter执行原理 | https://blog.csdn.net/miaomiao19971215/article/details/105487446 |
3 | Elasticsearch 倒排索引与重建索引 | https://blog.csdn.net/miaomiao19971215/article/details/105487532 |
4 | Elasticsearch Document写入原理 | https://blog.csdn.net/miaomiao19971215/article/details/105487574 |
5 | Elasticsearch 相关度评分算法 | https://blog.csdn.net/miaomiao19971215/article/details/105487656 |
6 | Elasticsearch Doc values | https://blog.csdn.net/miaomiao19971215/article/details/105487676 |
7 | Elasticsearch 搜索技术深入 | https://blog.csdn.net/miaomiao19971215/article/details/105487711 |
8 | Elasticsearch 聚合搜索技术深入 | https://blog.csdn.net/miaomiao19971215/article/details/105487885 |
9 | Elasticsearch 内存使用 | https://blog.csdn.net/miaomiao19971215/article/details/105605379 |
10 | Elasticsearch ES-Document数据建模详解 | https://blog.csdn.net/miaomiao19971215/article/details/105720737 |
一. 关系型数据库与Elasticsearch Document数据模型对比
所谓数据建模就是在考虑如何设计index的mapping使得documents与数据库中的数据产生对应关系。
首先来看看java中实体类型和数据模型的映射。
import java.util.List;
public class Category {
private Long id;
private String categoryName;
private String remark;
private List<Product> products;
}
public class Product {
private Long id;
private String productName;
private String remark;
private String salePoint;
private Long price;
private Category category;
}
一个Category对应多个Product,多个Product对应一个Category,总体来说就是1对N的关系。
上述的实体类型在关系型数据库中建立对应的数据模型如下:
tb_category
列名 | 类型 | 外键 |
---|---|---|
id | int | <pk> |
caregory_name | varchar(255) | |
remark | text |
tb_product
列名 | 类型 | 外键 |
---|---|---|
id | int | <pk> |
product_name | varchar(255) | |
remark | text | |
sale_point | varchar(255) | |
price | double | |
category_id | int | <fk> |
在正常情况下,关系型数据库中的数据建模需要遵循数据库的三大范式(商业项目中不完全遵守,因为商业项目中业务环境复杂,可能会为了提高性能和查询效率,在表中适当的加一些冗余字段。) ,理论上不会有冗余数据产生,如果需要查询商品名和对应的类别,只需要将两张表连接起来查询即可。
在Elasticsearch中进行数据建模时,基准字段可能会因为业务而有所不同:
情况1: 以"类别(category)"为基准,存储数据如下:
{
"cid" : 1,
"categoryName" : "手机",
"remark" : "移动通讯设备",
"products" : [
{
"pid" : 11,
"productName" : "IPhone 12",
"remark" : "苹果手机",
"salePoint" : "最新款",
"price" : 648800
},
{
"pid" : 12,
"productName" : "IPhone xr",
"remark" : "苹果手机",
"salePoint" : "经典款",
"price" : 948800
}
]
}
这种保存方式缺点很明显:
- 数据的耦合度太高,如果category数据更新,那么这个category下对应的所有product都会更新一次。
- 数据的粘粘度太高,搜索时,原本只想搜出某一个product的信息,但却不得不搜索出命中category下的所有product信息。
情况2: 以"商品"为基准,保存数据如下:
{
"pid" : 11,
"productName" : "IPhone 12",
"remark" : "苹果手机",
"salePoint" : "经典款",
"price" : 648800,
"category" : {
"cid" : 1,
"categoryName" : "手机",
"remark" : "移动通讯设备"
}
}
{
"pid" : 12,
"productName" : "IPhone xr",
"remark" : "苹果手机",
"salePoint" : "最新款",
"price" : 948800,
"category" : {
"cid" : 1,
"categoryName" : "手机",
"remark" : "移动通讯设备"
}
}
这种保存方式也有缺点: 若多个product中指向同一个category,则会造成category信息冗余存储。
总结:
Elasticsearch中存储数据的格式是json,json在局限性在于它使用字符串来描述复杂的数据结构,因此无论如何都没办法描述一个双向的引用关系,我们需要使用单向、可识别且清晰的数据逻辑来描述复杂的数据模型。所以在进行Elasticsearch数据建模时,需要考虑的内容与关系型数据库不太一样。
二. 模拟关系型数据库进行数据建模
- 创建索引ind_category,对应数据库中的tb_category表。
- 创建索引ind_product,对应数据库中的tb_product表。
PUT / ind_category {
"mappings": {
"properties": {
"id": {
"type": "long"
},
"categoryName": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"remark": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
PUT /ind_category/1
{
"id" : 1,
"categoryName" : "手机",
"remark" : "移动通讯设备"
}
PUT / ind_product {
"mappings": {
"properties": {
"id": {
"type": "long"
},
"productName": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"remark": {
"type": "text",
"analyzer": "ik_max_word"
},
"salePoint": {
"type": "text",
"analyzer": "ik_max_word"
},
"price": {
"type": "long"
},
"categoryId": {
"type": "long"
}
}
}
}
PUT /ind_product/1
{
"pid" : 11,
"productName" : "IPhone 12",
"remark" : "苹果手机",
"salePoint" : "最新款",
"price" : 648800,
"categoryId" : 1
}
PUT /ind_product/2
{
"pid" : 12,
"productName" : "IPhone xr",
"remark" : "苹果手机",
"salePoint" : "经典款",
"price" : 948800,
"categoryId" : 1
}
如果需要查询商品类别名为"手机"的所有商品数据,则需要执行以下操作:
GET ind_category/_search
{
"query": {
"match": {
"categoryName": "手机"
}
}
}
GET ind_product/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"categoryId": 1
}
}
}
}
}
优点: 几乎没有冗余数据
缺点: 查询逻辑复杂,需要对多个index发起多次搜索,执行效率较低。
ps: Elasticsearch中一个index下尽量使document的数据结构相似,降低存储压力。
三. document数据建模
Elasticsearch官方对于数据建模没有明确的给出对应的范式和规约,通常都是以业务和经验为指导,进行数据建模。
3.1 一对一数据建模
比如需要在Elasticsearch中保存公民和身份证信息,那么公民和对应的身份证就是一个典型的一对一关系。在这个时候,我们一般将数据进行组合,把其中的一个数据结构封装在另一个数据结构中。比如本例中,我们可以把身份证信息封装在整个公民数据结构中。
PUT person_index {
"mappings": {
"properties": {
"last_name": {
"type": "keyword"
},
"first_name": {
"type": "keyword"
},
"age": {
"type": "byte"
},
"identification_id": {
"properties": {
"id_no": {
"type": "keyword"
},
"address": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"keyword": {
"type": "keyword"
}
}
}
}
}
}
}
}
3.2 一对多数据建模
比如电商系统中,用户(A)和地址列表(B)就是典型的一对多数据模型(假设地址最小粒度到street街道)。在Elasticsearch中针对一对多数据关系,有两种通用的建模方式:
-
B包含A (地址数据包含用户数据)
这种建模方式虽然设计简单,但是会有非常多的冗余数据,而且对于用户数据的管理很不方便。一旦用户数据需要变更,会导致关联的多个document,连带着这些document下关联的其它用户信息全都发生了更新。因此不推荐使用。 -
A包含B (用户数据包含地址数据)
每个用户数据中包含一个装有地址数据的数组,同样会有非常多的冗余数据(地址数据被重复存储),并且针对地址进行搜索时,经常会得到一些不必要的数据。比如,在下述环境中,搜索province为北京,city为天津的用户。
PUT / user_index {
"mappings": {
"properties": {
"login_name": {
"type": "keyword"
},
"age ": {
"type": "short"
},
"address": {
"properties": {
"province": {
"type": "keyword"
},
"city": {
"type": "keyword"
},
"street": {
"type": "keyword"
}
}
}
}
}
}
POST /user_index/_doc/1
{
"login_name" : "jack",
"age" : 25,
"address" : [
{
"province" : "北京",
"city" : "北京",
"street" : "西三旗东路"
},
{
"province" : "天津",
"city" : "天津",
"street" : "古文化街"
}
]
}
POST /user_index/_doc/2
{
"login_name" : "rose",
"age" : 21,
"address" : [
{
"province" : "河北",
"city" : "廊坊",
"street" : "燕郊经济开发区"
},
{
"province" : "天津",
"city" : "天津",
"street" : "古文化街"
}
]
}
搜索地址中省province为北京,市city为天津的用户:
GET /user_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address.province": "北京"
}
},
{
"match": {
"address.city": "天津"
}
}
]
}
}
}
最终Elasticsearch会查出_id=1的用户,这个结果并不正确,因为我们希望查询的是省、市在同一个地址中,而非在数组中的多个地址中匹配到。
这个时候就可以使用nested object来定义数据建模了。
3.2.1 nexted object
将对象定义成nexted类型非常简单,只需要在定义时增加 “type”:"keyword"即可。
PUT / user_index {
"mappings": {
"properties": {
"login_name": {
"type": "keyword"
},
"age ": {
"type": "short"
},
"address": {
"type": "nested",
"properties": {
"province": {
"type": "keyword"
},
"city": {
"type": "keyword"
},
"street": {
"type": "keyword"
}
}
}
}
}
}
有意思的是,设置为nested类型后,搜索语法与之前有所不同。如果仍然使用之前的搜索语法,将搜不出任何数据!
GET /user_index/_search
{
"query": {
"bool": {
"must": [
{
"nested": {
"path": "address",
"query": {
"bool": {
"must": [
{
"match": {
"address.province": "北京"
}
},
{
"match": {
"address.city": "天津"
}
}
]
}
}
}
}
]
}
}
}
"path"用于指明本次需要搜索的是哪一个nested object
从搜索语法的形式上来看,复杂度的确增加了,但是在数据的读写操作上都不会发生错误,因此推荐使用。
为什么使用nested类型后,能够确保多个搜索条件作用在数组中的同一个对象上呢?
这是因为普通的数组数据在Elasticsearch存储时会被扁平化处理,处理方式如下:
{
"login_name" : "jack",
"address.province" : [ "北京", "天津" ],
"address.city" : [ "北京", "天津" ]
"address.street" : [ "西三旗东路", "古文化街" ]
}
直接对province搜索时,即便用must api,仍然相当于在address.province内使用类似contains()的语法。
Elasticsearch不会对nested object进行扁平化处理,处理方式如下:
{
"login_name" : "jack"
}
{
"address.province" : "北京",
"address.city" : "北京",
"address.street" : "西三旗东路"
}
{
"address.province" : "北京",
"address.city" : "北京",
"address.street" : "西三旗东路",
}
所以,在对nested类型对象进行搜索时,搜索条件一定能作用在数组中的某一个对象身上。
不要盲目的将数组类型数据设置成nested object类型,由于nested object阻碍了数据存储时的扁平化处理,因此会加大存储的压力。(并不会影响执行效率)
3.2.2 nested object 聚合分析
聚合分析每个省每个市对应的document数量。
GET /user_index/_search
{
"size": 0,
"aggs": {
"group_by_address": {
"nested": {
"path": "address"
},
"aggs": {
"group_by_province": {
"terms": {
"field": "address.province"
},
"aggs": {
"group_by_city": {
"terms": {
"field": "address.city"
}
}
}
}
}
}
}
}
执行结果:
"aggregations" : {
"group_by_address" : {
"doc_count" : 4,
"group_by_province" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "天津",
"doc_count" : 2,
"group_by_city" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "天津",
"doc_count" : 2
}
]
}
},
{
"key" : "北京",
"doc_count" : 1,
"group_by_city" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "北京",
"doc_count" : 1
}
]
}
},
{
"key" : "河北",
"doc_count" : 1,
"group_by_city" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "廊坊",
"doc_count" : 1
}
]
}
}
]
}
}
}
聚合分析每个市的用户平均年龄。
由于"年龄"字段不在nested类型的数组中,因此聚合时,我们需要在聚合外部寻找数据。Elasticsearch为我们提供了相应的api: reverse_nested。这个api代表可以使用nested object之外的field执行聚合分析。值得一提的是,reverse_nested只能在nested object聚合的子聚合中使用。
GET /user_index/_search
{
"size": 0,
"aggs": {
"group_by_address": {
"nested": {
"path": "address"
},
"aggs": {
"group_by_city": {
"terms": {
"field": "address.city"
},
"aggs": {
"reverse_ages": {
"reverse_nested": {},
"aggs": {
"avg_by_age": {
"avg": {
"field": "age"
}
}
}
}
}
}
}
}
}
}
执行结果为:
"aggregations" : {
"group_by_address" : {
"doc_count" : 4,
"group_by_city" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "天津",
"doc_count" : 2,
"reverse_ages" : {
"doc_count" : 2,
"avg_by_age" : {
"value" : 23.0
}
}
},
{
"key" : "北京",
"doc_count" : 1,
"reverse_ages" : {
"doc_count" : 1,
"avg_by_age" : {
"value" : 25.0
}
}
},
{
"key" : "廊坊",
"doc_count" : 1,
"reverse_ages" : {
"doc_count" : 1,
"avg_by_age" : {
"value" : 21.0
}
}
}
]
}
}
}
如果不使用reverse_nested,比如如下搜索语法:
GET /user_index/_search
{
"size": 0,
"aggs": {
"group_by_address": {
"nested": {
"path": "address"
},
"aggs": {
"group_by_city": {
"terms": {
"field": "address.city"
},
"aggs": {
"avg_by_age": {
"avg": {
"field": "age"
}
}
}
}
}
}
}
}
那么avg的值是无法被获取到的,执行结果为:
"aggregations" : {
"group_by_address" : {
"doc_count" : 4,
"group_by_city" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "天津",
"doc_count" : 2,
"avg_by_age" : {
"value" : null
}
},
{
"key" : "北京",
"doc_count" : 1,
"avg_by_age" : {
"value" : null
}
},
{
"key" : "廊坊",
"doc_count" : 1,
"avg_by_age" : {
"value" : null
}
}
]
}
}
}
3.3 多对多数据建模
多对多模型可以看成是两个一对多模型组合起来,建模时,只需要以其中一个因子作为出发点,创建一个索引,使用nested object存储另一个因子的信息。
比如,学生选课。学生与课程之间存在多对多关系,建模时既可以从学生的角度出发,每个学生document中包含若干个课程,也可以从课程的角度出发,每个课程包含若干学生。虽说会造成一定的数据冗余,但影响不大。
3.4 文件系统数据建模(path_hierarchy)
现在需要做一个代码管理系统,数据结构如下:
{
"fileName": "Test.java",
"path": "/aaa/bbb/ccc",
"content": "public class xxx..."
}
其中,字段"path"比较特殊,它是以文件路径的方式存储的。直接使用standard分词器分词结果如下:
GET /path_index/_analyze
{
"field": "path",
"text": "/aaa/bbb/ccc"
}
{
"tokens" : [
{
"token" : "aaa",
"start_offset" : 1,
"end_offset" : 4,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "bbb",
"start_offset" : 5,
"end_offset" : 8,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "ccc",
"start_offset" : 9,
"end_offset" : 12,
"type" : "<ALPHANUM>",
"position" : 2
}
]
}
/会被当作停用词直接忽略掉,并且各个词条没有逻辑可言。
Elasticsearch为包含文件路径的内容专门提供了一种分词器,创建方式如下:
"settings": {
"analysis": {
"analyzer": {
"my_path_analyzer": {
"tokenizer": "path_hierarchy"
}
}
}
}
通过这种分词器分词后,能够体现出层次感,请看执行结果:
{
"tokens" : [
{
"token" : "/aaa",
"start_offset" : 0,
"end_offset" : 4,
"type" : "word",
"position" : 0
},
{
"token" : "/aaa/bbb",
"start_offset" : 0,
"end_offset" : 8,
"type" : "word",
"position" : 0
},
{
"token" : "/aaa/bbb/ccc",
"start_offset" : 0,
"end_offset" : 12,
"type" : "word",
"position" : 0
}
]
}
3.5 父子类关系数据建模
前面的关联关系建模方式都有其局限性,要么存在冗余数据,要么需要频繁的访问Elasticsearch实现类似join的功能来搜索。实际上Elasticsearch已经为我们准备了一种特殊的数据建模方式——父子关系数据建模。
父子关系数据建模就是模拟关系型数据库的建模方式,用同一个索引保存双方的数据,通过Elasticsearch底层提供的父子类关系,实现类似关系型数据库的多表联合查询。
这种建模方式下几乎不存在冗余数据,并且查询效率很高,数据之间的关系都是通过Elasticsearch底层的父子类关系来维系的,不需要类似join查询。(Elasticsearch6.x对父子类关系做了较大的改版,与5.x完全不同)
比如设计一个电商系统中的"商品类型"和"商品"的索引,使用父子类关系数据建模如下:
专门建立一个字段,用来维护"商品"和"商品类型"之间的关系,就好像主键(pk)与外键(fk)。注意看ecommerce_join_field(自定义名称),这个属性是join类型,relations记录了父子类关系,其中category是父类,而product是子类。
PUT ecommerce_products_index
{
"mappings": {
"properties": {
"category_name": {
"type": "text",
"analyzer": "standard",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"product_name": {
"type": "text",
"analyzer": "standard",
"fields": {
"keyword": {
"type": "keyword"
}
}
},
"price" : {
"type" : "long"
},
"ecommerce_join_field" : {
"type" : "join",
"relations" : {
"category" : "product"
}
}
}
}
}
开始向索引中添加数据,所谓的建立关系,说白了就是在子类中存放父类的id,因此父类可以独立出现,但子类一定要依托于父类才能存在(必须要携带父类id,否则谁知道这个子类属于哪个父类)。此外,在父子类关系模型中,父类与子类的数据必须存放在相同的shard当中(Tips:这里并没有说只能放在1个分片中),否则Elasticsearch无法实现数据的关联。默认情况下,Elasticsearch使用document的_id作为routing的入参,所以在保存子数据时,必须要使用父数据的_id作为routing才行!
POST _bulk
{"index": {"_index": "ecommerce_products_index", "_id": "1"}}
{"category_name": "电视", "ecommerce_join_field": {"name": "category"}}
{"index": {"_index": "ecommerce_products_index", "_id": "2"}}
{"category_name": "电脑", "ecommerce_join_field": {"name": "category"}}
{"index": {"_index": "ecommerce_products_index", "_id": "3"}}
{"category_name": "手机", "ecommerce_join_field": {"name": "category"}}
{"index": {"_index": "ecommerce_products_index", "_id": "4", "routing": "1"}}
{"product_name": "三星", "price": "230000", "ecommerce_join_field":{"name": "product", "parent": "1"}}
{"index": {"_index": "ecommerce_products_index", "_id": "5", "routing": "1"}}
{"product_name": "索尼", "price": "500000", "ecommerce_join_field":{"name": "product", "parent": "1"}}
{"index": {"_index": "ecommerce_products_index", "_id": "6", "routing": "2"}}
{"product_name": "戴尔", "price": "250000", "ecommerce_join_field":{"name": "product", "parent": "2"}}
{"index": {"_index": "ecommerce_products_index", "_id": "7", "routing": "2"}}
{"product_name": "微星", "price": "220000", "ecommerce_join_field":{"name": "product", "parent": "2"}}
{"index": {"_index": "ecommerce_products_index", "_id": "8", "routing": "3"}}
{"product_name": "苹果", "price": "130000", "ecommerce_join_field":{"name": "product", "parent": "3"}}
{"index": {"_index": "ecommerce_products_index", "_id": "9", "routing": "3"}}
{"product_name": "诺基亚", "price": "80000", "ecommerce_join_field":{"name": "product", "parent": "3"}}
与普通的bulk操作不同,对于含有父子类关系的数据模型,在新增数据时,一定要把数据对应的类别给显示的写出来。比如"电视"、“电脑”、“手机"作为商品类别,在新增数据时,需要将"ecommerce_join_field"中"name"的值置为"category”(ecommerce_join_field中的name不是我们自己定义的,而是Elasticsearch对于类型为join的属性,自动维护的类型!) 同样的道理,我们将商品document对应的name设置成"product",并且在新增时将父类的_id作为自己的routing值,最后,不要忘了在"ecommerce_join_field"父子类关系中也要写明所属父类的_id。
需求: 搜索商品类型id为1的商品数据。 (根据父数据搜索子数据)
第一种写法: "parent_id"与terms、prefixs类似,可以视作是一种搜索方式。"type"用于表明本次需要搜索的数据的类型(对应mapping设置阶段relations中的值),而"id"指的是需要搜索的子数据的父数据id。
GET /ecommerce_products_index/_search
{
"query": {
"parent_id": {
"type": "product",
"id": 1
}
}
}
第二种写法:是否拥有父类( “has_parent”),如果拥有,那么需要指出父类的具体类型(category)以及父类需要满足的条件(query->term())
GET /ecommerce_products_index/_search
{
"query": {
"has_parent": {
"parent_type": "category",
"query": { // 父类数据需要满足的条件
"term": {
"_id": 1
}
}
}
}
}
需求: 搜索价格在120000~240000的商品对应的商品类型。 (根据子数据搜索父数据)
GET /ecommerce_products_index/_search
{
"query": {
"has_child": {
"type": "product",
"query": {
"range": {
"price": {
"gte": 120000,
"lte": 240000
}
}
}
}
}
}
需求: 统计每个商品种类的商品平均价格
首先对商品种类分组,由于category_name是text类型(没有正排索引),因此需要使用keyword子字段来进行分组。接着,对子数据进行聚合,或者称为聚拢更合适(每一组子类型数据对应着一个父类型数据),子聚合时必须指明子数据的类型(type),"children type->product"用于聚合子数据,统计并返回每组中子数据的个数。"aggs ->avg_by_price"则是在每组子类型数据的基础上,添加额外的聚合操作,比如avg获取某个商品种类中涉及商品的平均价格。
GET /ecommerce_products_index/_search
{
"size": 0,
"aggs": {
"group_by_category": {
"terms": {
"field": "category_name.keyword"
},
"aggs": {
"products_bucket": {
"children": { // 为子类型进行聚合
"type": "product"
},
"aggs": {
"avg_by_price": {
"avg":{
"field": "price"
}
}
}
}
}
}
}
}
最终执行结果:
"aggregations" : {
"group_by_category" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "手机",
"doc_count" : 1,
"products_bucket" : {
"doc_count" : 2,
"avg_by_price" : {
"value" : 105000.0
}
}
},
{
"key" : "电脑",
"doc_count" : 1,
"products_bucket" : {
"doc_count" : 2,
"avg_by_price" : {
"value" : 235000.0
}
}
},
{
"key" : "电视",
"doc_count" : 1,
"products_bucket" : {
"doc_count" : 2,
"avg_by_price" : {
"value" : 365000.0
}
}
}
]
}
需求: 按10000元为一个区间,统计指定区间内商品种类的id以及对应商品的数量
GET /ecommerce_products_index/_search
{
"size": 0,
"aggs": {
"histogram_by_price": {
"histogram": {
"field": "price",
"interval": 10000,
"min_doc_count": 1
},
"aggs": {
"parent_id_bucket": {
"terms": {
"field": "ecommerce_join_field#category" // 为父类型进行聚合 需要使用#
}
}
}
}
}
}
3.6 祖孙三代关系数据建模
原理和父子类关系数据模型一致,只不过关系层次更深入而已。
比如现在有国家、部门、员工,创建祖孙三代关系数据模型的语法如下 :
PUT statistic_index
{
"mappings": {
"properties": {
"country_name": {
"type": "keyword"
},
"department_name": {
"type": "keyword"
},
"employee_name":{
"type": "keyword"
},
"employee_age": {
"type": "long"
}
"statistic_join_field": {
"type": "join",
"relations": {
"country": "department",
"department": "employee"
}
}
}
}
}
在relations中描述多组数据类型关系即可。注意: 一个父类可以对应多个子类(通过中括号赋值,比如"A": [“B”, “C”, “D”]),但是一个子类不能对应多个父类!
要实现祖孙三代数据关系,必须保证祖孙三代数据都保存在同一个shard中,也就是说需要使用同一个routing值。默认情况下,祖、父、子全部使用祖宗的_id即可。
需求: 搜索包含开发部门的国家(祖父找父亲)
GET statistic_index/_search
{
"query": {
"has_child": {
"type": "department",
"query": {
"match": {
"department_name": "开发部门"
}
}
}
}
}
需求: 搜索员工Jack所在的国家(祖父找孙子)
GET /statistic_index/_search
{
"query": {
"has_child": {
"type": "department",
"query": {
"has_child": {
"type": "employee",
"query": {
"match": {
"employee_name": "Jack"
}
}
}
}
}
}
}
需求: 搜索每个公司的员工平均(身份证上的)年龄(祖父、孙子聚合)
GET /statistic_index/_search
{
"size": 0,
"aggs": {
"group_by_company": {
"terms": {
"field": "company_name.keyword"
},
"aggs": {
"employee_bucket": {
"children": {
"type": "employee"
},
"aggs": {
"identification_bucket": {
"children": {
"type": "identification"
},
"aggs": {
"avg_by_age": {
"avg": {
"field": "identified_age"
}
}
}
}
}
}
}
}
}
}
一般来说,Elasticsearch中最多只是用祖孙三代数据模型,如果层级更多,则可以考虑使用多索引。