Elasticsearch搜索服务学习之十二——数据建模(三)——父子关系模型

高新技术,ElasticSearch

2017-09-04

279

0

目录


父子关系模型

这种模型类似于上一篇所讲述的嵌套对象(nested object),都是将两个对象做关联,但是他们是有区别的:

  • 在嵌套对象中,对象都在同一个文档中,文档ID相同,只是在逻辑上标记出嵌入对象,实际上处于一个文档,查询文档的时候同时会将文档的所有嵌入文档也查询出来(目前我用的5.4版本暂不支持查询时按照条件过滤嵌套对象)。
  • 父子关系模型中,父对象和子对象都是相互独立的文档,只是通过映射(mapping)将索引的type指向另一个type,从而建立关联关系,形成一对多的关系。

相对于嵌套对象,父子关系模型主要有以下的优势:

  • 父子对象相对独立,更新是互不影响。更新父文档不会重新索引子文档,更新子文档同样不会重新父文档。而嵌套对象则不然,由于嵌套对象和主文档为同一个文档,更新嵌套对象整个文档都需要更新。
  • 创建、修改或删除子文档时,不会影响父文档或其他子文档。这一点在这种场景下尤其有用:子文档数量较多,并且子文档创建和修改的频率高时。
  • 子文档可以作为搜索结果独立返回,只是搜索的时候需要指定父文档id(稍后讲解)。

在es中,父子关系模型查询操作效率很高,这是因为es维护了父子的映射关系。但是也有一个限制条件,即父文档和所有子文档都必须存储在同一个分片中。为什么呢?这跟es的路由有关,在创建子文档时必须指定父文档id,es需要这个id去查找子文档,并且这个id还决定稳定分片路由。

即是说:父文档id有两个作用:建立父子关联关系,同时通过该id进行分片路由,这样就保证父子文档处于同一分片。

父-子文档ID映射存储在  Doc Values 中。当映射完全在内存中时, Doc Values 提供对映射的快速处理能力,另一方面当映射非常大时,可以通过溢出到磁盘提供足够的扩展能力。

父-子关系文档映射

建立父-子文档映射关系时只需要指定某一个文档 type 是另一个文档 type 的父亲,格式如下:
"_parent": {
  "type": "父文档类型"
}
这个映射关系可以在如下两个时间点设置:
1)创建索引时;
2)在子文档 type 创建之前更新父文档的 mapping。
 
举例说明,有一个公司在多个城市有分公司,并且每一个分公司下面都有很多员工。有这样的需求:按照分公司、员工的维度去搜索,并且把员工和他们工作的分公司联系起来。
 
我们需要告诉Elasticsearch,在创建员工 employee 文档 type 时,指定分公司 branch 的文档 type 为其父亲。
PUT /company
{
    "mappings": {
        "branch": {},
        "employee": {
            "_parent": {
                "type": "branch"
            }
        }
    }
}

上边的映射定义了两个文档类型,“branch”和“employee”,其中employee指定了父文档类型为branch,即是说employee是branch的子类型。

构建父-子文档索引

为父文档创建索引与为普通文档创建索引没有区别,父文档并不需要知道它有哪些子文档。
POST /company/branch/_bulk
{ "index": { "_id": "london" }}
{ "name": "London Westminster", "city": "London", "country": "UK" }
{ "index": { "_id": "liverpool" }}
{ "name": "Liverpool Central", "city": "Liverpool", "country": "UK" }
{ "index": { "_id": "paris" }}
{ "name": "Champs Élysées", "city": "Paris", "country": "France" }
创建子文档时,用户 必须要通过 parent 参数来指定该子文档的父文档 ID
PUT /company/employee/1?parent=london 
{
  "name":  "Alice Smith",
  "dob":   "1970-10-24",
  "hobby": "hiking"
}
前边已经提到,这里的parent的id“london”有两个作用:创建了父文档和子文档之间的关系,并且保证了父文档和子文档都在同一个分片上。路由分片的计算规则,参看 这里
 
默认es采用文档的id进行路由计算,如果指定了父文档的 ID,那么就会使用父文档的 ID 进行路由。也就是说,如果父文档和子文档都使用相同的值进行路由,那么父文档和子文档都会确定分布在同一个分片上。
 

父文档ID

在执行单文档的请求时需要指定父文档的 ID,单文档请求包括:通过 GET 请求获取一个子文档;创建、更新或删除一个子文档。而执行搜索请求时是不需要指定父文档的ID,这是因为搜索请求是向一个索引中的所有分片发起请求,而单文档的操作是只会向存储该文档的分片发送请求。因此,如果操作单个子文档时不指定父文档的 ID,那么很有可能会把请求发送到错误的分片上。
 
父文档的 ID 应该在 bulk API 中指定:
POST /company/employee/_bulk
{ "index": { "_id": 2, "parent": "london" }}
{ "name": "Mark Thomas", "dob": "1982-05-16", "hobby": "diving" }
{ "index": { "_id": 3, "parent": "liverpool" }}
{ "name": "Barry Smith", "dob": "1979-04-01", "hobby": "hiking" }
{ "index": { "_id": 4, "parent": "paris" }}
{ "name": "Adrien Grand", "dob": "1987-05-11", "hobby": "horses" }

 修改子文档的父文档ID

如果你想要改变一个子文档的 parent 值,仅通过更新这个子文档是不够的,因为新的父文档有可能在另外一个分片上。因此, 你必须要先把子文档删除,然后再重新索引这个子文档。
# 删除子文档
DELETE /company/employee/1?parent=原来的父文档id
# 重新索引子文档
PUT /company/employee/1?parent=新的父文档id

通过子文档查询父文档

has_child 的查询和过滤可以通过子文档的内容来查询父文档。 例如,我们根据如下查询,可查出所有80后员工所在的分公司:
GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type": "employee",
      "query": {
        "range": {
          "dob": {
            "gte": "1980-01-01"
          }
        }
      }
    }
  }
}
类似于 嵌套查询,has_child查询可以匹配多个子文档,并且每一个子文档的评分都不同。但是由于每一个子文档都带有评分,这些评分如何规约成父文档的总得分取决于 score_mode 这个参数。该参数有多种取值策略:默认为  none ,会忽略子文档的评分,并且会给父文档评分设置为 1.0 ; 除此以外还可以设置成  avg 、 min 、 max 和  sum 
 
下面的查询将会同时返回 london 和 liverpool ,不过由于 Alice Smith 要比 Barry Smith 更加匹配查询条件,因此 london 会得到一个更高的评分。
GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type":       "employee",
      "score_mode": "max",
      "query": {
        "match": {
          "name": "Alice Smith"
        }
      }
    }
  }
}
score_mode 为默认的 none 时,会显著地比其模式要快,这是因为Elasticsearch不需要计算每一个子文档的评分。 只有当你真正需要关心评分结果时,才需要为 source_mode 设值,例如设成 avg 、 min 、 max 或 sum 。

min_children 和 max_children

has_child 的查询和过滤都可以接受这两个参数:min_children 和 max_children 。 使用这两个参数时,只有当子文档数量在指定范围内时,才会返回父文档。
如下查询只会返回至少有两个雇员的分公司:
GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type":         "employee",
      "min_children": 2, // 至少有两个雇员的公司才符合查询条件
      "query": {
        "match_all": {}
      }
    }
  }
}
带有 min_children 和 max_children 参数的 has_child 查询或过滤,和允许评分的 has_child 查询的性能非常接近。

has_child Filter

has_child 查询和过滤在运行机制上类似,区别是 has_child 过滤 不支持 source_mode 参数。has_child 过滤仅用于筛选内容--如内部的一个 filtered 查询--和其他过滤行为类似: 包含或者排除,但没有进行评分
has_child 过滤的结果没有被缓存,但是 has_child 过滤内部的过滤方法适用于通常的缓存规则。
 
即是说,has_child过滤不会计算评分,而has_child查询可以支持评分。

注:在我用的5.4版本中,使用has_child filter会出现警告,Deprecated field [filter] used, expected [query] instead,不知道是不是官方已经取消。其实设置了source_mode为none,功能跟filter类似。

通过父文档查询子文档

虽然 nested 查询只能返回最顶层的文档 ,但是父文档和子文档本身是彼此独立并且可被单独查询的。我们使用 has_child 语句可以基于子文档来查询父文档,使用 has_parent 语句可以基于子文档来查询父文档。
 
虽然 nested 查询只能返回最顶层的文档 ,但是父文档和子文档本身是彼此独立并且可被单独查询的。我们使用 has_child 语句可以基于子文档来查询父文档,使用 has_parent 语句可以基于子文档来查询父文档。
has_parent 和 has_child 非常相似,下面的查询将会返回所有在 UK 工作的雇员:
GET /company/employee/_search
{
  "query": {
    "has_parent": {
      "type": "branch", // 返回父文档 type 是 branch 的所有子文档
      "query": {
        "match": {
          "country": "UK"
        }
      }
    }
  }
}
has_parent 查询也支持 score_mode 这个参数,但是该参数只支持两种值: none (默认)和 score 。每个子文档都只有一个父文档,因此这里不存在将多个评分规约为一个的情况, score_mode 的取值仅为 score 和 none 。

不带评分的 has_parent 查询

当 has_parent 查询用于非评分模式(比如 filter 查询语句)时, score_mode 参数就不再起作用了。因为这种模式只是简单地包含或排除文档,没有评分,那么 score_mode 参数也就没有意义了。

子文档聚合

在父-子文档中支持  子文档聚合,这一点和 嵌套聚合 类似。但是,对于父文档的聚合查询是不支持的(和 reverse_nested 类似)。
我们通过下面的例子来演示按照国家维度查看最受雇员欢迎的业余爱好:
GET /company/branch/_search
{
  "size" : 0,
  "aggs": {
    "country": {
      "terms": { // country 是 branch 文档的一个字段。
        "field": "country"
      },
      "aggs": {
        "employees": {
          "children": { // 子文档聚合查询通过 employee type 的子文档将其父文档聚合在一起。
            "type": "employee"
          },
          "aggs": {
            "hobby": {
              "terms": { // hobby 是 employee 子文档的一个字段。
                "field": "hobby"
              }
            }
          }
        }
      }
    }
  }
}

总结

嵌套对象模型有一定的局限性,而父子关系模型比较而言用途更为广泛,两者都有各自的适用场景:

  • 嵌套对象适用于子文档较少,更新又不会太频繁的场景;
  • 父子关系模型适用于子文档较多、更新频繁,或者子文档需要支持单独查询的场景。

在es中还有一种祖孙关系模型,与父子关系模型类似,只是层级更多。


前一篇:Elasticsearch搜索服务学习之十二——数据建模(二)——嵌套对象
后一篇:elasticsearch路由一个文档到一个分片

belonk

轻轻地我走了,正如我轻轻地来,我挥一挥衣袖,不带走一片云彩