如果我们想查询某段时间内购买了哪些商品,可以通过如下的联表方式进行查询。
选择。*
来自tb_order t
leftjointb_order_item ttont.order_id=tt.order_id
whereett.product_namelike'%产品名称%'
andt.createTime='2022-06-01 00:00:00'
andt.createTime='2022-06-16 00:00:00';然后,ElasticsSearch 与大多数NoSQL 数据库类似。它具有扁平的存储结构,无法像关系数据库那样通过连接进行搜索。
不过ElasticsSearch(以下简称ES)现在毕竟已经发展到8.x版本了,已经有几种可选的方式来高效支持这种一对多关系的映射和搜索。
常用的实用解决方案有以下三种:
第二种嵌套对象,嵌套文档,父子文档,是我们今天要重点讲的部分。废话不多说,下面以实际案例的形式给大家讲解一下具体的实践思路。
二、案例实践
2.1、嵌套对象
所谓嵌套对象,是指当前json对象中嵌入了一个json对象。以订单数据为例,其中包含多个订单项数据。格式如下:
{
'订单Id':'1',
'订单号':'123456',
'orderUserName':'张三',
'订单项目':[
{
'orderItemId':'12234',
'订单Id':'1',
'产品名称':'火腿肠',
'brandName':'双汇',
'销售价格':'28'
},
{
'orderItemId':'12235',
'订单Id':'1',
'产品名称':'果冻',
'brandName':'汇源',
'销售价格':'12'
}
]
}创建order_index 索引。我们将上面的文档数据存储到ES中,看看索引文档结构的映射是什么样的。内容如下:
{
'订单索引':{
'映射':{
'_doc':{
'属性':{
'订单ID':{
'输入':'关键字'
},
'订单号':{
'输入':'关键字'
},
'订单用户名':{
'输入':'关键字'
},
'订单项目':{
'属性':{
'orderItemId':{
'输入':'关键字'
},
'订单ID':{
'输入':'关键字'
},
'产品名称':{
'输入':'关键字'
},
'品牌名称':{
'输入':'关键字'
},
'销售价格':{
'输入':'关键字'
}
}
}
}
}
}
}
可以清楚地看到,订单索引映射字段中包含了一个orderItems字段,该字段是一个对象类型,内部有自己的字段属性。这实际上是一种包含关系,表示一个订单可以有多个行项目信息。
我们可以查询索引结果集看到结果,并使用postman查询索引下的所有文档数据!
POST order_index/_search
{
'查询':{
“match_all”:{}
}
}返回结果如下(为了方便观察,去掉了一些不重要的数据):
[
{
'_index':'order_index',
'_type':'_doc',
'_id':'1',
'_score':1,
'_源':{
'订单Id':'1',
'订单号':'123456',
'orderUserName':'张三',
'订单项目':[
{
'orderItemId':'12234',
'订单Id':'1',
'产品名称':'火腿肠',
'brandName':'双汇',
'销售价格':'28'
},
{
'orderItemId':'12235',
'订单Id':'1',
'产品名称':'果冻',
'brandName':'汇源',
'销售价格':'12'
}
]
}
}
】 可以清晰的看到返回的结果也完美的呈现出来了。 orderItems实际上是一个列表,包含两个对象,所有信息都在一个文档中。
我们尝试在ES中通过过滤产品名称和品牌名称、两个条件的并集来查询客户的订单信息,并编写一条DSL查询语句来搜索产品名称为火腿肠、品牌为汇源的订单和内容。如下:
POST order_index/_search
{
'查询':{
'布尔':{
‘必须’:[
{
'匹配':{
'orderItems.productName':'火腿香肠'
}
},
{
'匹配':{
'orderItems.brandName':'汇源'
}
}
]
}
}
}返回结果如下(为了方便观察,去掉了一些不重要的数据):
[
{
'_index':'order_index',
'_type':'_doc',
'_id':'1',
'_score':1,
'_源':{
'订单Id':'1',
'订单号':'123456',
'orderUserName':'张三',
'订单项目':[
{
'orderItemId':'12234',
'订单Id':'1',
'产品名称':'火腿肠',
'brandName':'双汇',
'销售价格':'28'
},
{
'orderItemId':'12235',
'订单Id':'1',
'产品名称':'果冻',
'brandName':'汇源',
'销售价格':'12'
}
]
}
}
] 从预期结果分析,没有客户购买过品牌名为汇源、产品名为火腿肠的订单。理论上应该没有数据!
然而结果却是返回了这条订单数据!这是为什么呢?
事实证明,ES 扁平化了json 对象数组。例如上面的例子,ES中存储的结构如下:
{
'订单Id':[1],
'orderItems.productName':['火腿肠','果冻'],
'orderItems.brandName':['双汇','汇源'],
.
}显然,这样的结构就失去了产品名称和品牌名称之间的关联性,导致查询时失败。如果业务需要精准搜索,那么这个方案就不能满足需求。
如果你的业务场景对这个问题不敏感,你可以选择这种方法,因为它足够简单,而且比下面介绍的两种解决方案更加高效。
2.2、嵌套文档
显然,上述对象数组解决方案没有处理内部对象的边界问题。 JSON 数组对象被ES 强制存储到一个平面的键值对列表中。为了解决这个问题,ES推出了所谓的嵌套文档解决方案。官方对该解决方案的介绍如下:
嵌套类型是对象数据类型的特殊版本,它允许以可以彼此独立查询的方式对对象数组进行索引。
可见,嵌套文档方案实际上是对普通内部对象方案的补充。我们将上面订单索引结构中的orderItems 数据类型更改为嵌套类型并重新创建索引。
{
'属性':{
'订单项目':{
'属性':{
.
},
'类型':'嵌套'
}
.
}
}orderItems数据类型,改为nested,表示是嵌入文档,其他属性不变。
我们尝试通过产品名称和品牌名称来查询客户的订单信息。不同的是,查询时需要指定嵌套关键字和路径。查询姿势如下:
POST order_index/_search
{
'查询':{
'嵌套':{
'路径':'orderItems',
'查询':{
'布尔':{
‘必须’:[
{
'匹配':{
'orderItems.productName':'火腿香肠'
}
},
{
'匹配':{
'orderItems.brandName':'汇源'
}
}
]
}
}
}
}
}查询结果为空[],与预期结果一致!
我们更改一下查询条件,以火腿肠的产品名称和双汇的品牌名称查询订单。
POST order_index/_search
{
'查询':{
'嵌套':{
'路径':'orderItems',
'查询':{
'布尔':{
‘必须’:[
{
'匹配':{
'orderItems.productName':'火腿香肠'
}
},
{
'匹配':{
'orderItems.brandName':'双汇'
}
}
]
}
}
}
}
}查询结果如下:
[
{
'_index':'order_index',
'_type':'_doc',
'_id':'1',
'_score':1,
'_源':{
'订单Id':'1',
'订单号':'123456',
'orderUserName':'张三',
'订单项目':[
{
'orderItemId':'12234',
'订单Id':'1',
'产品名称':'火腿肠',
'brandName':'双汇',
'销售价格':'28'
},
{
'orderItemId':'12235',
'订单Id':'1',
'产品名称':'果冻',
'brandName':'汇源',
'销售价格':'12'
}
]
}
}
] 与预期结果一致,说了这么多,看来嵌套文档还是很有用的。之前的方案不存在丢失对象边界的问题,而且使用起来并不显得复杂。那么它就没有缺点了吗?当然,我们先来做一个实验。
首先查看当前索引的文档数量。
GET _cat/indices?v 查询结果。
green open order_index FJsEIFf_QZW4Q4SlZBsqJg113017.7kb8.8kb 你可能注意到了,我这里没有使用下面的一个来查看文档数量。
GET order_index/_count 直接查看索引信息。它们之间的区别是:
第一个查询是查询索引数据库中每个索引的文档总数。
第二个查询是查询当前索引的文档数量,不包括嵌套文档数量。
可以明显看到order_index索引,ES中文档数据总计为3,为什么不是1?
这是因为嵌套的子文档实际上是ES内部的一个独立的lucene文档。然而,当我们查询时,ES内部为我们做了一个类似数据库的join过程。它最终看起来像一个独立的文档。
如果一个订单有1000个订单项,那么ES中存在的文档数量为1001个,随着订单数量的增加,这个数量将增加一倍。
可以想象,在同等条件下,这种方案的性能肯定不如普通内部对象。在实际业务应用中,需要根据实际情况决定是否选择该方案。
有一件事是肯定的,它可以满足内部对象数据的精确搜索的要求!
2.3、父子文档
我们还是看上面的例子。如果我需要更新文档的orderNo属性的值,ES更新文档的操作原理是:删除原来的数据,再插入一条,但是索引id是一样的。
这意味着即使我不需要更新orderItems 字段,它也会与主文档一起重新索引。
另外,如果某个表与某个表之间存在多对多关系,比如一个子文档可以属于多个主文档的场景,则无法通过嵌套来实现。这种情况,可以考虑使用父子文档结构来处理。
我们以考试题为例。一个问题可能有多个答案,一个答案可能对应多个问题。
首先我们定义索引文档结构如下:
PUT 考试索引
{
'映射':{
'_doc':{
'属性':{
'我的_id':{
'输入':'关键字'
},
'parent_join_child':{
'输入':'加入',
'关系':{
'问题':'答案'
}
}
}
}
}
}my_id是自定义字段,parent_join_child是我们父子文档关系的名称,这个可以自定义,join表示这是一个父子文档关系,relations表示问题是父子文档关系,答案是孩子。
首先我们插入两个父文档。
PUT exam_index/_doc/1
{
'我的_id':'1',
'text':'这是问题1',
'parent_join_child':{
'姓名':'问题'
}
}
PUT exam_index/_doc/2
{
'我的_id':'2',
'text':'这是问题2',
'parent_join_child':{
'姓名':'问题'
}
}其中'name':'question'表示插入父文档。
然后插入两个子文档。
PUT exam_index/_doc/3?routing=1
{
'我的_id':'3',
'text':'这是答案1,对应问题1',
'parent_join_child':{
'姓名':'答案',
'父级':'1'
}
}
PUT exam_index/_doc/4?routing=1
{
'我的_id':'4',
'text':'这是答案2,对应问题1',
'parent_join_child':{
'姓名':'答案',
'父级':'1'
}
关于子文档有很多东西需要解释。首先,我们可以从文档id判断子文档是独立文档(不同于嵌套)。其次,routing关键字指定路由id为父文档1。这个id与下面parent关键字对应的id一致。
需要强调的是,在索引子文档时路由是必须的,因为需要保证子文档和父文档在同一个分片上。
'name':'answer' 关键字表明这是一个子文档。
现在exam_index索引中有四个独立文档。我们来看看父子文档在搜索时的姿势。
让我们从无条件查询开始,返回所有文档数据。
发布exam_index/_search
{
'查询':{
'match_all':{
}
},
'排序':['my_id']
}返回结果如下:
[
{
'_index':'crm_exam_index',
'_type':'_doc',
'_id':'1',
'_score':null,
'_源':{
'我的_id':'1',
'text':'这是问题1',
'parent_join_child':{
'姓名':'问题'
}
}
},
{
'_index':'crm_exam_index',
'_type':'_doc',
'_id':'2',
'_score':null,
'_源':{
'我的_id':'2',
'text':'这是问题2',
'parent_join_child':{
'姓名':'问题'
}
}
},
{
'_index':'crm_exam_index',
'_type':'_doc',
'_id':'3',
'_score':null,
'_路由':'1',
'_源':{
'我的_id':'3',
'text':'这是答案1,对应问题1',
'parent_join_child':{
'姓名':'答案',
'父级':'1'
}
}
},
{
'_index':'crm_exam_index',
'_type':'_doc',
'_id':'4',
'_score':null,
'_路由':'1',
'_源':{
'我的_id':'4',
'text':'这是答案2,对应问题1',
'parent_join_child':{
'姓名':'答案',
'父级':'1'
}
}
}
] 可以看到返回的结果带有parent_join_child关键字,表示这是父文档还是子文档。
如果我们想通过子文档信息查询父文档,可以通过以下方式实现:
发布exam_index/_search
{
'查询':{
'has_child':{
'输入':'答案',
'查询':{
'匹配':{
'文本':'答案'
}
}
}
}
}返回结果:
[
{
'_index':'考试索引',
'_type':'_doc',
'_id':'1',
'_score':1,
'_源':{
'我的_id':'1',
'text':'这是问题1',
'parent_join_child':{
'姓名':'问题'
}
}
}
] 如果我们想通过父文档信息查询子文档,可以通过以下方式实现:
发布exam_index/_search
{
'查询':{
'has_parent':{
'parent_type':'问题',
'查询':{
'匹配':{
'文本':'问题'
}
}
}
}
}返回结果:
[
{
'_index':'crm_exam_index',
'_type':'_doc',
'_id':'3',
'_score':1,
'_路由':'1',
'_源':{
'我的_id':'3',
'text':'这是答案1,对应问题1',
'parent_join_child':{
'姓名':'答案',
'父级':'1'
}
}
},
{
'_index':'crm_exam_index',
'_type':'_doc',
'_id':'4',
'_score':1,
'_路由':'1',
'_源':{
'我的_id':'4',
'text':'这是答案2,对应问题1',
'parent_join_child':{
'姓名':'答案',
'父级':'1'
}
}
}
】 如果我们想通过父文档ID来查询子文档,可以通过以下方式实现:
发布exam_index/_search
{
'查询':{
'parent_id':{
'输入':'答案',
'id':'1'
}
}
}返回结果与上面相同,不同的是parent_id搜索默认使用相关性分数,而has_parent默认不使用分数。
使用父子文档模式时有一些需要特别注意的点:
每个索引只能定义一个连接字段。父子文档必须在同一个分片上,这意味着查询和更新操作需要路由。您可以将新关系添加到现有联接字段。父子文档适用于数据结构基本相同的场景。如果两个表结构完全不一致,不建议使用这种结构。亲子文件也有不足之处。查询速度是三种方案中最慢的。
三、小结
综上所述,嵌套对象通过冗余数据来提高查询性能,适合读多写少的场景。由于ES会将json数组对象压平,因此对嵌入对象的搜索不会很准确。如果业务场景搜索要求不高,推荐此方案。
如果业务场景需要精准搜索,可以使用嵌套文档解决方案。每次更新都会将文档数据删除然后再次插入,写入和查询性能会比嵌套对象低。
如果表之间是多对多的场景,可以采用父子文档的解决方案。每次更新只会更新单个文档的数据,写入会比嵌套文档更快。缺点是它的查询速度会比同一份文档要快。嵌套文档查询速度慢5 到10 倍!
具体的方案选择还需要根据当前的业务场景合理做出。
本文采摘于网络,不代表本站立场,转载联系作者并注明出处:https://www.iotsj.com//kuaixun/7016.html
用户评论
哇,终于找到一篇讲解ES嵌套JSON对象的查询了!一直卡在这个地方。
有9位网友表示赞同!
看标题感觉很详细,终于不用再在网上找来一堆资料看了。
有10位网友表示赞同!
希望能涵盖各种常见场景的查询方法,这样更有用!
有6位网友表示赞同!
我对嵌套更新操作比较感兴趣,希望这篇文章也能讲明白。
有13位网友表示赞同!
我试过一些开源库,效果都不太理想,希望能找到更好方法。
有10位网友表示赞同!
ES的文档有时候看不太明白,这篇文章能起到很好的辅助学习作用。
有12位网友表示赞同!
学习查询技巧很关键,这个题目我很需要!
有19位网友表示赞同!
我对JSON数据经常会用到嵌套操作,想多了解一些高级技巧。
有19位网友表示赞同!
文章讲的通俗易懂,适合初学者学习 ES 的基础知识。
有13位网友表示赞同!
分享一下我遇到的查询问题吧,看看能不能得到解答!
有20位网友表示赞同!
希望这篇文章能提供一些实用的代码示例,这样更容易理解。
有17位网友表示赞同!
我很喜欢ES的特性,但这嵌套查询一直让我头疼。
有5位网友表示赞同!
期待学习到更多关于JSON对象查询和更新的技巧!
有15位网友表示赞同!
最近的项目遇到很多这类问题,这篇文应该能帮上忙。
有13位网友表示赞同!
收藏这篇文章,下次用的时候再回来看一看。
有8位网友表示赞同!
ES确实很好用,但是文档太难懂了,希望能有更多这样的好文章!
有17位网友表示赞同!
学习新的技术总是很费力,希望这篇文章能让我省去不少时间!
有9位网友表示赞同!
对这些查询方法的理解对我做项目非常重要!
有9位网友表示赞同!
嵌套对象的更新操作确实比较复杂,希望能有更好理解的方法。
有7位网友表示赞同!
期待作者能够分享更多关于ES进阶技巧的文章!
有5位网友表示赞同!