elasticsearch中关系的处理

handling relatioships

现实世界里,关系(relationship)是尤其重要的:博客文章包含评论,银行账号有相应的交易,顾客有银行账户,订单也由订单线,而目录则包含文件和子目录。
关系数据库便以此而设计——下面这些描述对你来说也并不陌生:

  • 每个实体(entity,或者行 row)可以由一个主键唯一识别
  • 实体都是正规化了的。对唯一实体的数据只会存储以此,相关的实体则只需要存储其主键。改变实体的数据只会出现在一个地方
  • 实体在查询的时候可以被join从而支持多个实体的交叉查询
  • 大多数的关系型数据库支持在多个实体上的ACID事务

但是关系型数据库除了不支持全文检索外还拥有自身的缺陷。在查询时进行join常常耗资源。依赖不同的硬件执行实体的join不实用。这也就给存放在一个服务器上的数据量造成了一个限制。

ES,如同大多数的NoSQL数据库,就将显示世界看做是扁平的。index 就是独立的文档的扁平集合。单一的文档应当包含确定其被搜索请求命中所需的信息。

改变ES中的单一文档的数据是ACID的,包含多个文档的事务却不是。没有办法使得可以保证事务的正常回滚。

扁平世界的优点;

  • 索引快速且无锁
  • 搜索快速且无锁
  • 海量数据可以分布在若干节点上,因为每个文档都是独立于其他文档。

然而关系也是重要的。我们需要消除扁平世界和真实世界的隔阂。在 ES 中,四种通用的技术用来管理关系型数据:

  • Application-side joins
  • Data denormalization
  • Nested objects
  • Parent/Child relationships

而最终的解决方案需要这些技术的混合。

Application-side joins

我们可以部分地通过在应用中实现 join 模拟关系型数据库。例如,我们在索引用户和用户的博客文章。在关系型世界中,我们可以做下面的动作:

PUT /my_index/user/1 
{ "name": "John Smith", 
  "email": "[email protected]", 
  "dob": "1970/10/24"
}
PUT /my_index/blogpost/2 
{ "title": "Relationships", 
  "body": "It's complicated...", 
  "user": 1
}

index, type, id整体作为主键
blogpost通过存放用户的 id与用户相连。indextype 因为他们在应用中硬编码所以并没有强制。

在博客文章中查询用户ID为1的就很简单了:

GET /my_index/blogpost/_search
{
  "query": {
    "filtered": {
      "filter": {
        "term": { "user": 1 }
      }
    }
  }
}

查询用户名是 John 的博客文章,我们需要运行两个查询:第一个查找所有叫做John的用户得到他们的ID,第二步将这些ID传入一个查询中获得作者为John的文章

GET /my_index/user/_search
{ "query": { "match": { "name": "John" } }}

GET /my_index/blogpost/_search
{ "query": { "filtered": { "filter": { "terms": { "user": [1] }  } } } }

terms过滤器中的值就是从第一个查询中得到的结果。

application-side join的主要好处就是数据的正规化。改变用户的名字只会在一个地方:user文档。而其弱点就是你需要执行额外的查询在搜索时进行join。

这个例子中,只有一个用户匹配了我们第一查询,但是在现实情形下,我们常常会碰到百万个以John为名的用户。包含所有这些 ID 在第二个查询中就产生了一个巨大的查询,包含数百万的term检索。

这个场景适用于第一个搜索结果比较小的情形,并且最好他们基本不变化。这就使得ES可以缓存结果避免频繁执行第一个查询。

Data denormalization

从ES获得最佳搜索性能的方法就是通过在索引时的去正规化数据。对每个需要获取的文档保持冗余的拷贝将会去除join的需要。
如果我们希望通过作者名找到博客文章,包含这个用户的名字在该博客文章文档本身即可:

PUT /my_index/user/1
{
  "name":     "John Smith",
  "email":    "[email protected]",
  "dob":      "1970/10/24"
}

PUT /my_index/blogpost/2
{
  "title":    "Relationships",
  "body":     "It's complicated...",
  "user":     {
    "id":       1,
    "name": "John Smith"
  }
}

现在,我们既可以通过单个查询找到作者为John的文章了。

GET /my_index/blogpost/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title":     "relationships" }},
        { "match": { "user.name": "John"          }}
      ]
    }
  }
}

data denormalization 的优势是速度。因为每个文档包含需要确定是否满足查询的所有的信息,也就避免了额外的 join。

Field collapsing

一般要求是用一个特定的字段的 group 来展现搜索结果。我们可能希望返回最相关的博客文章,而使用用户名进行group。根据用户名进行group就代表着对 terms 聚合的要求。为了对用户的全名进行 group,name 字段就应被设置成 not_analyzed 形式,正如在 聚合和分析 中解释的那样:

PUT /my_index/_mapping/blogpost
{
  "properties": {
    "user": {
      "properties": {
        "name": {
          "type": "string", 
          "fields": {
            "raw": { 
              "type":  "string",
              "index": "not_analyzed"
            }
          }
        }
      }
    }
  }
}

user.name 字段用作全文检索
user.name.raw 字段用作terms聚合

然后添加一些数据:

PUT /my_index/user/1
{
  "name": "John Smith",
  "email": "[email protected]",
  "dob": "1970/10/24"
}

PUT /my_index/blogpost/2
{
  "title": "Relationships",
  "body": "It's complicated...",
  "user": {
    "id": 1,
    "name": "John Smith"
  }
}

PUT /my_index/user/3
{
  "name": "Alice John",
  "email": "[email protected]",
  "dob": "1979/01/04"
}

PUT /my_index/blogpost/4
{
  "title": "Relationships are cool",
  "body": "It's not complicated at all...",
  "user": {
    "id": 3,
    "name": "Alice John"
  }
}

现在我们就可以执行一个查询relationships的博客文章了,作者名为John,并使用作者名进行group,使用top_hits聚合

GET /my_index/blogpost/_search?search_type=count
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title":     "relationships" }},
        { "match": { "user.name": "John"          }}
      ]
    }
  },
  "aggs": {
    "users": {
      "terms": {
        "field":   "user.name.raw",
        "order": { "top_score": "desc" }
      },
      "aggs": {
        "top_score": { "max":      { "script":  "_score"           }},
        "blogposts": { "top_hits": { "_source": "title", "size": 5 }} 
      }
    }
  }
}

我们感兴趣的博客文章返回在 blogposts 聚合,所以我们可以通过设置 search_type=count 来关闭常用的搜索 hits.
query 返回用户为 John 的关于 relationships 博客文章
terms 聚合对每个 user.name.raw 值创建了一个桶
top_score 聚合将在 users 聚合中的项进行按桶的排序
top_hits 聚合返回对每个用户前5个相关文章

上面查询的简化结果如下:

...
"hits": {
  "total":     2,
  "max_score": 0,
  "hits":      []
},
"aggregations": {
  "users": {
     "buckets": [
        {
           "key":       "John Smith",
           "doc_count": 1,
           "blogposts": {
              "hits": {
                 "total":     1,
                 "max_score": 0.35258877,
                 "hits": [
                    {
                       "_index": "my_index",
                       "_type":  "blogpost",
                       "_id":    "2",
                       "_score": 0.35258877,
                       "_source": {
                          "title": "Relationships"
                       }
                    }
                 ]
              }
           },
           "top_score": {
              "value": 0.3525887727737427
           }
        },
...

hits 数组为空,因为我们设置了 search_type = count
对每个用户都有一个桶
在每个用户的桶下面都有一个blogposts.hits的数组包含了对那个用户的前列的结果
在每个用户的桶内是按照相关度进行排序的

使用top_hits聚合等价于执行查询返回用户的名字以及相应最为相关的博客文章,接着对每个用户执行同样地查询,来获得相应最相关的博客文章。但是更加高效。

在每个桶中返回的前面几条记录是执行一个基于原初主查询的微型查询的结果。这个微查询支持常用的特征,可以通过高亮和分页的特征。

Nested objects

Parent/Child relationships

http://blog.csdn.net/dm_vincent/article/details/47710367

http://blog.csdn.net/dm_vincent/article/details/47710591

猜你喜欢

转载自kfcman.iteye.com/blog/2254921