Elasticsearch ES-Document数据建模详解

序号内容链接地址
1SpringBoot整合Elasticsearch7.6.1https://blog.csdn.net/miaomiao19971215/article/details/105106783
2Elasticsearch Filter执行原理https://blog.csdn.net/miaomiao19971215/article/details/105487446
3Elasticsearch 倒排索引与重建索引https://blog.csdn.net/miaomiao19971215/article/details/105487532
4Elasticsearch Document写入原理https://blog.csdn.net/miaomiao19971215/article/details/105487574
5Elasticsearch 相关度评分算法https://blog.csdn.net/miaomiao19971215/article/details/105487656
6Elasticsearch Doc valueshttps://blog.csdn.net/miaomiao19971215/article/details/105487676
7Elasticsearch 搜索技术深入https://blog.csdn.net/miaomiao19971215/article/details/105487711
8Elasticsearch 聚合搜索技术深入https://blog.csdn.net/miaomiao19971215/article/details/105487885
9Elasticsearch 内存使用https://blog.csdn.net/miaomiao19971215/article/details/105605379
10Elasticsearch 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

列名类型外键
idint<pk>
caregory_namevarchar(255)
remarktext

tb_product

列名类型外键
idint<pk>
product_namevarchar(255)
remarktext
sale_pointvarchar(255)
pricedouble
category_idint<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
    }
  ]
}

这种保存方式缺点很明显:

  1. 数据的耦合度太高,如果category数据更新,那么这个category下对应的所有product都会更新一次。
  2. 数据的粘粘度太高,搜索时,原本只想搜出某一个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数据建模时,需要考虑的内容与关系型数据库不太一样。

二. 模拟关系型数据库进行数据建模

  1. 创建索引ind_category,对应数据库中的tb_category表。
  2. 创建索引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中针对一对多数据关系,有两种通用的建模方式:

  1. B包含A (地址数据包含用户数据)
    这种建模方式虽然设计简单,但是会有非常多的冗余数据,而且对于用户数据的管理很不方便。一旦用户数据需要变更,会导致关联的多个document,连带着这些document下关联的其它用户信息全都发生了更新。因此不推荐使用。

  2. 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中最多只是用祖孙三代数据模型,如果层级更多,则可以考虑使用多索引。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值