【学习笔记】Elasticsearch 实战操作

查找准确值

我们的目标是找出特定价格的产品。假如你有关系型数据库背景,可能用 SQL 来表现这次查询比较熟悉,它看起来像这样:

SELECT document
FROM   products
WHERE  price = 20

在 Elasticsearch DSL 中,我们使用 term 过滤器来实现同样的事。term 过滤器会查找我们设定的准确值。term 过滤器本身很简单,它接受一个字段名和我们希望查找的值:

{
    "term" : {
        "price" : 20
    }
}

term 过滤器本身并不能起作用。像在【查询 DSL】中介绍的一样,搜索 API 需要得到一个查询语句,而不是一个 过滤器。为了使用 term 过滤器,我们需要将它包含在一个过滤查询语句中:

GET /my_store/products/_search
{
    "query" : {
        "filtered" : { <1>
            "query" : {
                "match_all" : {} <2>
            },
            "filter" : {
                "term" : { <3>
                    "price" : 20
                }
            }
        }
    }
}

<1> filtered 查询同时接受 query 与 filter。
<2> match_all 用来匹配所有文档,这是默认行为,所以在以后的例子中我们将省略掉 query 部分。
<3> 这是我们上面见过的 term 过滤器。注意它在 filter 分句中的位置。

组合过滤

Elasticsearch 中表达这句 SQL 吗?

SELECT product
FROM   products
WHERE  (price = 20 OR productID = "XHDK-A-1293-#fJ3")
  AND  (price != 30)

这些情况下,你需要 bool 过滤器。这是以其他过滤器作为参数的组合过滤器,将它们结合成多种布尔组合。

布尔过滤器

bool 过滤器由三部分组成:

{
   "bool" : {
      "must" :     [],
      "should" :   [],
      "must_not" : [],
   }
}
must:所有分句都必须匹配,与 AND 相同。
must_not:所有分句都必须不匹配,与 NOT 相同。
should:至少有一个分句匹配,与 OR 相同。

这样就行了!假如你需要多个过滤器,将他们放入 bool 过滤器就行。
提示: bool 过滤器的每个部分都是可选的(例如,你可以只保留一个 must 分句),而且每个部分可以包含一到多个过滤器
为了复制上面的 SQL 示例,我们将两个 term 过滤器放在 bool 过滤器的 should 分句下,然后用另一个分句来处理 NOT 条件:

GET /my_store/products/_search
{
   "query" : {
      "filtered" : { <1>
         "filter" : {
            "bool" : {
              "should" : [
                 { "term" : {"price" : 20}}, <2>
                 { "term" : {"productID" : "XHDK-A-1293-#fJ3"}} <2>
              ],
              "must_not" : {
                 "term" : {"price" : 30} <3>
              }
           }
         }
      }
   }
}

<1> 注意我们仍然需要用 filtered 查询来包裹所有条件。
<2> 这两个 term 过滤器是 bool 过滤器的子节点,因为它们被放在 should 分句下,所以至少他们要有一个条件符合。
<3> 如果一个产品价值 30,它就会被自动排除掉,因为它匹配了 must_not 分句。

嵌套布尔过滤器

虽然 bool 是一个组合过滤器而且接受子过滤器,需明白它自己仍然只是一个过滤器。这意味着你可以在 bool 过滤器中嵌套 bool 过滤器,让你实现更复杂的布尔逻辑。
下面先给出 SQL 语句:

SELECT document
FROM   products
WHERE  productID      = "KDKE-B-9947-#kL5"
  OR (     productID = "JODL-X-1937-#pV7"
       AND price     = 30 )

我们可以将它翻译成一对嵌套的 bool 过滤器:

GET /my_store/products/_search
{
   "query" : {
      "filtered" : {
         "filter" : {
            "bool" : {
              "should" : [
                { "term" : {"productID" : "KDKE-B-9947-#kL5"}}, <1>
                { "bool" : { <1>
                  "must" : [
                    { "term" : {"productID" : "JODL-X-1937-#pV7"}}, <2>
                    { "term" : {"price" : 30}} <2>
                  ]
                }}
              ]
           }
         }
      }
   }
}

<1> 因为 term 和 bool 在第一个 should 分句中是平级的,至少需要匹配其中的一个过滤器。
<2> must 分句中有两个平级的 term 分句,所以他们俩都需要匹配。

查询多个准确值

term 过滤器在查询单个值时很好用,但是你可能经常需要搜索多个值。比如你想寻找 20 或 30 元的文档,该怎么做呢?
比起使用多个 term 过滤器,你可以用一个 terms 过滤器。terms 过滤器是 term 过滤器的复数版本。
它用起来和 term 差不多,我们现在来指定一组数值,而不是单一价格:

{
    "terms" : {
        "price" : [20, 30]
    }
}

像 term 过滤器一样,我们将它放在 filtered 查询中:

GET /my_store/products/_search
{
    "query" : {
        "filtered" : {
            "filter" : {
                "terms" : { <1>
                    "price" : [20, 30]
                }
            }
        }
    }
}

<1> 这是前面提到的 terms 过滤器,放置在 filtered 查询中

包含,而不是相等

理解 term 和 terms 是包含操作,而不是相等操作,这点非常重要。这意味着什么?
假如你有一个 term 过滤器 { “term” : { “tags” : “search” } },它将匹配下面两个文档:

{ "tags" : ["search"] }
{ "tags" : ["search", "open_source"] } <1>

<1> 虽然这个文档除了 search 还有其他短语,它还是被返回了

完全匹配

假如你真的需要完全匹配这种行为,最好是通过添加另一个字段来实现。在这个字段中,你索引原字段包含值的个数。引用上面的两个文档,我们现在包含一个字段来记录标签的个数:

{ "tags" : ["search"], "tag_count" : 1 }
{ "tags" : ["search", "open_source"], "tag_count" : 2 }

一旦你索引了标签个数,你可以构造一个 bool 过滤器来限制短语个数:

GET /my_index/my_type/_search
{
    "query": {
        "filtered" : {
            "filter" : {
                 "bool" : {
                    "must" : [
                        { "term" : { "tags" : "search" } }, <1>
                        { "term" : { "tag_count" : 1 } } <2>
                    ]
                }
            }
        }
    }
}

<1> 找出所有包含 search 短语的文档
<2> 但是确保文档只有一个标签

处理 Null 值

exists 过滤器

工具箱中的第一个利器是 exists 过滤器,这个过滤器将返回任何包含这个字段的文档,让我们用标签来举例,索引一些示例文档:

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> tags 字段有一个值和一个 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" : {
        "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 值,仍被返回了。这个字段存在是因为一个有值的标签被索引了,所以 null 对这个过滤器没有影响
结果很容易理解,所以在 tags 字段中有值的文档都被返回了。只排除了文档 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" }
            }
        }
    }
}

如你所愿,我们得到了两个没有包含标签字段的文档:

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

什么时候 null 才表示 null
有时你需要能区分一个字段是没有值,还是被设置为 null。用上面见到的默认行为无法区分这一点,数据都不存在了。幸运的是,我们可以将明确的 null 值用我们选择的占位符来代替
当指定字符串,数字,布尔值或日期字段的映射时,你可以设置一个 null_value 来处理明确的 null 值。没有值的字段仍将被排除在倒排索引外。
当选定一个合适的 null_value 时,确保以下几点:
它与字段的类型匹配,你不能在 date 类型的字段中使用字符串 null_value
它需要能与这个字段可能包含的正常值区分开来,以避免真实值和 null 值混淆

对象的 exists/missing

exists 和 missing 过滤器同样能在内联对象上工作,而不仅仅是核心类型。例如下面的文档:

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

你可以检查 name.first 和 name.last 的存在性,也可以检查 name 的。然而,在【映射】中,我们提到对象在内部被转成扁平化的键值结构,像下面所示:

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

所以我们是怎么使用 exists 或 missing 来检测 name 字段的呢,这个字段并没有真正存在于倒排索引中。
原因是像这样的一个过滤器

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

实际是这样执行的

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

同样这意味着假如 first 和 last 都为空,那么 name 就是不存在的。

嵌套

嵌套对象

事实上在Elasticsearch中,创建丶删除丶修改一个文档是是原子性的,因此我们可以在一个文档中储存密切关联的实体。举例来说,我们可以在一个文档中储存一笔订单及其所有内容,或是储存一个Blog文章及其所有回应,藉由传递一个comments阵列:

PUT /my_index/blogpost/1
{
  "title": "Nest eggs",
  "body":  "Making your money work...",
  "tags":  [ "cash", "shares" ],
  "comments": [ <1>
    {
      "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"
    }
  ]
}

<1> 如果我们依靠动态映射,comments栏位会被自动建立为一个object栏位。
因为所有内容都在同一个文档中,使搜寻时并不需要连接(join)blog文章与回应,因此搜寻表现更加优异。
问题在於以上的文档可能会如下所示的匹配一个搜寻:

GET /_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "name": "Alice" }},
        { "match": { "age":  28      }} <1>
      ]
    }
  }
}

<1> Alice是31岁,而不是28岁!
造成跨对象配对的原因如同我们在对象阵列中所讨论到,在于我们优美结构的JSON文档在索引中被扁平化为下方的 键-值 形式:

{
  "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 ]
}

Alice与31 以及 John与2014-09-01 之间的关联已经无法挽回的消失了。 当object类型的栏位用于储存单一对象是非常有用的。 从搜寻的角度来看,对於排序一个对象阵列来说关联是不需要的东西。
这是嵌套对象被设计来解决的问题。 藉由映射commments栏位为nested类型而不是object类型, 每个嵌套对象会被索引为一个隐藏分割文档,例如:

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

<1> 第一个嵌套对象
<2> 第二个嵌套对象
<3> 根或是父文档
藉由分别索引每个嵌套对象,对象的栏位中保持了其关联。 我们的查询可以只在同一个嵌套对象都匹配时才回应。
不仅如此,因嵌套对象都被索引了,连接嵌套对象至根文档的查询速度非常快–几乎与查询单一文档一样快。
这些额外的嵌套对象被隐藏起来,我们无法直接访问他们。 为了要新增丶修改或移除一个嵌套对象,我们必须重新索引整个文档。 要牢记搜寻要求的结果并不是只有嵌套对象,而是整个文档。

嵌套-映射

设定一个nested栏位很简单–在你会设定为object类型的地方,改为nested类型:

PUT /my_index
{
  "mappings": {
    "blogpost": {
      "properties": {
        "comments": {
          "type": "nested", <1>
          "properties": {
            "name":    { "type": "string"  },
            "comment": { "type": "string"  },
            "age":     { "type": "short"   },
            "stars":   { "type": "short"   },
            "date":    { "type": "date"    }
          }
        }
      }
    }
  }
}

<1> 一个nested栏位接受与object类型相同的参数。

查询嵌套对象

因嵌套对象(nested objects)会被索引为分离的隐藏文档,我们不能直接查询它们。而是使用 nested查询或 nested 过滤器来存取它们:

GET /my_index/blogpost/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "eggs" }}, <1>
        {
          "nested": {
            "path": "comments", <2>
            "query": {
              "bool": {
                "must": [ <3>
                  { "match": { "comments.name": "john" }},
                  { "match": { "comments.age":  28     }}
                ]
        }}}}
      ]
}}}

<1> title条件运作在根文档上
<2> nested条件深入嵌套的comments栏位。它不会在存取根文档的栏位,或是其他嵌套文档的栏位。
<3> comments.name以及comments.age运作在相同的嵌套文档。
TIP
一个nested栏位可以包含其他nested栏位。 相同的,一个nested查询可以包含其他nested查询。 嵌套阶层会如同你预期的运作。
当然,一个nested查询可以匹配多个嵌套文档。 每个文档的匹配会有各自的关联分数,但多个分数必须减少至单一分数才能应用至根文档。
在预设中,它会平均所有嵌套文档匹配的分数。这可以藉由设定score_mode参数为avg, max, sum或甚至none(为了防止根文档永远获得1.0的匹配分数时)来控制。

GET /my_index/blogpost/_search
{
  "query": {
    "bool": {
      "must": [
        { "match": { "title": "eggs" }},
        {
          "nested": {
            "path":       "comments",
            "score_mode": "max", <1>
            "query": {
              "bool": {
                "must": [
                  { "match": { "comments.name": "john" }},
                  { "match": { "comments.age":  28     }}
                ]
        }}}}
      ]
}}}

<1> 从最匹配的嵌套文档中给予根文档的_score值。
注意
nested过滤器类似於nested查询,除了无法使用score_mode参数。 只能使用在filter context—例如在filtered查询中–其作用类似其他的过滤器: 包含或不包含,但不评分。
nested过滤器的结果本身不会缓存,通常缓存规则会被应用於nested过滤器之中的过滤器。

以嵌套栏位排序

我们可以依照嵌套栏位中的值来排序,甚至藉由分离嵌套文档中的值。为了使其结果更加有趣,我们加入另一个记录:

PUT /my_index/blogpost/2
{
  "title": "Investment secrets",
  "body":  "What they don't tell you ...",
  "tags":  [ "shares", "equities" ],
  "comments": [
    {
      "name":    "Mary Brown",
      "comment": "Lies, lies, lies",
      "age":     42,
      "stars":   1,
      "date":    "2014-10-18"
    },
    {
      "name":    "John Smith",
      "comment": "You're making it up!",
      "age":     28,
      "stars":   2,
      "date":    "2014-10-16"
    }
  ]
}

想像我们要取回在十月中有收到回应的blog文章,并依照所取回的各个blog文章中最少stars数量的顺序作排序。 这个搜寻请求如下:

GET /_search
{
  "query": {
    "nested": { <1>
      "path": "comments",
      "filter": {
        "range": {
          "comments.date": {
            "gte": "2014-10-01",
            "lt":  "2014-11-01"
          }
        }
      }
    }
  },
  "sort": {
    "comments.stars": { <2>
      "order": "asc",   <2>
      "mode":  "min",   <2>
      "nested_filter": { <3>
        "range": {
          "comments.date": {
            "gte": "2014-10-01",
            "lt":  "2014-11-01"
          }
        }
      }
    }
  }
}

<1> nested查询限制了结果为十月份收到回应的blog文章。
<2> 结果在所有匹配的回应中依照comment.stars栏位的最小值(min)作递增(asc)的排序。
<3> 排序条件中的nested_filter与主查询query条件中的nested查询相同。 於下一个下方解释。
为什么我们要在nested_filter重复写上查询条件? 原因是排序在於执行查询后才发生。 此查询匹配了在十月中有收到回应的blog文章,回传blog文章文档作为结果。 如果我们不加上nested_filter条件,我们最後会依照任何blog文章曾经收到过的回应作排序,而不是在十月份收到的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值