如何正确使用 ES 搜索引擎——查询

Elasticsearch 是一个使用 Java 编写的开源搜索引擎,建立在全文搜索引擎库 Apache Lucene™ 基础之上,它的内部使用 Lucene 做索引与搜索,但隐藏了 Lucene 的复杂性,取而代之的是将所有功能打包成一个单独的服务,可以通过程序与它提供的一套简单的 RESTful API 进行通信。

查询表达式(query)

查询表达式(Query DSL)是一种非常灵活的查询语言,es 使用它以简单的 json 接口来展现 Lucene 的绝大部分功能,要使用这种查询表达式,只需将查询语句传递给 query 参数:

1
2
3
4
GET /_search
{
"query": YOUR_QUERY_HERE
}

query DSL

在查询上下文中,查询会回答这个问题——“这个文档匹不匹配这个查询,它的相关度高吗?”

如何验证匹配很好理解,如何计算相关度呢?es 中索引的数据都会存储一个 _score 分值,分值越高就代表越匹配。另外关于某个搜索的分值计算还是很复杂的,因此也需要一定的时间。

filter DSL

在过滤器上下文中,查询会回答这个问题——“这个文档匹不匹配?”

答案很简单,是或者不是。它不会去计算任何分值,也不会关心返回的排序问题,因此效率会高一点。另外,经常使用过滤器,es 会自动的缓存过滤器的内容,会提高很多查询的性能。

查询请求结果

hits

返回结果中最重要的部分是 hits ,它包含 total 字段来表示匹配到的文档总数,并且一个 hits 数组包含所查询结果的前十个文档。在 hits 数组中每个结果包含文档的 _index_type_id ,加上 _source 字段,这意味着我们可以直接从返回的搜索结果中使用整个文档,而不像其他的搜索引擎,仅仅返回文档的 ID。每个结果还有一个 _score ,它衡量了文档与查询的匹配程度,max_score 表示与查询所匹配文档的 _score 的最大值。

took

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

timeout

timed_out 值告诉我们查询是否超时。默认情况下,搜索请求不会超时。如果低响应时间比完成结果更重要,你可以指定 timeout 为 10ms 或者 1s。在请求超时之前,es 将会返回已经成功从每个分片获取的结果。

shards

_shards 部分告诉我们在查询中参与分片的总数,以及这些分片成功了多少个失败了多少个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 这是查询返回的结果
{
"took": 2, // 执行整个搜索请求耗费了多少毫秒
"timed_out": false, // 查询是否超时
"_shards": { // 查询中参与分片的总数,以及这些分片成功了多少个失败了多少个
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": { // 所有查询到的结果
"total": 1008, // 表示匹配到的文档总数
"max_score": 1, // 结果中最大的评分
"hits": [
{
"_index": "bank", // 索引名称
"_type": "account", // type 名称
"_id": "25", // id 名称
"_score": 1, // 评分
"_source": { // 存储的数据源信息
"account_number": 25,
"balance": 40540,
"firstname": "Virginia",
"lastname": "Ayala",
"age": 39,
"gender": "F",
"address": "171 Putnam Avenue",
"employer": "Filodyne",
"email": "virginiaayala@filodyne.com",
"city": "Nicholson",
"state": "PA"
}
}
]
}
}

多索引,多类型

  • /_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 类型

分页(from、size)

  • from

    ​ 显示应该跳过的初始结果数量,默认是 0

  • size

    ​ 显示应该返回的结果数量,默认是 10

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

1
2
3
4
5
GET /_search
{
"from": 30,
"size": 10
}

精确值查找(term,terms)

查找单个精确值

可以用 term 处理数字、布尔值、日期、以及文本。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 查找价格字段值为 $20 的文档
GET /my_store/products/_search
{
"query" : {
"constant_score": {
"filter": {
"term": {
"price": 20
}
}
}
}
}

查找多个精确值

term 查询对于查找单个值非常有用,但如果我们想要搜索多个值该如何处理呢?不需要使用多个 term 查询,我们只要用单个 terms 查询,terms 允许指定多个匹配条件,除此之外它几乎与 term 的使用方式一模一样,我们只要将 term 字段的值改为数组即可:

它几乎与 term 的使用方式一模一样,与指定单个价格不同,我们只要将 term 字段的值改为数组即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 查找价格字段值为 $20 或 $30 的文档
GET /my_store/products/_search
{
"query": {
"constant_score": {
"filter": {
"terms": {
"price": [20, 30]
}
}
}
}
}

当进行精确值查找时,请尽可能多的使用 过滤式查询(filters),过滤器很重要,因为它们执行速度非常快(不会计算相关度,而且可以被缓存)。

当我们不关心 TF/IDF 对搜索结果排序的影响,只想知道一个词是否在某个字段中出现过时,可以使用 constant_scorequery 查询语句或者 filter 过滤语句包装起来,同时可以使用 boost 指定权重:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 忽略 TF/IDF
GET /_search
{
"query": {
"bool": {
"should": [
{ "constant_score": {
"query": { "match": { "features": "wifi" }}
}},
{ "constant_score": {
"query": { "match": { "features": "garden" }}
}},
{ "constant_score": {
"boost": 2 // pool 语句的权重提升值为 2 ,而其他的语句为 1
"query": { "match": { "features": "pool" }}
}}
]
}
}
}

范围(range)

range 过滤允许我们按照指定范围查找某个文档,适用于数字、日期和字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 查找价格字段值在 $20 和 $30 之间的文档
GET /my_store/products/_search
{
"query": {
"constant_score": {
"filter": {
"range": {
"price": {
"gte": 20,
"lt": 30
}
}
}
}
}
}

// 查找 2014-01-01 到 2014-01-07 之间的文档
GET /my_store/products/_search
{
"query": {
"range" : {
"timestamp" : {
"gt" : "2014-01-01 00:00:00",
"lt" : "2014-01-07 00:00:00"
}
}
}
}

可供组合的选项如下:

  • gt> 大于(greater than)
  • lt< 小于(less than)
  • gte>= 大于等于(greater than or equal to)
  • lte<= 小于等于(less than or equal to)

组合过滤器(bool)

布尔过滤器

一个 bool 过滤器可以由三部分组成:

  • must

    所有的语句都必须匹配,与 AND 等价。

  • must_not

    所有的语句都不能匹配,与 NOT 等价。

  • should

    至少有一个语句要匹配,与 OR 等价。

使用组合过滤器进行查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /my_store/products/_search
{
"query": {
"filtered": {
"filter": {
"bool": {
"should": [
{ "term": {"price": 20}},
{ "term": {"productID": "XHDK-A-1293-#fJ3"}}
],
"must_not": {
"term": {"price": 30}
}
}
}
}
}
}

嵌套布尔过滤器

尽管 bool 是一个复合过滤器,可以接受多个子过滤器,但它本身仍然还只是一个过滤器。因此我们可以将一个 bool 过滤器嵌套在其他 bool 过滤器内部,这为我们提供了处理任意复杂布尔逻辑的能力。

全文匹配(match、match_all、multi_match)

match 查询

匹配查询 match 是一个 标准 查询,无论需要查询什么字段, match 查询都应该会是首选的查询方式。同时它是一个高级 全文查询 ,这表示它既能处理全文字段,又能处理精确字段。

我们用一个示例来解释使用 match 搜索全文字段中的单个词:

1
2
3
4
5
6
7
8
9
GET /my_index/my_type/_search
{
"query": {
"match": {
"title": "QUICK!"
}
}
}

可以使用 operator 参数来提高精度,默认情况下该操作符是 or ,可以修改成 and 让所有指定词项都必须匹配。

可以使用 minimum_should_match 最小匹配参数来控制精度,以排除那些不太相关的文档。

match_all 查询

查询所有文档,是没有查询条件下的默认语句:

1
2
3
4
5
6
{
"query": {
"match_all": {}
}
}

multi_match 查询

multi_match查询允许你在 match 查询的基础上同时搜索多个字段:

1
2
3
4
5
6
7
8
{
"multi_match": {
"query": "Quick brown fox",
"fields": [ "title", "body" ],
"minimum_should_match": "30%"
}
}

短语匹配(match_phrase

就像 match 查询对于标准全文检索是一种最常用的查询一样,当你想寻找邻近的几个单词时,就会使用 match_phrase 查询:

1
2
3
4
5
6
7
8
9
GET /my_index/my_type/_search
{
"query": {
"match_phrase": {
"title": "quick brown fox"
}
}
}

match 查询类似,match_phrase 查询首先将查询字符串解析成一个词项列表,然后对这些词项进行搜索,但只保留那些包含 全部 搜索词项,且 位置 与搜索词项相同的文档。比如对于 quick fox 的短语搜索可能不会匹配到任何文档,因为没有文档包含的 quick 词之后紧跟着 fox

近似匹配(prefix、wildcards、regexp)

prefix 前缀查询

prefix 查询是一个词级别的底层的查询,它不会在搜索之前分析查询字符串,而是假定传入前缀就正是要查找的前缀。为了找到所有以 W1 开始的邮编,可以使用简单的 prefix 查询:

1
2
3
4
5
6
7
8
9
GET /my_index/address/_search
{
"query": {
"prefix": {
"postcode": "W1"
}
}
}

wildcard 通配符查询

prefix 前缀查询的特性类似,wildcard 通配符查询也是一种底层基于词的查询,与前缀查询不同的是它使用标准的 shell 通配符查询,允许匹配指定的正则式,其中:

  • * 匹配多个任意字符(包括零个或一个)
  • ? 匹配一个任意字符(不包括零个)

这个查询会匹配包含 W1F 7HWW2F 8HW 的文档:

1
2
3
4
5
6
7
8
9
GET /my_index/address/_search
{
"query": {
"wildcard": {
"postcode": "W?F*HW"
}
}
}

regexp 正则表达式查询

假如现在只想匹配 W 区域的所有邮编(只以 W 开始并跟随一个数字),prefix 前缀匹配可能会包括以 WC 开头的所有邮编,wildcard 通配符匹配也可能会遇到同样的问题,如果想匹配,这时可以使用 regexp 正则表达式查询写出更复杂的模式:

1
2
3
4
5
6
7
8
9
GET /my_index/address/_search
{
"query": {
"regexp": {
"postcode": "W[0-9].+"
}
}
}

处理 Null 值(exists、missing)

存在查询

exists 查询可以用于查找文档中是否包含指定字段:

1
2
3
4
5
6
7
8
9
10
11
GET /my_index/posts/_search
{
"query" : {
"constant_score" : {
"filter" : {
"exists" : { "field" : "tags" }
}
}
}
}

缺失查询

missing 查询本质上与 exists 恰好相反,它返回没有某个字段的文档:

1
2
3
4
5
6
7
8
9
10
11
GET /my_index/posts/_search
{
"query" : {
"constant_score" : {
"filter": {
"missing" : { "field" : "tags" }
}
}
}
}

聚合和过滤(aggs、filter)

agg 聚合

聚合操作被置于顶层参数 aggs 之下,可以为聚合指定一个我们想要名称,然后定义单个桶的类型 terms。比如汽车经销商可能会想知道哪个颜色的汽车销量最好,用聚合可以轻易得到结果:

1
2
3
4
5
6
7
8
9
10
11
12
GET /cars/transactions/_search
{
"size" : 0, // 将返回记录数设置为 0 来提高查询速度
"aggs" : {
"popular_colors" : {
"terms" : {
"field" : "color"
}
}
}
}

在本例中,我们使用 popular_colors 作为聚合的名字,并定义了一个 terms 桶,这个 terms 桶会使用 color 字段为每个颜色动态创建新桶。

filter 过滤

聚合范围限定还有一个自然的扩展就是过滤,因为聚合是在查询结果范围内操作的,任何可以适用于查询的过滤器也可以应用在聚合上。如果我们想找到售价在 $10,000 美元之上的所有汽车同时为这些车计算平均售价,可以简单地使用一个 constant_score 查询和 filter 约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET /cars/transactions/_search
{
"size" : 0,
"query" : {
"constant_score": {
"filter": {
"range": {
"price": {
"gte": 10000
}
}
}
}
},
"aggs": {
"avg_price": {
"avg": {
"field": "price"
}
}
}
}

avg 为均值度量,min 为最小度量,max 为最大度量。

排序(sort)

简单排序

我们可以简单的使用 sort 参数进行实现按照字段的值排序:

1
2
3
4
5
6
7
8
9
10
GET /_search
{
"query": {
"bool": {
"filter": { "term": { "user_id": 1 }}
}
},
"sort": { "date": { "order": "desc" }}
}

多级排序

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /_search
{
"query": {
"bool": {
"must": { "match": { "tweet": "manage text search" }},
"filter": { "term": { "user_id" : 2 }}
}
},
"sort": [
{ "date": { "order": "desc" }}, // 先按照日期排序
{ "_score": { "order": "desc" }}
]
}

参考资料:Elasticsearch: 权威指南 - 深入搜索