天天看点

如何在 ES 中实现嵌套json对象查询,一次讲明白!

作者:打篮球的程序员

项目需求

实际的软件项目开发过程中,因为业务上的需要,我们的数据库表与表之间的结构是一对多的关系,以订单表中的数据为例,在 mysql 数据库里面,他们的关系如下图:

如何在 ES 中实现嵌套json对象查询,一次讲明白!

如果是在mysql中查询,完全可以使用join语句来实现;而ES本身就是平铺的NoSQL,对于这种1:N的关联关系应该如何来实现呢

需求落地

比较常用的实践方案,有以下三种:

● 嵌套对象

● 嵌套文档

● 父子文档

嵌套对象

先看一下像什么样子

{
    
    "order_id":{
        "type":"keyword"
    },
    "cancel_time":{
        "type":"date",
        "format":"yyyy-MM-dd HH:mm:ss"
    },
    "order_status":{
        "type":"keyword"
    },
    "completed_time":{
        "type":"date",
        "format":"yyyy-MM-dd HH:mm:ss"
    },
    "create_time":{
        "type":"date",
        "format":"yyyy-MM-dd HH:mm:ss"
    },
    "is_delete":{
        "type":"keyword"
    },
    "modify_time":{
        "type":"date",
        "format":"yyyy-MM-dd HH:mm:ss"
    },
    "order_user":{
        "properties":{
            "name":{
                "type":"text",
                "analyzer":"ik_max_word"
            },
            "tel":{
                "type":"text",
                "analyzer":"ik_max_word"
            },
            "user_id":{
                "type":"keyword"
            }
        }
    }
}           

那嵌套对象在ES中如何筛选呢,请看示例

POST order_index/_search

{
    "query":{
        "bool":{
            "must":[
                {
                    "match":{
                        "order_user.name":"张三"
                    }
                },
                {
                    "match":{
                        "order_user.tel":"18866668888"
                    }
                }
            ]
        }
    }
}           

优点:查询时不涉及join,查询效率很高

缺点:由于json对象数组的处理是压扁了处理的,存储成扁平化的键值对列表,因此缺失关联关系。

嵌套文档

先看一下像什么样子

{
    "address_list":{
        "type":"nested",
        "properties":{
            "city_id":{
                "type":"keyword"
            },
            "city_name":{
                "type":"text",
                "analyzer":"ik_max_word"
            }
            "district_id":{
                "type":"keyword"
            },
            "district_name":{
                "type":"text",
                "analyzer":"ik_max_word"
            },
            "lat":{
                "type":"geo_point"
            },
              "lon":{
                "type":"geo_point"
            },
            "name":{
                "type":"text",
                "analyzer":"ik_max_word"
            },
            "province_id":{
                "type":"keyword"
            },
            "province_name":{
                "type":"text",
                "analyzer":"ik_max_word"
            }
        }
    }
           

那嵌套对象在ES中如何筛选呢,请看示例

POST order_index/_search

{
    "query":{
        "nested":{
            "path":"address_list",
            "query":{
                "bool":{
                    "must":[
                        {
                            "match":{
                                "address_list.province_name":"上海市"
                            }
                        },
                        {
                            "match":{
                                "address_list.district_name":"浦东新区"
                            }
                        }
                    ]
                }
            }
        }
    }
}           
  • 优点:
    • 嵌套文档将实体关系嵌套组合在单文档内部(类似与json的一对多层级结构)
  • 缺点:
    • nested子文档在ES内部是独立的文档,会造成数据量剧增,会影响查询性能。
    • 更新主文档的时候要全部更新,影响写的性能。
    • 不支持子文档从属多个主文档的场景。

父子文档

先看一下像什么样子

{
    "mappings":{
        "_doc":{
            "properties":{
                "parent_id":{
                    "type":"keyword"
                },
                "parent_join_child":{
                    "type":"join",
                    "relations":{
                        "question":"answer"
                    }
                }
            }
        }
    }
}           

parent_id是自定义的字段,parent_join_child是给我们的父子文档关系的名字,这个可以自定义,join表示这是一个父子文档关系,relations里面表示question是父,answer是子。

首先我们插入两个父文档。

PUT exam_index/_doc/1

{
    "parent_id":"1",
    "text":"这是一个问题1",
    "parent_join_child":{
        "name":"question"
    }
}

PUT exam_index/_doc/2

{
    "parent_id":"2",
    "text":"这是一个问题2",
    "parent_join_child":{
        "name":"question"
    }
}           

其中"name":"question"表示插入的是父文档。

然后插入两个子文档

PUT exam_index/_doc/3?routing=1

{
    "parent_id":"3",
    "text":"这是一个回答1,对应问题1",
    "parent_join_child":{
        "name":"answer",
        "parent":"1"
    }
}

PUT exam_index/_doc/4?routing=1

{
    "parent_id":"4",
    "text":"这是一个回答2,对应问题1",
    "parent_join_child":{
        "name":"answer",
        "parent":"1"
    }
}           

子文档要解释的东西比较多,首先从文档id我们可以判断子文档都是独立的文档(跟nested不一样)。其次routing关键字指明了路由的id是父文档1, 这个id和下面的parent关键字对应的id是一致的。需要强调的是,索引子文档的时候,routing是必须的,因为要确保子文档和父文档在同一个分片上。"name":"answer"关键字指明了这是一个子文档。

特点:父子文档类似关系型数据库中的关联关系,适用于写多的场景

现在exam_index索引中有四个独立的文档,我们来看父子文档在搜索的时候是什么姿势。

如果我们想通过子文档信息,查询父文档,可以通过如下方式实现:

POST exam_index/_search

{
    "query":{
        "has_child":{
            "type":"answer",
            "query":{
                "match":{
                    "text":"回答"
                }
            }
        }
    }
}           

返回结果:

[
    {
        "_index":"exam_index",
        "_type":"_doc",
        "_id":"1",
        "_score":1,
        "_source":{
            "my_id":"1",
            "text":"这是一个问题1",
            "parent_join_child":{
                "name":"question"
            }
        }
    }
]           

如果我们想通过父文档信息,查询子文档,可以通过如下方式实现:

POST exam_index/_search

{
    "query":{
        "has_parent":{
            "parent_type":"question",
            "query":{
                "match":{
                    "text":"问题"
                }
            }
        }
    }
}           

返回结果:

[
    {
        "_index":"crm_exam_index",
        "_type":"_doc",
        "_id":"3",
        "_score":1,
        "_routing":"1",
        "_source":{
            "my_id":"3",
            "text":"这是一个回答1,对应问题1",
            "parent_join_child":{
                "name":"answer",
                "parent":"1"
            }
        }
    },
    {
        "_index":"crm_exam_index",
        "_type":"_doc",
        "_id":"4",
        "_score":1,
        "_routing":"1",
        "_source":{
            "my_id":"4",
            "text":"这是一个回答2,对应问题1",
            "parent_join_child":{
                "name":"answer",
                "parent":"1"
            }
        }
    }
]           

如果我们想通过父 ID 查询子文档,可以通过如下方式实现:

POST exam_index/_search

{
    "query":{
        "parent_id":{
            "type":"answer",
            "id":"1"
        }
    }
}           

返回结果和上面一样,区别在于parent_id搜索默认使用相关性算分,而has_parent默认情况下不使用算分。

使用父子文档的模式有一些需要特别关注的点:

● 每一个索引只能定义一个join field

● 父子文档必须在同一个分片上,意味着查询,更新操作都需要加上routing

● 可以向一个已经存在的join field上新增关系

● 父子文档,适合那种数据结构基本一致的场景,如果两个表结构完全不一致,不建议使用这种结构

● 父子文档也有缺点,查询速度是这三个方案里面最慢的一个