Elasticsearch 的核心功能是搜索,我们可以使用 Search API 来搜索存储在一个或多个 Elasticsearch 索引中的数据,Search API 有如下两种类型:

  • URI Search:查询是通过查询参数提供的,功能往往比较简单,适用于测试

  • Request Body Search:查询是通过请求的 JSON 主体提供的,即 Query DSL 编写的

这两种方式都支持对多个索引进行操作,支持逗号分隔的索引列表、正则匹配、搜索全部索引则用 *、_all。下面就来介绍 Elasticsearch 提供的丰富的 Search API 及其用法。

URI Search

通过提供请求参数,可以只使用 URI 执行搜索请求。当使用这种模式执行搜索时,并不是所有的搜索选项都会被公开,但对于测试来说它很方便。URI 中支持的搜索参数如下所示:

  • q:指定查询字符串,使用 Query String Syntax 格式
  • df:默认字段,不指定时会对所有字段进行查询
  • analyzer:查询时使用的分析器名称
  • default_operator:使用的默认操作符,取值为 AND 或 OR,默认为 OR
  • lenient:如果为 true 则忽略搜索提供的字段值与文档实际字段类型不匹配的错误
  • sort:按指定字段排序,取值为 fieldname、fieldname:asc、fieldname:desc
  • timeout:搜索的超时时间
  • from:分页查询参数,默认为 0,即从 0 开始
  • size:分页查询参数,默认为 10,即一次返回 10 条

下面示例用于查询 user 字段的值包含 kimchy 的文档:

  1. curl -X GET "localhost:9200/twitter/_search?df=user&q=kimchy&pretty"
  2. # 或者
  3. curl -X GET "localhost:9200/twitter/_search?q=user:kimchy&pretty"

返回结果如下:

  1. {
  2. "took" : 104,
  3. "timed_out" : false,
  4. "_shards" : {
  5. "total" : 1,
  6. "successful" : 1,
  7. "skipped" : 0,
  8. "failed" : 0
  9. },
  10. "hits" : {
  11. "total" : {
  12. "value" : 2,
  13. "relation" : "eq"
  14. },
  15. "max_score" : 0.35667494,
  16. "hits" : [
  17. {
  18. "_index" : "twitter",
  19. "_type" : "_doc",
  20. "_id" : "pOwcJXoBg2C-dN53ZQUN",
  21. "_score" : 0.35667494,
  22. "_source" : {
  23. "user" : "kimchy",
  24. "post_date" : "2009-11-15T14:12:12",
  25. "message" : "trying out Elasticsearch"
  26. }
  27. },
  28. {
  29. "_index" : "twitter",
  30. "_type" : "_doc",
  31. "_id" : "1",
  32. "_score" : 0.35667494,
  33. "_source" : {
  34. "user" : "kimchy",
  35. "post_date" : "2009-11-15T14:12:12",
  36. "message" : "trying out Elasticsearch"
  37. }
  38. }
  39. ]
  40. }
  41. }
  • took:索引数据花费的时间
  • timed_out:查询是否超时
  • _shards:搜索了多少个分片,以及有多少分片搜索成功、失败或跳过
  • hits:结果集
    • total
      • value:搜索到的匹配文档的数量
    • max_score:搜索到的文档的最大相关性算分
    • hits
      • _index:文档所在索引名称
      • _type:文档类型
      • _id:文档 ID
      • _score:文档的相关性得分
      • _source:文档源数据

下面讲解下 Query String Syntax 在 URI Search 中的用法:

1. TermQuery、PhraseQuery

TermQuery 会将句子拆分成单词然后分别进行查询,此时需要将句子用括号括起来,默认采用 OR 操作符,我们也可以指定要执行的布尔操作:

  • 布尔操作:AND、OR、NOT(必须大写)或者 &&、||、! 符号
  • 加减操作:+ 表示 must、- 表示 must_not

使用示例如下:

  1. # 查询包含 Beautiful 或 Mind 单词的文档
  2. http://localhost:9200/twitter/_search?q=title:(Beautiful Mind)&pretty
  3. # 查询包含 Beautiful 和 Mind 单词的文档
  4. http://localhost:9200/twitter/_search?q=title:(Beautiful AND Mind)&pretty
  5. # 查询包含 Beautiful 但不包含 Mind 单词的文档
  6. http://localhost:9200/twitter/_search?q=title:(Beautiful NOT Mind)&pretty
  7. # 查询包含 Beautiful 但不包含 Mind 单词的文档
  8. http://localhost:9200/twitter/_search?q=title:(+Beautiful -Mind)&pretty

使用 PhraseQuery 查询时会将整个句子进行查询,并且要求单词的前后顺序需保持一致。此时需要将句子用引号引起来。使用示例如下:

  1. # 查询包含 Beautiful Mind 的文档
  2. http://localhost:9200/twitter/_search?q=title:"Beautiful Mind"&pretty

2. 范围查询

我们可以通过区间来为日期、数字或字符串字段指定范围,具体如下:

  • [] 表示闭区间,如 date:[2012-01-01 TO 2012-12-31]、count:[10 TO *]
  • {} 表示开区间,如 date:{* TO 2012-01-01}

此外,还可以通过算数符号确定范围(支持布尔操作和加减操作),具体如下:

  • age:>10
  • age:>=10
  • age:<10
  • age:<=10
  • age:(>=10 AND <20)
  • age:(+>=10 +<20)

3. 其他

通配符查询(效率低、占用内存大,不建议使用)

  • ?代表 1 个字符,* 代表 0 或多个字符

正则表达式

  • 如:title:[bt]oy、title:*oy

模糊匹配与近似查询

  • 如:title:beautify~1,用于 TermQuery 查询,代表允许有一个字母可以和 beautify 有差别
  • 如:title:”lord rings”~2,用于 PhraseQuery 查询,代表 lord 和 rings 之间允许有 2 个单词

Request Body Search

使用 Elasticsearch 提供的,基于 JSON 格式的一种查询语言(Query Domain Specific Language,DSL)来执行搜索请求,提供了非常完备的搜索参数,下面详细介绍:

1. query

query 元素允许我们通过 Query DSL 进行查询,Query DSL 的详细内容后面再讲。如下示例是一个基本的查询语句:

  1. curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "query" : {
  4. "match_phrase" : { "user" : "kimchy" }
  5. }
  6. }'

2. form/size

Elasticsearch 默认返回第 1 页的前 10 条结果,可以通过使用 fromsize 参数来配置分页。from 定义了想要获取的第一个结果的偏移量,size 参数允许配置要返回的最大命中量。

如下示例,表示从第二个文档开始搜索,最多返回 5 条命中的文档:

  1. curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "from" : 1,
  4. "size" : 5,
  5. "query" : {
  6. "term" : { "user" : "kimchy" }
  7. }
  8. }'

使用时要注意,分页的深度不能超过 index.max._result_window 配置的值,该值默认为 10000。因为由于搜索请求通常跨越多个分片的,每个分片必须生成自己的排序结果,然后再将这些单独的结果组合起来进行排序,以确保总体排序顺序是正确的。因此越向后翻页,其消耗的内存成本会越高。

作为深度分页的替代方案,我们建议使用 ScrollSearch After API 获得更有效的深度滚动方法。

3. sort

通过 sort 参数可以对搜索结果按指定字段排序,Elasticsearch 默认按照文档的相关性得分降序排序,对于 match_all 查询而言,由于只返回所有文档,不需要评分,文档的顺序为添加文档的顺序。

最好在数字型与日期型字段上使用排序。此外通过 _score 还可以按文档的相关性得分进行排序,以及通过 _doc 按索引顺序排序。排序顺序通过以下参数控制:

  • asc:按升序排序(默认)
  • desc:按降序排序,对 _score 排序时默认降序

sort 使用示例如下:

  1. curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "sort" : [
  4. { "post_date" : "desc"}
  5. ],
  6. "query" : {
  7. "term" : { "user" : "kimchy" }
  8. }
  9. }'

Elasticsearch 支持对数组多值字段排序,可以通过 mode 选项来控制选择哪一个数组值来对它所属的文档进行排序。mode 可以有以下值:

  • max:最大值(desc 的默认值)
  • min:最小值(asc 的默认值)
  • sum:使用所有值的和作为排序值,仅适用于基于数字的数组字段。
  • avg:使用所有值的平均值作为排序值,仅适用于基于数字的数组字段。
  • median:使用所有值的中位数作为排序值,仅适用于基于数字的数组字段。

对数组字段的排序示例如下:

  1. curl -X PUT "localhost:9200/my_index/_doc/1?refresh&pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "product": "chocolate",
  4. "price": [20, 4]
  5. }'
  6. curl -X POST "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
  7. {
  8. "query" : {
  9. "term" : { "product" : "chocolate" }
  10. },
  11. "sort" : [
  12. {"price" : {"order" : "asc", "mode" : "avg"}}
  13. ]
  14. }'

Elasticsearch 还支持根据一个或多个 nested 对象中的字段进行排序。按嵌套字段排序支持具有以下属性的嵌套排序选项:

  • path:定义要对哪个嵌套对象排序,实际的排序字段必须是这个嵌套对象中的一个直接字段。当按嵌套字段排序时,该字段是必需的。
  • filter:过滤器,嵌套路径内的内部对象应该与之匹配,以便通过排序将其字段值考虑在内。

对 nested 字段的排序示例如下,offer 为 nested 字段类型:

  1. curl -X POST "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "query" : {
  4. "term" : { "product" : "chocolate" }
  5. },
  6. "sort" : [
  7. {
  8. "offer.price" : {
  9. "mode" : "avg",
  10. "order" : "asc",
  11. "nested": {
  12. "path": "offer",
  13. "filter": {
  14. "term" : { "offer.color" : "blue" }
  15. }
  16. }
  17. }
  18. }
  19. ]
  20. }'

排序是针对原始内容进行的,倒排索引无法发挥作用,需要用到正排索引,即通过文档 ID 和字段快速得到字段的原始内容。对此 Elasticsearch 有两种实现方式:

  • Fielddata
  • Doc Values(列式存储,对 Text 类型无效)
Doc Values Field data
何时创建 索引时,和倒排索引一起创建 搜索时动态创建
创建位置 磁盘文件 JVM Heap
优点 避免大量内存占用 索引速度快,不占用额外磁盘空间
缺点 降低索引速度,占用额外磁盘空间 文档过多时,动态创建开销大,占用过多 JVM Heap
缺省值 ES 2.x 之后默认方式 ES 1.x 之前

我们可以在 Mapping 设置中显示关闭 doc_values 以增加索引的速度、减少磁盘占用。但前提是明确不需要做排序及聚合分析的字段,如果重新打开,则需要重建索引。

4. _source

默认情况下,搜索请求将返回 _source 字段的全部内容。如果 _source 元字段没有被存储,那就只返回匹配的文档的元数据。我们可以在搜索时通过 _source 参数不让元字段返回或只返回部分字段。

禁用源字段返回,只返回匹配的文档的元数据:

  1. curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "_source": false,
  4. "query" : {
  5. "term" : { "user" : "kimchy" }
  6. }
  7. }'

返回 _source 字段中的部分数据,支持使用通配符:

  1. curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "_source": ["obj.*", "message"],
  4. "query" : {
  5. "term" : { "user" : "kimchy" }
  6. }
  7. }'

5. highlight

通过 highlight 参数能够从搜索结果中的一个或多个字段中获得高亮显示的片段,以便向用户显示查询匹配的位置。当请求高亮显示时,响应为每个搜索命中的文档包含一个额外的 highlight 元素,其中包括高亮显示的字段和高亮显示的片段。

  1. curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
  2. {
  3. "query" : {
  4. "match": { "message": "Elasticsearch" }
  5. },
  6. "highlight" : {
  7. "fields" : {
  8. "message" : {}
  9. }
  10. }
  11. }'

返回结果如下:

  1. {
  2. "took": 58,
  3. "timed_out": false,
  4. "_shards": {
  5. "total": 13,
  6. "successful": 13,
  7. "skipped": 0,
  8. "failed": 0
  9. },
  10. "hits": {
  11. "total": {
  12. "value": 1,
  13. "relation": "eq"
  14. },
  15. "max_score": 0.10536051,
  16. "hits": [
  17. {
  18. "_index": "twitter",
  19. "_type": "_doc",
  20. "_id": "pOwcJXoBg2C-dN53ZQUN",
  21. "_score": 0.10536051,
  22. "_source": {
  23. "user": "kimchy",
  24. "post_date": "2009-11-15T14:12:12",
  25. "message": "trying out Elasticsearch"
  26. },
  27. "highlight": {
  28. "message": [
  29. "trying out <em>Elasticsearch</em>"
  30. ]
  31. }
  32. }
  33. ]
  34. }
  35. }

在返回结果中,Elasticsearch 默认会用 标签标记关键字。如果我们想使用自定义标签,在高亮属性中给需要高亮的字段加上 pre_tags 和 post_tags 即可。使用示例如下:

curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "query" : {
        "match": { "message": "Elasticsearch" }
    },
    "highlight" : {
        "fields" : {
            "message" : {
                    "pre_tags" : ["<strong>"],
                "post_tags" : ["</strong>"]
            }
        }
    }
}'

Elasticsearch 提供了三种高亮器,分别是默认的 highlighter 高亮器、postings-highlighter 高亮器和 fast-vector-highlighter 高亮器。

默认的 highlighter 是最基本的高亮器,highlighter 高亮器实现高亮功能需要对 _source 中保存的原始文档进行二次分析,其速度在三种高亮器里是最慢的,但优点是不需要额外的存储空间。

postings-highighlighter 高亮器实现高亮功能不需要二次分析,但需要在字段的映射中设置 index_options 参数的取值为 offsets,即保存关键词的偏移量,速度快于默认的 highlighter 高亮器。

fast-vector-highlighter 高亮器实现高亮功能速度最快,但是需要在字段的映射中设置 term_vector 参数的取值为 with_positions_offsets,即保存关键词的位置和偏移信息,占用的存储空间最大,是典型的空间换时间。

6. scroll

Scroll API 可用于从单个搜索请求检索大量结果甚至所有结果,其方式与传统数据库的游标使用非常相似。滚动请求返回的结果反映了初始搜索请求发出时索引的状态,就像即时的快照一样。对文档的后续更改(索引、更新或删除)只会影响后续的搜索请求。

为了使用滚动请求,初始搜索请求应该在查询字符串中指定滚动参数,用来告诉 Elasticsearch 应该保持搜索上下文要存活多长时间,如下示例:

curl -X POST "localhost:9200/twitter/_search?scroll=1m&pretty" -H 'Content-Type: application/json' -d'
{
    "size": 100,
    "query": {
        "match" : {
            "message" : "elasticsearch"
        }
    }
}'

返回结果会包含一个 _scroll_id 字段,可以将该值传递给 Scroll API 以搜索下一批结果。在搜索下一批结果时我们还可以再指定 scroll 参数以延长上下文保存时间。如果搜索间隔超过了 scroll 设置的时间后,将自动删除搜索上下文,此时再执行滚动则抛出异常。

curl -X POST "localhost:9200/_search/scroll?pretty" -H 'Content-Type: application/json' -d'
{
    "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFlRseGEtbko4UWJXVURiRE4yek1NV3cAAAAAAAAAOBY3Zi1iTzhLWlFqLWJqeGE2V3hsdDBB" 
}'
# 将上下文保存时间延长1分钟
curl -X POST "localhost:9200/_search/scroll?pretty" -H 'Content-Type: application/json' -d'
{
    "scroll" : "1m", 
    "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFlRseGEtbko4UWJXVURiRE4yek1NV3cAAAAAAAAAOBY3Zi1iTzhLWlFqLWJqeGE2V3hsdDBB" 
}'

通过 size 参数可以配置每批结果返回的最大命中数。每次对 Scroll API 的调用都会返回下一批结果,直到没有更多的结果可以返回,即 hits 数组为空。

7. search_after

前面讲到过,如果要对命中结果进行分页可以通过使用 from 和 size 来完成,但是当达到深层分页时,成本就变得令人望而却步,且搜索请求占用的堆内存和时间与分页的大小是成正比的。

对于有效的深度滚动,推荐使用 Scroll API,但是滚动上下文代表的是某个时刻的快照,因此不推荐用于实时的用户请求。而 search_after 参数可以绕过使用游标的方式,使用前一页的结果来帮助检索下一页,因此比较适用于深度分页,scroll 则适合导出全部文档数据的场景。

假设检索第一个页面的查询示例如下:

curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "size": 10,
    "query": {
        "match" : {
            "user" : "kimchy"
        }
    },
    "sort": [
        {"post_date": "asc"}     
    ]
}'

该请求返回的结果中包含了每个文档的排序值数组,即 sort 元素。

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "twitter",
        "_type" : "_doc",
        "_id" : "pOwcJXoBg2C-dN53ZQUN",
        "_score" : null,
        "_source" : {
          "user" : "kimchy",
          "post_date" : "2009-11-15T14:12:12",
          "message" : "trying out Elasticsearch"
        },
        "sort" : [
          1258294332000
        ]
      },
      {
        "_index" : "twitter",
        "_type" : "_doc",
        "_id" : "1",
        "_score" : null,
        "_source" : {
          "user" : "kimchy",
          "post_date" : "2009-11-15T14:12:12",
          "message" : "trying out Elasticsearch"
        },
        "sort" : [
          1258294332000
        ]
      }
    ]
  }
}

这些排序值可以与参数 search_after 一起使用。例如,我们可以使用上一个文档的排序值,并将其传递给之后的搜索请求,以检索结果的下一页(内部先按 search_after 过滤再分页以达到滚动分页的效果):

curl -X GET "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "size": 10,
    "query": {
        "match" : {
            "user" : "kimchy"
        }
    },
    "search_after": [1258294332000],
    "sort": [
        {"post_date": "asc"}
    ]
}'

在使用 search_after 时,我们应该选择具有唯一值的字段作为排序字段,否则,具有相同排序值的文档的排序顺序将是未定义的,有可能导致丢失或结果重复。当使用 search_after 时,from 参数必须设置为 0 或 -1。

它与 scroll API 的区别在于,search_after 是无状态的,它始终针对最新版本的搜索器进行解析。 因此,排序顺序可能会在分页搜索期间发生变化,具体取决于索引的更新和删除。

8. explain

对搜索请求进行解释,说明文档的相关性算分是如何计算的:

curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "explain": true,
    "query" : {
        "term" : { "user" : "kimchy" }
    }
}'

9. profile

可以查看搜索请求是如何执行的,采用的是什么匹配查询算法:

curl -X GET "localhost:9200/_search?pretty" -H 'Content-Type: application/json' -d'
{
    "profile": true,
    "query" : {
        "term" : { "user" : "kimchy" }
    }
}'

Suggesters

建议功能会根据提供的文本,使用提示符(suggest)来建议外观相似的术语。原理是将输入的文本分解为多个 token,然后在搜索的字典里根据编辑距离查找相似的 term 并返回。

Suggester 就是一种特殊类型的搜索,通常与 _search 请求中的查询部分一起定义,如果没有查询部分,则只返回建议信息。

1. Term Suggester

对建议文本进行分词,对每个词分别进行建议。如下示例,query 部分用于执行搜索请求,suggest 部分用于请求建议,通过 field 参数指定在特定字段中搜索建议项:

curl -X POST "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "size" : 1,
  "query" : {
    "match": {
      "message": "tring out Elasticsearch"
    }
  },
  "suggest" : {
    "my-suggestion" : {
      "text" : "tring out Elasticsearch",
      "term" : {
        "field" : "message"
      }
    }
  }
}'

由于 message 字段里有 trying 这个 term,所以会对 tring 这个词返回 trying 的建议:

{
  "suggest" : {
    "my-suggestion" : [
      {
        "text" : "tring",
        "offset" : 0,
        "length" : 5,
        "options" : [
          {
            "text" : "trying",
            "score" : 0.8,
            "freq" : 16
          }
        ]
      },
      {
        "text" : "out",
        "offset" : 6,
        "length" : 3,
        "options" : [ ]
      },
      {
        "text" : "elasticsearch",
        "offset" : 10,
        "length" : 13,
        "options" : [ ]
      }
    ]
  }
}

从返回中可以看到,每个建议都包含了一个算分,表示建议文本与原文本的相似程度。其通过 Levenshtein Edit Distance 算法实现,核心思想就是一个词改动了多少字符就可以和另外一个词一致。Elasticsearch 提供了很多参数来控制相似性的模糊程度:

  • max_edits:最大编辑距离,只能是 1 到 2 之间的值,默认为 2。
  • prefix_length:必须匹配的最小前缀字符的数量,默认为 1,增加该值能提高拼写检查性能。
  • min_word_length:建议词必须包含的最小长度,默认为 4。

2. Phrase Suggester

Term Suggester 提供了非常方便的 API 来访问特定字符串距离内每个 token 的代替者,Phrase Suggester 在其基础上增加了一些额外的逻辑以选择整个正确的语句而非词项。

curl -X POST "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "size" : 1,
  "query" : {
    "match": {
      "message": "tring out Elasticsearch"
    }
  },
  "suggest" : {
    "my-suggestion" : {
      "text" : "tring out Elasticsearch",
      "phrase" : {
        "field" : "message"
      }
    }
  }
}'

3. Complete Suggester

Completion Suggester 提供了自动完成的功能,用户每输入一个字符,就需要即时发送一个查询请求到后端查找匹配项。该功能可以在用户输入时引导他们找到相关的结果,提高搜索精度,并不会对词进行纠正。

理想情况下,自动完成功能应该与用户输入的速度是一样快的,以便提供与用户输入内容相关的即时反馈。因此 Elasticsearch 采用了不同的数据结构,没有使用倒排索引,而是将 Analyze 的数据编码成 FST 和索引存放在一起,而 FST 会被整个加载进内存,因此速度非常快,不过 FST 只能用于前缀查找。

由于对需要自动补全的字段进行了特殊处理,因此在 Mapping 中需要显示指定字段类型为 completion

curl -X PUT "localhost:9200/music?pretty" -H 'Content-Type: application/json' -d'
{
    "mappings": {
        "properties" : {
            "title_completion" : {
                "type" : "completion"
            }
        }
    }
}'

执行查询:

curl -X POST "localhost:9200/twitter/_search?pretty" -H 'Content-Type: application/json' -d'
{
  "size" : 1,
  "suggest" : {
    "my-suggestion" : {
      "prefix" : "e",
      "completion" : {
        "field" : "title_completion"
      }
    }
  }
}'

此外,还提供了以下参数:

  • size:返回建议的数量
  • skip_duplicates:跳过重复文档