Neil Zhu,简书ID Not_GOD,University AI 创始人 & Chief Scientist,致力于推进世界人工智能化进程。制定并实施 UAI 中长期增长战略和目标,带领团队快速成长为人工智能领域最专业的力量。
作为行业领导者,他和UAI一起在2014年创建了TASA(中国最早的人工智能社团), DL Center(深度学习知识中心全球价值网络),AI growth(行业智库培训)等,为中国的人工智能人才建设输送了大量的血液和养分。此外,他还参与或者举办过各类国际性的人工智能峰会和活动,产生了巨大的影响力,书写了60万字的人工智能精品技术内容,生产翻译了全球第一本深度学习入门书《神经网络与深度学习》,生产的内容被大量的专业垂直公众号和媒体转载与连载。曾经受邀为国内顶尖大学制定人工智能学习规划和教授人工智能前沿课程,均受学生和老师好评。
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": "john@smith.com",
"dob": "1970/10/24"
}
PUT /my_index/blogpost/2
{ "title": "Relationships",
"body": "It's complicated...",
"user": 1
}
index
,type
,id
整体作为主键
blogpost
通过存放用户的id
与用户相连。index
和type
因为他们在应用中硬编码所以并没有强制。
在博客文章中查询用户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": "john@smith.com",
"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": "john@smith.com",
"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": "alice@john.com",
"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
聚合等价于执行查询返回用户的名字以及相应最为相关的博客文章,接着对每个用户执行同样地查询,来获得相应最相关的博客文章。但是更加高效。
在每个桶中返回的前面几条记录是执行一个基于原初主查询的微型查询的结果。这个微查询支持常用的特征,可以通过高亮和分页的特征。
网友评论