ElasticSearch权威指南:基础入门(中)

2023-09-24 20:21:15 浏览数 (2)

官方网站:https://www.elastic.co/guide/index.html

5.搜索——最基本工具

空搜索

搜索API的最基础的形式是没有指定任何查询的空搜索 ,它简单地返回集群中所有索引下的所有文档:

代码语言:javascript复制
GET /_search

返回的结果(为了界面简洁编辑过的)像这样:

{
   "hits" : {
      "total" :       14,
      "hits" : [
        {
          "_index":   "us",
          "_type":    "tweet",
          "_id":      "7",
          "_score":   1,
          "_source": {
             "date":    "2014-09-17",
             "name":    "John Smith",
             "tweet":   "The Query DSL is really powerful and flexible",
             "user_id": 2
          }
       },
        ... 9 RESULTS REMOVED ...
      ],
      "max_score" :   1
   },
   "took" :           4,
   "_shards" : {
      "failed" :      0,
      "successful" :  10,
      "total" :       10
   },
   "timed_out" :      false
}
  • hits

返回结果中最重要的部分是 hits ,它 包含 total 字段来表示匹配到的文档总数,并且一个 hits 数组包含所查询结果的前十个文档。

hits 数组中每个结果包含文档的 _index_type_id ,加上 _source 字段。这意味着我们可以直接从返回的搜索结果中使用整个文档。这不像其他的搜索引擎,仅仅返回文档的ID,需要你单独去获取文档。

每个结果还有一个 _score ,它衡量了文档与查询的匹配程度。默认情况下,首先返回最相关的文档结果,就是说,返回的文档是按照 _score 降序排列的。在这个例子中,我们没有指定任何查询,故所有的文档具有相同的相关性,因此对所有的结果而言 1 是中性的 _score

max_score 值是与查询所匹配文档的 _score 的最大值。

  • took

took 值告诉我们执行整个搜索请求耗费了多少毫秒。

  • shards

_shards 部分 告诉我们在查询中参与分片的总数,以及这些分片成功了多少个失败了多少个。正常情况下我们不希望分片失败,但是分片失败是可能发生的。如果我们遭遇到一种灾难级别的故障,在这个故障中丢失了相同分片的原始数据和副本,那么对这个分片将没有可用副本来对搜索请求作出响应。假若这样,Elasticsearch 将报告这个分片是失败的,但是会继续返回剩余分片的结果。

  • time_out

timed_out 值告诉我们查询是否超时。默认情况下,搜索请求不会超时。 如果低响应时间比完成结果更重要,你可以指定 timeout 为 10 或者 10ms(10毫秒),或者 1s(1秒):

代码语言:javascript复制
GET /_search?timeout=10ms

在请求超时之前,Elasticsearch 将会返回已经成功从每个分片获取的结果。

应当注意的是 timeout 不是停止执行查询,它仅仅是告知正在协调的节点返回到目前为止收集的结果并且关闭连接。在后台,其他的分片可能仍在执行查询即使是结果已经被发送了。

多索引、多类型

如果不对某一特殊的索引或者类型做限制,就会搜索集群中的所有文档。Elasticsearch 转发搜索请求到每一个主分片或者副本分片,汇集查询出的前10个结果,并且返回给我们。

然而,经常的情况下,你 想在一个或多个特殊的索引并且在一个或者多个特殊的类型中进行搜索。我们可以通过在URL中指定特殊的索引和类型达到这种效果,如下所示:

  • /_search

在所有的索引中搜索所有的类型

  • /gb/_search

gb 索引中搜索所有的类型

  • /gb,us/_search

gbus 索引中搜索所有的文档

  • /g*,u*/_search

在任何以 g 或者 u 开头的索引中搜索所有的类型

  • /gb/user/_search

gb 索引中搜索 user 类型

  • /gb,us/user,tweet/_search

gbus 索引中搜索 usertweet 类型

  • /_all/user,tweet/_search

在所有的索引中搜索 usertweet 类型

当在单一的索引下进行搜索的时候,Elasticsearch 转发请求到索引的每个分片中,可以是主分片也可以是副本分片,然后从每个分片中收集结果。多索引搜索恰好也是用相同的方式工作的--只是会涉及到更多的分片。

搜索一个索引有五个主分片和搜索五个索引各有一个分片准确来所说是等价的。

分页

在之前的 空搜索 中说明了集群中有 14 个文档匹配了(empty)query 。 但是在 hits 数组中只有 10 个文档。如何才能看到其他的文档?

和 SQL 使用 LIMIT 关键字返回单个 page 结果的方法相同,Elasticsearch 接受 fromsize 参数:

  • size:显示应该返回的结果数量,默认是 10
  • from:显示应该跳过的初始结果数量,默认是 0

如果每页展示 5 条结果,可以用下面方式请求得到 1 到 3 页的结果:

代码语言:javascript复制
GET /_search?size=5
GET /_search?size=5&from=5
GET /_search?size=5&from=10

在分布式系统中深度分页 理解为什么深度分页是有问题的,我们可以假设在一个有 5 个主分片的索引中搜索。 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。 现在假设我们请求第 1000 页--结果从 10001 到 10010 。所有都以相同的方式工作除了每个分片不得不产生前10010个结果以外。 然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。 可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 1000 个结果的原因。

轻量搜索

有两种形式的 搜索 API:一种是 “轻量的” 查询字符串 版本,要求在查询字符串中传递所有的 参数,另一种是更完整的 请求体 版本,要求使用 JSON 格式和更丰富的查询表达式作为搜索语言。

查询字符串搜索非常适用于通过命令行做即席查询。例如,查询在 tweet 类型中 tweet 字段包含 elasticsearch 单词的所有文档:

代码语言:javascript复制
GET /_all/tweet/_search?q=tweet:elasticsearch

下一个查询在 name 字段中包含 john 并且在 tweet 字段中包含 mary 的文档。实际的查询就是这样

代码语言:javascript复制
 name:john  tweet:mary

但是查询字符串参数所需要的 百分比编码 (译者注:URL编码)实际上更加难懂:

代码语言:javascript复制
GET /_search?q=+name:john +tweet:mary

前缀表示必须与查询条件匹配。类似地, - 前缀表示一定不与查询条件匹配。没有 或者 - 的所有其他条件都是可选的——匹配的越多,文档就越相关。

  • _all字段

这个简单搜索返回包含 mary 的所有文档:

代码语言:javascript复制
GET /_search?q=mary

之前的例子中,我们在 tweetname 字段中搜索内容。然而,这个查询的结果在三个地方提到了 mary

  • 有一个用户叫做 Mary
  • 6条微博发自 Mary
  • 一条微博直接 @mary

Elasticsearch 是如何在三个不同的字段中查找到结果的呢?

当索引一个文档的时候,Elasticsearch 取出所有字段的值拼接成一个大的字符串,作为 _all 字段进行索引。例如,当索引这个文档时:

代码语言:javascript复制
{
    "tweet":    "However did I manage before Elasticsearch?",
    "date":     "2014-09-14",
    "name":     "Mary Jones",
    "user_id":  1
}

这就好似增加了一个名叫 _all 的额外字段:

代码语言:javascript复制
"However did I manage before Elasticsearch? 2014-09-14 Mary Jones 1"

除非设置特定字段,否则查询字符串就使用 _all 字段进行搜索。

在刚开始开发一个应用时,_all 字段是一个很实用的特性。之后,你会发现如果搜索时用指定字段来代替 _all 字段,将会更好控制搜索结果。当 _all 字段不再有用的时候,可以将它置为失效,正如在 元数据: _all 字段 中所解释的。

  • 更复杂的查询

下面的查询针对tweents类型,并使用以下的条件:

  • name 字段中包含 mary 或者 john
  • date 值大于 2014-09-10
  • _all 字段包含 aggregations 或者 geo
代码语言:javascript复制
 name:(mary john)  date:>2014-09-10  (aggregations geo)

查询字符串在做了适当的编码后,可读性很差:

代码语言:javascript复制
?q=+name:(mary john) +date:>2014-09-10 +(aggregations geo)

从之前的例子中可以看出,这种 轻量 的查询字符串搜索效果还是挺让人惊喜的。 它的查询语法在相关参考文档中有详细解释,以便简洁的表达很复杂的查询。对于通过命令做一次性查询,或者是在开发阶段,都非常方便。

但同时也可以看到,这种精简让调试更加晦涩和困难。而且很脆弱,一些查询字符串中很小的语法错误,像 -:/ 或者 " 不匹配等,将会返回错误而不是搜索结果。

最后,查询字符串搜索允许任何用户在索引的任意字段上执行可能较慢且重量级的查询,这可能会暴露隐私信息,甚至将集群拖垮。

因为这些原因,不推荐直接向用户暴露查询字符串搜索功能,除非对于集群和数据来说非常信任他们。

相反,我们经常在生产环境中更多地使用功能全面的 request body 查询API,除了能完成以上所有功能,还有一些附加功能。但在到达那个阶段之前,首先需要了解数据在 Elasticsearch 中是如何被索引的。

6.映射和分析

当摆弄索引里面的数据时,我们发现一些奇怪的事情。一些事情看起来被打乱了:在我们的索引中有12条推文,其中只有一条包含日期 2014-09-15 ,但是看一看下面查询命中的 总数 (total):

代码语言:javascript复制
GET /_search?q=2014              # 12 results
GET /_search?q=2014-09-15        # 12 results !
GET /_search?q=date:2014-09-15   # 1  result
GET /_search?q=date:2014         # 0  results !

为什么在 _all 字段查询日期返回所有推文,而在 date 字段只查询年份却没有返回结果?为什么我们在 _all 字段和 date 字段的查询结果有差别?

推测起来,这是因为数据在 _all 字段与 date 字段的索引方式不同。所以,通过请求 gb 索引中 tweet 类型的_映射_(或模式定义),让我们看一看 Elasticsearch 是如何解释我们文档结构的:

代码语言:javascript复制
GET /gb/_mapping/tweet

这将得到如下结果:

代码语言:javascript复制
{
   "gb": {
      "mappings": {
         "tweet": {
            "properties": {
               "date": {
                  "type": "date",
                  "format": "strict_date_optional_time||epoch_millis"
               },
               "name": {
                  "type": "string"
               },
               "tweet": {
                  "type": "string"
               },
               "user_id": {
                  "type": "long"
               }
            }
         }
      }
   }
}

基于对字段类型的猜测, Elasticsearch 动态为我们产生了一个映射。这个响应告诉我们 date 字段被认为是 date 类型的。由于 _all 是默认字段,所以没有提及它。但是我们知道 _all 字段是 string 类型的。

所以 date 字段和 string 字段 索引方式不同,因此搜索结果也不一样。这完全不令人吃惊。你可能会认为 核心数据类型 strings、numbers、Booleans 和 dates 的索引方式有稍许不同。没错,他们确实稍有不同。

但是,到目前为止,最大的差异在于 代表 精确值 (它包括 string 字段)的字段和代表 全文 的字段。这个区别非常重要——它将搜索引擎和所有其他数据库区别开来。

精确值V全文

Elasticsearch 中的数据可以概括的分为两类:精确值和全文。

精确值 如它们听起来那样精确。例如日期或者用户 ID,但字符串也可以表示精确值,例如用户名或邮箱地址。对于精确值来讲,Foofoo 是不同的,20142014-09-15 也是不同的。

另一方面,全文 是指文本数据(通常以人类容易识别的语言书写),例如一个推文的内容或一封邮件的内容。

全文通常是指非结构化的数据,但这里有一个误解:自然语言是高度结构化的。问题在于自然语言的规则是复杂的,导致计算机难以正确解析。例如,考虑这条语句: May is fun but June bores me.它指的是月份还是人?

精确值很容易查询。结果是二进制的:要么匹配查询,要么不匹配。这种查询很容易用 SQL 表示:

代码语言:javascript复制
WHERE name    = "John Smith"
  AND user_id = 2
  AND date    > "2014-09-15"

查询全文数据要微妙的多。我们问的不只是“这个文档匹配查询吗”,而是“该文档匹配查询的程度有多大?”换句话说,该文档与给定查询的相关性如何?

我们很少对全文类型的域做精确匹配。相反,我们希望在文本类型的域中搜索。不仅如此,我们还希望搜索能够理解我们的 意图

  • 搜索 UK ,会返回包含 United Kindom 的文档。
  • 搜索 jump ,会匹配 jumpedjumpsjumping ,甚至是 leap
  • 搜索 johnny walker 会匹配 Johnnie Walkerjohnnie depp 应该匹配 Johnny Depp
  • fox news hunting 应该返回福克斯新闻( Foxs News )中关于狩猎的故事,同时, fox hunting news 应该返回关于猎狐的故事。

为了促进这类在全文域中的查询,Elasticsearch 首先 分析 文档,之后根据结果创建 倒排索引 。在接下来的两节,我们会讨论倒排索引和分析过程。

倒排索引

Elasticsearch 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。

例如,假设我们有两个文档,每个文档的 content 域包含如下内容:

  1. The quick brown fox jumped over the lazy dog
  2. Quick brown foxes leap over lazy dogs in summer

为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:

代码语言:javascript复制
Term      Doc_1  Doc_2
-------------------------
Quick   |       |  X
The     |   X   |
brown   |   X   |  X
dog     |   X   |
dogs    |       |  X
fox     |   X   |
foxes   |       |  X
in      |       |  X
jumped  |   X   |
lazy    |   X   |  X
leap    |       |  X
over    |   X   |  X
quick   |   X   |
summer  |       |  X
the     |   X   |
------------------------

现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:

代码语言:javascript复制
Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
quick   |   X   |
------------------------
Total   |   2   |  1

两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单 相似性算法 ,那么,我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。

但是,我们目前的倒排索引有一些问题:

  • Quickquick 以独立的词条出现,然而用户可能认为它们是相同的词。
  • foxfoxes 非常相似, 就像 dogdogs ;他们有相同的词根。
  • jumpedleap, 尽管没有相同的词根,但他们的意思很相近。他们是同义词。

使用前面的索引搜索 Quick fox 不会得到任何匹配文档。(记住, 前缀表明这个词必须存在。)只有同时出现 Quickfox 的文档才满足这个查询条件,但是第一个文档包含 quick fox ,第二个文档包含 Quick foxes

我们的用户可以合理的期望两个文档与查询匹配。我们可以做的更好。

如果我们将词条规范为标准模式,那么我们可以找到与用户搜索的词条不完全一致,但具有足够相关性的文档。例如:

  • Quick 可以小写化为 quick
  • foxes 可以 词干提取 --变为词根的格式-- 为 fox 。类似的, dogs 可以为提取为 dog
  • jumpedleap 是同义词,可以索引为相同的单词 jump

现在索引看上去像这样:

代码语言:javascript复制
Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
dog     |   X   |  X
fox     |   X   |  X
in      |       |  X
jump    |   X   |  X
lazy    |   X   |  X
over    |   X   |  X
quick   |   X   |  X
summer  |       |  X
the     |   X   |  X
------------------------

这还远远不够。我们搜索 Quick fox 仍然 会失败,因为在我们的索引中,已经没有 Quick 了。但是,如果我们对搜索的字符串使用与 content 域相同的标准化规则,会变成查询 quick fox ,这样两个文档都会匹配!

这非常重要。你只能搜索在索引中出现的词条,所以索引文本和查询字符串必须标准化为相同的格式。

分析与分析器

分析 包含下面的过程:

  • 首先,将一块文本分成适合于倒排索引的独立的 词条
  • 之后,将这些词条统一化为标准格式以提高它们的“可搜索性”,或者 recall

分析器执行上面的工作。 分析器 实际上是将三个功能封装到了一个包里:

  • 字符过滤器

首先,字符串按顺序通过每个 字符过滤器他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML,或者将 & 转化成 `and`。

  • 分词器

其次,字符串被 分词器 分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。

  • Token 过滤器

最后,词条按顺序通过每个 token 过滤器这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 a`, `and`, `the 等无用词),或者增加词条(例如,像 jumpleap 这种同义词)。

Elasticsearch提供了开箱即用的字符过滤器、分词器和token 过滤器。 这些可以组合起来形成自定义的分析器以用于不同的目的。我们会在 自定义分析器 章节详细讨论。

内置分析器

但是, Elasticsearch还附带了可以直接使用的预包装的分析器。 接下来我们会列出最重要的分析器。为了证明它们的差异,我们看看每个分析器会从下面的字符串得到哪些词条:

代码语言:javascript复制
"Set the shape to semi-transparent by calling set_trans(5)"
  • 标准分析器:标准分析器是Elasticsearch默认使用的分析器。它是分析各种语言文本最常用的选择。它根据 Unicode 联盟 定义的 单词边界 划分文本。删除绝大部分标点。最后,将词条小写。它会产生
代码语言:javascript复制
set, the, shape, to, semi, transparent, by, calling, set_trans, 5
  • 简单分析器:简单分析器在任何不是字母的地方分隔文本,将词条小写。
代码语言:javascript复制
set, the, shape, to, semi, transparent, by, calling, set, trans
  • 空格分析器:空格分析器在空格的地方划分文本。
代码语言:javascript复制
Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
  • 语言分析器:特定语言分析器可用于 很多语言它们可以考虑指定语言的特点。例如, 英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。 由于理解英语语法的规则,这个分词器可以提取英语单词的 词干
代码语言:javascript复制
英语 分词器会产生下面的词条:set, shape, semi, transpar, call, set_tran, 5

注意看 transparent`、 `callingset_trans 已经变为词根格式。

什么时候使用分析器

当我们 索引 一个文档,它的全文域被分析成词条以用来创建倒排索引。 但是,当我们在全文域 搜索 的时候,我们需要将查询字符串通过 相同的分析过程 ,以保证我们搜索的词条格式与索引中的词条格式一致。

全文查询,理解每个域是如何定义的,因此它们可以做正确的事:

  • 当你查询一个全文域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。
  • 当你查询一个 精确值 域时,不会分析查询字符串, 而是搜索你指定的精确值。

现在你可以理解在 开始章节 的查询为什么返回那样的结果:

  • date 域包含一个精确值:单独的词条 `2014-09-15`。
  • _all 域是一个全文域,所以分词进程将日期转化为三个词条: `2014`, `09`, 和 `15`。

当我们在 _all 域查询 2014`,它匹配所有的12条推文,因为它们都含有 `2014

代码语言:javascript复制
GET /_search?q=2014              # 12 results

当我们在 _all 域查询 2014-09-15`,它首先分析查询字符串,产生匹配 `2014`, `09`, 或 `15任意 词条的查询。这也会匹配所有12条推文,因为它们都含有 2014

代码语言:javascript复制
GET /_search?q=2014-09-15        # 12 results !

当我们在 date 域查询 `2014-09-15`,它寻找 精确 日期,只找到一个推文:

代码语言:javascript复制
GET /_search?q=date:2014-09-15   # 1  result

当我们在 date 域查询 `2014`,它找不到任何文档,因为没有文档含有这个精确日志:

代码语言:javascript复制
GET /_search?q=date:2014         # 0  results !

测试分析器

有些时候很难理解分词的过程和实际被存储到索引中的词条,特别是你刚接触 Elasticsearch。为了理解发生了什么,你可以使用 analyze API 来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本:

代码语言:javascript复制
GET /_analyze
{
  "analyzer": "standard",
  "text": "Text to analyze"
}

结果中每个元素代表一个单独的词条:

代码语言:javascript复制
{
   "tokens": [
      {
         "token":        "text",
         "start_offset": 0,
         "end_offset":   4,
         "type":         "<ALPHANUM>",
         "position":     1
      },
      {
         "token":        "to",
         "start_offset": 5,
         "end_offset":   7,
         "type":         "<ALPHANUM>",
         "position":     2
      },
      {
         "token":        "analyze",
         "start_offset": 8,
         "end_offset":   15,
         "type":         "<ALPHANUM>",
         "position":     3
      }
   ]
}

token 是实际存储到索引中的词条。 position 指明词条在原始文本中出现的位置。 start_offsetend_offset 指明字符在原始字符串中的位置。

每个分析器的 type 值都不一样,可以忽略它们。它们在Elasticsearch中的唯一作用在于keep_types token 过滤器。

analyze API 是一个有用的工具,它有助于我们理解Elasticsearch索引内部发生了什么,随着深入,我们会进一步讨论它。

指定分析器

当Elasticsearch在你的文档中检测到一个新的字符串域 ,它会自动设置其为一个全文 字符串 域,使用 标准 分析器对它进行分析。

你不希望总是这样。可能你想使用一个不同的分析器,适用于你的数据使用的语言。有时候你想要一个字符串域就是一个字符串域--不使用分析,直接索引你传入的精确值,例如用户ID或者一个内部的状态域或标签。要做到这一点,我们必须手动指定这些域的映射。

映射

为了能够将时间域视为时间,数字域视为数字,字符串域视为全文或精确值字符串, Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射中。

数据输入和输出 中解释的, 索引中每个文档都有 类型 。每种类型都有它自己的 映射 ,或者 模式定义 。映射定义了类型中的域,每个域的数据类型,以及Elasticsearch如何处理这些域。映射也用于配置与类型有关的元数据。

我们会在 类型和映射 详细讨论映射。本节,我们只讨论足够让你入门的内容。

核心简单域类型

Elasticsearch 支持 如下简单域类型:

  • 字符串: string
  • 整数 : byte, short, integer, long
  • 浮点数: float, double
  • 布尔型: boolean
  • 日期: date

当你索引一个包含新域的文档--之前未曾出现-- Elasticsearch 会使用 动态映射 ,通过JSON中基本数据类型,尝试猜测域类型,使用如下规则:

JSON type

域 type

布尔型: true 或者 false

boolean

整数: 123

long

浮点数: 123.45

double

字符串,有效日期: 2014-09-15

date

字符串: foo bar

string

这意味着如果你通过引号( "123" )索引一个数字,它会被映射为 string 类型,而不是 long 。但是,如果这个域已经映射为 long ,那么 Elasticsearch 会尝试将这个字符串转化为 long ,如果无法转化,则抛出一个异常。

查看映射

通过 /_mapping ,我们可以查看 Elasticsearch 在一个或多个索引中的一个或多个类型的映射 。在 开始章节 ,我们已经取得索引 gb 中类型 tweet 的映射:

代码语言:javascript复制
GET /gb/tweet/_mapping

Elasticsearch 根据我们索引的文档,为域(称为 属性 )动态生成的映射。

代码语言:javascript复制
{
   "gb": {
      "mappings": {
         "tweet": {
            "properties": {
               "date": {
                  "type": "date",
                  "format": "strict_date_optional_time||epoch_millis"
               },
               "name": {
                  "type": "string"
               },
               "tweet": {
                  "type": "string"
               },
               "user_id": {
                  "type": "long"
               }
            }
         }
      }
   }
}

错误的映射,例如 将 age 域映射为 string 类型,而不是 integer ,会导致查询出现令人困惑的结果。检查一下!而不是假设你的映射是正确的。

自定义域映射

尽管在很多情况下基本域数据类型已经够用,但你经常需要为单独域自定义映射,特别是字符串域。自定义映射允许你执行下面的操作:

  • 全文字符串域和精确值字符串域的区别
  • 使用特定语言分析器
  • 优化域以适应部分匹配
  • 指定自定义数据格式
  • 还有更多

域最重要的属性是type 。对于不是string的域,你一般只需要设置type

代码语言:javascript复制
{
    "number_of_clicks": {
        "type": "integer"
    }
}

默认,string 类型域会被认为包含全文。就是说,它们的值在索引前会通过 一个分析器,针对于这个域的查询在搜索前也会经过一个分析器。string 域映射的两个最重要 属性是 indexanalyzer

index 属性控制怎样索引字符串。它可以是下面三个值:

  • analyzed首先分析字符串,然后索引它。换句话说,以全文索引这个域。
  • not_analyzed索引这个域,所以它能够被搜索,但索引的是精确值。不会对它进行分析。
  • no不索引这个域。这个域不会被搜索到。

string 域 index 属性默认是 analyzed 。如果我们想映射这个字段为一个精确值,我们需要设置它为 not_analyzed :

代码语言:javascript复制
{
    "tag": {
        "type":     "string",
        "index":    "not_analyzed"
    }
}

其他简单类型(例如 longdoubledate 等)也接受 index 参数,但有意义的值只有 nonot_analyzed , 因为它们永远不会被分析。

analyzed 字符串域,用 analyzer 属性指定在搜索和索引时使用的分析器。默认, Elasticsearch 使用 standard 分析器, 但你可以指定一个内置的分析器替代它,例如 whitespace、simple、english

代码语言:javascript复制
{
    "tweet": {
        "type":     "string",
        "analyzer": "english"
    }
}

在 自定义分析器 ,我们会展示怎样定义和使用自定义分析器。

更新映射

当你首次创建一个索引的时候,可以指定类型的映射。你也可以使用 /_mapping 为新类型(或者为存在的类型更新映射)增加映射。

尽管你可以 增加一个存在的映射,你不能修改存在的域映射。如果一个域的映射已经存在,那么该域的数据可能已经被索引。如果你意图修改这个域的映射,索引的数据可能会出错,不能被正常的搜索。

我们可以更新一个映射来添加一个新域,但不能将一个存在的域从 analyzed 改为 not_analyzed

为了描述指定映射的两种方式,我们先删除 gd 索引:

代码语言:javascript复制
DELETE /gb

然后创建一个新索引,指定 tweet 域使用 english 分析器:

代码语言:javascript复制
PUT /gb  通过消息体中指定的 mappings 创建了索引。 
{
  "mappings": {
    "tweet" : {
      "properties" : {
        "tweet" : {
          "type" :    "string",
          "analyzer": "english"
        },
        "date" : {
          "type" :   "date"
        },
        "name" : {
          "type" :   "string"
        },
        "user_id" : {
          "type" :   "long"
        }
      }
    }
  }
}

稍后,我们决定在tweet 映射增加一个新的名为tagnot_analyzed的文本域,使用_mapping

代码语言:javascript复制
PUT /gb/_mapping/tweet
{
  "properties" : {
    "tag" : {
      "type" :    "string",
      "index":    "not_analyzed"
    }
  }
}

注意,我们不需要再次列出所有已存在的域,因为无论如何我们都无法改变它们。新域已经被合并到存在的映射中。

测试映射

你可以使用 analyze API 测试字符串域的映射。比较下面两个请求的输出:

代码语言:javascript复制
GET /gb/_analyze
{
  "field": "tweet",
  "text": "Black-cats"  消息体里面传输我们想要分析的文本。 
}

GET /gb/_analyze
{
  "field": "tag",
  "text": "Black-cats"  消息体里面传输我们想要分析的文本。 
} 

tweet 域产生两个词条 blackcattag 域产生单独的词条 Black-cats 。换句话说,我们的映射正常工作。

复杂核心域类型

除了我们提到的简单标量数据类型, JSON 还有null值,数组,和对象,这些 Elasticsearch 都是支持的。

多值域

很有可能,我们希望 tag 域 包含多个标签。我们可以以数组的形式索引标签:

代码语言:javascript复制
{ "tag": [ "search", "nosql" ]}

对于数组,没有特殊的映射需求。任何域都可以包含0、1或者多个值,就像全文域分析得到多个词条。

这暗示数组中所有的值必须是相同数据类型的。你不能将日期和字符串混在一起。如果你通过索引数组来创建新的域,Elasticsearch会用数组中第一个值的数据类型作为这个域的类型。

当你从 Elasticsearch 得到一个文档,每个数组的顺序和你当初索引文档时一样。你得到的 _source 域,包含与你索引的一模一样的 JSON 文档。 但是,数组是以多值域索引的—可以搜索,但是无序的。 在搜索的时候,你不能指定 “第一个” 或者 “最后一个”。 更确切的说,把数组想象成装在袋子里的值

空域

当然,数组可以为空。这相当于存在零值。 事实上,在 Lucene 中是不能存储null值的,所以我们认为存在 null值的域为空域。

下面三种域被认为是空的,它们将不会被索引:

代码语言:javascript复制
"null_value":               null,
"empty_array":              [],
"array_with_null_value":    [ null ]

多层级对象

我们讨论的最后一个 JSON 原生数据类是 对象 -- 在其他语言中称为哈希,哈希 map,字典或者关联数组。

内部对象 经常用于 嵌入一个实体或对象到其它对象中。例如,与其在 tweet 文档中包含 user_nameuser_id 域,我们也可以这样写:

代码语言:javascript复制
{
    "tweet":            "Elasticsearch is very flexible",
    "user": {
        "id":           "@johnsmith",
        "gender":       "male",
        "age":          26,
        "name": {
            "full":     "John Smith",
            "first":    "John",
            "last":     "Smith"
        }
    }
}

内部对象的映射

Elasticsearch会动态监测新的对象域并映射它们为对象,在properties 属性下列出内部域:

代码语言:javascript复制
{
  "gb": {
    "tweet": {   根对象 
      "properties": {
        "tweet":            { "type": "string" },
        "user": {   内部对象 
          "type":             "object",
          "properties": {
            "id":           { "type": "string" },
            "gender":       { "type": "string" },
            "age":          { "type": "long"   },
            "name":   {   内部对象 
              "type":         "object",
              "properties": {
                "full":     { "type": "string" },
                "first":    { "type": "string" },
                "last":     { "type": "string" }
              }
            }
          }
        }
      }
    }
  }
}

username 域的映射结构与 tweet 类型的相同。事实上, type 映射只是一种特殊的对象映射,我们称之为根对象 。除了它有一些文档元数据的特殊顶级域,例如 _source_all 域,它和其他对象一样。

内部对象是如何索引的

Lucene 不理解内部对象。 Lucene 文档是由一组键值对列表组成的。为了能让 Elasticsearch 有效地索引内部类,它把我们的文档转化成这样:

代码语言:javascript复制
{
    "tweet":            [elasticsearch, flexible, very],
    "user.id":          [@johnsmith],
    "user.gender":      [male],
    "user.age":         [26],
    "user.name.full":   [john, smith],
    "user.name.first":  [john],
    "user.name.last":   [smith]
}

内部域可以通过名称引用(例如, first )。为了区分同名的两个域,我们可以使用全路径 (例如, user.name.first ) 或 type 名加路径( tweet.user.name.first )。

在前面简单扁平的文档中,没有 useruser.name 域。Lucene 索引只有标量和简单值,没有复杂数据结构。

内部对象数组

最后,考虑包含 内部对象的数组是如何被索引的。 假设我们有个followers数组:

代码语言:javascript复制
{
    "followers": [
        { "age": 35, "name": "Mary White"},
        { "age": 26, "name": "Alex Jones"},
        { "age": 19, "name": "Lisa Smith"}
    ]
}

这个文档会像我们之前描述的那样被扁平化处理,结果如下所示:

代码语言:javascript复制
{
    "followers.age":    [19, 26, 35],
    "followers.name":   [alex, jones, lisa, smith, mary, white]
}

{age: 35}{name: Mary White} 之间的相关性已经丢失了,因为每个多值域只是一包无序的值,而不是有序数组。这足以让我们问,“有一个26岁的追随者?”但是我们不能得到一个准确的答案:“是否有一个26岁 名字叫 Alex Jones 的追随者?”

相关内部对象被称为 nested 对象,可以回答上面的查询,我们稍后会在嵌套对象中介绍它。

7.请求体查询

简易 查询 —query-string search— 对于用命令行进行即席查询(ad-hoc)是非常有用的。 然而,为了充分利用查询的强大功能,你应该使用 请求体 search API, 之所以称之为请求体查询(Full-Body Search),因为大部分参数是通过 Http 请求体而非查询字符串来传递的。

请求体查询 —下文简称 查询—不仅可以处理自身的查询请求,还允许你对结果进行片段强调(高亮)、对所有或部分结果进行聚合分析,同时还可以给出你是不是想找 的建议,这些建议可以引导使用者快速找到他想要的结果。

空查询

让我们以 最简单的 search API 的形式开启我们的旅程,空查询将返回所有索引库(indices)中的所有文档:

代码语言:javascript复制
GET /_search
{}  这是一个空的请求体。 

只用一个查询字符串,你就可以在一个、多个或者 _all 索引库(indices)和一个、多个或者所有types中查询:

代码语言:javascript复制
GET /index_2014*/type1,type2/_search
{}

同时你可以使用 fromsize 参数来分页:

代码语言:javascript复制
GET /_search
{
  "from": 30,
  "size": 10
}

一个带请求体的 GET 请求? 某些特定语言(特别是 JavaScript)的 HTTP 库是不允许 GET 请求带有请求体的。 事实上,一些使用者对于 GET 请求可以带请求体感到非常的吃惊。而事实是这个RFC文档 RFC 7231— 一个专门负责处理 HTTP 语义和内容的文档 — 并没有规定一个带有请求体的GET 请求应该如何处理!结果是,一些 HTTP 服务器允许这样子,而有一些 — 特别是一些用于缓存和代理的服务器 — 则不允许。 对于一个查询请求,Elasticsearch 的工程师偏向于使用 GET 方式,因为他们觉得它比 POST 能更好的描述信息检索(retrieving information)的行为。然而,因为带请求体的 GET 请求并不被广泛支持,所以 search API 同时支持 POST 请求:POST /_search { "from": 30, "size": 10 }。类似的规则可以应用于任何需要带请求体的 GET API。

我们将在聚合 聚合 章节深入介绍聚合(aggregations),而现在,我们将聚焦在查询。相对于使用晦涩难懂的查询字符串的方式,一个带请求体的查询允许我们使用 查询领域特定语言(query domain-specific language) 或者 Query DSL 来写查询语句。

查询表达式

查询表达式(Query DSL)是一种非常灵活又富有表现力的查询语言。 Elasticsearch 使用它可以以简单的 JSON 接口来展现 Lucene 功能的绝大部分。在你的应用中,你应该用它来编写你的查询语句。它可以使你的查询语句更灵活、更精确、易读和易调试。

要使用这种查询表达式,只需将查询语句传递给 query 参数:

代码语言:javascript复制
GET /_search
{
    "query": YOUR_QUERY_HERE
}

空查询(empty search){}— 在功能上等价于使用 match_all 查询, 正如其名字一样,匹配所有文档:

代码语言:javascript复制
GET /_search
{
    "query": {
        "match_all": {}
    }
}

查询语句的结构

一个查询语句 的典型结构:

代码语言:javascript复制
{
    QUERY_NAME: {
        ARGUMENT: VALUE,
        ARGUMENT: VALUE,...
    }
}

如果是针对某个字段,那么它的结构如下:

代码语言:javascript复制
{
    QUERY_NAME: {
        FIELD_NAME: {
            ARGUMENT: VALUE,
            ARGUMENT: VALUE,...
        }
    }
}

举个例子,你可以使用 match 查询语句 来查询 tweet 字段中包含 elasticsearch 的 tweet:

代码语言:javascript复制
{
    "match": {
        "tweet": "elasticsearch"
    }
}

完整的查询请求如下:

代码语言:javascript复制
GET /_search
{
    "query": {
        "match": {
            "tweet": "elasticsearch"
        }
    }
}

合并查询语句

查询语句(Query clauses) 就像一些简单的组合块 ,这些组合块可以彼此之间合并组成更复杂的查询。这些语句可以是如下形式:

  • 叶子语句(Leaf clauses) (就像 match 语句) 被用于将查询字符串和一个字段(或者多个字段)对比。
  • 复合(Compound) 语句 主要用于合并其它查询语句。 比如,一个 bool 语句 允许在你需要的时候组合其它语句,无论是must匹配、 must_not匹配还是should匹配,同时它可以包含不评分的过滤器(filters):
代码语言:javascript复制
{
    "bool": {
        "must":     { "match": { "tweet": "elasticsearch" }},
        "must_not": { "match": { "name":  "mary" }},
        "should":   { "match": { "tweet": "full text" }},
        "filter":   { "range": { "age" : { "gt" : 30 }} }
    }
}

一条复合语句可以合并 任何 其它查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。

例如,以下查询是为了找出信件正文包含business opportunity的星标邮件,或者在收件箱正文包含 business opportunity的非垃圾邮件:

代码语言:javascript复制
{
    "bool": {
        "must": { "match":   { "email": "business opportunity" }},
        "should": [
            { "match":       { "starred": true }},
            { "bool": {
                "must":      { "match": { "folder": "inbox" }},
                "must_not":  { "match": { "spam": true }}
            }}
        ],
        "minimum_should_match": 1
    }
}

到目前为止,你不必太在意这个例子的细节,我们会在后面详细解释。最重要的是你要理解到,一条复合语句可以将多条语句 — 叶子语句和其它复合语句 — 合并成一个单一的查询语句。

查询与过滤

Elasticsearch 使用的查询语言(DSL)拥有一套查询组件,这些组件可以以无限组合的方式进行搭配。这套组件可以在以下两种情况下使用:过滤情况(filtering context)和查询情况(query context)。

当使用于过滤情况 时,查询被设置成一个“不评分”或者“过滤”查询。即,这个查询只是简单的问一个问题:“这篇文档是否匹配?”。回答也是非常的简单,yes 或者 no ,二者必居其一。

  • created 时间是否在 20132014 这个区间?
  • status 字段是否包含 published 这个单词?
  • lat_lon 字段表示的位置是否在指定点的 10km 范围内?

当使用于查询情况时,查询就变成了一个“评分”的查询。和不评分的查询类似,也要去判断这个文档是否匹配,同时它还需要判断这个文档匹配的有多好(匹配程度如何)。 此查询的典型用法是用于查找以下文档:

  • 查找与 full text search 这个词语最佳匹配的文档
  • 包含 run 这个词,也能匹配 runsrunningjog 或者 sprint
  • 包含 quickbrownfox 这几个词 — 词之间离的越近,文档相关性越高
  • 标有 lucenesearch 或者 java 标签 — 标签越多,相关性越高

一个评分查询计算每一个文档与此查询的相关程度,同时将这个相关程度分配给表示相关性的字段 `_score`,并且按照相关性对匹配到的文档进行排序。这种相关性的概念是非常适合全文搜索的情况,因为全文搜索几乎没有完全 “正确” 的答案。

自 Elasticsearch 问世以来,查询与过滤(queries and filters)就独自成为 Elasticsearch 的组件。但从 Elasticsearch 2.0 开始,过滤(filters)已经从技术上被排除了,同时所有的查询(queries)拥有变成不评分查询的能力。 然而,为了明确和简单,我们用 "filter" 这个词表示不评分、只过滤情况下的查询。你可以把 "filter" 、 "filtering query" 和 "non-scoring query" 这几个词视为相同的。 相似的,如果单独地不加任何修饰词地使用 "query" 这个词,我们指的是 "scoring query"

性能差异

过滤查询(Filtering queries)只是简单的检查包含或者排除,这就使得计算起来非常快。考虑到至少有一个过滤查询(filtering query)的结果是 “稀少的”(很少匹配的文档),并且经常使用不评分查询(non-scoring queries),结果会被缓存到内存中以便快速读取,所以有各种各样的手段来优化查询结果。

相反,评分查询(scoring queries)不仅仅要找出 匹配的文档,还要计算每个匹配文档的相关性,计算相关性使得它们比不评分查询费力的多。同时,查询结果并不缓存。

多亏倒排索引(inverted index),一个简单的评分查询在匹配少量文档时可能与一个涵盖百万文档的filter表现的一样好,甚至会更好。但是在一般情况下,一个filter 会比一个评分的query性能更优异,并且每次都表现的很稳定。

过滤(filtering)的目标是减少那些需要通过评分查询(scoring queries)进行检查的文档。

如何选择查询与过滤

通常的规则是,使用 查询(query)语句来进行 全文 搜索或者其它任何需要影响 相关性得分 的搜索。除此以外的情况都使用过滤(filters)。

最重要的查询

虽然 Elasticsearch自带很多的查询,但经常用到的也就那么几个。我们将在 深入搜索 章节详细讨论那些查询的细节,接下来我们对最重要的几个查询进行简单介绍。

  • match_all 查询简单的匹配所有文档。在没有指定查询方式时,它是默认的查询。
代码语言:javascript复制
{ "match_all": {}}

它经常与 filter 结合使用--例如,检索收件箱里的所有邮件。所有邮件被认为具有相同的相关性,所以都将获得分值为 1 的中性 `_score`。

  • match查询无论你在任何字段上进行的是全文搜索还是精确查询,match 查询是你可用的标准查询。

如果你在一个全文字段上使用 match 查询,在执行查询前,它将用正确的分析器去分析查询字符串:

代码语言:javascript复制
{ "match": { "tweet": "About Search" }}

如果在一个精确值的字段上使用它, 例如数字、日期、布尔或者一个 not_analyzed 字符串字段,那么它将会精确匹配给定的值:

代码语言:javascript复制
{ "match": { "age":    26           }}
{ "match": { "date":   "2014-09-01" }}
{ "match": { "public": true         }}
{ "match": { "tag":    "full_text"  }}

对于精确值的查询,你可能需要使用 filter 语句来取代 query,因为 filter 将会被缓存。接下来,我们将看到一些关于 filter 的例子。

不像我们在 轻量 搜索 章节介绍的字符串查询(query-string search),match 查询不使用类似 user_id:2 tweet:search 的查询语法。它只是去查找给定的单词。这就意味着将查询字段暴露给你的用户是安全的;你需要控制那些允许被查询字段,不易于抛出语法异常。

  • multi_match 查询可以在多个字段上执行相同的 match 查询。
代码语言:javascript复制
{
    "multi_match": {
        "query":    "full text search",
        "fields":   [ "title", "body" ]
    }
}
  • range 查询找出那些落在指定区间内的数字或者时间。
代码语言:javascript复制
{
    "range": {
        "age": {
            "gte":  20,
            "lt":   30
        }
    }
}

被允许的操作符如下:

gt大于

gte大于等于

lt小于

lte小于等于

  • term 查询被用于精确值 匹配,这些精确值可能是数字、时间、布尔或者那些 not_analyzed 的字符串。
代码语言:javascript复制
{ "term": { "age":    26           }}
{ "term": { "date":   "2014-09-01" }}
{ "term": { "public": true         }}
{ "term": { "tag":    "full_text"  }}

term 查询对于输入的文本不 分析 ,所以它将给定的值进行精确查询。

  • terms 查询和 term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件。
代码语言:javascript复制
{ "terms": { "tag": [ "search", "full_text", "nosql" ] }}

term 查询一样,terms 查询对于输入的文本不分析。它查询那些精确匹配的值(包括在大小写、重音、空格等方面的差异)。

  • exists 查询和 missing 查询被用于查找那些指定字段中有值 (exists) 或无值 (missing) 的文档。这与SQL中的 IS_NULL (missing) 和 NOT IS_NULL (exists) 在本质上具有共性:
代码语言:javascript复制
{
    "exists":   {
        "field":    "title"
    }
}

这些查询经常用于某个字段有值的情况和某个字段缺值的情况。

组合多查询

现实的查询需求从来都没有那么简单;它们需要在多个字段上查询多种多样的文本,并且根据一系列的标准来过滤。为了构建类似的高级查询,你需要一种能够将多查询组合成单一查询的查询方法。

你可以用 bool 查询来实现你的需求。这种查询将多查询组合在一起,成为用户自己想要的布尔查询。它接收以下参数:

must文档 必须 匹配这些条件才能被包含进来。

must_not文档 必须不 匹配这些条件才能被包含进来。

should如果满足这些语句中的任意语句,将增加_score,否则,无任何影响。它们主要用于修正每个文档的相关性得分。

filter必须匹配,但它以不评分、过滤模式来进行。这些语句对评分没有贡献,只是根据过滤标准来排除或包含文档。

由于这是我们看到的第一个包含多个查询的查询,所以有必要讨论一下相关性得分是如何组合的。每一个子查询都独自地计算文档的相关性得分。一旦他们的得分被计算出来,bool查询就将这些得分进行合并并且返回一个代表整个布尔操作的得分。

下面的查询用于查找title 字段匹配 how to make millions 并且不被标识为 spam 的文档。那些被标识为 starred 或在2014之后的文档,将比另外那些文档拥有更高的排名。如果 _两者_ 都满足,那么它排名将更高:

代码语言:javascript复制
{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }},
            { "range": { "date": { "gte": "2014-01-01" }}}
        ]
    }
}

如果没有 must 语句,那么至少需要能够匹配其中的一条 should 语句。但,如果存在至少一条 must 语句,则对 should 语句的匹配没有要求。

增加带过滤器的查询

如果我们不想因为文档的时间而影响得分,可以用 filter 语句来重写前面的例子:

代码语言:javascript复制
{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }}
        ],
        "filter": {
          "range": { "date": { "gte": "2014-01-01" }} 
        }
    }
}

range 查询已经从should 语句中移到 filter 语句

通过将 range 查询移到 filter 语句中,我们将它转成不评分的查询,将不再影响文档的相关性排名。由于它现在是一个不评分的查询,可以使用各种对 filter 查询有效的优化手段来提升性能。

所有查询都可以借鉴这种方式。将查询移到 bool 查询的 filter 语句中,这样它就自动的转成一个不评分的 filter 了。

如果你需要通过多个不同的标准来过滤你的文档,bool 查询本身也可以被用做不评分的查询。简单地将它放置到 filter 语句中并在内部构建布尔逻辑:

代码语言:javascript复制
{
    "bool": {
        "must":     { "match": { "title": "how to make millions" }},
        "must_not": { "match": { "tag":   "spam" }},
        "should": [
            { "match": { "tag": "starred" }}
        ],
        "filter": {
          "bool": {   将 bool 查询包裹在 filter 语句中,我们可以在过滤标准中增加布尔逻辑 
              "must": [
                  { "range": { "date": { "gte": "2014-01-01" }}},
                  { "range": { "price": { "lte": 29.99 }}}
              ],
              "must_not": [
                  { "term": { "category": "ebooks" }}
              ]
          }
        }
    }
}

通过混合布尔查询,我们可以在我们的查询请求中灵活地编写 scoring 和 filtering 查询逻辑。

constant_score查询

尽管没有 bool 查询使用这么频繁,constant_score 查询也是你工具箱里有用的查询工具。它将一个不变的常量评分应用于所有匹配的文档。它被经常用于你只需要执行一个 filter 而没有其它查询(例如,评分查询)的情况下。可以使用它来取代只有 filter 语句的 bool 查询。在性能上是完全相同的,但对于提高查询简洁性和清晰度有很大帮助。

代码语言:javascript复制
{
    "constant_score":   {
        "filter": {
            "term": { "category": "ebooks" }   term 查询被放置在 constant_score 中,转成不评分的 filter。这种方式可以用来取代只有 filter 语句的 bool 查询。 
        }
    }
}

验证查询

查询可以变得非常的复杂,尤其 和不同的分析器与不同的字段映射结合时,理解起来就有点困难了。不过validate-queryAPI 可以用来验证查询是否合法。

代码语言:javascript复制
GET /gb/tweet/_validate/query
{
   "query": {
      "tweet" : {
         "match" : "really powerful"
      }
   }
}

以上validate 请求的应答告诉我们这个查询是不合法的:

代码语言:javascript复制
{
  "valid" :         false,
  "_shards" : {
    "total" :       1,
    "successful" :  1,
    "failed" :      0
  }
}

理解错误信息

为了找出查询不合法的原因,可以将explain参数加到查询字符串中:

代码语言:javascript复制
GET /gb/tweet/_validate/query?explain   explain 参数可以提供更多关于查询不合法的信息。
{
   "query": {
      "tweet" : {
         "match" : "really powerful"
      }
   }
}

很明显,我们将查询类型(match)与字段名称 (tweet)搞混了:

代码语言:javascript复制
{
  "valid" :     false,
  "_shards" :   { ... },
  "explanations" : [ {
    "index" :   "gb",
    "valid" :   false,
    "error" :   "org.elasticsearch.index.query.QueryParsingException:
                 [gb] No query registered for [tweet]"
  } ]
}

理解查询语句

对于合法查询,使用explain 参数将返回可读的描述,这对准确理解 Elasticsearch 是如何解析你的 query 是非常有用的:

代码语言:javascript复制
GET /_validate/query?explain
{
   "query": {
      "match" : {
         "tweet" : "really powerful"
      }
   }
}

我们查询的每一个 index 都会返回对应的explanation ,因为每一个 index 都有自己的映射和分析器:

代码语言:javascript复制
{
  "valid" :         true,
  "_shards" :       { ... },
  "explanations" : [ {
    "index" :       "us",
    "valid" :       true,
    "explanation" : "tweet:really tweet:powerful"
  }, {
    "index" :       "gb",
    "valid" :       true,
    "explanation" : "tweet:realli tweet:power"
  } ]
}

explanation中可以看出,匹配really powerfulmatch 查询被重写为两个针对tweet字段的 single-term 查询,一个single-term查询对应查询字符串分出来的一个term。

当然,对于索引 us ,这两个 term 分别是 reallypowerful ,而对于索引 gb ,term 则分别是 reallipower 。之所以出现这个情况,是由于我们将索引 gbtweet 字段的分析器修改为 english 分析器。

8.排序与相关性

默认情况下,返回的结果是按照相关性进行排序的——最相关的文档排在最前。 在本章的后面部分,我们会解释相关性意味着什么以及它是如何计算的, 不过让我们首先看看 sort 参数以及如何使用它。

排序

为了按照相关性来排序,需要将相关性表示为一个数值。在 Elasticsearch 中, 相关性得分 由一个浮点数进行表示,并在搜索结果中通过 _score 参数返回, 默认排序是 _score 降序。

有时,相关性评分对你来说并没有意义。例如,下面的查询返回所有 user_id 字段包含 1 的结果:

代码语言:javascript复制
GET /_search
{
    "query" : {
        "bool" : {
            "filter" : {
                "term" : {
                    "user_id" : 1
                }
            }
        }
    }
}

这里没有一个有意义的分数:因为我们使用的是 filter (过滤),这表明我们只希望获取匹配 user_id: 1 的文档,并没有试图确定这些文档的相关性。 实际上文档将按照随机顺序返回,并且每个文档都会评为零分。

如果评分为零对你造成了困扰,你可以使用 constant_score 查询进行替代:

代码语言:javascript复制
GET /_search {     "query" : {         "constant_score" : {             "filter" : {                 "term" : {                     "user_id" : 1                 }             }         }     } }

这将让所有文档应用一个恒定分数(默认为 1 )。它将执行与前述查询相同的查询,并且所有的文档将像之前一样随机返回,这些文档只是有了一个分数而不是零分。

按照字段的值排序

在这个案例中,通过时间来对 tweets 进行排序是有意义的,最新的 tweets 排在最前。 我们可以使用 sort 参数进行实现:

代码语言:javascript复制
GET /_search
{
    "query" : {
        "bool" : {
            "filter" : { "term" : { "user_id" : 1 }}
        }
    },
    "sort": { "date": { "order": "desc" }}
}

你会注意到结果中的两个不同点:

代码语言:javascript复制
"hits" : {
    "total" :           6,
    "max_score" :       null,   _score 不被计算, 因为它并没有用于排序。 
    "hits" : [ {
        "_index" :      "us",
        "_type" :       "tweet",
        "_id" :         "14",
        "_score" :      null,   _score 不被计算, 因为它并没有用于排序。 
        "_source" :     {
             "date":    "2014-09-24",
             ...
        },
        "sort" :        [ 1411516800000 ]   date 字段的值表示为自 epoch (January 1, 1970 00:00:00 UTC)以来的毫秒数,通过 sort 字段的值进行返回。 
    },
    ...
}

首先我们在每个结果中有一个新的名为 sort 的元素,它包含了我们用于排序的值。 在这个案例中,我们按照 date 进行排序,在内部被索引为 自 epoch 以来的毫秒数 。 long 类型数 1411516800000 等价于日期字符串 2014-09-24 00:00:00 UTC

其次 _scoremax_score 字段都是 null 。 计算 _score 的花销巨大,通常仅用于排序; 我们并不根据相关性排序,所以记录 _score 是没有意义的。如果无论如何你都要计算 _score , 你可以将 track_scores 参数设置为 true

一个简便方法是, 你可以 指定一个字段用来排序:

代码语言:javascript复制
"sort": "number_of_children"

字段将会默认升序排序 ,而按照 _score 的值进行降序排序。

多级排序

假定我们想要结合使用date_score 进行查询,并且匹配的结果首先按照日期排序,然后按照相关性排序:

代码语言:javascript复制
GET /_search
{
    "query" : {
        "bool" : {
            "must":   { "match": { "tweet": "manage text search" }},
            "filter" : { "term" : { "user_id" : 2 }}
        }
    },
    "sort": [
        { "date":   { "order": "desc" }},
        { "_score": { "order": "desc" }}
    ]
}

排序条件的顺序是很重要的。结果首先按第一个条件排序,仅当结果集的第一个 sort 值完全相同时才会按照第二个条件进行排序,以此类推。

多级排序并不一定包含 _score 。你可以根据一些不同的字段进行排序, 如地理距离或是脚本计算的特定 值。

代码语言:javascript复制
Query-string 搜索 也支持自定义排序,可以在查询字符串中使用 sort 参数:

GET /_search?sort=date:desc&sort=_score&q=search

多值字段的排序

一种情形是字段有多个值的排序, 需要记住这些值并没有固有的顺序;一个多值的字段仅仅是多个值的包装,这时应该选择哪个进行排序呢?

对于数字或日期,你可以将多值字段减为单值,这可以通过使用 min、max、avg、sum排序模式 。 例如你可以按照每个date字段中的最早日期进行排序,通过以下方法:

代码语言:javascript复制
"sort": {
    "dates": {
        "order": "asc",
        "mode":  "min"
    }
}

字符串排序与多字段

被解析的字符串字段也是多值字段, 但是很少会按照你想要的方式进行排序。如果你想分析一个字符串,如 fine old art , 这包含 3 项。我们很可能想要按第一项的字母排序,然后按第二项的字母排序,诸如此类,但是 Elasticsearch 在排序过程中没有这样的信息。

你可以使用 minmax 排序模式(默认是 min ),但是这会导致排序以 art 或是 old ,任何一个都不是所希望的。

为了以字符串字段进行排序,这个字段应仅包含一项: 整个 not_analyzed 字符串。 但是我们仍需要 analyzed 字段,这样才能以全文进行查询

一个简单的方法是用两种方式对同一个字符串进行索引,这将在文档中包括两个字段:analyzed用于搜索, not_analyzed用于排序

但是保存相同的字符串两次在_source 字段是浪费空间的。 我们真正想要做的是传递一个单字段 但是却用两种方式索引它。所有的 _core_field 类型 (strings, numbers, Booleans, dates) 接收一个 fields 参数。该参数允许你转化一个简单的映射如:

代码语言:javascript复制
"tweet": {
    "type":     "string",
    "analyzer": "english"
}

为一个多字段映射如:

代码语言:javascript复制
"tweet": {   tweet 主字段与之前的一样: 是一个 analyzed 全文字段。 
    "type":     "string",
    "analyzer": "english",
    "fields": {
        "raw": { 
            "type":  "string",
            "index": "not_analyzed"  新的 tweet.raw 子字段是 not_analyzed. 
        }
    }
}

现在,至少只要我们重新索引了我们的数据,使用 tweet 字段用于搜索,tweet.raw 字段用于排序:

代码语言:javascript复制
GET /_search
{
    "query": {
        "match": {
            "tweet": "elasticsearch"
        }
    },
    "sort": "tweet.raw"
}

以全文 analyzed 字段排序会消耗大量的内存。获取更多信息请看 聚合与分析 。

什么是相关性

我们曾经讲过,默认情况下,返回结果是按相关性倒序排列的。 但是什么是相关性? 相关性如何计算?每个文档都有相关性评分,用一个正浮点数字段 _score 来表示 。 _score 的评分越高,相关性越高。

查询语句会为每个文档生成一个_score 字段。评分的计算方式取决于查询类型不同的查询语句用于不同的目的: fuzzy 查询会计算与关键词的拼写相似程度,terms 查询会计算找到的内容与关键词组成部分匹配的百分比,但是通常我们说的 relevance 是我们用来计算全文本字段的值相对于全文本检索词相似程度的算法。

Elasticsearch 的相似度算法 被定义为检索词频率/反向文档频率, TF/IDF ,包括以下内容:

  • 检索词频率:检索词在该字段出现的频率?出现频率越高,相关性也越高。 字段中出现过 5 次要比只出现过 1 次的相关性高。
  • 反向文档频率:每个检索词在索引中出现的频率?频率越高,相关性越低。检索词出现在多数文档中会比出现在少数文档中的权重更低。(出现越多,就是越不重要)
  • 字段长度准则:字段的长度是多少?长度越长,相关性越低。 检索词出现在一个短的 title 要比同样的词出现在一个长的 content 字段权重更大。

单个查询可以联合使用 TF/IDF 和其他方式,比如短语查询中检索词的距离或模糊查询里的检索词相似度。

相关性并不只是全文本检索的专利。也适用于 yes|no 的子句,匹配的子句越多,相关性评分越高。如果多条查询子句被合并为一条复合查询语句 ,比如 bool 查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。

我们有一️整章着眼于相关性计算和如何让其配合你的需求 控制相关度

理解评分标准

当调试一条复杂的查询语句时, 想要理解 _score 究竟是如何计算是比较困难的。Elasticsearch 在 每个查询语句中都有一个 explain 参数,将 explain 设为 true 就可以得到更详细的信息。

代码语言:javascript复制
GET /_search?explain   explain 参数可以让返回结果添加一个 _score 评分的得来依据。 
{
   "query"   : { "match" : { "tweet" : "honeymoon" }}
}

增加一个 explain 参数会为每个匹配到的文档产生一大堆额外内容,但是花时间去理解它是很有意义的。 如果现在看不明白也没关系 — 等你需要的时候再来回顾这一节就行。下面我们来一点点的了解这块知识点。

首先,我们看一下普通查询返回的元数据:

代码语言:javascript复制
{
    "_index" :      "us",
    "_type" :       "tweet",
    "_id" :         "12",
    "_score" :      0.076713204,
    "_source" :     { ... trimmed ... },

这里加入了该文档来自于哪个节点哪个分片上的信息,这对我们是比较有帮助的,因为词频率和文档频率是在每个分片中计算出来的,而不是每个索引中:

代码语言:javascript复制
 "_shard" :      1,
 "_node" :       "mzIVYCsqSWCG_M_ZffSs9Q",

然后它提供了_explanation 。每个 入口都包含一个 descriptionvaluedetails 字段,它分别告诉你计算的类型、计算结果和任何我们需要的计算细节。

代码语言:javascript复制
"_explanation": {   honeymoon 相关性评分计算的总结 
   "description": "weight(tweet:honeymoon in 0)
                  [PerFieldSimilarity], result of:",
   "value":       0.076713204,
   "details": [
      {  
         "description": "fieldWeight in 0, product of:",
         "value":       0.076713204,
         "details": [
            {    检索词频率 
               "description": "tf(freq=1.0), with freq of:",
               "value":       1,
               "details": [
                  {
                     "description": "termFreq=1.0",
                     "value":       1
                  }
               ]
            },
            {   反向文档频率 
               "description": "idf(docFreq=1, maxDocs=1)",
               "value":       0.30685282
            },
            {   字段长度准则 
               "description": "fieldNorm(doc=0)",
               "value":        0.25,
            }
         ]
      }
   ]
}

输出 explain 结果代价是十分昂贵的,它只能用作调试工具 。千万不要用于生产环境。

第一部分是关于计算的总结。告诉了我们 honeymoontweet 字段中的检索词频率/反向文档频率或 TF/IDF, (这里的文档 0 是一个内部的 ID,跟我们没有关系,可以忽略。)

然后它提供了权重是如何计算的细节:

  • 检索词频率:
代码语言:javascript复制
检索词 `honeymoon` 在这个文档的 `tweet` 字段中的出现次数。
  • 反向文档频率:
代码语言:javascript复制
检索词 `honeymoon` 在索引上所有文档的 `tweet` 字段中出现的次数。
  • 字段长度准则:
代码语言:javascript复制
在这个文档中, `tweet` 字段内容的长度 -- 内容越长,值越小。

复杂的查询语句解释也非常复杂,但是包含的内容与上面例子大致相同。 通过这段信息我们可以了解搜索结果是如何产生的。 JSON 形式的 explain 描述是难以阅读的, 但是转成 YAML 会好很多,只需要在参数中加上 format=yaml

理解文档是如何被索引到的

explain 选项加到某一文档上时, explain api 会帮助你理解为何这个文档会被匹配,更重要的是,一个文档为何没有被匹配。

请求路径为 /index/type/id/_explain ,如下所示:

代码语言:javascript复制
GET /us/tweet/12/_explain
{
   "query" : {
      "bool" : {
         "filter" : { "term" :  { "user_id" : 2           }},
         "must" :  { "match" : { "tweet" :   "honeymoon" }}
      }
   }
}

不只是我们之前看到的充分解释 ,我们现在有了一个 description 元素,它将告诉我们:

代码语言:javascript复制
"failure to match filter: cache(user_id:[2 TO 2])"

也就是说我们的 user_id 过滤子句使该文档不能匹配到

Doc Values介绍

本章的最后一个话题是关于Elasticsearch 内部的一些运行情况。在这里我们先不介绍新的知识点,所以我们应该意识到,Doc Values 是我们需要反复提到的一个重要话题。

当你对一个字段进行排序时,Elasticsearch 需要访问每个匹配到的文档得到相关的值。倒排索引的检索性能是非常快的,但是在字段值排序时却不是理想的结构。

  • 在搜索的时候,我们能通过搜索关键词快速得到结果集。
  • 当排序的时候,我们需要倒排索引里面某个字段值的集合。换句话说,我们需要 转置 倒排索引。

Doc values通过逆置term和doc间的关系来前面提到的数据聚合的问题。倒排索引将term映射到包含它们的doc,doc values将doc映射到它们包含的所有词项,下面是一个示例:

代码语言:javascript复制
Doc      Terms
-----------------------------------------------------------------
Doc_1 | brown, dog, fox, jumped, lazy, over, quick, the
Doc_2 | brown, dogs, foxes, in, lazy, leap, over, quick, summer
Doc_3 | dog, dogs, fox, jumped, over, quick, the
-----------------------------------------------------------------

转置结构在其他系统中经常被称作列存储 。实质上,它将所有单字段的值存储在单数据列中,这使得对其进行操作是十分高效的,例如排序。

Elasticsearch 中,Doc Values 就是一种列式存储结构,默认情况下每个字段的 Doc Values 都是激活的,Doc Values 是在索引时创建的,当字段索引时,Elasticsearch 为了能够快速检索,会把字段的值加入倒排索引中,同时它也会存储该字段的 `Doc Values`。

Elasticsearch 中的 Doc Values 常被应用到以下场景:

  • 对一个字段进行排序
  • 对一个字段进行聚合
  • 某些过滤,比如地理位置过滤
  • 某些与字段相关的脚本计算

因为文档值被序列化到磁盘,我们可以依靠操作系统的帮助来快速访问。当 working set 远小于节点的可用内存,系统会自动将所有的文档值保存在内存中,使得其读写十分高速; 当其远大于可用内存,操作系统会自动把 Doc Values 加载到系统的页缓存中,从而避免了 jvm 堆内存溢出异常。

我们稍后会深入讨论 `Doc Values`。现在所有你需要知道的是排序发生在索引时建立的平行数据结构中

0 人点赞