什么是 Elasticsearch

Elasticsearch 是一个实时的分布式搜索分析引擎, 它能让你以一个之前从未有过的速度和规模,去探索你的数据。

Elasticsearch 中,并没有特别革命性的组件,其革命性的地方在于,它将这些单独的组件融合到了一个单一的,一致的,实时的应用中。

为什么要使用 Elasticsearch

Elasticsearch 鼓励探索与利用数据,而不是因为查询数据太困难就让数据烂在数据库中。

可以这样描述 Elasticsearch:

  • 一个分布式的实时文档存储,每个字段都可以被索引与搜索
  • 一个分布式的实时分析搜索引擎
  • 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据

Elasticsearch 的特性

Elasticsearch 尽可能地屏蔽了分布式系统的复杂性。这里列举了一些在后台自动执行的操作:

  • 分配文档到不同的容器或分片中,文档可以储存在一个或多个节点中
  • 按集群节点来均衡分配这些分片,从而对索引和搜索过程进行负载均衡
  • 复制每个分片以支持数据冗余,从而防止硬件故障导致的数据丢失
  • 将集群中任一节点的请求路由到存有相关数据的节点
  • 集群扩容时无缝整合新节点,重新分配分片以便从离群节点恢复

如何与 Elasticsearch 进行交互

如果采用非 Java 语言作为客户端,Elasticsearch 则采用 RESTful API 加上 JSON 来进行交互。

此处我们以 Node 为例。

下载好了官方所提供的 Elasticsearch 的安装包之后,使用 ./bin/elasticsearch 来启动 Elasticsearch 服务。这样,就可以通过客户端来对其进行搜索了。

一开始 Elasticsearch 并没有提供特别多的数据给我们进行查询。当我们直接请求 http://localhost:9200 的时候,返回的是这样的一段 JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "LvaJrMJ",
"cluster_name": "elasticsearch",
"cluster_uuid": "x7LaqCn_TfSjS1O3cPmYrQ",
"version": {
"number": "6.2.4",
"build_hash": "ccec39f",
"build_date": "2018-04-12T20:37:28.497551Z",
"build_snapshot": false,
"lucene_version": "7.2.1",
"minimum_wire_compatibility_version": "5.6.0",
"minimum_index_compatibility_version": "5.0.0"
},
"tagline": "You Know, for Search"
}

这段 JSON 只是描述了 Elasticsearch 的集群信息和版本信息,并没有我们想要的数据。如果我们要查询数据,那么,就需要将请求地址改为 http://localhost:9200/_search,这样,我们获取到的 JSON 就是下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"took": 63,
"timed_out": false,
"_shards": {
"total": 0,
"successful": 0,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 0,
"max_score": 0,
"hits": []
}
}

所以,我们就能够看出来,现在我们的 Elasticsearch 中还没有数据可供搜索,我们需要为其添加一些数据进去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 此处我们采用 faker 来生成随机数据
function createData() {
const requests = [];
for (let i = 0; i < 10; i++) {
faker.seed(30);
requests.push(
// 存储的地址为 http://localhost:9200/megacorp/employee/id
axios.put(`http://localhost:9200/megacorp/employee/${i}`, {
first_name: faker.name.firstName(),
last_name: faker.name.lastName(),
age: faker.random.number({
min: 20,
max: 50,
}),
jobType: faker.name.jobType(),
interests: [faker.lorem.word(), faker.lorem.word()],
})
);
}
return Promise.all(requests);
}

这样,我们就初始化了十条数据到我们的数据库中去了。现在,我们就可以对这些数据进行搜索了。

根据 id 来搜索

由于我们采用了 faker 来生成随机数据,所以,如果每次修改了代码并重启了 Node 服务的话,每一次获取的数据可能是不一样的。

按照 RESTful API 的格式,我们如果要根据 id 来搜索我们的数据,只需要在 URL 上带上 id 就行了。

1
2
3
axios.get('http://localhost:9200/megacorp/employee/1').then((data) => {
console.log(JSON.stringify(data.data, null, 2))
});

获取到的数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_version": 31,
"found": true,
"_source": {
"first_name": "Luigi",
"last_name": "Von",
"age": 31,
"jobType": "Manager",
"interests": [
"nam",
"voluptate"
]
}
}

如果我们要获取到所有的数据,那么只需要将请求的 URL 修改为 http://localhost:9200/megacorp/employee/_search 就可以了。

根据 query-string 来搜索

现在,我们通过一个查询字符串(query-string)来进行搜索。比如,我们要搜索 first_name 为 Hailee 的员工:

1
axios.get('http://localhost:9200/megacorp/employee/_search?q=first_name:Hailee')

获取到的数据如下:

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
{
"took": 5,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0.9808292,
"hits": [
{
"_index": "megacorp",
"_type": "employee",
"_id": "5",
"_score": 0.9808292,
"_source": {
"first_name": "Hailee",
"last_name": "Donnelly",
"age": 45,
"jobType": "Liaison",
"interests": [
"adipisci",
"nesciunt"
]
}
}
]
}
}

同理,如果要用其他的字段进行查询,请求的格式也是一样的。

使用 DSL 进行查询

DSL 即领域特定语言。Elasticsearch 提供了一个丰富灵活的查询语言叫做查询表达式,它支持构建更加复杂和健壮的查询。

比如,我们依然要查询 first_name 为 Hailee 的员工,使用 DSL 的话可以像这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const queryBody = {
query: {
match: {
first_name: 'Hailee',
}
}
};
axios.get('http://localhost:9200/megacorp/employee/_search', {
params: {
// 如果采用第三方库来请求的话,需要加上 source 和 source_content_type 字段
// 详见 https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#_request_body_in_query_string
source: JSON.stringify(queryBody),
source_content_type: 'application/json'
},
})

使用 DSL 进行更复杂的查询

我们现在尝试进行一些复杂的查询,比如,我们要查询 jobType 为 Designer 并且 age 大于 40 的员工。

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
const queryBody = {
query: {
bool: {
must: {
match: {
jobType: 'Designer',
},
},
filter: {
range: {
age: {
// gt 即 greater than
gt: 40,
},
},
},
},
},
};
axios.get('http://localhost:9200/megacorp/employee/_search', {
params: {
source: JSON.stringify(queryBody),
source_content_type: 'application/json'
},
})

这样,我们查询到的结果如下:

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
{
"took": 3,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 0.6931472,
"hits": [
{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_score": 0.6931472,
"_source": {
"first_name": "Oceane",
"last_name": "Boyer",
"age": 44,
"jobType": "Designer",
"interests": [
"tenetur",
"non"
]
}
}
]
}
}

全文搜索

现在,我们再来试试全文搜索,我们要查询 jobDescriptor 中包含 Quidem et 的员工:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const queryBody = {
query: {
match: {
jobDescriptor: 'Quidem et',
},
},
};
axios.get('http://localhost:9200/megacorp/employee/_search', {
params: {
source: JSON.stringify(queryBody),
source_content_type: 'application/json',
},
})

这样返回来的结果如下:

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
{
"took": 18,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 3,
"max_score": 1.6799116,
"hits": [
{
"_index": "megacorp",
"_type": "employee",
"_id": "9",
"_score": 1.6799116,
"_source": {
"first_name": "Melody",
"last_name": "White",
"age": 38,
"jobType": "Planner",
"jobDescriptor": "Quidem et officiis at.",
"interests": [
"dolorem",
"et"
]
}
},
{
"_index": "megacorp",
"_type": "employee",
"_id": "5",
"_score": 0.44000342,
"_source": {
"first_name": "Leanna",
"last_name": "Kunze",
"age": 47,
"jobType": "Officer",
"jobDescriptor": "Enim praesentium suscipit et laboriosam placeat qui.",
"interests": [
"quia",
"ab"
]
}
},
{
"_index": "megacorp",
"_type": "employee",
"_id": "3",
"_score": 0.2876821,
"_source": {
"first_name": "Carolina",
"last_name": "Harris",
"age": 20,
"jobType": "Designer",
"jobDescriptor": "Fugiat fuga asperiores reiciendis aperiam tempore totam voluptatem et.",
"interests": [
"odio",
"aut"
]
}
}
]
}
}

我们可以看到,返回来有三条数据,但是只有第一条是包含了 Quidem et 的。另外两条数据的 jobDescriptor 中包含有 et 字符,所以也给返回回来的。同时,Elasticsearch 帮助我们做了一个相关性评分,即 _score 字段。第一条数据评分最高,依次往下逐渐降低。

那么,如果我们想要完全匹配一个短语怎么办呢?当然有办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const queryBody = {
query: {
match_phrase: {
jobDescriptor: 'Quidem et',
},
},
};
axios.get('http://localhost:9200/megacorp/employee/_search', {
params: {
source: JSON.stringify(queryBody),
source_content_type: 'application/json',
},
})

这样,返回的数据就只有一条了:

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
{
"took": 12,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1.6799116,
"hits": [
{
"_index": "megacorp",
"_type": "employee",
"_id": "9",
"_score": 1.6799116,
"_source": {
"first_name": "Melody",
"last_name": "White",
"age": 38,
"jobType": "Planner",
"jobDescriptor": "Quidem et officiis at.",
"interests": [
"dolorem",
"et"
]
}
}
]
}
}

高亮搜索(highlight)

如果需要在结果中高亮搜索的片段,只需要在查询参数中添加 highlight 参数就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const queryBody = {
query: {
match_phrase: {
jobDescriptor: 'Quidem et',
},
},
highlight: {
fields: {
jobDescriptor: {},
},
},
};
axios.get('http://localhost:9200/megacorp/employee/_search', {
params: {
source: JSON.stringify(queryBody),
source_content_type: 'application/json',
},
})

以下是返回结果,同样的会有一个 highlight 字段:

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
{
"took": 374,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1.6799116,
"hits": [
{
"_index": "megacorp",
"_type": "employee",
"_id": "9",
"_score": 1.6799116,
"_source": {
"first_name": "Melody",
"last_name": "White",
"age": 38,
"jobType": "Planner",
"jobDescriptor": "Quidem et officiis at.",
"interests": [
"dolorem",
"et"
]
},
"highlight": {
"jobDescriptor": [
"<em>Quidem</em> <em>et</em> officiis at."
]
}
}
]
}
}

聚合(Aggregations)

Elasticsearch 允许我们基于数据生成一些精细的分析结果。类似于将信息分组。

比如,我们要分析员工目录中的职位类别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const queryBody = {
aggs: {
all_job_types: {
terms: {
field: 'jobType'
}
},
},
};
axios.get('http://localhost:9200/megacorp/employee/_search', {
params: {
source: JSON.stringify(queryBody),
source_content_type: 'application/json',
},
})

此处,在请求的时候,可能会遇到 java.lang.IllegalArgumentException: Fielddata is disabled on text fields by default. Set fielddata=true on [jobType] in order to load fielddata in memory by uninverting the inverted index 这样的错误。这时候,需要在客户端加上一个请求来设置 fielddata
详见 https://stackoverflow.com/questions/38145991/how-to-set-fielddata-true-in-kibana

1
2
3
4
5
6
7
8
9
10
11
// 设置 fielddata
axios.put('http://localhost:9200/megacorp/_mapping/employee', {
employee: {
properties: {
jobType: {
type: 'text',
fielddata: true,
},
},
},
});

这样,我们查询出来的数据就会有下面这样的字段:

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
"aggregations": {
"all_job_types": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "assistant",
"doc_count": 2
},
{
"key": "orchestrator",
"doc_count": 2
},
{
"key": "associate",
"doc_count": 1
},
{
"key": "designer",
"doc_count": 1
},
{
"key": "developer",
"doc_count": 1
},
{
"key": "facilitator",
"doc_count": 1
},
{
"key": "planner",
"doc_count": 1
},
{
"key": "technician",
"doc_count": 1
}
]
}
}

总结

以上是直接使用前端的 ajax 请求来发送参数进行查询。Elasticsearch 为我们提供了一个官方的 easticsearch.js,以便我们能够更加方便的对数据进行查询。

本文中的完整代码地址:https://github.com/Erichain/elasticsearch-with-koa

References

Elasticsearch: 权威指南