在Elasticsearch的实际应用中,嵌套文档是一个常见的需求,尤其是当我们需要对对象数组进行独立索引和查询时。在Elasticsearch中,这类嵌套结构被称为父子文档,它们能够“彼此独立地进行查询”。实现这一功能主要有两种方式:
Nested类型:
父子类型:
Nested类型和父子Join类型在处理关联数据时各有优势。Nested类型更适合处理静态的、紧密关联的嵌套数据,而父子Join类型则更适合处理需要动态更新或具有一对多关系的文档。
对象数组的默认存储方式:
Elasticsearch内部并不直接支持对象的层次结构,而是将对象层次结构扁平化为一个字段名和字段值的简单列表。这种处理方式可能导致数据关联性的丢失。例如,考虑以下文档:
PUT user/user_info/1
{
"group": "man",
"userName": [
{
"first": "张",
"last": "三"
},
{
"first": "李",
"last": "四"
}
]
}
如果我们尝试查询first
为“张”且last
为“四”的数据,按照常理,这样的数据应该不存在。然而,使用以下查询:
GET /user/user_info/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"userName.first": "张"
}
},
{
"match": {
"userName.last": "四"
}
}
]
}
}
}
意外地,我们可能会得到结果。这是因为Lucene(Elasticsearch的底层库)没有内部对象的概念,它将内部对象扁平化处理了。在内部,文档实际上被存储为:
{
"group": "man",
"userName.first": ["张", "李"],
"userName.last": ["三", "四"]
}
可以看到,userName.first
和userName.last
被扁平化为多值字段,它们之间的关联性已经丢失,因此查询结果可能不符合我们的预期。
在Elasticsearch中,父子索引类型join是通过特殊的字段类型来实现的,该字段类型被称为“join”。这个字段允许我们定义文档之间的父子关系。当我们创建一个包含join字段的索引时,我们需要指定哪些文档类型是父文档,哪些是子文档。
在底层,Elasticsearch使用特殊的路由机制来确保父子文档存储在同一个分片上。这是非常重要的,因为这样可以提高查询性能并确保数据的一致性。当我们索引一个子文档时,需要使用routing参数来指定其父文档的ID,以便Elasticsearch可以将它们路由到相同的分片。
虽然父子索引类型提供了解决上述问题的有效手段,但它也带来了一些额外的复杂性和性能考虑。因此,在使用之前需要仔细评估数据模型和查询需求,以确定是否适合使用父子索引类型。
join字段提供了一种在索引中明确定义父子文档之间关系的方法。使用join字段的优势在于:
创建一个新的索引,并定义好父子文档的映射关系。在映射中加入join字段,并设置好父子关系的名称。例如,我们可以定义一个订单索引,其中包含商品子文档。
PUT order-join
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1
},
"mappings": {
"properties": {
"orderid": { "type": "integer" },
"buyer": { "type": "keyword" },
"order_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" },
"goodsid": { "type": "integer" },
"goods_name": { "type": "keyword" },
"price": { "type": "double" },
"produce_time": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss" },
"my_join_field": {
"type": "join",
"relations": {
"order": "goods"
}
}
}
}
}
在添加文档时,需要明确指定文档的父子关系。父文档只需指定join字段的关系名称,而子文档则需指定父文档的主键和关系名称。
PUT order-join/_doc/1
{
"orderid": "1",
"buyer": "tom",
"order_time": "2020-11-04 00:00:00",
"my_join_field": {
"name": "order"
}
}
PUT order-join/_doc/2?routing=1
{
"goodsid": "1",
"goods_name": "milk",
"price": 5.2,
"produce_time": "2020-10-04 00:00:00",
"my_join_field": {
"name": "goods",
"parent": "1"
}
}
利用join字段,可以实现一些特殊的搜索操作:
以父搜子:通过父文档的属性来查询子文档。例如,我们可以查询所有属于特定买家的商品。
POST order-join/_search
{
"query": {
"has_parent": {
"parent_type": "order",
"query": {
"term": {
"buyer": {
"value": "tom"
}
}
}
}
}
}
以子搜父:通过子文档的属性来查询父文档。例如,我们可以查询所有包含特定商品的订单。
POST order-join/_search
{
"query": {
"has_child": {
"type": "goods",
"query": {
"match_all": {}
}
}
}
}
父文档主键搜索:通过父文档的主键值来查询所有关联的子文档。例如,我们可以查询订单号为1的所有商品。
POST order-join/_search
{
"query": {
"parent_id": {
"type": "goods",
"id": "1"
}
}
}
join字段还支持children和parent聚集操作,用于对父子文档进行统计分析。
children聚集:统计每个父文档的子文档数据。例如,我们可以统计每个买家购买的商品名称和数量。
POST order-join/_search
{
"query": {
"match_all": {}
},
"aggs": {
"orders": {
"terms": {
"field": "buyer",
"size": 10
},
"aggs": {
"goods_data": {
"children": {
"type": "goods"
},
"aggs": {
"goods_name": {
"terms": {
"field": "goods_name",
"size": 10
}
}
}
}
}
}
}
}
parent聚集:统计每个子文档的父文档数据。例如,我们可以统计每种商品的购买者信息。
POST order-join/_search
{
"aggs": {
"goods": {
"terms": {
"field": "goods_name",
"size": 10
},
"aggs": {
"goods_data": {
"parent": {
"type": "goods"
},
"aggs": {
"orders": {
"terms": {
"field": "buyer",
"size": 10
}
}
}
}
}
}
}
}
除了使用join字段,还可以在应用层通过外键字段来实现父子关联。这种方法需要为父文档和子文档分别建立索引,并在查询时进行多次请求。虽然这种方法在处理父子关系时可能不如join字段高效,但它提供了更多的灵活性。
Elasticsearch中的父子索引类型join是一个强大的工具,它允许我们在同一索引中创建具有层级关系的文档。通过正确使用join字段和相关的查询DSL,我们可以有效地表示和查询具有父子关系的数据模型。然而,在使用时需要注意性能影响和数据一致性等问题,并确保与当前Elasticsearch版本的兼容性。