Elasticsearch 搜索(上)

Elasticsearch 搜索(上)

搜索简介

Elasticsearch 支持多种搜索:

  • 精确搜索(词项搜索):搜索数值、日期、IP 或字符串的精确值或范围。
  • 全文搜索:搜索非结构化文本数据并查找与查询项最匹配的文档。
  • 向量搜索:存储向量,并使用 ANN 或 KNN 搜索来查找相似的向量,从而支持 语义搜索 等场景。

可以使用 _search API 来搜索和聚合 Elasticsearch 数据流或索引中的数据。API 的 query 请求采用 DSL 语义来进行查询。

Elasticsearch 支持两种搜索方式:URI Query 和 Request Body Query(DSL)

::: details URI Query 示例

1
2
3
GET /kibana_sample_data_ecommerce/_search?q=customer_first_name:Eddie
GET /kibana*/_search?q=customer_first_name:Eddie
GET /_all/_search?q=customer_first_name:Eddie

:::

::: details Request Body Query(DSL)示例

1
2
3
4
5
6
POST /kibana_sample_data_ecommerce/_search
{
"query": {
"match_all": {}
}
}

:::

当文档存储在 Elasticsearch 中时,它会在 1 秒内近乎实时地被索引和完全搜索。

Elasticsearch 基于 Lucene 开发,并引入了分段搜索的概念。分段类似于倒排索引,但 Lucene 中的单词 index 表示“段的集合加上提交点”。提交后,将向提交点添加新分段并清除缓冲区。

位于 Elasticsearch 和磁盘之间的是文件系统缓存。内存中索引缓冲区的文档会被写入新的分段,然后写入文件系统缓存,然后才刷新到磁盘。

A Lucene index with new documents in the in-memory buffer

Lucene 允许写入和打开新分段,使其包含的文档对搜索可见,而无需执行完全提交。这是一个比提交到磁盘要轻松得多的过程,并且可以频繁地完成而不会降低性能。

The buffer contents are written to a segment, which is searchable, but is not yet committed

在 Elasticsearch 中,写入和打开新分段的这一过程称为刷新。刷新使自上次刷新以来对索引执行的所有操作都可用于搜索。

默认情况下,Elasticsearch 每秒定期刷新一次索引,但仅限于在过去 30 秒内收到一个或多个搜索请求的索引。这就是我们说 Elasticsearch 具有近实时搜索能力的原因:文档更改不会立即对搜索可见,但会在此时间范围内变得可见。

排序

在 Elasticsearch 中,默认排序是按照相关性的评分(_score进行降序排序。_score 是浮点数类型,_score 评分越高,相关性越高。评分模型的选择可以通过 similarity 参数在映射中指定。

在 5.4 版本以前,默认的相关性算法是 TF-IDF。TF 是词频(term frequency),IDF 是逆文档频率(inverse document frequency)。一个简短的解释是,一个词条出现在某个文档中的次数越多,它就越相关;但是,如果该词条出现在不同的文档的次数越多,它就越不相关。5.4 版本以后,默认的相关性算法 BM25。

此外,也可以通过 sort 自定排序规则,如:按照字段的值排序、多级排序、多值字段排序、基于 geo(地理位置)排序以及自定义脚本排序。

::: details 排序示例

单字段排序

1
2
3
4
5
6
7
8
9
10
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {}
},
"sort": [
{"order_date": {"order": "desc"}}
]
}

多字段排序

1
2
3
4
5
6
7
8
9
10
11
12
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {}
},
"sort": [
{"order_date": {"order": "desc"}},
{"_doc":{"order": "asc"}},
{"_score":{ "order": "desc"}}
]
}

:::

详情参考:Sort search results

分页

默认情况下,Elasticsearch 搜索会返回前 10 个匹配的匹配项。

Elasticsearch 支持三种分页查询方式。

  • from + size
  • search after
  • scroll

from + size

可以使用 fromsize 参数分别指定起始页和每页记录数。

当一个查询:from = 990, size = 10,会在每个分片上先获取 1000 个文档。然后,通过协调节点聚合所有结果。最后,再通过排序选取前 1000 个文档。

页数越深,占用内存越多。为了避免深分页问题,ES 默认限定最多搜索 10000 个文档,可以通过 index.max_result_window 进行设置。

::: details from + size 分页查询示例

1
2
3
4
5
6
7
8
POST /kibana_sample_data_ecommerce/_search
{
"from": 2,
"size": 5,
"query": {
"match_all": {}
}
}

:::

scroll

scroll 搜索方式类似于 RDBMS 中的游标,只允许向下翻页。每次下一页查询后,使用返回结果的 scroll id 来作为下一次翻页的标记。

scroll 在搜索初始化阶段会生成快照,后续数据的变化无法及时体现在查询结果,因此更加适合一次性批量查询或非实时数据的分页查询。

启用游标查询时,需要注意设定期望的过期时间(scroll = 1m),以降低维持游标查询窗口所需消耗的资源。

注意:Elasticsearch 官方不再建议使用 scroll 查询方式进行深分页,而是推荐使用 search_after 和时间点(PIT)一起使用。

::: details scroll 分页查询示例

1
2
3
4
5
6
7
8
9
10
POST /kibana_sample_data_ecommerce/_search?scroll=1m
{
"size": 3,
"query": {
"match": {
"currency": "EUR"
}
}
}

响应结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"_scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAmTkWRTMzNmxBYmZUbUdsdFNqMnJoTl84Zw==",
"took": 0,
"timed_out": false,
"_shards": {
// 略
},
"hits": {
"total": {
"value": 4675,
"relation": "eq"
},
"max_score": 1,
"hits": [] // 略
}
}

:::

详情参考:Paginate search results

search after

search after 搜索方式不支持指定页数,只能向下翻页;并且需要指定 sort,并保证值是唯一的。然后,可以反复使用上次结果中最后一个文档的 sort 值进行查询。

search after 实现的思路同 scroll 方式基本一致,通过记录上一次分页的位置标识,来进行下一次分页查询。相比于 scroll 方式,它的优点是可以实时获取数据的变化,解决了查询快照导致的查询结果延迟问题。

::: details search after 分页查询示例

第一次查询

1
2
3
4
5
6
7
8
9
10
11

POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {}
},
"sort": [
{"order_date": {"order": "desc"}}
]
}

响应结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"took": 2609,
"timed_out": false,
"_shards": {
// 略
},
"hits": {
"total": {
"value": 4675,
"relation": "eq"
},
"max_score": null,
"hits": [
// 略多条记录
// 最后一条记录
{
// 略
"sort": [1642893235000]
}
]
}
}

从上次查询的响应中获取 sort 值,然后将 sort 值插入 search after 数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {}
},
"search_after": [
1642893235000
],
"sort": [
{
"order_date": {
"order": "desc"
}
}
]
}

:::

限定字段

默认情况下,搜索响应中的每个点击都包含 _source,该字段保存了原始文本的 JSON 对象。有两种推荐的方法可以从搜索查询中检索所选字段:

  • 使用 fields 选项指定响应结果中返回的值。
  • 如果需要在查询时返回原始文本数据,可以使用 _source 选项。

折叠搜索结果

Elasticsearch 中,可以通过 collapse 对搜索结果进行分组,且每个分组只显示该分组的一个代表文档。

::: details collapse 查询示例

1
2
3
4
5
6
7
8
9
10
POST /kibana_sample_data_ecommerce/_search
{
"size": 10,
"query": {
"match_all": {}
},
"collapse": {
"field": "day_of_week"
}
}

响应结果:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
{
"took": 106,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 4675,
"relation": "eq"
},
"max_score": null,
"hits": [
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "yZUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Monday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "ypUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Sunday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "1JUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Tuesday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "1ZUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Wednesday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "2JUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Saturday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "2ZUtBX4BU8KXl1YJRBrH",
"_score": 1,
"fields": {
"day_of_week": ["Thursday"]
}
},
{
"_index": "kibana_sample_data_ecommerce",
"_type": "_doc",
"_id": "35UtBX4BU8KXl1YJRBrI",
"_score": 1,
"fields": {
"day_of_week": ["Friday"]
}
}
]
}
}

:::

过滤搜索结果

使用带有 filter 子句的布尔查询,可以过滤搜索和聚合的结果。

使用 post_filter 可以过滤搜索的结果,但不能过滤聚合结果。

:::details filter 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /kibana_sample_data_ecommerce/_search
{
"size": 10,
"query": {
"bool": {
"filter": {
"range": {
"taxful_total_price": {
"gte": 0,
"lte": 10
}
}
}
}
}
}

:::

高亮

Elasticsearch 的高亮(highlight)可以从搜索结果中的一个或多个字段中获取突出显示的摘要,以便向用户显示查询匹配的位置。当请求突出显示(即高亮)时,响应结果的 highlight 字段中包括高亮的字段和高亮的片段。Elasticsearch 默认会用 <em></em> 标签标记关键字。

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

::: details 高亮结果示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /kibana_sample_data_ecommerce/_search
{
"size": 10,
"query": {
"match_all": {}
},
"highlight": {
"fields": {
"user": {
"pre_tags": [
"<strong>"
],
"post_tags": [
"</strong>"
]
}
}
}
}

:::

详情参考:Highlighting

分片路由搜索

Elasticsearch 可以在多个节点上的多个分片中存储索引数据的副本。在运行搜索请求时,Elasticsearch 会选择包含索引数据副本的节点,并将搜索请求转发到该节点的分片。此过程称为路由

默认情况下,Elasticsearch 使用自适应副本选择来路由搜索请求。默认情况下,自适应副本选择从所有符合条件的节点和分片中进行选择。如果要限制符合搜索请求条件的节点和分片集,可以使用 preference 查询参数。

详情参考:Search shard routing

查询规则

Elasticsearch 允许自定义查询规则来进行搜索。

详情参考:Searching with query rules

搜索模板

搜索模板是可以使用不同变量运行的存储搜索。

详情参考:Search templates

参考资料