初识 Elasticsearch

引言

在很多应用场景下,我们都需要搜索功能,不管是在 App 中还是网站中。搜索每时每刻都在发生,比如搜索喜欢的零食、好看的衣服、喜欢的文章、想要学习的课程等等。如今要为我们的应用添加搜索已经非常容易了,早期我们可以使用 MySQL 的 MyISAM 存储引擎来做全文本搜索支持,不过今天要说的 Elasticsearch 同样可以做到,而且更强大。简单来说,Elasticsearch 是一个分布式搜索和分析引擎,也是众所周知的 ELK Stack 核心成员。它以 JSON 的格式存储文档到索引当中,能够高效地存储多种类型,并提供快速搜索的能力。此外,整个 ELK Stack 生态非常完善。

那么接下来,我们将跟着官方文档学习下 Elasticsearch 具体是什么?适用的场景有哪些?支持什么数据类型存储和检索?

PS: 我们在生产环境中,使用 ES 构建了课程商品(SKU)索引,方便对用户、管理后台提供课程搜索能力。

安装

docker-elk 仓库提供了在 Docker 环境下搭建 ELK Stack 服务的配置,我们可以参考其说明文档进行安装(在 macOS Catalina 环境下操作)。

  1. 克隆仓库:git clone git@github.com:deviantony/docker-elk.git
  2. 第一次需要运行 docker-compose build,等待构建完成
  3. 需要在 docker-compose.yml 中修改下 ES 的密码,当然还有 elasticsearch/configkibana/config, logstash/config 中都有密码需要修改。由于是在本地学习,所以只是简单设置了下密码。生产环境建议参考文档,生成复杂的密码
  4. 运行 docker-compose up [-d] 即可启动
  5. 关闭 docker-compose down -v

注意事项:

  1. 配置文件并非动态加载,每次变更后需要重启对应的组件
  2. 端口映射说明:
    1. 9200: ES HTTP
    2. 9300: ES TCP
    3. 5000: Logstash TCP input
    4. 5601: Kibana

启动成功后,可以打开 Kibana 开始探索啦~

Elasticsearch 简介

Elasticsearch 是一个分布式搜索和分析引擎,ELK 栈的核心组成部分。而 Logstash 和 Beats 用于收集聚合转换数据,并将其存储在 Elasticsearch 当中。而 Kibana 则让我们能够便捷探索、查看并分享数据,同时也用来管理 ELK 栈。

ES 提供了实时的搜索和分析能力,支持多种数据类型(结构化、非结构化、数字、地理位置等),ES 会高效地存储并索引这些数据,从而支持快速搜索。由于分布式的特点,随着数据集的增加,可以轻松地进行水平扩展来提高服务可靠性和性能。

ES 支持的使用场景包括:

  1. 为我们的 App 或网站提供搜索支持
  2. 可以存储分析日志(analyze logs)、指标数据(metrics)、安全事件数据(security event data)
  3. 基于机器学习根据数据实时建模
  4. 可以用作 GIS 系统(地理信息系统),用于管理、集成并分析空间信息
  5. 可以用于生物学研究工具,存储并处理基因数据

文档和索引

ES 本质上还是一个分布式文档存储服务(类比 Mongo?),实际在索引中存储的是 JSON 格式的文档。在 ES 集群中,文档会被分布到整个集群存储的,并且能够从任意一个节点立刻访问到。

一旦新的文档存储到索引后,几乎可以立刻被搜索到(1s 以内),因此可以构建近乎实时的搜索引擎。那么 ES 究竟是如何做到的呢?简单来说,它使用了倒排索引(inverted index)这种数据结构来支持全文本搜索。所谓的倒排索引就是对文本进行分词,然后记录每个分词所在的文档 id(当然还有词频等信息),这样在查询时可以基于分词快速定位所在的文档。

在 ES 中,索引是一个很重要的概念(回顾下 MySQL InnoDB 的聚簇索引,表中的记录存储在 B-Tree Node 上,基于 B+ Tree 这种数据结构快速定位记录所在的页),但是这里的索引和在关系数据库中提到的索引有所差别。总结下特点:

  1. ES 的索引(index)可以看成文档(document)的集合;
  2. 每个文档(document)可以看成字段(field)(类比下字典中的 key-value)的集合;

默认设置下,ES 会为文档中的每个字段建立索引,并且每个字段都会匹配专门优化的数据结构以达到高效存储和快速搜索的目的。比如:文本使用倒排索引,数字和地理位置使用 BKD 树

ES 支持 schema-less,这样我们就不用为每个字段显式指定索引方式。当我们启用动态映射(dynamic mapping)时(这个是默认设置),ES 会自动检测到新增字段,自动将文档中的 bool 值、浮点数、整数、日期、字符串等映射成合适的 ES 数据类型。

当然,某些情况下,我们可能想要关闭动态映射的功能,这时可以自定义映射规则,从而控制每个字段存储和索引的方式。自定义映射带可以:

  1. 区分全文本字符串字段和精确匹配的字符串字段;
  2. 执行特定语言的文本分析;
  3. 优化部分匹配;
  4. 使用自定义日期格式;
  5. 使用一些无法自动检测到的数据类型(如 geo_point, geo_shape)。

此外,ES 可以为相同的字段建立不同的索引,以满足不同的需求场景。

搜索与分析

ES 提供了非常简单易于使用的 REST API,可以利用它们管理集群、索引和搜索数据。我们既可以在 Kibana 控制台中和 ES 交互,也可以使用各个语言(支持 Java, JavaScript, Go, .Net, PHP, Perl, Python, Ruby 等)的客户端进行交互。

搜索数据

目前支持两种查询方式:JSON Query DSL 和 SQL。

ES API 允许我们使用结构化查询、全文本查询或者二者组合:

  1. 结构化查询:类似 SQL 那样,我们可以搜索某几个字段,基于某字段排序等;
  2. 全文本查询:返回匹配关键词的文档,返回的结果基于相关性排序。

此外,还支持单独分词(individual terms)查询,我们可以执行短语搜索(phrase searches)、相似度搜索(similarity searches)和前缀搜索(prefix searches),甚至可以得到自动补全的建议。当然,对于地理位置或者数字类型的数据,ES 也可以提供高效的查询功能。

分析数据

ES 允许我们执行聚合(aggregation)操作,从而对关键的指标、 增长趋势等有更为深刻的认识。ES 高效聚合的能力,可以让我们能够实时地分析和可视化数据。我们可以把搜索和聚合联合在一起使用,这样就可以在搜索、过滤文档的同时,对相同的结果进行分析。

此外,ES 还提供了自动分析时序数据的功能,也就是利用机器学习来发现数据中的异常,不过这块属于高级功能了,有兴趣的话可以自行研究下~

高可用、可伸缩与可靠性

既然是分布式搜索引擎,高可用和可伸缩的能力自然是 ES 必备技能。我们可以按需给集群增加服务器(节点)实现扩容。ES 会自动将数据和查询负载均衡到可用节点上,以实现伸缩和高可用。

那么,ES 是如何存储和维护索引的呢?简单来说,每个 ES 索引(index)其实是一个到多个物理分片(physical shards) 所组成的逻辑分组(logical group);每个分片实际是自包含索引(self-contained index)。通过将索引放到多个分片存储,然后将分片分布到多个节点上,ES 可以为分片提供冗余备份,从而达到水平扩展和高可用的目的。随着集群的扩容(或缩容),ES 会自动再均衡,必要时也会合并分片。

关于分片(shards)

分片分为两种类型:

  1. 主分片(primary shard):索引中的每个文档都属于某个主分片。
  2. 副本分片(replicas):副本分片其实就是主分片的拷贝,一方面实现数据冗余,另一方面也对外提供读服务(搜索、获取文档等)。

需要注意的是,索引的主分片数在创建后就不能修改了,只能重建靠重建索引来扩展主分片数,但是副本分片数可以随时修改,不会打断索引和查询操作。

那么问题来了,索引的主分片数越多越好吗?显然不是这样的。我们需要考虑索引的数据量大小,尽可能保证每个分片不要太大,分片数量不要过多。

我们先来看看分片数过多会导致的问题吧:

  1. 每个分片其实都是一个 Lucene 索引,会占用更多的文件描述符、内存和 CPU 资源;
  2. 搜索会被分配到各个分片上,如果分片都位于不同节点还好,要是集中在某个节点,则会出现热点问题,资源竞争激烈,性能也会下降;
  3. 过多的分片也会导致过多的分片请求,会产生一定的网络开销等;
  4. ES 使用词频来统计匹配的相关性,统计会被分配到各个分片上,如果大量分片上只维护了很少的数据,则会导致最终的文档的相关性较差。

如果每个分片过大,则会导致集群再均衡办时,搬迁数据更耗时。总而言之,我们需要通过测试来确定最佳的配置。起初可以这样:

  1. 保证每个分片的大小在 GB 到几十 GB 范围。对于常见的时序数据,分片大小在 20GB ~ 40GB 也很常见。参考文献中给出的大小限制为 30GB。
  2. 避免过大的分片问题。每个节点可容纳的分片数量和可用的堆空间成正比,每 GB 堆空间分片数应该小于 20。

灾备

考虑性能的因素,通常集群中的节点应该位于相同的网络环境。跨数据中心均衡分片非常耗时,可靠性也会降低。但是高可用架构设计就是要保证我们不要把鸡蛋放在一个篮子里,这样一旦某处的服务器宕机,位于其它位置的服务器可以接替继续提供服务。

ES 为我们提供了 CCR 功能(跨集群复制,Cross-cluster replication),这样可以将主集群的索引同步到远程的集群作为热备,同时也可以基于地理位置使用其它集群提供读请求。所有的写入操作都在主集群完成,其它的副本集群都是只读的 Followers。

架构

实践

生产环境创建索引模板:

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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
PUT /_template/km_server_warehouse_template
{
"order": 1,
"index_patterns": [
"km_server_warehouse*"
],
"settings": {
"index": {
"analysis": {
"analyzer": {
"znlp-coarse": {
"type": "custom",
"tokenizer": "znlp-fine"
}
}
},
"number_of_shards": "1",
"translog": {
"sync_interval": "5s",
"durability": "async"
},
"number_of_replicas": "4",
"similarity": {
"scripted_bm25": {
"type": "scripted",
"script": {
"source": "double k1 = 1.2; double b = 0.75; double tfNorm = doc.freq * (k1 + 1) / (doc.freq + k1 * (1 - b + b * doc.length / (1.0 * field.sumTotalTermFreq/field.docCount))); return query.boost * tfNorm;"
}
}
}
}
},
"mappings": {
"contents": {
"dynamic": false,
"properties": {
"content_basic": {
"type": "nested",
"properties": {
"sku_id": {
"type": "long"
},
"business_id": {
"type": "long"
},
"business_type": {
"type": "keyword"
},
"producer": {
"type": "keyword"
},
"svip_right": {
"type": "nested",
"properties": {
"free": {
"type": "boolean"
},
"discount": {
"type": "short"
}
}
},
"instabook_right": {
"type": "nested",
"properties": {
"free": {
"type": "boolean"
},
"discount": {
"type": "short"
}
}
},
"svip_privileges": {
"type": "boolean"
},
"title": {
"similarity": "scripted_bm25",
"analyzer": "znlp-fine",
"type": "text"
},
"authors": {
"type": "keyword"
},
"right_list": {
"type": "keyword"
},
"serial_status": {
"type": "short"
},
"public_status": {
"type": "short"
},
"online_time": {
"type": "long"
},
"categories": {
"type": "nested",
"properties": {
"id": {
"type": "long"
},
"level": {
"type": "short"
},
"weight": {
"type": "short"
},
"name": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"name_en": {
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
}
}
},
"is_test": {
"type": "boolean"
}
}
},
"content_aggregation": {
"type": "nested",
"properties": {
"interest_count": {
"type": "long"
}
}
},
"created_at": {
"type": "long"
},
"updated_at": {
"type": "long"
}
}
}
},
"aliases": {}
}

参考

0%