朱小虎,University AI 创始人 & Chief Scientist,致力于推进世界人工智能化进程。制定并实施 UAI 中长期增长战略和目标,带领团队快速成长为人工智能领域最专业的力量。
作为行业领导者,他和UAI一起在2014年创建了TASA(中国最早的人工智能社团), DL Center(深度学习知识中心全球价值网络),AI growth(行业智库培训)等,为中国的人工智能人才建设输送了大量的血液和养分。此外,他还参与或者举办过各类国际性的人工智能峰会和活动,产生了巨大的影响力,书写了60万字的人工智能精品技术内容,生产翻译了全球第一本深度学习入门书《神经网络与深度学习》,生产的内容被大量的专业垂直公众号和媒体转载与连载。曾经受邀为国内顶尖大学制定人工智能学习规划和教授人工智能前沿课程,均受学生和老师好评。
源地址
考虑前面的例子,其中文档有一个称为 tags
的字段。这是一个多值字段。文档可以有一个、多个或者没有标签。如果字段没有值,那么它是怎么在倒排索引中存储的呢?
这个问题真诡异,因为答案是,它并没有被存储。让我们看一下上一节提到的倒排索引:
Token | DocIDs |
---|---|
open_source |
2 |
search |
1,2 |
如何存储一个在那个数据结构中不存在的字段呢?压根不行!倒排索引是一系列 token 和包含它的文档的列表。如果字段不存在,那也不会保存任何 token,所以在倒排索引中也不会有任何表示。
最终,这意味着 null
、[]
和 [null]
都是等价的。都不存在于倒排索引中!
很显然,真实世界没有这么简单,数据的字段经常会丢失或者包含显式的 null
或者空数组。为了解决这些问题,elasticsearch 有一些工具来处理空值或者丢失的数据。
exists Filter
exists
过滤器是第一件武器。这个过滤器返回特定字段中拥有任何值的文档。让我们使用 tagging 的例子,索引几个样本文档:
POST /my_index/posts/_bulk
{ "index": { "_id": "1" }}
{ "tags" : ["search"] } ...(1)
{ "index": { "_id": "2" }}
{ "tags" : ["search", "open_source"] } ...(2)
{ "index": { "_id": "3" }}
{ "other_field" : "some data" } ...(3)
{ "index": { "_id": "4" }}
{ "tags" : null } ...(4)
{ "index": { "_id": "5" }}
{ "tags" : ["search", null] } ...(5)
- (1)
tags
字段有一个值 - (2)
tags
字段有两个值 - (3)
tags
字段丢失 - (4)
tags
字段设置为null
- (5)
tag
字段有一个值和一个null
最终的倒排索引就是:
Token | DocIDs |
---|---|
open_source |
2 |
search |
1,2,5 |
我们的目标是找到设置了 tag 的所有文档。不管是 tag 是什么,只要它出现在文档中。在 SQL 中,我们通常可以使用 IS NOT NULL
查询:
SELECT tags
FROM posts
WHERE tags IS NOT NULL
在 elasticsearch 中,我们就使用 exists
过滤器:
GET /my_index/posts/_search
{
"query" : {
"filtered" : {
"filter" : {
"exists" : { "field" : "tags" }}}}}
最后返回三个文档:
"hits" : [
{
"_id" : "1",
"_score" : 1.0,
"_source" : { "tags" : ["search"] }
},
{
"_id" : "5",
"_score" : 1.0,
"_source" : { "tags" : ["search", null] } ...(1)
},
{
"_id" : "2",
"_score" : 1.0,
"_source" : { "tags" : ["search", "open source"] }
}
]
- (1) 文档 5 即使包含
null
值也返回了。因为真实值的 tag 被索引了,所以这个字段存在。所以null
对过滤器没有影响。
结果很容易理解。任何在 tags
字段中有 term 的文档都作为命中结果返回了。被排除在外的两个文档就是 3 和 4。
missing 过滤器
missing
过滤器本质上是 exists
的逆:它返回对应一个特定的字段没有任何值的文档,就像 SQL:
SELECT tags
FROM posts
WHERE tags IS NULL
让我们用 missing
来替换上面例子中 exists
过滤器:
GET /my_index/posts/_search
{
"query" : {
"filtered" : {
"filter": {
"missing" : { "field" : "tags" }
}
}
}
}
正如你所期望的,我们拿到了在 tags
字段上没有真实值的文档——文档 3 和 4:
"hits" : [
{
"_id" : "3",
"_score" : 1.0,
"_source" : { "other_field" : "some data" }
},
{
"_id" : "4",
"_score" : 1.0,
"_source" : { "tags" : null }
}
]
在 null 表示 null 时
有时候你需要能够区分字段没有值和字段被显式地设置为
null
。根据我们前面看到的默认行为,这是不可能的;数据丢失了。幸运的是,还有一种方法我们可以用一个占位符来替换显式的null
。
当指定一个 string、numeric、Boolean 或者日期字段时,你同样能设置null_value
可以用在任何遇到显式的null
值的地方。没有一个值的字段显然可以从倒排索引中排除。
选择合适的null_value
,确保下面的事项:
- 匹配了字段的类型(type)。你不能在一个类型为
date
的字段上用一个 string 的null_value
- 不同于字段可能包含的正常值,来避免出令人困惑的出现
null
的真实值
exists/missing on Objects
exists/missing 过滤器同样可以用在内部对象上(inner objects),不仅仅核心类型(core types)。假如有下面的文档
{
"name" : {
"first" : "John",
"last" : "Smith"
}
}
你可以检查 name.first
和 name.last
不仅仅是 name
的存在。然而,在Types and Mappings中,我们提到对象在内部会进行平化展开成一个简单的字段值结构,像这样:
{
"name.first" : "John",
"name.last" : "Smith"
}
所以,我们如何在 name
字段上使用 exists
和 missing
过滤器,这实际上并不存在于倒排索引中?
其原因就是,这会按照如下的过滤器那样:
{ "exists" : { "field" : "name" }}
实际上是按照:
{
"bool": {
"should": [
{ "exists": { "field": { "name.first" }}},
{ "exists": { "field": { "name.last" }}}
]
}
}
执行的。
这样也意味着如果 first
和 last
同时是空,name
命名空间就不会存在。