本文ES的版本是 7.1.1
参考资料
- 阮一峰:http://www.ruanyifeng.com/blog/2017/08/elasticsearch.html
- 官方指南(中文,2.x版本)https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html
- 官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.1/index.html
- Java Api:https://www.elastic.co/guide/en/elasticsearch/client/java-api/7.1/java-api.html
- 某大佬博客:https://gitbook.cn/books/5cf386ddfed1e2779b66ec3d/index.html#1
- 某大佬博客:https://gitbook.cn/books/5c52c6923417565017a61ce0/index.html
- 某巨佬博客:https://me.csdn.net/wojiushiwo987
- ES中文社区:https://elasticsearch.cn
- 倒排索引结构原理:https://www.cnblogs.com/cjsblog/p/10327673.html
建议
2019-10-30如果看到此文,建议先看阮一峰的大致了解,然后再看官方指南深入学习一下,下面的都是我自己的一些学习记录,可能对你没有什么价值,没必要浪费时间。但你可以看这个笔记是是我看官方指南遇到的问题,或许有些帮助
- 大多数问题是因为 JVM 内存
- 单个节点数据量不超过 5 TB
- 避免热点数集中在少数节点上,可以通过 index.total_shards_per_node 调整
- 1 TB 的索引大约占 2 GB JVM 内存
- JVM 内存
- 单个节点分片数量不宜过多,不然会造成管理压力,官方建议 1 GB 内存配 20 个分片数量,当然分配数越少越好
- 一个节点默认有所有角色,但应该角色分明,各司其职。主节点要求独立,只负责分片管理和管理集群操作;协调节点也独立;ES 支持数据写入时预处理,如果需要预处理该节点也要独立出来,写入数据之前能进入 Kafka 之类的MQ 缓冲一下最好
- mapping 定义了文档的各个字段如何被索引以及如何被存储,建议在创建索引就定义好mapping
- 1. 是否需要索引,这个字段是否要被搜索到 2. 是否分词,这个字段搜索时时整体匹配还是单词匹配 3. 是否存储,这个字段是否要在页面上显示
Elasticsearch
普通安装
安装
下载,Mac下直接解压即可
运行
./bin/elasticsearch -d 在后台运行
Docker 安装
拉取镜像
docker pull elasticsearch:版本号
运行
# 运行该镜像,6c0bdf761f3b 是我本地的镜像id
docker run -di --name es5.6.8-sc-v1 -p 9200:9200 -p 9300:9300 6c0bdf761f3b
# 查看该容器的日志,18dfc4ed407a 是我本地容器id
docker logs 18dfc4ed407a
一个实例启动多个节点
# 指定节点名称,集群名称,节点数据存放路径
./bin/elasticsearch -E node.name=node0 -E cluster.name=geektime \
-E path.data=node0_data -d
./bin/elasticsearch -E node.name=node1 -E cluster.name=geektime \
-E path.data=node1_data -d
./bin/elasticsearch -E node.name=node2 -E cluster.name=geektime \
-E path.data=node2_data -d
ES 架构
(来源:博客)
RESTful API
ES的一大特点是支持 RESTful 风格的查询,请求类型有 GET/PUT/POST/DELETE,RESTful风格:
/<索引>/<类型>/<文档id>?<查询参数>
ES中的关键字都是下划线开头的,例如:_all、_search、_update 等。
基本概念
ES 中一些术语和关系数据库的对比:
(图片来源:极客时间)
索引(Index)
类似于 MySQL 中的数据库表,它是类似特征文档的集合,索引用于定义文档类型的存储,它可以包含多个文档,同一个索引中,同一个字段只能定义一个数据类型。每个索引都有多个分片,每一个分片就是一个Lucene索引。
结构化和非结构化索引
下面是一个索引的信息,mappings 这个字段如果是空的就是非结构化索引,反之。
{
"state": "open",
"settings": {
"index": {
"creation_date": "1572251699297",
"number_of_shards": "1",
"number_of_replicas": "1",
"uuid": "cvw9ERk7RSqQfBfpZwTZgA",
"version": {
"created": "7010199"
},
"provided_name": "weather"
}
},
"mappings": {}, // 如果是空的就是非结构化索引,反之
"aliases": [],
"primary_terms": {
"0": 6
},
"in_sync_allocations": {
"0": [
"-Em2Q2P5SZikD_IJgFy_yw"
,
"0mgz6jCrSsKYsh_nOBy2gQ"
]
}
}
索引创建规则
索引名称必须小写,不能有下划线
检索类型
- text,非结构化文本数据,用来分词,将一段文字分成若干个词组,例如:怎么点外卖?会被分词器切分为:怎么、点、外卖、点外卖、怎么点、怎么点外卖等,以便用来做全文检索。该类型不适用于排序或聚合
- keyword,无需分词、整段完整精确匹配,直接将完整的文本保存到倒排索引中。例如数字、日期、具体的字符串,如“Apple Store”就不需要分词
自定义分词 Analyzer
自定义分词设置,可以根据自己的需要对全文本进行分词,在mapping中定义。一个 Analyzer由 Character Filter + Tokenizer + Token Filter
冻结索引
6.7 开始增加了冻结索引的特性,平时不占用内存,只有查询的时候才去加载内存,虽然查询慢,但是节点可以持有更多的数据总量
倒排索引
ES 中存储的是非结构化的数据,存储海量数据查询还这么快,ES 索引用的是倒排索引,查询速度比MySQL中的B+ Tree 快,但是插入效率会慢一点。
ES 文档中的每个字段都有自己的倒排索引,但也可以指定某些字段不做索引,节省空间,但缺点是无法被搜索。要注意的是,ES 中字段不建立索引是无法被搜索到的,也就是说如果你希望某个字段可以被搜索,就一定要建索引
倒排索引会把文档中所有内容拆分为若干个词组,然后统计出每个词组在文档中出现的次数,再然后统计出每个词组在每个文档中出现的位置,如图:
(图片来源:极客时间)
上面可以看出倒排索引有两个重要的部分:
- 单词词典(Term Dictionary),记录文档中所有的单词到倒排列表的关系,一般是B+树和哈希拉链法实现
- 倒排列表(Posting List),记录单词对应文档结合,由倒排索引项组成
- 倒排索引项
- 文档id
- 词频,该单词在文档中出现的次数,用于相关性评分
- 位置,单词在文档中分词的位置,用于语句搜索
- 偏移,记录单词的开始结束位置,用于高亮显示
- 倒排索引项
优点
倒排索引采用不可变原则,一旦生成,不可更改,这样的好处是:
- 不需要考虑并发问题,避免锁机制带来的性能问题
- 一旦生成,就可以缓存在内存中,请求速度快
- 容易生成和维护缓存,数据可以被压缩
缺点
如果需要让一个新文档可以被搜索,需要重建整个索引。
类型(Type)
7.x 版本后就被丢弃了,默认只有一个类型,也就是和索引成一对一的关系。默认 type 是 _doc
Mapping
是定义文档及其包含的字段的存储和索引方式的过程,类似于MySQL中给字段定义类型,mapping 一旦创建无法修改。例如定义:日期的格式。
PUT movies // 索引的名称是 movies
{
"mappings": {
"properties": {
"firstName": { // firstName这个字段的类型
"type": "text"
},
"lastName": {
"type": "text" // lastName这个字段的类型
},
"mobile" : {
"type": "text", // mobile这个字段的类型
"index": false // false 表示这个字段不会被检索到,比如用户手机号码
}
}
}
}
重要的参数:
- index:该属性决定了字段可否用于查询,默认定义到mapping中的字段该属性都为true,在indexing过程中是消耗cpu资源的,不查询的字段可以设为false
- store:该属性决定了查询结果可否显示,默认false即可
多字段特性
索引模板
事先定义好一个模板在创建索引时自动应用。例如:
PUT _template/test_template
{
"index_patterns": [
"test_index_*",
"test_*"
],
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1,
"max_result_window": 100000,
"refresh_interval": "30s"
},
"mappings": {
"_doc": {
"properties": {
"id": {
"type": "long"
},
"title": {
"type": "keyword"
},
"content": {
"analyzer": "ik_max_word",
"type": "text",
"fields": {
"keyword": {
"ignore_above": 256,
"type": "keyword"
}
}
},
"available": {
"type": "boolean"
},
"review": {
"type": "nested",
"properties": {
"nickname": {
"type": "text"
},
"text": {
"type": "text"
},
"stars": {
"type": "integer"
}
}
},
"publish_time": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
},
"expected_attendees": {
"type": "integer_range"
},
"ip_addr": {
"type": "ip"
},
"suggest": {
"type": "completion"
}
}
}
}
}
文档(Document)
ES 是面向文档的,以 JSON 形式保存,每个文档有一个唯一 ID。类似于数据库中的一条记录,ES 中文档是一个JSON 对象,它有一些固定字段来描述文档信息,这些称为元数据
- _index:文档所属的索引名称
- _type:文档所属的类型名称
- _id:文档的唯一ID
- _source:文档的原始JSON数据
- _all:v7.0开始该字段已废弃
- _version:文档的版本信息,每当文档发生变化它都会增加
- _score:相关性打分
Lucene 索引
在 Lucene 中,单个倒排索引文件被称为 Segment,一个 Segment 就是一个不可变的整体。多个 Segment 汇总在一起称为 Lucene 索引,对应 ES 中的 分片(Shard)。
当有新文档写入时,会生成新的 Segment,查询时会查询所有的 Segment,并对结果进行汇总。Lucene 有一个叫 Commit Point 文件来记录所有 Segment 的信息。
Segment 会有很多,所以会定期合并,来减少 Segment,或者删除已经删除的文档。ES 删除一个文档不会立即删除,而是会存在一个 .del 的文件中。
文档写入过程
新文档先会被写入到 Index Buffer 中,当 Buffer 到达一定程度时,会写入到 Segment 中,那么这些文档就可以被搜索到了,把文档从 Buffer 写入到 Segment 中,这个过程叫做 Refresh。
- Refresh 不会执行 fsync 操作。
- Refresh 操作默认 1 秒一次,可以通过 index.refresh_interval 配置,这就是为什么 ES 的搜索时近实时的。
- Index Buffer 被占满也会触发 Refresh 操作,Index Buffer 默认占 JVM 10%。
fsync 的功能是确保文件 fd 所有已修改的内容已经正确同步到硬盘上,该调用会阻塞等待直到设备报告IO完成。
Transaction Log
和 MySQL 一样,为了保证数据不丢失,在把文档写入 Index Buffer 时,还会写入 Transaction Log,它会做落盘操作,每一个分片都有一个 Transaction Log。恢复的时候会先执行 Transaction Log 来恢复数据。Transaction Log 默认是 512 MB。
Fulsh 执行过程
当 ES 执行 Flush 操作时:
- 会先调用 Refresh 操作,清空 Index Buffer;
- 调用 fsync 操作,将缓存中的 Segment 写入到磁盘;
- 清空 Transaction Log;
由于 Flush 操作比较重,所以是 30 分钟执行一次。但当 Transaction Log 写满了也会执行 Flush 操作。
排序
排序是对字段原始内容进行的,倒排索引无法发挥作用。需要使用正排索引,有两种实现方法:
分页
Scroll
如果需要查询所有文档,使用这种方式,会建立一个快照,但是新的数据无法被查到
Pagination
使用 from 和 size 进行分页查询,最大查询 10000 个文档,如果需要要深度分页,要使用 search after 功能。
Suggester API
ES 也提供搜索推荐功能。原理:将输入的文本分解为 Token,然后再索引字典里查找相似的 Term 并返回。类似这个功能:
Completion Suggester
ES 也提供搜索补全功能。需要定义索引 mapping 类型为 completion。例如:定义 articles 索引
PUT articles
{
"mappings": {
"properties": {
"title_completion" : {
"type": "completion"
}
}
}
}
跨集群搜索
5.3 版本以后支持的新功能。
启动三个单节点的集群:
PUT _cluster/settings
{
"persistent": {
"cluster": {
"remote": {
"cluster0": {
"seeds": [
"127.0.0.1:9300"
],
"transport.ping_schedule": "30s"
},
"cluster1": {
"seeds": [
"127.0.0.1:9301"
],
"transport.compress": true,
"skip_unavailable": true
},
"cluster2": {
"seeds": [
"127.0.0.1:9302"
]
}
}
}
}
}
分词器
ES 提供了自带的几个分词器:
- Standard:按单词切分,并且会转换为小写
- Simple:按非单词处切分,并且小写处理
- Whitespace:按空格切分
- Stop:去除语气词
- keyword:传入就是关键字,不做分词处理
使用ES自带的 simple 分词器,对 text 进行分词解析:
POST /_analyze
{
"analyzer": "simple",
"text": "我是一个程序员,咿呀咿呀呦!"
}
会得到如下结果:
{
"tokens" : [
{
"token" : "我是一个程序员",
"start_offset" : 0,
"end_offset" : 7,
"type" : "word",
"position" : 0
},
{
"token" : "咿呀咿呀呦",
"start_offset" : 8,
"end_offset" : 13,
"type" : "word",
"position" : 1
}
]
}
或者
http://localhost:9200/_analyze?analyzer=chinese&pretty=true
&text=我是一个程序员
ik 分词插件
要和 ES 的版本保持一致,下载好放到plugin目录下,官方文档
使用 ik 的ik_smart
分词器(还有另一个 ik_max_word
),对 text 进行分词解析:
POST /_analyze
{
"analyzer": "ik_smart",
"text": "我是一个程序员,咿呀咿呀呦!"
}
得到如下结果:
{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "一个",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "程序员",
"start_offset" : 4,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "咿呀",
"start_offset" : 8,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "咿呀",
"start_offset" : 10,
"end_offset" : 12,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "呦",
"start_offset" : 12,
"end_offset" : 13,
"type" : "CN_CHAR",
"position" : 6
}
]
}
加如扩展词条
在 config 下的 IKAnalyzer.cfg.xml 添加
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<!-- custom.dic 这个文件就是自己新建的,在里面添加自己的扩展词条 -->
<entry key="ext_dict">custom.dic/entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
pinyin 分词插件
要和 ES 的版本保持一致,下载好放到plugin目录下
拼音分词器需要先创建一个分词索引,按照文档创建:
PUT /medcl/
{
"settings" : {
"analysis" : {
"analyzer" : {
"pinyin_analyzer" : {
"tokenizer" : "my_pinyin"
}
},
"tokenizer" : {
"my_pinyin" : {
"type" : "pinyin",
"keep_separate_first_letter" : false,
"keep_full_pinyin" : true,
"keep_original" : true,
"limit_first_letter_length" : 16,
"lowercase" : true,
"remove_duplicated_term" : true
}
}
}
}
}
再对 text 进行分词:
GET /medcl/_analyze
{
"text": ["刘德华"],
"analyzer": "pinyin_analyzer" # 使用上面自己定义的分词器
}
得到如下结果:
{
"tokens" : [
{
"token" : "liu",
"start_offset" : 0,
"end_offset" : 1,
"type" : "word",
"position" : 0
},
{
"token" : "de",
"start_offset" : 1,
"end_offset" : 2,
"type" : "word",
"position" : 1
},
{
"token" : "hua",
"start_offset" : 2,
"end_offset" : 3,
"type" : "word",
"position" : 2
},
{
"token" : "刘德华",
"start_offset" : 0,
"end_offset" : 3,
"type" : "word",
"position" : 3
},
{
"token" : "ldh",
"start_offset" : 0,
"end_offset" : 3,
"type" : "word",
"position" : 4
}
]
}
或者
http://localhost:9200/_analyze?analyzer=ik_smart&pretty=true&text=我是一个程序员
常见错误
429:集群过于繁忙
4xx:请求体格式有错
500:集群内部错误
数据存储
当一个写请求发送到 ES 后,ES 会将数据写入 memory buffer 中,并添加事务日志 translog。如果每一天数据写入内存后都立即写到磁盘,那么数据是离散的,写入磁盘操作也是随机写,效率很低,降低 ES 的性能。
当写请求发送到 ES 后,此时写入的数据还不能被查询到。默 认设置下,ES 每1秒钟将 memory buffer 中的数据 refresh 到 Linux 的 File system cache ,并清 空 memory buffer ,此时写入的数据就可以被查询到了。
但 File system cache 依然是内存数据,一旦断电,则 File system cache 中的数据全部丢失。默认设置下,ES 每30分钟调用 fsync 将 File system cache 中的数据 flush 到硬盘。因此需要通过translog 来保证即使因为断电 File system cache 数据丢失,ES 重启后也能通过日志回放找回丢失 的数据。
translog 默认设置下,每一个 index 、 delete 、 update 或 bulk 请求都会直接 fsync 写入硬盘。 为了保证 translog 不丢失数据,在每一次请求之后执行 fsync 确实会带来一些性能问题。对于一些 允许丢失几秒钟数据的场景下,可以通过设置 index.translog.durability 和 index.translog.sync_interval 参数让 translog 每隔一段时间才调用 fsync 将事务日志数据写入硬盘。
JVM 配置
修改config/jvm.options中的参数
一些原理
文档是如何被路由到的
一个查询时如何路由到一个分片中的?根据以下公式:
shard = hash(routing) % number_of_primary_shards
routing 默认是文档 id,对这个值进行路由,再除以主分片的数量,得到余数,这个数就是我们文档所在的位置。
如果主分片的的数量改了,那么有些值就无法正确路由到,所以主分片的数量一旦创建不可改变。当然你可以自定义routing 的值。例如:
PUT blogs/_doc/100?routing=bigdata
{
"content":"文本信息 "
}
文档是如何被更新的
一个请求到一个节点上,该节点通过 Hash 算法,路由到具体的节点分片上,然后先删除原来的文档,再创建,再原来返回响应结果。
文档是如何被删除的
一个请求到一个节点上,该节点通过 Hash 算法,路由到具体的节点分片上,然后删除文档,再对其它分片发送删除操作,再原来返回响应结果。
主分片和副本分片如何交互
读操作所有的节点都可以,事务操作(新建、更新、删除)只有主分片可以操作:
- 客户端向任意一个节点发送事务请求,该接收到请求的节点称为:协调节点(ingest)
- 该请求会被协调节点转发到主分片所在的节点上
- 主分片收到请求并执行,成功后会将请求转发给其它节点的副本分片上执行,一旦所有副本分片都成功了,主节点就会向协调节点报告成功,协调节点向客户端报告成功
其实在事务操作之前主节点要求副本节点大多处于活跃状态,否则不操作,默认等待1分钟,这是避免由于网络原因而导致数据不一致的问题
插件
安装中文分词插件
// ik版本要和ES的版本保持一致,可以去官方找 https://github.com/medcl/elasticsearch-analysis-ik/releases
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.1.1/elasticsearch-analysis-ik-7.1.1.zip
Cerebro
查看集群状态,内存、CPU等使用情况
Head
一个简陋的界面,在GitHub下载 Head、然后解压,在目录中执行node install(需要node版本大于v6.0)安装依赖,再运行:npm run start,访问:http://localhost:9100/
在es的配置文件elasticsearch.yml最后面添加跨域配置:
http.cors.enabled: true
http.cors.allow-origin: "*"
如果不想下载,也可以安装 Google 插件 ElasticSearch Head
docker中使用
docker中启动:docker-compose.yaml
启动:docker-compose up
version: '2.2'
services:
cerebro:
image: lmenezes/cerebro:0.8.3
container_name: hwc_cerebro
ports:
- "9000:9000"
command:
- -Dhosts.0.host=http://elasticsearch:9200
networks:
- hwc_es7net
kibana:
image: docker.elastic.co/kibana/kibana:7.1.0
container_name: hwc_kibana7
environment:
#- I18N_LOCALE=zh-CN
- XPACK_GRAPH_ENABLED=true
- TIMELION_ENABLED=true
- XPACK_MONITORING_COLLECTION_ENABLED="true"
ports:
- "5601:5601"
networks:
- hwc_es7net
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.1.0
container_name: es7_hot
environment:
- cluster.name=geektime-hwc
- node.name=es7_hot
- node.attr.box_type=hot
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- discovery.seed_hosts=es7_hot,es7_warm,es7_cold
- cluster.initial_master_nodes=es7_hot,es7_warm,es7_cold
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- hwc_es7data_hot:/usr/share/elasticsearch/data
ports:
- 9200:9200
networks:
- hwc_es7net
elasticsearch2:
image: docker.elastic.co/elasticsearch/elasticsearch:7.1.0
container_name: es7_warm
environment:
- cluster.name=geektime-hwc
- node.name=es7_warm
- node.attr.box_type=warm
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- discovery.seed_hosts=es7_hot,es7_warm,es7_cold
- cluster.initial_master_nodes=es7_hot,es7_warm,es7_cold
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- hwc_es7data_warm:/usr/share/elasticsearch/data
networks:
- hwc_es7net
elasticsearch3:
image: docker.elastic.co/elasticsearch/elasticsearch:7.1.0
container_name: es7_cold
environment:
- cluster.name=geektime-hwc
- node.name=es7_cold
- node.attr.box_type=cold
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- discovery.seed_hosts=es7_hot,es7_warm,es7_cold
- cluster.initial_master_nodes=es7_hot,es7_warm,es7_cold
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- hwc_es7data_cold:/usr/share/elasticsearch/data
networks:
- hwc_es7net
volumes:
hwc_es7data_hot:
driver: local
hwc_es7data_warm:
driver: local
hwc_es7data_cold:
driver: local
networks:
hwc_es7net:
driver: bridge