ElasticSearch处理关联关系的方式-应用层关联、内部对象、嵌套对象、父子关系文档

简介

  • 应用层关联
  • 内部对象
  • 嵌套对象
  • 父子关系文档
  • 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
}

其中,userorder之前通过字段user_id进行关联。

应用层关联查询

查询Jack是否从Beijing打过车的处理过程:

  1. 查询user获取Jackuser_id:

    GET /taxi_system/user/_search
    {
      "query": {
        "match": {
          "user_name": "Jack"
        }
      }
    }
    
  2. 根据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的本质是LuceneLucene文档就是一组键值对列表构成的。

考虑如下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

父子关系文档

建立父子关系说明

建立父子关系演示

  1. 建立父子关系

    PUT /taxi_system_fcc
    {
      "mappings": {
        "user": {}, 
        "order":{
          "_parent": {
            "type": "user"
          }
        }
      }
    }
    
  2. 创建父文档

    PUT /taxi_system_fcc/user/1
    {
      "user_id":1,
      "user_name":"Jack"
    }
    PUT /taxi_system_fcc/user/2
    {
      "user_id":2,
      "user_name":"Smith"
    }
    
  3. 创建子文档

    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是全新的。
  • 父子关系变得概率极小。
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值