处理 Null 值edit

回想在之前例子中,有的文档有名为 tags (标签)的字段,它是个多值字段,一个文档可能有一个或多个标签,也可能根本就没有标签。如果一个字段没有值,那么如何将它存入倒排索引中的呢?

这是个有欺骗性的问题,因为答案是:什么都不存。让我们看看之前内容里提到过的倒排索引:

Token

DocIDs

open_source

2

search

1,2

如何将某个不存在的字段存储在这个数据结构中呢?无法做到!简单的说,一个倒排索引只是一个 token 列表和与之相关的文档信息,如果字段不存在,那么它也不会持有任何 token,也就无法在倒排索引结构中表现。

最终,这也就意味着,null, [] (空数组)和 [null] 所有这些都是等价的,它们无法存于倒排索引中。

显然,世界并不简单,数据往往会有缺失字段,或有显式的空值或空数组。为了应对这些状况,Elasticsearch 提供了一些工具来处理空或缺失值。

存在查询edit

第一件武器就是 exists 存在查询。这个查询会返回那些在指定字段有任何值的文档,让我们索引一些示例文档并用标签的例子来说明:

POST /my_index/posts/_bulk
{ "index": { "_id": "1"              }}
{ "tags" : ["search"]                }  
{ "index": { "_id": "2"              }}
{ "tags" : ["search", "open_source"] }  
{ "index": { "_id": "3"              }}
{ "other_field" : "some data"        }  
{ "index": { "_id": "4"              }}
{ "tags" : null                      }  
{ "index": { "_id": "5"              }}
{ "tags" : ["search", null]          }  

tags 字段有 1 个值。

tags 字段有 2 个值。

tags 字段缺失。

tags 字段被置为 null

tags 字段有 1 个值和 1 个 null

以上文档集合中 tags 字段对应的倒排索引如下:

Token

DocIDs

open_source

2

search

1,2,5

我们的目标是找到那些被设置过标签字段的文档,并不关心标签的具体内容。只要它存在于文档中即可,用 SQL 的话就是用 IS NOT NULL 非空进行查询:

SELECT tags
FROM   posts
WHERE  tags IS NOT NULL

在 Elasticsearch 中,使用 exists 查询的方式如下:

GET /my_index/posts/_search
{
    "query" : {
        "constant_score" : {
            "filter" : {
                "exists" : { "field" : "tags" }
            }
        }
    }
}

这个查询返回 3 个文档:

"hits" : [
    {
      "_id" :     "1",
      "_score" :  1.0,
      "_source" : { "tags" : ["search"] }
    },
    {
      "_id" :     "5",
      "_score" :  1.0,
      "_source" : { "tags" : ["search", null] } 
    },
    {
      "_id" :     "2",
      "_score" :  1.0,
      "_source" : { "tags" : ["search", "open source"] }
    }
]

尽管文档 5 有 null 值,但它仍会被命中返回。字段之所以存在,是因为标签有实际值( search )可以被索引,所以 null 对过滤不会产生任何影响。

显而易见,只要 tags 字段存在项(term)的文档都会命中并作为结果返回,只有 3 和 4 两个文档被排除。

缺失查询edit

这个 missing 查询本质上与 exists 恰好相反:它返回某个特定 值字段的文档,与以下 SQL 表达的意思类似:

SELECT tags
FROM   posts
WHERE  tags IS NULL

我们将前面例子中 exists 查询换成 missing 查询:

GET /my_index/posts/_search
{
    "query" : {
        "constant_score" : {
            "filter": {
                "missing" : { "field" : "tags" }
            }
        }
    }
}

按照期望的那样,我们得到 3 和 4 两个文档(这两个文档的 tags 字段没有实际值):

"hits" : [
    {
      "_id" :     "3",
      "_score" :  1.0,
      "_source" : { "other_field" : "some data" }
    },
    {
      "_id" :     "4",
      "_score" :  1.0,
      "_source" : { "tags" : null }
    }
]

对象上的存在与缺失edit

不仅可以过滤核心类型, exists and missing 查询 还可以处理一个对象的内部字段。以下面文档为例:

{
   "name" : {
      "first" : "John",
      "last" :  "Smith"
   }
}

我们不仅可以检查 name.firstname.last 的存在性,也可以检查 name ,不过在 映射 中,如上对象的内部是个扁平的字段与值(field-value)的简单键值结构,类似下面这样:

{
   "name.first" : "John",
   "name.last"  : "Smith"
}

那么我们如何用 existsmissing 查询 name 字段呢? name 字段并不真实存在于倒排索引中。

原因是当我们执行下面这个过滤的时候:

{
    "exists" : { "field" : "name" }
}

实际执行的是:

{
    "bool": {
        "should": [
            { "exists": { "field": "name.first" }},
            { "exists": { "field": "name.last" }}
        ]
    }
}

这也就意味着,如果 firstlast 都是空,那么 name 这个命名空间才会被认为不存在。