0. 前言

ES 中的查询分为三大类,一是 Term-level queries(字段匹配),二是 Full-text queries(全文搜索),三是不常用的 Specialized queries(专门查询)。各自可细分为如下几种:

  • Term-level queries
    • exists query 字段是否存在值
    • fuzzy query 模糊查询
    • ids query ID 查询
    • prefix query 前缀查询
    • range query 范围查询
    • regexp query 正则查询
    • term query 精确匹配单个字段
    • terms query 精确匹配单个字段,但使用多值进行匹配,类似于 SQL 中的 in 操作
    • terms_set query 字段集合查询。文档需包含字段集合中指定的最少数量字段
    • wildcard query 通配符查询
  • Full-text queries
    • match query 单字段搜索(匹配分词结果,不需要全文匹配)
  • Specialized queries
    • script query 脚本查询

这里并未将每个大类下的全部子类列举出来,只列举了本文涉及到查询。其他可到官网了解。

1. 根据 ID 查询

1.1 获取单个文档

根据文档 ID 获取单个文档信息。对应的 RESTful API 为:

GET <index>/_doc/<_id>
GET <index>/_source/<_id>

比如查询 index 为 es_index_userinfo 中文档 ID 为 1 的用户信息:

GET es_index_userinfo/_doc/1

如果只想返回部分字段,可以使用_source_includes_source_excludes 参数来包括或过滤掉特定字段。

例如不返回创建时间(create_time) 和更新时间(update_time),支持通配符。

GET /es_index_userinfo/_doc/1?_source_includes=*&_source_excludes=*time

翻译成 Go 为:

1
2
3
4
5
6
7
8
9
10
11
12
// GetByID4ES 根据ID查询单个文档
func GetByID4ES(ctx context.Context, index, id string) (string, error) {
res, err := GetESClient().
Get().
Index(index).
Id(id).
Do(ctx)
if err != nil {
return "", err
}
return string(res.Source), nil
}

注意:查询不存在的 ID,会报 elastic: Error 404 (Not Found) 错误。

1.2 批量获取多个文档

利用 Multi get 可以根据文档 ID 批量获取多个文档。

GET /_mget
{
  "docs": [
    {
      "_index": "my-index-000001",
      "_id": "1"
    },
    {
      "_index": "my-index-000001",
      "_id": "2"
    }
  ]
}

GET /<index>/_mget
{
  "docs": [
    {
      "_id": "1"
    },
    {
      "_id": "2"
    }
  ]
}

借助 MgetServiceMultiGetItem 可实现批量获取文档。翻译成 Go 为:

1
2
3
4
5
mgetSvc := EsCli.MultiGet()
for _, id := range ids {
mgetSvc.Add(elastic.NewMultiGetItem().Index(index).Id(id))
}
rsp, err := mgetSvc.Do(ctx)

2. 精确匹配单个字段

比如获指定用户名的用户。

1
2
3
4
5
6
7
8
9
// 创建 term 查询条件,用于精确查询
termQuery := elastic.NewTermQuery("username", "cat")
searchResult, err := GetESClient().Search().
Index("es_index_userinfo"). // 设置索引名
Query(termQuery). // 设置查询条件
Sort("create_time", true). // 设置排序字段,根据 create_time 字段升序排序
From(0). // 设置分页参数 - 起始偏移量,从第 0 行记录开始
Size(10). // 设置分页参数 - 每页大小
Do(ctx) // 执行请求

对应的 RESTful api 为:

GET /es_index_userinfo/_search
{
  "query": {
    "term": {"username": "bob"}
  },
  "sort": [
    {"create_time": "asc"}
  ],
  "from": 0,
  "size":10
}

注意: term 精确匹配 text 类型的字段可能匹配不到,因为 text 类型的字段会被分词.。如果分词的结果集中没有 term 指定的内容,那么将无法匹配。keyword 类型字段不会进行分词,所以可以用 term 进行精确匹配。

解决办法:给 text 类型的字段取一个别名,别名的类型为 keyword,即不进行分词。

"ancestral":{                 
    "type": "text",         
    "fields": {             
      "alias": {          
        "type": "keyword"
      }
    }
}

那么可以通过 ancestral.alias 访问字段 ancestral,其类型设为 keyword。

3. 精确匹配单个字段的多个值

通过 TermsQuery 实现单个字段的多值精确匹配,类似于 SQL 的 in 查询。

比如获指定用户名的用户,只需要命中一个即可。

1
2
3
4
5
6
7
8
9
// 创建 terms 查询条件,用于多值精确查询
termsQuery := elastic.NewTermsQuery("username", "cat", "bob")
searchResult, err := GetESClient().Search().
Index("es_index_userinfo"). // 设置索引名
Query(termsQuery). // 设置查询条件
Sort("create_time", true). // 设置排序字段,根据 create_time 字段升序排序
From(0). // 设置分页参数 - 起始偏移量,从第 0 行记录开始
Size(10). // 设置分页参数 - 每页大小
Do(ctx) // 执行请求

对应的 RESTful api 为:

GET /es_index_userinfo/_search
{
  "query": {
    "terms": {"username": ["bobs","bob"]}
  },
  "sort": [
    {"create_time": "asc"}
  ],
  "from": 0,
  "size":10
}

4. 全文查询

全文查询 Full text queries 是个 ES 的核心查询。

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

使用 MatchQuery 对字段进行全文搜索,即匹配分词结果。如果分词出现在 MatchQuery 中指定的内容(指定的内容也会分词),如果存在相同的分词,则匹配。

假设 “我爱中国” 的分词结果为 “我”、“爱”、“中国”,那么搜索 “我是第一名” 也会匹配,因为 “我是第一名” 的分词结果中也有 “我”。

ES 查看某个字段数据的分词结果。

GET /{index}/{type}/{id}/_termvectors?fields={fields_name}

注意:
(1)如果想对输入不进行分词,请使用 term query
(2)如果想对输入的分词结果全部匹配,请使用 match phrase query
(3)如果想对输入的分词结果全部匹配且最后一个分词支持前缀匹配,请使用 match phrase prefix query
(4)如果是对 keyword 字段进行 MatchQuery,因为该类型不会分词,所以是精确匹配。

比如获取指定用户名的用户。

1
2
3
4
5
6
7
8
9
// 创建 match 查询条件
matchQuery := elastic.NewMatchQuery("username", "bob")
searchResult, err := GetESClient().Search().
Index("es_index_userinfo"). // 设置索引名
Query(matchQuery). // 设置查询条件
Sort("create_time", true). // 设置排序字段,根据 create_time 字段升序排序
From(0). // 设置分页参数 - 起始偏移量,从第 0 行记录开始
Size(10). // 设置分页参数 - 每页大小
Do(ctx) // 执行请求

对应的 RESTful api 为:

GET /es_index_userinfo/_search
{
  "query": {
    "match": {"username": "bob"}
  },
  "sort": [
    {"create_time": "asc"}
  ],
  "from": 0,
  "size":10
}

5. 范围查询

实现类似 age >= 18 and age < 35 的范围查询条件。

1
2
3
4
5
6
7
8
9
// 创建 range 查询条件
rangeQuery := elastic.NewRangeQuery("age").Gte(18).Lte(35)
searchResult, err := GetESClient().Search().
Index("es_index_userinfo"). // 设置索引名
Query(rangeQuery). // 设置查询条件
Sort("create_time", true). // 设置排序字段,根据 create_time 字段升序排序
From(0). // 设置分页参数 - 起始偏移量,从第 0 行记录开始
Size(10). // 设置分页参数 - 每页大小
Do(ctx) // 执行请求

对应的 RESTful api 为:

GET /es_index_userinfo/_search
{
  "query": {
    "range":{"age" : {"gte" : 18, "lte": 35}}
  },
  "sort": [
    {"create_time": "asc"}
  ],
  "from": 0,
  "size":10
}

6. 判断某个字段是否存在

有时,我们需要查询不包含指定字段的记录,此时我们可以借助 Exists Query 来完成。其表示某个字段存在,如果表示不存在,需要借助 must_not boolean query 来完成。

注意:如果字段为 null 或者 [],虽然值为空,但是字段是存在的。

比如查询不存在 phone 字段且年龄大于 18 的用户记录,条件可以这么写:

GET /es_index_userinfo/_search
{
  "query": {
    "bool": {
      "must_not": {"exists": {"field": "phone"}},
      "filter":{"range":{"age":{"gte":18}}}
    }
  }
}

翻译成 Golang,对应的条件写为:

1
2
3
boolQuery := elastic.NewBoolQuery()
boolQuery.MustNot(elastic.NewExistsQuery("phone")))
boolQuery.Filter(elastic.NewRangeQuery("age").Gte(18))

7.bool 组合查询

BoolQuery 是一种组合查询,将多个条件通过类似 SQL 语句 and 和 or 组合在一起来作为查询条件。

其有四种类型的子句:

类型

描述

must

条件必须要满足,并将对分数起作用

filter

条件必须要满足,但又不同于 must 子句,在 filter context 中执行,这意味着忽略评分,并考虑使用缓存。效率会高于 must

should

条件应该满足。可以通过 minimum_should_match 参数指定应该满足的条件个数。如果 bool 查询包含 should 子句,并且没有 must 和 filter 子句,则默认值为 1,否则默认值为 0

must_not

条件必须不能满足。在 filter context 中执行,这意味着评分被忽略,并考虑使用缓存。因为评分被忽略,所以会返回所有 0 分的文档

must

类似 SQL 的 and,代表必须匹配的条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建 bool 查询
boolQuery := elastic.NewBoolQuery()

// 创建查询条件
termQuery := elastic.NewTermQuery("username", "bob")
rangeQuery := elastic.NewRangeQuery("age").Gte(18).Lte(35)

// 设置 bool 查询的 must 条件, 组合了两个子查询
// 搜索用户名为 bob 且年龄在 18~35 岁的用户
boolQuery.Must(termQuery, rangeQuery)

searchResult, err := GetESClient().Search().
Index("es_index_userinfo"). // 设置索引名
Query(boolQuery). // 设置查询条件
Sort("create_time", true). // 设置排序字段,根据 create_time 字段升序排序
From(0). // 设置分页参数 - 起始偏移量,从第 0 行记录开始
Size(10). // 设置分页参数 - 每页大小
Do(ctx) // 执行请求

对应的 RESTful api 为:

GET /es_index_userinfo/_search
{
  "query":{
    "bool":{
      "must":[
        {"term":{"username": "bob"}},
        {"range":{"age":{"gte":18, "lte":35}}}
      ]
    }
  },
  "sort": [
    {"create_time": "asc"}
  ],
  "from": 0,
  "size":10
}

filter

类似 SQL 的 and,代表必须匹配的条件。不计算匹配分值,且子句被考虑用于缓存。

使用 filter 替代 must 条件,查询用户名为 bob 且年龄在 18~35 岁的用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建 bool 查询
boolQuery := elastic.NewBoolQuery()

// 创建查询条件
termQuery := elastic.NewTermQuery("username", "bob")
rangeQuery := elastic.NewRangeQuery("age").Gte(18).Lte(35)

// 设置 bool 查询的 filter 条件, 组合了两个子查询
// 搜索用户名为 bob 且年龄在 18~35 岁的用户
boolQuery.Filter(termQuery, rangeQuery)

searchResult, err := GetESClient().Search().
Index("es_index_userinfo"). // 设置索引名
Query(boolQuery). // 设置查询条件
Sort("create_time", true). // 设置排序字段,根据 create_time 字段升序排序
From(0). // 设置分页参数 - 起始偏移量,从第 0 行记录开始
Size(10). // 设置分页参数 - 每页大小
Do(ctx) // 执行请求

对应的 RESTful api 为:

GET /es_index_userinfo/_search
{
  "query":{
    "bool":{
      "filter":[
        {"term":{"username": "bob"}},
        {"range":{"age":{"gte":18, "lte":35}}}
      ]
    }
  },
  "sort": [
    {"create_time": "asc"}
  ],
  "from": 0,
  "size":10
}

should

类似 SQL 中的 or, 可以通过 minimum_should_match 参数指定应该满足的条件个数。如果 bool 查询包含 should 子句,并且没有 must 和 filter 子句,则默认值为 1,否则默认值为 0。

比如查询用户名为 bob 且年龄为 18 或 35 岁的用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建 bool 查询
boolQuery := elastic.NewBoolQuery()

// 创建查询条件
termQuery := elastic.NewTermQuery("username", "bob")
termQuery1 := elastic.NewTermQuery("age", 18)
termQuery2 := elastic.NewTermQuery("age", 35)

// 设置 bool 查询的 filter 条件, 组合了两个子查询
// 搜索用户名为 bob 且年龄为 18 或 35 岁的用户
boolQuery.Filter(termQuery, termQuery)
boolQuery.Should(termQuery, termQuery1, termQuery2)
boolQuery.MinimumNumberShouldMatch(1) // 至少满足 should 中的一个条件

searchResult, err := GetESClient().Search().
Index("es_index_userinfo"). // 设置索引名
Query(boolQuery). // 设置查询条件
Sort("create_time", true). // 设置排序字段,根据 create_time 字段升序排序
From(0). // 设置分页参数 - 起始偏移量,从第 0 行记录开始
Size(10). // 设置分页参数 - 每页大小
Do(ctx) // 执行请求

对应的 RESTful api 为:

GET /es_index_userinfo/_search
{
  "query":{
    "bool":{
      "filter": {"term":{"username": "bob"}},
      "should":[
        {"term":{"age":18}},
        {"term":{"age":35}}
      ],
      "minimum_should_match" : 1
    }
  },
  "sort": [
    {"create_time": "asc"}
  ],
  "from": 0,
  "size":10
}

must_not

跟 must 作用相反,表示条件必须不能满足。

比如搜索用户名为 bob 且年龄不为 18 或 35 岁的用户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建 bool 查询
boolQuery := elastic.NewBoolQuery()

// 创建查询条件
termQuery := elastic.NewTermQuery("username", "bob")
termQuery1 := elastic.NewTermQuery("age", 18)
termQuery2 := elastic.NewTermQuery("age", 35)

// 设置 bool 查询的 filter 条件, 组合了两个子查询
// 搜索用户名为 bob 且年龄不为 18 和 35 岁的用户
boolQuery.Filter(termQuery)
boolQuery.MustNot(termQuery1, termQuery2)

searchResult, err := GetESClient().Search().
Index("es_index_userinfo"). // 设置索引名
Query(boolQuery). // 设置查询条件
Sort("create_time", true). // 设置排序字段,根据 create_time 字段升序排序
From(0). // 设置分页参数 - 起始偏移量,从第 0 行记录开始
Size(10). // 设置分页参数 - 每页大小
Do(ctx) // 执行请求

对应的 RESTful api 为:

GET /es_index_userinfo/_search
{
  "query":{
    "bool":{
      "filter": {"term":{"username": "bob"}},
      "must_not":[
        {"term":{"age":18}},
        {"term":{"age":35}}
      ]
    }
  },
  "sort": [
    {"create_time": "asc"}
  ],
  "from": 0,
  "size":10
}

8. 分页查询

我们也可以根据条件分页查询。

ES 分页搜索一般有三种方案,from + size、search after、scroll api,这三种方案分别有自己的优缺点。

from+size(浅分页)

这是 ES 分页中最常用的一种方式,与 MySQL 类似,from 指定起始位置,size 指定返回的文档数。

这种分页方式,在分布式的环境下的深度分页是有性能问题的,一般不建议用这种方式做深度分页,可以用下面将要介绍的两种方式。

理解为什么深度分页是有问题的,假设取的页数较大时(深分页),如请求第 20 页,Elasticsearch 不得不取出所有分片上的第 1 页到第 20 页的所有文档,并做排序,最终再取出 from 后的 size 条结果作爲最终的返回值。

所以,当索引记录非常非常多 (千万或亿),是无法使用 from + size 做深分页的,分页越深则越容易 OOM。即便不 OOM,也很消耗 CPU 和内存资源。

所以 ES 为了避免深分页,不允许使用 from + size 的方式查询 1 万条以后的数据,即 from + size 大于 10000 会报错,不过可以通过 index.max_result_window 参数进行修改。

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
// GetByQueryPage4ES 分页查询
// param: index 索引; query 查询条件; page 起始页(从 1 开始); size 页大小
func GetByQueryPage4ES(ctx context.Context, index string, query elastic.Query, page, size int) ([]string, error) {
start := (page - 1) * size
res, err := GetESClient().Search(index).Query(query).From(start).Size(size).Do(ctx)
if err != nil {
return nil, err
}
sl := make([]string, 0, res.TotalHits())
for _, hit := range res.Hits.Hits {
sl = append(sl, string(hit.Source))
}
return sl, nil
}

// GetByQueryPageSort4ES 根据条件分页查询 & 指定字段排序
// param: index 索引; query 查询条件; page 起始页(从 1 开始); size 页大小; field 排序字段; ascending 升序
func GetByQueryPageSort4ES(ctx context.Context, index string, query elastic.Query, page, size int, field string,
ascending bool) ([]string, error) {
from := (page - 1) * size
res, err := GetESClient().
Search(index).
Query(query).
Sort(field, ascending).
From(from).
Size(size).
Do(ctx)
if err != nil {
return nil, err
}
sl := make([]string, 0, res.TotalHits())
for _, hit := range res.Hits.Hits {
sl = append(sl, string(hit.Source))
}
return sl, nil
}

比如分页查询年龄 >=18 且按照创建时间降序排序:

1
2
3
query := elastic.NewBoolQuery()
query.Filter(elastic.NewRangeQuery("age").Gte(18))
sl, err := GetByQueryPageSort4ES(context.Background(), index, query, 1, 500, "create_time", false)

对应的 RESTful api 为:

GET /es_index_userinfo/_search
{
    "query": {
        "bool": {
            "filter":[{"range" : {"age" : {"gte" : 18}}}]
        }
    },
    "from": 0, 
    "size" : 500,
    "sort" : [{"create_time":"desc"}]
}

注意:如果想控制返回哪些字段,可以使用 _source 来指定。比如只返回用户名(username)和年龄(age)。

GET /es_index_userinfo/_search
{
    "query": {
        "bool": {
            "filter":[{"range" : {"age" : {"gte" : 18}}}]
        }
    },
    "from": 0, 
    "size" : 500,
    "sort" : [{"create_time":"desc"}],
    "_source": ["username", "age"]
}

Go 代码带上_source 的方式。

1
2
3
4
5
6
7
8
9
fsc := elastic.NewFetchSourceContext(true).Include("username", "age")
res, err := GetESClient().
Search(index).
Query(query).
FetchSourceContext(fsc).
Sort(field, ascending).
From(from).
Size(size).
Do(ctx)

scroll api(深分页)

创建一个快照,有新的数据写入以后,无法被查到。每次查询后,输入上一次的 scroll_id。目前官方已经不推荐使用这个 API 了,建议使用 search after。

首先需要获取第一页数据并获取游标 ID,然后便可以根据游标 ID 继续获取下一页数据。如果下一页为空会报 EOF 错误,此时便可知拉取结束了。

比如我们还是要分页获取籍贯为安徽的用户,且按照创建时间降序。

GET es_index_userinfo/_search?scroll=1m
{
  "size": 1,
  "query": {
    "match": {"ancestral": "安徽"}
  },
  "sort": [
    {"create_time": "desc"}
  ]
}

在返回的数据中,有一个 _scroll_id 字段,下次搜索的时候带上这个数据,并且使用下面的查询语句。

POST _search/scroll
{
  "scroll" : "1m",
  "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFFRpdno4bm9CU3A1TEhvY3ktQjZzAAAAAAKolSoWbm04UWQ5SHlRdDJRRjZaeGFBdjFEQQ=="
}

上面的 scroll 指定搜索上下文保留的时间,1m 代表 1 分钟,还有其他时间可以选择,有 d、h、m、s 等,分别代表天、时、分钟、秒。

搜索上下文有过期自动删除,但如果自己知道什么时候该删,可以自己手动删除,减少资源占用。

DELETE /_search/scroll
{
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAA6UWWVJRTk9TUXFTLUdnU28xVFN6bEM4QQ=="
}

Go 代码示例:

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
// GetByQueryPageSortScroll4ES 获取第一页数据 & 获取游标ID
// ret: 文档切片, 游标ID, error
func GetByQueryPageSortScroll4ES(ctx context.Context, index string, query elastic.Query, size int, field string,
ascending bool) ([]string, string, error) {
res, err := GetESClient().
Scroll(index).
Query(query).
Sort(field, ascending).
Size(size).
Do(ctx)
if err != nil {
return nil, "", err
}
sl := make([]string, 0, res.TotalHits())
for _, hit := range res.Hits.Hits {
sl = append(sl, string(hit.Source))
}
return sl, res.ScrollId, nil
}

// GetByQScrollID4ES 根据游标 ID 获取下一页
func GetByQScrollID4ES(ctx context.Context, scrollID string) ([]string, error) {
res, err := GetESClient().
Scroll().
ScrollId(scrollID).
Do(ctx)
if err != nil {
return nil, err
}
sl := make([]string, 0, res.TotalHits())
for _, hit := range res.Hits.Hits {
sl = append(sl, string(hit.Source))
}
return sl, nil
}

search after(深分页)

search after 利用实时游标来帮我们解决实时滚动的问题。

第一次搜索时需要指定 sort,并且保证值是唯一的,可以通过加入 _id 保证唯一性。

比如获取籍贯为安徽的用户,且按照创建时间降序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
matchQuery := elastic.NewMatchQuery("ancestral", "安徽")

// 查询第一页(无需指定 SearchAfter)
searchResult, err := GetESClient().Search().
Index("es_index_userinfo"). // 设置索引名
Query(matchQuery). // 设置查询条件
Sort("create_time", false). // 按照创建时间降序
Sort("_id", false). // 加入 ID 排序保证 after id 唯一
Size(1000). // 设置分页参数
Do(ctx)

// 查询第 n 页(n > 1,需要指定 SearchAfter)
searchResult, err := GetESClient().Search().
Index("es_index_userinfo"). // 设置索引名
Query(matchQuery). // 设置查询条件
Sort("create_time", false). // 按照创建时间降序
Sort("_id", false). // 加入 ID 排序保证 after id 唯一
SearchAfter(lastCreateTime, id). // 上页最后一条的创建时间和 ID
Size(1000). // 设置分页参数
Do(ctx)

对应 RESTful api 的示例。

GET es_index_userinfo/_search
{
  "size": 1,
  "query": {
    "match": {"ancestral": "安徽"}
  },
  "sort": [
    {"create_time": "desc"},
    {"_id": "desc"}
  ]
}

在返回的结果中,最后一个文档有类似下面的数据,由于我们排序用的是两个字段,返回的是两个值。

"sort" : [
    1627522828,
    "2"
]

第二次搜索,带上这个 sort 信息即可,如下:

GET es_index_userinfo/_search
{
  "size": 1,
  "query": {
    "match": {"ancestral": "安徽"}
  },
  "sort": [
    {"create_time": "desc"},
    {"_id": "desc"}
  ],
  "search_after": [
    1627522828,
    "2"
  ]
}

小结

from + size 的优点是简单,缺点是在深度分页的场景下系统开销比较大。

search after 可以实时高效的进行分页查询,但是它只能做下一页这样的查询场景,不能随机的指定页数查询。

scroll api 方案也很高效,但是它基于快照,不能用在实时性高的业务场景,且官方已不建议使用。

9. 查询文档是否存在

借助 ExistsService 使用 HEAD 检查文档是否存在判断。

如果文档存在, Elasticsearch 将返回一个 200 ok 的状态码,若文档不存在, Elasticsearch 将返回一个 404 Not Found 的状态码。

9.1 根据 ID 判断文档是否存在

1
2
3
4
// IsDocExists 某条记录是否存在
func IsDocExists(ctx context.Context, id, index string) (bool, error) {
return GetESClient().Exists().Index(index).Id(id).Do(ctx)
}

RESTful api 示例:

head es_index_userinfo/_doc/1

返回:

200 - OK

9.2 查询符合条件的文档数量

可以借助 CountService 查询符合条件的文档数量,进而判断文档是否存在。

比如查询年龄 >=18 的用户数量。

1
2
3
4
5
6
// 创建 range 查询条件
rangeQuery := elastic.NewRangeQuery("age").Gte(18)
cnt, err := GetESClient().
Count("es_index_userinfo").
Query(rangeQuery).
Do(ctx)

RESTful api 示例:

GET es_index_userinfo/_count
{
    "query": {
    "range": {
        "age": {"gte" : 18}
    }
  }
}

10. 获取文档数量

上一节已经说了可以借助 CountService 查询符合条件的文档数量,如果想查询 index 下的所有文档呢?

很简单,不指定条件即可。

1
2
3
4
// GetIndexDocNum 获取索引文档总数
func GetIndexDocNum(ctx context.Context, index string) (int64, error) {
return GetESClient().Count(index).Do(ctx)
}

RESTful API 示例:

GET es_index_userinfo/_count

# 示例结果
{
  "count" : 337,
  "_shards" : {
    "total" : 20,
    "successful" : 20,
    "skipped" : 0,
    "failed" : 0
  }
}

12. 脚本查询

有时候,我们需要通过脚本来设置复杂的查询条件。

注意

  • 脚本查询属于慢查询,在对性能要求严格的场景下谨慎使用;
  • 如果 search.allow_expensive_queries 被设置为 false(缺省为 true),则无法进行脚本查询。

12.1 判断数组长度

ES 中每一个字段都是数组,我们无需对字段做额外的设置,便可以将其当作数组来使用。

有时我们需要根据某个字段的元素个数,即数组长度来查询,此时便需要借助于脚本来完成。比如查询 phone 字段的长度为 1 个且等于某个值。

POST /es_index_userinfo/_search
{
  "query": {
    "bool":{
      "filter": [
        {"term":{"phone":18819064334}},
        {"script": {"script": "doc['phone'].length == 1"}}
      ]
    }
  }
}

对应的 Golang 条件设置如下:

1
2
3
boolQuery := elastic.NewBoolQuery().
Filter(elastic.NewTermQuery("phone", 18819064334)).
Filter(elastic.NewScriptQuery(elastic.NewScript("doc['phone'].length == 1")))

如果需要向脚本传参,我们在脚本中设置相关的参数即可。

POST /es_index_userinfo/_search
{
  "query": {
    "bool":{
      "filter": [
        {"term":{"phone":18819064334}},
        {
          "script": {
            "script": {
              "source": "doc['phone'].length == params.length",
              "params": {"length": 1}
            }
          }
        }
      ]
    }
  }
}

对应 Golang 条件设置如下:

1
2
3
4
5
6
7
boolQuery := elastic.NewBoolQuery().
Filter(elastic.NewTermQuery("phone", 18819064334)).
Filter(elastic.NewScriptQuery(elastic.NewScript("doc['phone'].length == 1").Params(
map[string]interface{}{
"length": 1,
},
)))

13. 遍历查询结果

获取到查询结果后,我们可以借助 olivere/elastic 提供的工具函数 func (*SearchResult) Each 完成对查询结果的遍历。下面是一个示例。

假设 ES 中的文档对应的 Go struct 如下。

1
2
3
4
5
6
7
8
9
10
11
type UserInfo struct {
Id uint64 `json:"id,omitempty"`
Age int32 `json:"age,omitempty"`
Username string `json:"username,omitempty"`
Nickname string `json:"nickname,omitempty"`
Identity string `json:"identity,omitempty"`
Phone uint64 `json:"phone,omitempty"`
Ancestral string `json:"ancestral,omitempty"`
UpdateTime int64 `json:"update_time,omitempty"`
CreateTime int64 `json:"create_time,omitempty"`
}

遍历取出查询到的文档。

1
2
3
4
for _, v := range res.Each(reflect.TypeOf(UserInfo{})) {
u := v.(UserInfo)
...
}