一、ElasticSearch的用法
ES是基于Lucene开发的分布式高性能全文检索系统,支持分布式存储,水平扩展,主要能力是:存储、搜索、分析。我目前接触过的主要有两种用法:作为二级索引提高查询效率和基于关键词的全文检索。
Lucene:java语言开发的,搜索引擎类库。特点:高性能,学习曲线陡峭,不易扩展。
高性能检索是依赖内存实现的,所以尽量保证ES内存足够存储所有数据,如果内存不足每次查询都要走磁盘肯定性能会有很大的降低。
用法一:作为二级索引
MYsql分库分表 + ES二级索引,依赖数据同步工具保证数据一致性,Mysql写同步到ES,ES数据会有一定延迟。
部署情况:3个Master节点,4个Data节点,分别部署不同的机房。(数据节点之前因为故障下了一个,一直没有补充,所以现在是3个Data节点)
用法二:全文检索
基于关键词搜索,支持精确查询、范围查询、近似匹配、模糊匹配、高亮显示等功能。
为了实现商品信息数据检索功能,部署情况:MYsql + ES,每天凌晨定时任务同步,实时更新使用双写机制保证一致性。
二、ES中的基本概念
1、文档 Document
文档可以理解为关系型数据库的一条行记录,在Es中是JSON格式存储的。下面表格可以看作3个文档数据。
id |
name |
age |
1 |
张三 |
18 |
2 |
李四 |
20 |
3 |
李五 |
20 |
2、倒排索引
倒排索引是通过关键词(内容)查找目录页的结构。
倒排索引结构
以2.1中的数据为例,name字段的倒排索引大概结构如下:
term(单词) |
count(出现次数) |
docId:position(文档id:位置) |
张 |
1 |
1:0 |
李 |
2 |
2:0,3:0 |
三 |
1 |
1:1 |
四 |
1 |
2:1 |
五 |
1 |
3:1 |
单词词典(分词词典)Term Dictionary
用B+树或者哈希结构满足高性能插入与查询。类似mysql的二级索引。上面的term栏可以理解为是一个单词词典。
倒排列表
- 文档id
- 词频:在文档中出现次数,用于评分
- 位置:单词在文档中分词的位置,用于语句搜索phrase query
- 偏移:记录单词的开始结束位置,用于高亮显示
以2.1表数据为例,Term是“李”的倒排列表
docId |
词频 |
Position |
offset |
2 |
1 |
0 |
0,1 |
3 |
1 |
0 |
0,1 |
3、索引段 Segment
Lucene中多个倒排索引和文档组成Segment,Segment形成后不可变更。多个Segment汇总在一起称为Lucene Index,对应Es中的Shard分片。查询时会查所有Segment然后汇总返回结果。
新文档写入会生成新的segment(与上一个新文档间隔1s以上)。
Refresh操作
Es写入文档时,先写入Index Buffer中,从Index Buffer写入Segment的操作称为Refresh,Refresh之后Index Buffer被清空。
Refresh频率:默认1秒一次,可配置。文档写入segment之后才能被搜索到,所以写入es后查询会有延迟。Index Buffer被写满时也会触发refresh,buffer默认值是分配给JVM的堆内存大小 的10%,可配置。
Segment如何落盘
segment写入磁盘比较耗时,所以refresh时先写入缓存开放查询。为了保证数据不丢失,写入index buffer时会同时写入shard的transaction log落盘,transaction log在flush时清空。
Flush落盘
- 调用Refresh
- 调用Fsync将缓存中Segment写入磁盘中。
- 清空transaction log
调用频率:默认30分钟调用一次或者transaction log写满时调用。
Segment Merge
- 合并Segment,减少Segment数量,提高搜索速度
- 将.del文件中的数据彻底删除,释放空间
自动merge,也可手动merge。
不同的策略频率不同,ES默认tiered合并策略 ,根据最大索引段数量判断,如果到达阈值(根据最大值计算)就开始合并。
文档的删改
因为Segment是不可变的,所以修改文档是先删除后新增,将新增的文档写入新的Segment。文档删除不立即释放空间:删除的文件记入.del文件,
如何控制并发:使用乐观锁,每个文档有版本号version。更新数据删除原来文档,然后插入一条新的文档,version+1。
4、索引 Index
这里的索引Index类似于关系型数据库的表Schema,需要倒排索引概念区分开。
Mapping设置
- 字段名
- 字段类型
- 是否建立倒排索引
字段类型
- 简单类型:text、keyword、date、integer、float、boolean、ipv4&ipv6
- 复杂类型:对象类型、嵌套类型
- 特殊类型:geo_point&geo_shape
自动判断类型参数 dynamic
有新增字段的文档写入时:
- dynamic设为true,Mapping也会更新
- dynamic设为false,Mapping不会更新,数据无法索引,但是数据会出现在_source中
- dynamic设为Strict,写入文档失败
5、分片 Shard
一个索引的数据可以分为多个主分片,每个主分片又可以设置多个副本分片冗余数据。
主分片和副本分片
- 分片分为主分片Primary Shard和副本分片Replica Shard,主分片故障时副本分片会选出主分片,由此做到冗余数据,容灾。
- 主副分片都会处理查询请求,Coordinating 节点会随机选取主副分片其中一个发送请求。新增修改删除请求只能主分片处理,然后通过主从同步到副本分片。
- 相同的一个索引,primary shard、replica shard不能分配到同一个节点上。
主副分片数据同步规则 :
Master节点维护一个主副分片集合,写操作时由协调节点发给主分片primary shard,主分片需要将所有操作发送给副本分片执行,全部执行完之后主分片返回给协调节点结果(副本分片执行操作时是并行的)。
ES官网原文:https://www.elastic.co/guide/en/elasticsearch/reference/7.12/docs-replication.html#basic-write-model
文档映射到分片上的算法
hash算法,默认是文档id(hash(id)%shardConut),可以自定义设置,比如将相同城市的数据分到一个分片上。所以索引创建后不能改分片数,想改只能重新reindex。
问题:相关性算分不准问题
每个分片查询时的算分根据自己分片上的数据进行算分(根据分片上的词频等数据),所以主分片越多算分越不准。
解决办法:
- 数据量小时将主分片设置为1(这是为什么默认主分片为1的原因),如果数据量大尽量保证文档均匀分配(这样各个词出现的频率会相对均匀,算分也会差异小一点)。
- 搜索URL中指定DFS Query Then Fetch参数:到每个分片把分片的词频和文档的词频进行搜索然后进行一次完整的相关性算分。会耗费更多cpu和内存,性能低下,一般不建议使用。
- 使用shard_size设置 (1.5 * size + 10)来设置协调节点向各个分片请求的词根个数,然后在协调节点进行聚合,最后只返回size个词根给到客户端,提高聚合精确度
6、节点 Node
6.1 Master Node
作用:
- 处理创建删除索引请求
- 决定如何把分片分发到数据节点
- 维护更新Cluster State(集群状态信息)
最佳实践:
- Master节点负责单一职责(只负责集群状态信息管理)避免受到其他功能影响,集群设置多个Master Eligible节点,在Master节点故障时参与选主成为Master节点。
- 单一职责Master节点只需要较低配置CPU、较低配置内存和磁盘。
如何选主:
每个节点启动时默认是Master Eligible节点,集群中第一个Master Eligible节点启动时会将自己选为Master节点。所有Master Eligible节点互相ping对方,发现Master节点丢失,Node Id最低的被选举为主节点。
Cluster State(集群状态信息)
- 所有节点信息
- 所有索引和其相关的Mapping与Setting信息
- 分片的路由信息
所有节点都会保存集群状态信息,但是只有Master节点可以修改并同步给其他节点。
6.2 Coordinating Node
处理请求的节点,所有节点默认都是Coordinating Node节点。
Coordinating节点接收到请求,创建(删除)索引请求转给Master节点,查询数据请求转发给存储相关数据的node,每个data node都会在自己本地执行这个请求操作,同时返回结果给coordinating node,接着coordinating node会将返回过来的所有的请求结果进行缩减和合并,返回给用户。
需要高CPU和中等大小内存。
6.3 Ingest Node
数据预处理的节点,可以在文档写入文件前执行一条ingest pipeline,比如为某个字段付默认值、重命名或者split操作,还可以支持painless脚本对数据进行复杂加工(类似于Java中的过滤器)。每个节点默认都是Ingest Node。
需要高CPU、中等内存和较低的磁盘。
6.4 Data Node
可以保存数据的节点,节点启动默认就是Data Node,用于保存分片数据。
DataNode需要存储数据+计算结果,所以需要高速大容量磁盘、高CPU、高RAM内存。
6.5 冷热节点Hot&Warm架构
可以自定义节点类型,使用不同的配置节省成本。
Hot节点:处理最新的数据和常用数据
Warm节点:存储只读的不常用数据,使用大容量HDD机械硬盘节省成本
例如日志文件:
log-202011索引创建时可以指定它在hot节点,当12月份之后,使用命令动态将其指定到warm节点,ES会自动将索引中数据搬运到warm节点上。
7、节点-分片-主副分片-Segment-文档关系:
三、搜索示例
只写出请求参数和结论,返回结果需要自己在ES实例中去跑。
Query和Filter区别
query会进行相关性算分,filter不会算分并且会利用缓存来提高搜索性能。
1、设置索引结构
PUT users
{
"mappings" : {
"properties" : {
"id" : {
"type" : "long"
},
"name" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"lables":{
"type":"completion"
},
"age" : {
"type" : "integer"
},
"desc" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
},
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
}
}
ignore_above关键字:超过ignore_above设置大小的字符串不会被索引或存储
2、创建数据
POST users/_doc/1
{
"name":"张三",
"lables":"java开发",
"age":20,
"desc":"every day eat"
}
POST users/_doc/2
{
"name":"张四三",
"lables":"java是不是开发",
"age":30,
"desc":"every day sleep"
}
POST users/_doc/3
{
"name":"李四",
"lables":"c开发",
"age":20,
"desc":"every day play game"
}
3、Term Query
TermQuery不会对关键字分词;matchQuery会分词然后根据分好的term查询,如果字段类型是keyword,matchQuery会自动转为termQuery。
GET users/_search
{
"query": {
"term": {
"name": {
"value": "张三"
}
}
}
}
上面的命令查不出来结果,因为name被分词了,分词词典里没有“张三”这个词。这种情况name下有keyword子字段,可以使用来查出结果:
GET users/_search
{
"query": {
"term": {
"name.keyword": {
"value": "张三"
}
}
}
}
4、范围查询(区间查询)
支持范围查询的类型:
数字 integer float long double
日期 date
ip
请求:
GET /users/_search
{
"query":{
"constant_score":{
"filter":{
"range":{
"age":{
"gte":21,
"lte":100
}
}
}
}
}
}
constant_score表示不关心词频,不评分。
5、Match Query
5.1 Match
GET users/_search
{
"query": {
"match": {
"name": "张三"
}
}
}
会返回name=张三和张四三的数据,match全文搜索,会将关键字分词,然后去name的单词字典中匹配。
5.2 Match Phrase Query
match phrase query的分词结果必须在text字段分词中都包含,而且顺序必须相同,而且必须都是连续的。
GET users/_search
{
"query": {
"match_phrase": {
"name": "张三"
}
}
}
结果只返回name=张三的数据,因为match_phrase会保证分词都存在,顺序一致且连续。
slop=1代表两个关键字间可以有一个其他字符介入
GET /users/_search
{
"query":{
"match_phrase":{
"name":{
"query":"张三",
"slop":1
}
}
}
}
返回的结果是name=张三和张四三的数据。
6、Query String
AND表示并且,取两个分词查询结果的交集;OR 标识或者,取两个分词查询的并集。必须大写。
GET /users/_search
{
"query":{
"query_string": {
"default_field": "desc",
"query": "every AND sleep"
}
}
}
# 多个字段同时查询
GET /users/_search
{
"query":{
"query_string": {
"fields": ["esc","name"],
"query": "sleep OR 张"
}
}
}
7、BoolQuery
must、must_not、should
GET users/_search
{
"query": {
"bool": {
"must": [{
"exists":{
"field": "desc"
}
},
{
"match": {
"name": "张三"
}
}]
}
}
}
8、自动补全 Completion Suggester
用户每输入一个字符都要及时发送一个请求到后端查找匹配项,对性能要求较苛刻。Es使用了不同的数据结构,不是倒排索引:将Analyze的数据编码为FST(Finite State Transducer)和索引一起存放,FST会被ES整个加载进内存,速度很快。FST只能前缀查找。
FST是什么:
摘自:https://blog.csdn.net/AAA821/article/details/82014792
优点:
1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间;
2)查询速度快。O(len(str))的查询时间复杂度。
最终我们得到了一个有向无环图。利用该结构可以很方便的进行查询,如给定一个term “dog”,我们可以通过上述结构很方便的查询存不存在,甚至我们在构建过程中可以将单词与某一数字、单词进行关联,从而实现key-value的映射。也就是它这种压缩,就是让每个节点都不会放重复值,节省了大量的空间。
自动补全请求参数:
GET users/_search
{
"size":0,
"suggest":{
"article-suggester":{
"prefix":"java",
"completion":{
"field":"lables"
}
}
}
}
返回建议词:“java开发”和“java是不是开发”
9、近似匹配和模糊匹配
#近似匹配,一个字符差异的相近查询
GET /users/_search?q=desc:slee~1
{
"profile":false
}
#模糊匹配,能搜出两词相隔一个和两个单词的结果,不必须挨着
GET /users/_search?q=desc:"every sleep"~2
{
"profile":false
}
“profile”:"false"表示不展示profile信息,profile信息可以展示查询如何被执行的。
10、分页查询
GET /users/_search
{
"query": {
"match_all" : {}
},
"from": 0,
"size": 2
}
分页问题
查询过程Query then Fetch:
多个分片时,查询走到coordinating节点,然后coordinating node节点选取所有分片(主从分片随机选一个),分片中查询、排序拿from+size条数据,只返回排序和排名相关的信息(不包括文档document)给coordinating节点汇总。coordinating节点拿到数据后重新排序后选from到from+size个文档的id,再根据id去不同的分片获取对应的文档数据。
ES设定搜索10000条以内的数据,超过之后报错,防止深度分页问题。
问题:深度分页时,效率极低
- 依赖业务或产品上解决。
- Search After 滚动翻页,不能支持指定from参数跳到某页。
- Scroll Api:生成一个快照,指定多长时间有效。每次查询输入上一个scroll id,缺点:有效时间内插入新的文档无法被查到。使用场景:想将全部文档导出,可以使用scroll api;Reindex也是使用的Scroll Api功能。
Search After演示:
请求:
GET users/_search
{
"query": {"match_all": {}},
"sort": [
{
"_id": {
"order": "asc"
},
"age":{
"order": "desc"
}
}
],
"size": 2
}
返回:
{
"took" : 4,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "users",
"_type" : "_doc",
"_id" : "1",
"_score" : null,
"_source" : {
"name" : "张三",
"lables" : "java开发",
"age" : 20,
"desc" : "every day eat"
},
"sort" : [
"1",
20
]
},
{
"_index" : "users",
"_type" : "_doc",
"_id" : "2",
"_score" : null,
"_source" : {
"name" : "张四三",
"lables" : "java是不是开发",
"age" : 30,
"desc" : "every day sleep"
},
"sort" : [
"2",
30
]
}
]
}
}
根据返回的sort值构建下一页请求:
将之前返回的最后一个文档的sort值赋到查询的search_after下,就会查这条数据之后的数据。必须保证sort唯一(所以加上id)
GET users/_search
{
"query": {"match_all": {}},
"sort": [
{
"_id": {
"order": "asc"
},
"age":{
"order": "desc"
}
}
],
"search_after":[
"2",
30
],
"size": 2
}
四、Docker Compose安装ES集群脚本
本地使用docker启动ES集群:
- 安装docker和docker compose,mac上docker安装时会自动安装compose。
- 在docker-compose.yaml文件所在的目录执行命令
docker-compose up
启动集群
- 访问Kibana:
http://localhost:5601/
- 访问Cerebro(集群监控):
http://localhost:9000/
,Node address输入:http://172.17.0.1:9200
地址
docker-compose.yaml 文件
version: '2.2'
services:
es7_01:
image: docker.elastic.co/elasticsearch/elasticsearch:7.1.0
container_name: es7_01
environment:
- cluster.name=geektime
- node.name=es7_01
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- discovery.seed_hosts=es7_01,es7_02
- cluster.initial_master_nodes=es7_01,es7_02
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- ./es7data1/data:/usr/share/elasticsearch/data
- ./es7data1/logs:/usr/share/elasticsearch/logs
ports:
- 9200:9200
networks:
- es7net
es7_02:
image: docker.elastic.co/elasticsearch/elasticsearch:7.1.0
container_name: es7_02
environment:
- cluster.name=geektime
- node.name=es7_02
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- discovery.seed_hosts=es7_01,es7_02
- cluster.initial_master_nodes=es7_01,es7_02
#- xpack.security.enabled=true
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- ./es7data2/data:/usr/share/elasticsearch/data
- ./es7data2/logs:/usr/share/elasticsearch/logs
networks:
- es7net
cerebro:
image: lmenezes/cerebro:0.8.3
container_name: cerebro
ports:
- "9000:9000"
command:
- -Dhosts.0.host=http://172.17.0.1:9200
networks:
- es7net
kibana:
image: docker.elastic.co/kibana/kibana:7.1.0
container_name: kibana7
environment:
- I18N_LOCALE=zh-CN
- XPACK_GRAPH_ENABLED=true
- TIMELION_ENABLED=true
- XPACK_MONITORING_COLLECTION_ENABLED="true"
- ELASTICSEARCH_SSL_VERIFY=false
- ELASTICSEARCH_URL="http://es7_01:9200" #容器对容器,所以写容器内的端口
- ELASTICSEARCH_HOSTS="http://es7_01:9200"
- ELASTICSEARCH_USERNAME="kibana"
- ELASTICSEARCH_PASSWORD="kibana"
ports:
- "5601:5601"
networks:
- es7net
networks:
es7net:
driver: bridge