1、前言

对于solr来说是无法做两个collection之间的关联的,es是否可以做到类似于表的join关联那,这就是本篇需要研究的内容,

主要参考内容是官方文档。

先说下结论,如果不做特殊处理,es是无法完成类似与表Join的关联查询的。

2、ES如何做关联

官网里面有几种支持关联查询的办法:

2.1 应用程序做关联

这个没有什么好说的,其实不算真正的关联,需要先查询一个索引,得到值构造出条件再去查询另外一个索引。

  缺点也很明显,就是需要查询多次。

2.2 冗余数据

简单的来说就是将需要查询的数据保存在一起,举个例子如下:

    假设有用户索引和用户发布博客的索引,需要将用户和博客信息关联起来,对于用户来说,我们只需要取得用户的姓名信息即可,则可以这样做:

es索引和mysql索引区别 es索引关联_elasticsearch

 

可以看下map信息如下:

实际在es内,已经将user下面的id和name进行了扁平化处理,可以通过如下的方式查询:

 

es索引和mysql索引区别 es索引关联_elasticsearch_02

 

es索引和mysql索引区别 es索引关联_搜索引擎_03

 

优点:查询速度非常快,缺点是存在数据的冗余。

 

2.3 嵌套对象

在ES中,对单个文档的增删改都是原子操作,有时候为了方便我们将实体和它相关的明细是放在一个文档中存储的。比如论坛发的帖子和它的回复信息。

其实和冗余对象有点类似,但是如果只是做查询会发现有问题,因为es扁平处理之后:

{
  "title":            [ eggs, nest ],
  "body":             [ making, money, work, your ],
  "tags":             [ cash, shares ],
  "comments.name":    [ alice, john, smith, white ],
  "comments.comment": [ article, great, like, more, please, this ],
  "comments.age":     [ 28, 31 ],
  "comments.stars":   [ 4, 5 ],
  "comments.date":    [ 2014-09-01, 2014-10-22 ]
}

tilte、body、tags被称为父文档或根文档。

这样数组内之间是没有顺序关系的,这就导致了后面的查询仍然可以查到数据,嵌套对象是为了解决这个问题的,先看下普通的对象:

 

es索引和mysql索引区别 es索引关联_大数据_04

 

{
  "query": {
    "bool": {
      "must": [
        { "match": { "comments.name": "Alice" }},
        { "match": { "comments.age":  28      }} ,
        { "match": { "comments.comment.keyword":  "Great article"     }}
      ]
    }
  }
}

es索引和mysql索引区别 es索引关联_es索引和mysql索引区别_05

 

嵌套对象,上面的例子是没有定义map的情况直接发送数据,comments被定义为object,失去了数组内的顺序关系,如果先定义了nested对象,则如下:

es索引和mysql索引区别 es索引关联_大数据_06

 

es索引和mysql索引区别 es索引关联_elasticsearch_07

 

PUT my_index2
{
  "mappings": {
    "blogpost2": {
      "properties": {
        "comments": {
          "type": "nested",
          "properties": {
            "name":    { "type": "text"  },
            "comment": { "type": "text"  },
            "age":     { "type": "short"   },
            "stars":   { "type": "short"   },
            "date":    { "type": "date"    }
          }
        }
      }
    }
  }
}

再次发送相同的数据:

PUT /my_index2/blogpost2/10
{
  "title": "Nest eggs",
  "body":  "Making your money work...",
  "tags":  [ "cash", "shares" ],
  "comments": [
    {
      "name":    "John Smith",
      "comment": "Great article",
      "age":     28,
      "stars":   4,
      "date":    "2014-09-01"
    },
    {
      "name":    "Alice White",
      "comment": "More like this please",
      "age":     31,
      "stars":   5,
      "date":    "2014-10-22"
    }
  ]
}

再次发起查询:

es索引和mysql索引区别 es索引关联_关联查询_08

 

为什么查不到,是因为nested对象有自己特定的语法如下:

es索引和mysql索引区别 es索引关联_大数据_09

 

{
  "query": {
    "nested": {
      "path": "comments",
       "score_mode": "max",
 
      "query": {
        "bool": {
          "must": [
            {
              "term": {
                "comments.age": 31
              }
            },
            {
              "term": {
                "comments.name": "alice"
             }
            }
          ]
        }
      }
    }
  }
}

score_mode:表示嵌套文档的最高得分纳入到根文档的计算之中。

嵌套模型的缺点如下:

当对嵌套文档做增加、修改或者删除时,整个文档都要重新被索引。嵌套文档越多,这带来的成本就越大。
查询结果返回的是整个文档,而不仅仅是匹配的嵌套文档。尽管目前有计划支持只返回根文档中最佳匹配的嵌套文档,但目前还不支持。

 

2.4 父子对象

父子对象是最类似与表join的对象,父子关系的对象分别位于不同的文档中,做到了很好的隔离。
 

有以下优点:
1)更新父文档或子文档时候,另一方不受影响。
2)创建和删除子文档,父文档不受到影响。
3)子文档可以作为独立的结果单独返回。

缺点是:
1)父文档和子文档必须存在同一个shard中。
2)貌似只能是同一个index的两个type(对于es6.x版本只能支持一个type,如何处理,目前还未看到)

原理:

Elasticsearch 维护了一个父文档和子文档的映射关系,得益于这个映射,父-子文档关联查询操作非常快。
但是这个映射也对父-子文档关系有个限制条件:父文档和其所有子文档,都必须要存储在同一个分片中。
父-子文档ID映射存储在 Doc Values 中。当映射完全在内存中时, Doc Values 提供对映射的快速处理能力,
另一方面当映射非常大时,可以通过溢出到磁盘提供足够的扩展能力
 

如何建立父子映射:
建立父-子文档映射关系时只需要指定某一个文档 type 是另一个文档 type 的父亲。 该关系可以在如下两个时间点设置:

1)创建索引时;

2)在子文档 type 创建之前更新父文档的 mapping。
举例来说,对于公司和员工之间存在着类似的关系,即可以将公司信息看成员工信息的父文档。

如下:

PUT /company
{
  "mappings": {
    "branch": {}, //公司type
    "employee": {
      "_parent": {
        "type": "branch" //employee 文档 是 branch 文档的子文档。
      }
    }
  }
}

 

父子文档的创建
1)对于父对象来说,它是不知道有多少个子对象的,所以按照一般的对象创建方法即可。
2)子对象创建方法:

PUT /company/employee/1?parent=london
{
  "name":  "Alice Smith",
  "dob":   "1970-10-24",
  "hobby": "hiking"
}

父文档 ID 有两个作用:创建了父文档和子文档之间的关系,并且保证了父文档和子文档都在同一个分片上。

这里面的父ID london 会作为路由的依据,这样子对象就会路由到父文档同一个shard上。
在执行单文档的请求时需要指定父文档的 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" }

通过子文档查询父文档
查询80后所在的公司信息:

GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type": "employee",
      "query": {
        "range": {
          "dob": {
            "gte": "1980-01-01"
          }
        }
      }
    }
  }
}

es索引和mysql索引区别 es索引关联_搜索引擎_10

 

查询至少两个员工的公司:

GET /company/branch/_search
{
  "query": {
    "has_child": {
      "type":         "employee",
      "min_children": 2,
      "query": {
        "match_all": {}
      }
    }
  }
}

 

通过父文档查询子文档

GET /company/employee/_search
{
  "query": {
    "has_parent": {
      "type": "branch",
      "query": {
        "match": {
          "country": "UK"
        }
      }
    }
  }
}
 
查询公司在UK的所有员工。

 

每个子文档都保存了父文档的ID。

es索引和mysql索引区别 es索引关联_关联查询_11

 

当你考虑父子关系是否适合你现有关系模型时,请考虑下面这些建议:

  • 尽量少地使用父子关系,仅在子文档远多于父文档时使用。
  • 避免在一个查询中使用多个父子联合语句。
  • 在 has_child 查询中使用 filter 上下文,或者设置 score_mode 为 none 来避免计算文档得分。
  • 保证父 IDs 尽量短,以便在 doc values 中更好地压缩,被临时载入时占用更少的内存。
  • 父子关联查询比普通的查询慢5-10倍。