简介
- 应用层关联
- 内部对象
- 嵌套对象
- 父子关系文档
- ES Version: 5.1.1
场景
假定需要在ES中存储以下两类信息(用户打车记录):
- 用户信息:user_id,user_name
- 订单信息:order_id,from(出发地),to(目的地),cost(打车费用)
范式存储+应用层关联查询application-side-joins
范式存储
所谓范式存储,就是遵从类似于关系型数据库的范式规则,进行数据存储。
简单来说,就是每类实体单独存储,实体间的关联关系通过外键连接。
根据三大范式,可简单得到如下的存储结构:
PUT /taxi_system/user/1
{
"user_id":1,
"user_name":"Jack"
}
PUT /taxi_system/order/1
{
"order_id":10001,
"user_id":1,
"from":"Beijing",
"to":"Shanghai",
"cost":420
}
其中,user
和order
之前通过字段user_id
进行关联。
应用层关联查询
查询Jack
是否从Beijing
打过车的处理过程:
-
查询
user
获取Jack
的user_id
:GET /taxi_system/user/_search { "query": { "match": { "user_name": "Jack" } } }
-
根据
user_id
,查询order
,获取是否存from=Beijing
的记录:GET /taxi_system/order/_search { "query": { "bool": { "must": [ { "match": { "user_id": 1 } },{ "match": { "from": "Beijing" } } ] } } }
也就是说,通过两次查询,最终得到了想要的结果。
总结说明
优点:
- 索引之间完全独立,互不影响。
- 易与数据库表结构一一对应,方便增量同步。
缺点:
- 只适用于
第一层实体数据量少
的情景,否则后续查询参数过多,难以处理。 - 需要多次查询。
- 对聚集查询的支持较差。
适用场景:
- 第一层实体数据量少。
非范式的内部对象data denormalization
内部对象
所谓内部对象,就是指将json
格式的数据直接存储至ES,ES通过动态映射
生成的存储结构。
例如,我们考虑以json
的形式存储用户信息和订单信息,ES会通过动态映射
生成的相应的存储结构:
PUT /taxi_system/user_order/1
{
"user_id":1,
"user_name":"Jack",
"order":[
{
"order_id":10001,
"user_id":1,
"from":"Beijing",
"to":"Shanghai",
"cost":420
}
]
}
通过GET /taxi_system/user_order/_mapping
查询其存储结构如下:
{
"taxi_system": {
"mappings": {
"user_order": {
"properties": {
"order": {
"properties": {
"cost": {
"type": "long"
},
"from": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"order_id": {
"type": "long"
},
"to": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"user_id": {
"type": "long"
}
}
},
"user_id": {
"type": "long"
},
"user_name": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
}
关联查询
查询Jack
是否从Beijing
打过车的处理过程:
GET /taxi_system/user_order/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"user_name": "Jack"
}
},{
"match": {
"order.from": "Beijing"
}
}
]
}
}
}
也就是说,只需要一次查询就可以得到结果。
陷进
从上面的内容来看,内部对象很美好,不仅存储简单(形如json),而且查询也很方便。
其实,内部对象也存在很大的不足,这要从其索引结构来理解。
我们知道ES的本质是Lucene
,Lucene
文档就是一组键值对列表构成的。
考虑如下user_order
对象:
PUT /taxi_system/user_order/2
{
"user_id":2,
"user_name":"Smith",
"order":[
{
"order_id":10002,
"user_id":2,
"from":"Beijing",
"to":"Shanghai",
"cost":420
},
{
"order_id":10003,
"user_id":2,
"from":"Shanghai",
"to":"Beijing",
"cost":300
}
]
}
ES本身不理解对象的关联关系,为了有效的索引内部对象,它将上述user_order
存储成如下形式:
{
"user_id":[2],
"user_name":["Smith"],
"order.order_id":[10002,10003],
"order.user_id":[2],
"order.from":["Beijing","Shanghai"],
"order.to":["Shanghai,Beijing"],
"order.cost":[420,300]
}
现在,查询Smith
用户是否存在从Beijing
出发且车费小于400
的订单:
GET /taxi_system/user_order/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"user_name": "Smith"
}
},{
"range": {
"order.cost": {
"lte": 400
}
}
}
]
}
}
}
实际情况是不存在这样的订单,但是通过这个查询却能够查询出来。
问题的根本在于,一个user_order
记录的多个内部对象是共同存储的,ES无法区分他们。
也就是说:
- 如果搜索针对的是内部对象的单一字段,查询结果是正确的;
- 如果针对的是内部对象的多个字段,则不能保证查询结果的正确性。
- 此结论同样适用于聚集操作。
总结说明
优点:
- 无需多次查询。
- 无需额外处理,直接将json对象通过动态映射进行存储。
- 扁平化存储,搜索与索引快速而无锁。
缺点:
- 内部对象与根文档是同一个文档,灵活性很低,所以无法单独修改内部对象,需要修改整个文档。
- es结构与db结构难以一一对应,不利于增量同步。
- 丢失了内部对象字段的关联性,当对内部对象多字段进行查询或聚集时,结果不准确。
适用场景:
- 一对少量内部对象 且 数据更新不频繁。
- 最理想的情况,内部对象只要一个字段,或者所有查询和聚集只针对一个字段。
嵌套对象nested object
嵌套对象
嵌套对象需要手动设置其映射结构,例如:
PUT /taxi_system_n
{
"mappings": {
"user_order": {
"properties": {
"order": {
"type": "nested",
"properties": {
"cost": {
"type": "long"
},
"from": {
"type": "text"
},
"order_id": {
"type": "long"
},
"to": {
"type": "text"
},
"user_id": {
"type": "long"
}
}
},
"user_id": {
"type": "long"
},
"user_name": {
"type": "text"
}
}
}
}
}
P.s.:关键在于"type": "nested",
这个配置。
然后,添加数据:
PUT /taxi_system_n/user_order/2
{
"user_id":2,
"user_name":"Smith",
"order":[
{
"order_id":10002,
"user_id":2,
"from":"Beijing",
"to":"Shanghai",
"cost":420
},
{
"order_id":10003,
"user_id":2,
"from":"Shanghai",
"to":"Beijing",
"cost":300
}
]
}
关联查询
查询Smith
是否从Beijing
打过车的处理过程:
GET /taxi_system_n/user_order/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"user_name": "Smith"
}
},{
"nested": {
"path": "order",
"query": {
"bool": {
"must": [
{
"match": {
"order.from": "Beijing"
}
}
]
}
}
}
}
]
}
}
}
虽然嵌套对象对于关联查询也可以一次获得结果,但是需要注意其查询形式为nested
查询,而且需要指定嵌套对象的path
。
嵌套对象多字段查询
对比与内部对象,继续查询Smith
用户是否存在从Beijing
出发且车费小于400
的订单:
GET /taxi_system_n/user_order/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"user_name": "Smith"
}
},
{
"nested": {
"path": "order",
"query": {
"bool": {
"must": [
{
"match": {
"order.from": "Beijing"
}
},{
"range": {
"order.cost": {
"lte": 400
}
}
}
]
}
}
}
}
]
}
}
}
查询结果为空,因为"user_id":2
这条记录中的嵌套对象会以独立索引的形式隐藏存储
在文档内部,形式如下:
# 根文档
{
"user_id":[2],
"user_name":["Smith"],
}
{
"order.order_id":[10002],
"order.user_id":[2],
"order.from":["Beijing"],
"order.to":["Shanghai"],
"order.cost":[420]
}
{
"user_id":[2],
"user_name":["Smith"],
"order.order_id":[10003],
"order.user_id":[2],
"order.from":["Shanghai"],
"order.to":[Beijing"],
"order.cost":[300]
}
通过这种存储结构,嵌套对象的内部字段关联性得以保存,所以可以多字段查询。
总结说明
优点:
- 嵌套对象是
隐藏存储
在父文档中,速度和单独存储几乎一样。 - 每个嵌套对象单独存储,其字段之间的关联性得以保存,对象之间互不影响。
缺点:
- 需要手动创建映射结构,添加
nested
属性。 - 嵌套对象是
隐藏存储
在父文档中,灵活性较低,无法获取单独的嵌套对象文档,获取的是整个文档。 - 如果需要增删改一个嵌套对象,需要重新索引整个文档才可以,嵌套文档越多,这带来的成本就越大。
nested object
无法通过通用查询方式,需要使用nested query、nested filter、nested facet
等相对复杂的查询方式。
适用场景:
- 一对少量嵌套对象 且 数据更新不频繁。
- 可对嵌套对象的多个字段进行查询和聚集。
父子关系文档father/children
父子关系文档
建立父子关系说明
- 只需要指定一个文档的父文档是谁即可。
- 设置时间点:创建index时设置父子关系
- 2.x不支持的设置方式:创建子文档的mapping之前更新父文档的mapping。
建立父子关系演示
-
建立父子关系
PUT /taxi_system_fcc { "mappings": { "user": {}, "order":{ "_parent": { "type": "user" } } } }
-
创建父文档
PUT /taxi_system_fcc/user/1 { "user_id":1, "user_name":"Jack" } PUT /taxi_system_fcc/user/2 { "user_id":2, "user_name":"Smith" }
-
创建子文档
PUT /taxi_system_fcc/order/10001?parent=1 { "order_id":10001, "from":"Beijing", "to":"Shanghai", "cost":420 } PUT /taxi_system_fcc/order/10002?parent=1 { "order_id":10002, "from":"Shanghai", "to":"Beijing", "cost":320 } PUT /taxi_system_fcc/order/10003?parent=1 { "order_id":10003, "from":"Shanghai", "to":"Tianjin", "cost":700 } PUT /taxi_system_fcc/order/10004?parent=2 { "order_id":10004, "from":"Shanghai", "to":"Tianjin", "cost":300 } PUT /taxi_system_fcc/order/10005?parent=2 { "order_id":10005, "from":"Beijing", "to":"Tianjing", "cost":300 }
通过子文档查询父文档
查询去过北京(to=Beijing)的用户:
GET /taxi_system_fcc/user/_search
{
"query": {
"has_child": {
"type": "order",
"query": {
"match": {
"to": "Beijing"
}
}
}
}
}
查询最多打过两次车的用户
GET /taxi_system_fcc/user/_search
{
"query": {
"has_child": {
"type": "order",
"max_children": 2,
"query": {
"match_all": {}
}
}
}
}
查询至少打过3次车的用户
GET /taxi_system_fcc/user/_search
{
"query": {
"has_child": {
"type": "order",
"min_children": 3,
"query": {
"match_all": {}
}
}
}
}
通过父文档查询子文档
查询user_id=2的所有打车记录
GET /taxi_system_fcc/order/_search
{
"query": {
"has_parent": {
"parent_type": "user",
"query": {
"match": {
"user_id": 2
}
}
}
}
}
父文档聚集查询
以用户姓名为维度,进行统计分析
GET /taxi_system_fcc/user/_search
{
"aggs": {
"by_name": {
"terms": {
"field": "user_name.keyword",
"size": 10000
}
}
}
}
子文档聚集查询
以打车记录的出发地为维度,进行统计分析:
GET /taxi_system_fcc/user/_search
{
"aggs": {
"by_orders_from": {
"children": {
"type": "order"
},
"aggs": {
"by_from": {
"terms": {
"field": "from.keyword",
"size": 10000
}
}
}
}
}
}
三代关系
建立三代关系:公司-用户-打车记录
PUT /taxi_system_fccc
{
"mappings": {
"company": {},
"user": {
"_parent": {
"type": "company"
}
},
"order": {
"_parent": {
"type": "user"
}
}
}
}
通过bulk添加company数据
POST /taxi_system_fccc/company/_bulk
{"index":{"_id":"c1"}}
{"name":"KFC"}
{"index":{"_id":"c2"}}
{"name":"MDL"}
通过bulk添加user数据
POST /taxi_system_fccc/user/_bulk
{"index":{"_id":"u1","parent":"c1"}}
{"name":"Jack","user_id":"u1"}
{"index":{"_id":"u2","parent":"c1"}}
{"name":"Smith","user_id":"u2"}
{"index":{"_id":"u3","parent":"c2"}}
{"name":"James","user_id":"u3"}
{"index":{"_id":"u4","parent":"c2"}}
{"name":"Clock","user_id":"u4"}
通过bulk添加order数据
POST /taxi_system_fccc/order/_bulk
{"index":{"_id":"o1","parent":"u1","routing":"c1"}}
{"order_id":"o1","from":"Beijing","to":"Shanghai","cost":444}
{"index":{"_id":"o2","parent":"u1","routing":"c1"}}
{"order_id":"o2","from":"Beijing","to":"Tianjin","cost":333}
{"index":{"_id":"o3","parent":"u1","routing":"c1"}}
{"order_id":"o3","from":"Beijing","to":"Shanghai","cost":222}
{"index":{"_id":"o4","parent":"u2","routing":"c1"}}
{"order_id":"o4","from":"Tianjin","to":"Shanghai","cost":111}
{"index":{"_id":"o5","parent":"u3","routing":"c2"}}
{"order_id":"o5","from":"Shanghai","to":"Tianjin","cost":333}
{"index":{"_id":"o6","parent":"u3","routing":"c2"}}
{"order_id":"o6","from":"Beijing","to":"Xianggang","cost":400}
注意:"routing":"c1"
用于确保孙文档与父文档和子文档处于同一分片。原因参考官方文档。
查询:KFC公司中是否存在员工从Tianjin打过车
GET taxi_system_fccc/company/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "KFC"
}
},{
"has_child": {
"type": "user",
"query": {
"has_child": {
"type": "order",
"query": {
"match": {
"from": "Tianjin"
}
}
}
}
}
}
]
}
}
}
总结说明
优点:
- 父对象和子对象都是完全独立的文档,非常灵活,各自更新互不影响。
- 子文档可以作为搜索结果单独返回。
缺点:
- 父-子文档ID映射存储在
Doc Values
中,为了维护父子关系,父子文档必须保存着统一分片中。 - 2.x的父子关系需要父文档和子文档在创建index时指定,难以追加。
- 牺牲查询性能换取索引性能,内存和CPU消耗较多,比
嵌套对象
方式慢5~10倍。 - 因
父子文档必须处于同一分片
的限制,在滚动索引和多索引联合查询场景支持不好。
适用场景:
- 一对大量子对象 且不是海量。
- 可对子对象的多个字段进行查询和聚集。
- ES索引index是全新的。
- 父子关系变得概率极小。