介绍
Elasticsearch(简称 ES) 是一个开源的、分布式的、高度可扩展的搜索和分析引擎。
ES 经常与 Logstash(数据收集和处理管道)和 Kibana(数据可视化)一起使用,组成著名的 ELK Stack(现在官方称为 Elastic Stack),能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。
核心区别:
- 结构化数据(固定的数据模型和格式),例子:关系型数据库(MySQL, Oracle)表中的数据
- 半结构化数据(具有某种结构,但格式不固定),例子:NoSQL数据库、文件系统
- 非结构化数据(没有预定义的数据模型或格式),例子:文本文档、图片、音频、视频、PDF
安装
ES的官方地址:www.elastic.co/cn/
下载地址:www.elastic.co/cn/download…
7.8版本下载地址:www.elastic.co/cn/download…
目录结构:
- bin 可执行脚本目录
- config 配置目录
- jdk 内置 JDK 目录(可替换)
- lib 类库
- logs 日志目录
- modules 模块目录
- plugins 插件目录
解压后,进入 bin 文件目录,点击 elasticsearch.bat 文件启动 ES 服务 。
注意:9300 端口为 ES集群间组件的通信端口, 9200 端口为浏览器访问的 http协议 RESTful 端口。
打开浏览器,输入地址: http://localhost:9200,有一串json代表安装成功。
如果安装失败,可能空间不足,修改 config/jvm.options 配置文件:
# 设置 JVM 初始内存为 1G。此值可以设置与-Xmx 相同,以避免每次垃圾回收完成后 JVM 重新分配内存
# Xms represents the initial size of total heap space
# 设置 JVM 最大可用内存为 1G
# Xmx represents the maximum size of total heap space
-Xms1g
-Xmx1g
数据格式
ES是面向文档型数据库,一条数据在这里就是一个文档。为了方便大家理解,我们将 ES里存储文档数据和关系型数据库 MySQL 存储数据的概念进行一个类比。
ES 里的 Index 可以看做一个库,而 Types 相当于表(7版本后已经删除),Documents 则相当于表的行。
- 正排索引(文档 -> 包含哪些词),通过文档ID快速获取文档内容
- 倒排索引(词 -> 出现在哪些文档?),通过关键词快速查找包含它的所有文档
HTTP 基本操作
索引操作
对比关系型数据库,创建索引就等同于创建数据库。
创建索引
# 创建索引 shopping是索引名,注意:PUT请求具有幂等性
PUT http://localhost:9200/shopping
Content-Type:application/json
# 返回响应:
{
"acknowledged": true, //响应结果
"shards_acknowledged": true, //分片结果
"index": "shopping" //索引名称
}
查询索引
# 查询索引 shopping是索引名
GET http://localhost:9200/shopping
删除索引
# 删除索引 shopping是索引名
DELETE http://localhost:9200/shopping
查看所有索引
# _cat 表示查看的意思, indices 表示索引,相当于Mysql的show tables
GET http://localhost:9200/_cat/indices?v
# 返回响应:
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open shopping J0WlEhh4R7aDrfIc3AkwWQ 1 1 0 0 208b 208b
-----解释-----
health 当前服务器健康状态: green(集群完整) yellow(单点正常、集群不完整) red(单点不正常)
status 索引打开、关闭状态
index 索引名
uuid 索引统一编号
pri 主分片数量
rep 副本数量
docs.count 可用文档数量
docs.deleted 文档删除状态(逻辑删除)
store.size 主分片和副分片整体占空间大小
pri.store.size 主分片占空间大小
文档操作
这里的文档可以类比为关系型数,据库中的表数据,添加的数据格式为 JSON 格式。
文档创建
# 创建文档, shopping是索引名, 在该索引创建文档 (返回的_id,会随机生成)
POST http://localhost:9200/shopping/_doc
Content-Type:application/json
{
"title": "小米",
"value": "content",
"price": 3999
}
# 返回响应:
{
"_index": "shopping", //索引
"_type": "_doc", //类型-文档
"_id": "ANQqsHgBaKNfVnMbhZYU", //唯一标识,可以类比为 MySQL中的主键,随机生成
"_version": 1, //版本
"result": "created", //结果,这里的 create 表示创建成功
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
# 如果想要自定义唯一性标识, 101就是唯一标识
POST http://localhost:9200/shopping/_doc/101
Content-Type:application/json
{
"title": "小米2",
"value": "content",
"price": 3999
}
主键查询 & 全查询
# 根据主键查询
GET http://localhost:9200/shopping/_doc/101
# 查询该索引下,全部文档
GET http://localhost:9200/shopping/_search
全量修改 & 局部修改 & 删除
# 全量修改,将原有的数据内容进行覆盖,101就是唯一标识
PUT http://localhost:9200/shopping/_doc/101
Content-Type:application/json
{
"title": "小米2-修改",
"value": "content-修改",
"price": 3999
}
# 局部修改,只修改某一给条数据的局部信息,101就是唯一标识
POST http://localhost:9200/shopping/_update/101
Content-Type:application/json
{
"doc": {
"title": "修改华为"
}
}
# 删除某一个文档,101就是唯一标识,注意:删除一个文档不会立即从磁盘上移除
DELETE http://localhost:9200/shopping/_doc/101
# 再查看是否删除成功
GET http://localhost:9200/shopping/_doc/101
# 返回响应:foun=false,代表成功
{
"_index": "shopping",
"_type": "_doc",
"_id": "1",
"found": false
}
# 条件删除文档
POST http://localhost:9200/shopping/_delete_by_query
Content-Type:application/json
{
"query": {
"match": {
"price":3999
}
}
}
条件查询
# 条件查询:方式1:查询title=小米
GET http://localhost:9200/shopping/_search?q=title:小米
# 条件查询:方式2:查询title=小米
GET http://localhost:9200/shopping/_search
Content-Type:application/json
{
"query": {
"match": {
"title": "小米"
}
}
}
# 字段匹配查询,在多个字段中查询
GET http://localhost:9200/shopping/_search
Content-Type:application/json
{
"query":{
"multi_match": {
"query": "小米",
"fields": ["title","value"]
}
}
}
# 全查询
#GET http://localhost:9200/shopping/_search
#Content-Type:application/json
{
"query":{
"match_all":{}
}
}
# 查询指定字段
#GET http://localhost:9200/shopping/_search
#Content-Type:application/json
{
"query":{
"match_all":{}
},
"_source":["title","price"] // 只展示title,其他字段不展示
}
分页查询 & 查询排序
# 分页查询
GET http://localhost:9200/shopping/_search
Content-Type:application/json
{
"query":{
"match_all":{}
},
"from":0, //当前页的起始索引,默认从 0 开始。 from = (pageNum - 1) * size
"size":2 //每页显示多少条
}
# 查询排序
GET http://localhost:9200/shopping/_search
Content-Type:application/json
{
"query":{
"match_all":{}
},
"sort":{
"price":{ // 排序字段
"order":"desc" // desc 降序,asc 升序
},
"title":{ // 排序字段
"order":"desc" // desc 降序,asc 升序
}
}
}
多条件查询 & 范围查询
# bool组合查询通过:must(必须)、must_not(必须不)、should(应该)进行组合
# 多条件查询,查询小米,价格为3999元的。(must相当于数据库的&&)
GET http://localhost:9200/shopping/_search
Content-Type:application/json
{
"query":{
"bool":{
"must":[{ // 并且
"match": {"title":"小米"}
},{
"match": {"price":3999}
}]
}
}
}
# 多条件查询,查询小米和华为。(should相当于数据库的||)
GET http://localhost:9200/shopping/_search
Content-Type:application/json
{
"query":{
"bool":{
"should":[{ // 或者
"match":{"title":"小米"}
},{
"match":{"title":"华为"}
}]
},
"filter":{ // 范围查询:价格大于2000元的
"range":{
"price":{"gt":2000 }
}
}
}
}
# 范围查询:range 查询允许以下字符
gt 大于>
gte 大于等于>=
lt 小于<
lte 小于等于<=
全文检索 & 完全匹配 & 高亮查询
# 全文检索
# 如搜索引擎那样,输入“小华”,返回结果带回品牌有小米和华为的数据
GET http://localhost:9200/shopping/_search
Content-Type:application/json
{
"query":{
"match":{
"title" : "小华"
}
}
}
# 完全匹配
GET http://localhost:9200/shopping/_search
Content-Type:application/json
{
"query":{
"match_phrase":{
"title" : "为"
}
}
}
# 高亮查询,highlight 高亮显示
GET http://localhost:9200/shopping/_search
Content-Type:application/json
{
"query":{
"match_phrase":{
"title" : "为"
}
},
"highlight":{
"fields":{
"title":{} //<----高亮这字段
}
}
// "highlight": {
// "pre_tags": "<font color='red'>", //前置标签
// "post_tags": "</font>", //后置标签
// "fields": { //需要高亮的字段
// "name": {}
// }
// }
}
聚合查询
聚合允许使用者对 es 文档进行统计分析,类似与关系型数据库中的 group by,当然还有很多其他的聚合,例如取最大值max、平均值avg等等。
# 聚合操作 terms 分组
GET http://localhost:9200/shopping/_search
Content-Type:application/json
{
"aggs":{ //聚合操作
"price_group":{ //名称,可以随意起名
"terms":{ //分组
"field":"price" //聚合字段
}
}
},
"size":0 // 不显示全量数据,hits里不再返回数据
}
# 聚合操作 avg 平均值
GET http://localhost:9200/shopping/_search
Content-Type:application/json
{
"aggs":{ //聚合操作
"price_avg":{ //名称,随意起名
"avg":{ //求平均值
"field":"price" //聚合字段
}
}
},
"size":0
}
映射关系
类似于数据库(database)中的表结构(table),创建数据库表需要设置字段名称,类型,长度,约束等;索引库也一样,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做映射(mapping)。
# 创建映射,还可以通过GET进行查询
PUT http://localhost:9200/user/_mapping
Content-Type:application/json
{
"properties": {
"name":{
"type": "text", //可分词
"index": true //是否索引,默认为 true,可以用来搜索
},
"sex":{
"type": "keyword", //不可分词,数据会作为完整字段进行匹配
"index": true
},
"tel":{
"type": "keyword",
"index": false //字段不会被索引,不能用来搜索
}
}
}
# 其他映射数据说明
store:是否将数据进行独立存储,默认为 false
analyzer:分词器,这里的 ik_max_word 即使用 ik 分词器
JavaAPI-环境准备
可在我的码云ES项目,有JAVA项目连接ES,进行基本API操作。
Elasticsearch环境
集群 Cluster
一个集群就是由一个或多个服务器节点组织在一起,共同持有整个的数据,并一起提供索引和搜索功能。一个 Elasticsearch 集群有一个唯一的名字标识,这个名字默认就是”elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。
节点 Node
集群中包含很多服务器, 一个节点就是其中的一个服务器。 作为集群的一部分,它存储数据,参与集群的索引和搜索功能。
例子:节点1存入数据a、数据b;节点2存入数据c、数据d;查询数据c,需要访问节点2。
Windows集群部署
步骤1:部署集群
创建 elasticsearch-cluster 文件夹,分别在该目录下创建node1、node2、node3文件夹,分别安装ES服务;
步骤2:修改配置文件
每个节点的 config/elasticsearch.yml 配置文件
node1节点配置
# 节点1-配置信息:
# 集群名称,节点之间要保持一致
cluster.name: my-elasticsearch
# 节点名称,集群内要唯一
node.name: node-1
node.master: true
node.data: true
# ip 地址
network.host: localhost
# http 端口
http.port: 1001
# tcp 监听端口
transport.tcp.port: 9301
# 节点发现其他节点,查询node1节点
discovery.seed_hosts: ["localhost:9302", "localhost:9303"]
discovery.zen.fd.ping_timeout: 1m
discovery.zen.fd.ping_retries: 5
# 跨域配置
# action.destructive_requires_name: true
http.cors.enabled: true
http.cors.allow-origin: "*"
node2节点配置
# 节点2-配置信息:
# 集群名称,节点之间要保持一致
cluster.name: my-elasticsearch
# 节点名称,集群内要唯一
node.name: node-2
node.master: true
node.data: true
# ip 地址
network.host: localhost
# http 端口
http.port: 1002
# tcp 监听端口
transport.tcp.port: 9302
# 节点发现其他节点,查询node1节点
discovery.seed_hosts: ["localhost:9301", "localhost:9303"]
discovery.zen.fd.ping_timeout: 1m
discovery.zen.fd.ping_retries: 5
# 跨域配置
# action.destructive_requires_name: true
http.cors.enabled: true
http.cors.allow-origin: "*"
node3节点配置
# 节点3-配置信息:
# 集群名称,节点之间要保持一致
cluster.name: my-elasticsearch
# 节点名称,集群内要唯一
node.name: node-3
node.master: true
node.data: true
network.host: localhost
http.port: 1003
transport.tcp.port: 9303
# 候选主节点的地址,在开启服务后可以被选为主节点
discovery.seed_hosts: ["localhost:9301", "localhost:9302"]
discovery.zen.fd.ping_timeout: 1m
discovery.zen.fd.ping_retries: 5
#集群内的可以被选为主节点的节点列表
#cluster.initial_master_nodes: ["node-1", "node-2","node-3"]
# 跨域配置
# action.destructive_requires_name: true
http.cors.enabled: true
http.cors.allow-origin: "*"
步驟3:启动服务
分别启动每个服务,进入 bin 文件目录,点击 elasticsearch.bat 文件启动 ES 服务 。
注意:启动前删除每个节点中的 data 目录中所有内容、和logs目录里日志文件。
步骤4:测试集群
访问机器节点状态:
GET http://127.0.0.1:1001/_cluster/healthGET http://127.0.0.1:1002/_cluster/healthGET http://127.0.0.1:1003/_cluster/health
# 访问:http://localhost:1001/_cluster/health
# 响应结果:
{
"cluster_name": "my-application",
"status": "green", // green 所有的主分片和副本分片都正常运行。
// yellow:所有的主分片都正常运行,但不是所有的副本分片都正常运行。
// red:有主分片没能正常运行。
"timed_out": false,
"number_of_nodes": 3, // 集群中有3个节点
"number_of_data_nodes": 3, // 集群中有3个数据节点
"active_primary_shards": 0,
"active_shards": 0,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 0,
"delayed_unassigned_shards": 0,
"number_of_pending_tasks": 0,
"number_of_in_flight_fetch": 0,
"task_max_waiting_in_queue_millis": 0,
"active_shards_percent_as_number": 100
}
Linux集群部署
Elasticsearch 7.8.0下载地址:www.elastic.co/cn/download…
单节点部署
步骤1:解压缩操作
将压缩包,上传到,该目录/opt/module/software下
cd /opt/module/software
# 解压缩,压缩到上级目录/opt/module下
tar -zxvf elasticsearch-7.8.0-linux-x86_64.tar.gz -C /opt/module
# 进行重命名,为es目录
cd ../
mv elasticsearch-7.8.0 es
步骤2:创建用户
因为安全问题,Elasticsearch不允许root用户直接运行,所以要创建新用户,在root用户中创建新用户。
useradd es # 新增 es 用户
passwd es # 为 es 用户设置密码 (需要输入两遍密码)
userdel -r es # 如果错了,可以删除再加
chown -R es:es /opt/module/es # 文件夹所有者
# 语法解释:chown [选项] [所有者]:[所属组] 目标文件/目录
# 验证用户是否创建成功
id es
步骤3:修改ES配置文件
# 修改/opt/module/es/config/elasticsearch.yml文件
# 集群名称
cluster.name: elasticsearch
# 节点名称
node.name: node-1
network.host: 0.0.0.0
# 节点端口号
http.port: 9200
# 当前机器当成主节点
cluster.initial_master_nodes: ["node-1"]
步骤4:修改系统配置文件
因为ES生成数据量比较多,文件数量较多,避免我们控制这些文件出问题,所以要改系统相关配置。
需要修改以下配置文件:
vim /etc/security/limits.conf
# 添加内容:
# 在文件末尾中增加下面内容
# 每个进程可以打开的文件数的限制
es soft nofile 65536
es hard nofile 65536
vim /etc/security/limits.d/20-nproc.conf
# 添加内容:
# 在文件末尾中增加下面内容
# 每个进程可以打开的文件数的限制
es soft nofile 65536
es hard nofile 65536
# 操作系统级别对每个用户创建的进程数的限制
* hard nproc 4096
# 注: * 带表 Linux 所有用户名称
vim /etc/sysctl.conf
# 添加内容:
# 在文件中增加下面内容
# 一个进程可以拥有的 VMA(虚拟内存区域)的数量,默认值为 65536
vm.max_map_count=655360
重新加载
# 重新加载
sysctl -p
# 如果需要,关闭防火墙
systemctl stop firewalld
步骤5:启动ES
# 使用 ES 用户启动,可能需要输入es密码
su es
cd /opt/module/es
# 启动
bin/elasticsearch
# 后台启动
bin/elasticsearch -d
# 关闭ES
# 查找 ES 进程的PID
ps aux | grep elasticsearch
# 进行杀掉
kill PID
步骤6:测试ES
# 访问:http://localhost:9200
# 响应结果:部署成功
{
"name" : "node-1",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "p_fYxXpvSnaO8ubTtoyj9Q",
"version" : {
"number" : "7.8.0",
"build_flavor" : "default",
"build_type" : "tar",
"build_hash" : "757314695644ea9a1dc2fecd26d1a43856725e65",
"build_date" : "2020-06-14T19:35:50.234439Z",
"build_snapshot" : false,
"lucene_version" : "8.5.1",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
集群配置
集群配置与单节点配置一致,分别在三台机器node-1、node-2、node-3,进行上传压缩包。
步骤1:解压缩操作
# 与点节点部署的步骤1一致,将压缩包,上传到,该目录/opt/module下
# 直接进行解压,和重命名es-cluster
tar -zxvf elasticsearch-7.8.0-linux-x86_64.tar.gz
mv elasticsearch-7.8.0 es-cluster
步骤2:创建用户
# 与点节点部署的步骤2一致,3台机器同步执行
chown -R es:es /opt/module/es-cluster # 文件夹所有者
步骤3:修改系统配置文件
# 与点节点部署的步骤4一致,3台机器同步执行
步骤4:修改ES配置文件
修改/opt/module/es-cluster/config/elasticsearch.yml文件
# 加入如下配置,注意每台node的配置,分别:node-1、node-2、node-3
# 集群名称
cluster.name: cluster-es
# 节点名称,每个节点的名称不能重复
node.name: node-1
#ip 地址, 每个节点的地址不能重复,主私网:IP
network.host: node-1IP
#是不是有资格主节点
node.master: true
node.data: true
http.port: 9200
transport.tcp.port: 9300
# head 插件需要这打开这两个配置
http.cors.allow-origin: "*"
http.cors.enabled: true
http.max_content_length: 200mb
#es7.x 之后新增的配置,初始化一个新的集群时需要此配置来选举 master
cluster.initial_master_nodes: ["node-1"]
#es7.x 之后新增的配置,节点发现
discovery.seed_hosts: ["node-1-IP","node-2-IP","node-3-IP"]
gateway.recover_after_nodes: 2
network.tcp.keep_alive: true
network.tcp.no_delay: true
transport.tcp.compress: true
#集群内同时启动的数据任务个数,默认是 2 个
cluster.routing.allocation.cluster_concurrent_rebalance: 16
#添加或删除节点及负载均衡时并发恢复的线程个数,默认 4 个
cluster.routing.allocation.node_concurrent_recoveries: 16
#初始化数据恢复时,并发恢复线程的个数,默认 4 个
cluster.routing.allocation.node_initial_primaries_recoveries: 16
步骤5:启动ES
# 使用 ES 用户启动,可能需要输入es密码
su es
cd /opt/module/es-cluster
# 启动
bin/elasticsearch
# 后台启动
bin/elasticsearch -d
# 关闭ES
# 查找 ES 进程的PID
ps aux | grep elasticsearch
# 进行杀掉
kill PID
查看节点
# 访问:http://localhost:9200/_cat/nodes
# 响应结果:部署成功
172.18.123.259 62 98 0 0.12 0.17 0.03 dilmrt - node-3
172.18.123.239 50 96 0 0.14 0.13 0.05 dilmrt - node-2
172.18.123.240 14 95 1 0.07 0.25 0.15 dilmrt * node-1
Elasticsearch进阶
核心概念
- 索引(Index):一个索引就是一个拥有几分相似特征的文档的集合,为了提高搜索的性能
- 类型(Type):默认不再支持自定义索引类型(默认类型为: _doc)
- 文档(Document):是一条数据,以 JSON格式标识
- 字段(Field):相当于是数据表的字段,对文档数据根据不同属性进行的分类标识
- 映射(Mapping):某个字段的数据类型、默认值、分析器、是否被索引等
- 分片(Shards):一个索引可以存储超出单个节点硬件限制的大量数据,相当于Mysql中的分表
- 副本(Replicas):避免数据丢失,将数据进行多个备份
- 分配(Allocation):将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。这个过程是由 master 节点完成的。
系统架构
一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 任何节点都可以成为主节点。
如图:有3个Node节点,Node1是主节点,P0的备份数据在Node2上,P1的备份数据在Node3上,P2的备份数据在Node1上,每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。
单节点集群
把一个完整的索引分成3个(主分片),每个分片都有一个备份(副本),合在一块就6个分片。默认情况:一个索引的分片是1,副本也是1。
我们在包含一个集群内创建名为 users 的索引,我们将分配 3个主分片和一份副本(每个主分片拥有一个副本分片)。因为我们是单节点, 所有3个主分片都被分配在node-1。
# 创建索引:user是索引名
PUT http://localhost:1001/user
Content-Type:application/json
{
"settings" : {
"number_of_shards" : 3, // 主分片是3
"number_of_replicas" : 1 // 每主分片,都有1个副本
}
}
elasticsearch-head chrome插件安装
接着点击Chrome右上角选项->工具->管理扩展,选择打开“开发者模式”,让后点击“加载已解压得扩展程序”,选择(插件所在的目录),即可完成chrome插件安装。
集群健康值:yellow( 3 of 6 ):表示当前集群的全部主分片都正常运行,但是副本分片没有全部处在正常状态。
3 个副本分片都是 Unassigned,它们都没有被分配到任何节点。 在同一个节点上既保存原始数据又保存副本是没有意义的,因为一旦失去了那个节点,我们也将丢失该节点 上的所有副本数据。
故障转移
为了解决上面的数据丢失问题,我们只需再启动一个节点即可防止数据丢失。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name 配置,它就会自动发现集群并加入到其中。但是在不同机器上启动节点的时候,为了加入到同一集群,你需要配置一个可连接到的单播主机列表。之所以配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上 运行的节点才会自动组成集群。
黑色加粗边框=主分片,不加粗的=副本。
水平扩容
当启动了第三个节点,我们的集群将会拥有三个节点的集群:为了分散负载而对分片进行重新分配。
Node1和Node2上各有一个分片被迁移到了新的Node3节点,现在每个节点上都拥有2个分片,而不是之前的3个。这表示每个分片的性能将会得到提升。
但是如果我们想要扩容超过 6 个节点怎么办呢?
主分片的数目在索引创建时就已经确定了下来。这个数目定义了这个索引能够存储的最大数据量。读操作,搜索和返回数据,可以同时被主分片或副本分片所处理,所以当你拥有越多的副本分片时,也将拥有越高的吞吐量。
可以动态调整副本分片数目的,我们可以按需伸缩集群。让我们把副本数从默认的1增加到2。
# 把副本数从默认的1增加到2 ,这样:主分片有3个,每个主分片,都有2个副本,共有9个分片
PUT http://localhost:1001/user/_settings
Content-Type:application/json
{
"number_of_replicas" : 2 // 每个主分片,都有2个副本
}
应对故障
当我们关闭第一个节点,这时集群的状态为:关闭了一个节点后的集群。
关闭的节点是一个主节点。而集群必须拥有一个主节点来保证正常工作,在其它节点上存在着这两个主分片的完整副本,Node2已经升为主节点,任然可以正常提供服务。
当我们重启Node1,集群可以将缺失的副本分片再次进行分配,那么集群的状态也将恢复成之前的状态。如果Node1依然拥有着之前的分片,它将尝试去重用它们,同时仅从主分片复制发生了修改的数据文件。和之前的集群相比,只是 Master 节点切换了。
路由计算 & 分片控制
路由计算
ES通过哈希来决定将文档存储到哪一个主分片中,具体的路由计算公式:
# 路由计算:hash(主键id)% 主分片数量 = 【0,1,2】
shard = hash(routing) % number_of_primary_shards
routing是一个可变值,默认是文档的 _id,也可以设置成一个自定义的值。routing通过hash函数生成一个数字,然后这个数字再除以number_of_primary_shards(主分片的数量)后得到余数。这个分布在0到number_of_primary_shards-1之间的余数,就是我们所寻求的文档所在分片的位置。通过这个路由计算过程,ES确保文档被均匀地分布到索引的各个主分片上,实现了数据的分散存储。
分片控制
我们可以发送请求到集群中的任一节点。每个节点都有能力处理任意请求。每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。
# 分片控制:用户可以访问任何一个节点获取数据,这个节点称为协调节点
数据写流程
新建、索引、删除请求都是写操作, 必须在主分片上面完成之后才能被复制到相关的副本分片。
- 客户端请求到集群节点(任何节点都可以)
- 协调节点,将请求转换到制定的节点
- 主分片将数据保存
- 主分片将数据发送给副本节点
- 副本保存完成,进行反馈给主分
- 主分片进行反馈
- 客户端收到反馈信息
数据写入相关参数选择配置:
replication
复制默认的值是sync。这将导致主分片得到复制分片的成功响应后才返回。 如果你设置replication为async,请求在主分片上被执行后就会返回给客户端。它依旧会转发请求给复制节点,但你将不知道复制节点成功与否。
注意:async复制可能会因为在不等待其它分片就绪的情况下发送过多的请求而使Elasticsearch过载。
consistency
即一致性。默认主分片在尝试写入时需要规定数量(quorum)或过半的分片(可以是主节点或复制节点)可用。这是防止数据被写入到错的网络分区。规定的数量计算公式如下:int ( ( primary + number_of_replicas ) / 2 ) + 1
参数的值可以设为:
- one :只要主分片状态 ok 就允许执行写操作。
- all:必须要主分片和所有副本分片的状态没问题才允许执行写操作。
- quorum:默认值为quorum , 即大多数的分片副本状态没问题就允许执行写操作。
注意:number_of_replicas是在索引中的的设置,用来定义复制分片的数量,而不是现在活动的复制节点的数量。如果你定义了索引有3个复制节点,那规定数量是:int ( ( primary + 3 replicas ) / 2 ) + 1 = 3
但如果你只有2个节点,那你的活动分片不够规定数量,也就不能索引或删除任何文档。
timeout
如果没有足够的副本分片会发生什么?ES会等待,希望更多的分片出现。默认情况下,它最多等待1分钟。如果你需要,你可以使用timeout参数使它更早终止:100是100毫秒,30s是30秒。
注意:新索引默认有1个复制分片,这意味着为了满足quorum的要求需要两个活动的分片。当然,这个默认设置将阻止我们在单一节点集群中进行操作。为了避开这个问题,规定数量只有在number_of_replicas大于一时才生效。
数据读流程
在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。
- 客户端发送查询请求到协调节点
- 协调节点计算数据所在的分片,还有全部的副本位置
- 为了负载均衡,可以轮询所有节点
- 将请求转发给具体的节点
- 节点返回查询数据结构,反馈给客户端
更新流程
先进行数据读流程,再进行数据写流程;将客户端的读请求转发到主分片,主分片检索文档,修改_source字段中的JSON,并且尝试重新索引主分片的文档。如果文档已经被另一个进程修改,它会进行重试,超过retry_on_conflict次后放弃。如果成功更新文档,它将新版本的文档并行转发到其它的副本分片,重新建立索引。一旦所有副本分片都返回成功,主分片向协调节点也返回成功,协调节点向客户端返回成功。
倒排索引
分片是Elasticsearch最小的工作单元。倒排索引是搜索引擎和数据库系统中一种核心的数据结构,它与传统的正向索引相反。在正向索引中,我们存储的是文档到其包含词汇的映射关系,而在倒排索引中,我们存储的是词汇到包含该词汇的文档列表的映射关系。
# 例子:
1001 my name is goodyan
# 索引
name 1001
good 1001
goodyan 1001
- 词条:索引中最小存储和查询单元
- 词典:字典,词条的集合
- 倒排表:记录词汇到包含该词汇的文档列表的映射关系
当用户进行搜索时,系统会根据查询词在倒排索引中快速定位到相关的文档列表,从而实现高效的全文检索。例如,如果要搜索包含“Elasticsearch”和“搜索”的文档,系统会先在倒排索引中找到包含这两个词的文档,然后通过计算相关性得分来排序返回结果。
倒排索引的优势在于其能够快速定位包含特定词汇的文档,这对于大规模文档集合的搜索至关重要。然而,它也存在一些挑战,如需要处理词汇的同义词、词形变化等问题,以及在处理大量数据时需要考虑存储和计算效率。
文档搜索
不可改变的倒排索引:
早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。倒排索引被写入磁盘后是不可改变的:它永远不会修改。
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
- 写入单个大的倒排索引允许数据被压缩,减少磁盘IO和需要被缓存到内存的索引的使用量。
动态更新索引:
核心在增量更分段机。传统的索引一旦建立就不可更改,但动态索引通过将索引划分为多个段(Segment)来实现更新。每个段都是一个独立的倒排索引,当有新数据时,会创建新的段而不是修改现有段。
实现方式
-
分段更新机制
查询一致:需要处理新旧版本文档的查询结果 存储空间:需要清理已删除文档的段 性能权衡:动态更新可能影响查询性能 -
增量索引
新增文档时,系统维护临时倒排索引来记录新数据 当新增文档达到一定数量时,将临时索引与老文档的倒排索引进行合并 合并过程按照字典序排序,提高合并效率
当一个文档被删除时,它实际上只是在 .del 文件中被标记删除。一个被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前从结果集中移除。
文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
文档刷新
近实时搜索
是指从数据被索引到数据变得可搜索之间存在一个短暂的延迟,通常这个延迟在一秒以内。这种特性是 Elasticsearch 的核心优势之一,它在保证高性能的同时,提供了几乎实时的数据更新可见性。
一个主分片,多个副本,当写入文档,根据路由规则将文档发送特定的分片来创建索引,写入流程如下图:
具体写操作,到磁盘中如下图:
在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做refresh。默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说 Elasticsearch是近实时搜索:文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。
这些行为可能会对新用户造成困惑:他们索引了一个文档然后尝试搜索它,但却没有搜到。这个问题的解决办法是用refresh API执行一次手动刷新:/usersl_refresh
并不是所有的情况都需要每秒刷新。可能你正在使用Elasticsearch索引大量的日志文件,你可能想优化索引速度而不是近实时搜索,可以通过设置refresh_interval ,降低每个索引的刷新频率:
# 查询当前refresh_interval设置
GET http://localhost:1001/索引名/_settings?include_defaults=true
# 修改刷新频率
PUT http://localhost:1001/索引名/_settings
Content-Type:application/json
{
"index" : {
"refresh_interval": "30s"
}
}
或者
PUT http://localhost:1001/*/_settings
Content-Type:application/json
...
PUT http://localhost:1001/xxx-*/_settings
Content-Type:application/json
refresh_interval可以在既存索引上进行动态更新。在生产环境中,当你正在建立一个大的新索引时,可以先关闭自动刷新,待开始使用该索引时,再把它们调回来。
# 关闭自动刷新
PUT http://localhost:1001/索引名/_settings
{ "refresh_interval": -1 }
# 每一秒刷新
PUT http://localhost:1001/索引名/_settings
{ "refresh_interval": "1s" }
文档分析
分析包含下面的过程:
- 将一块文本分成适合于倒排索引的独立的词条。
- 将这些词条统一化为标准格式以提高它们的“可搜索性”,或者recall。
分析器执行上面的工作。分析器实际上是将三个功能封装到了一个包里:
- 字符过滤器:首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉 HTML,或者将 & 转化成 and。
- 分词器:其次,字符串被分词器分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。
- Token 过滤器:最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像jump和leap这种同义词)
内置分析器
- 标准分析器:标准分析器是ES默认的分析器。它是分析各种语言文本最常用的选择。它根据Unicode联盟定义的单词边界划分文本。删除绝大部分标点。最后,将词条小写。
- 简单分析器:在任何不是字母的地方分隔文本,将词条小写。
- 空格分析器:在空格的地方划分文本。
- 语言分析器:特定语言分析器可用于很多语言。它们可以考虑指定语言的特点。例如,英语分析器附带了一组英语无用词(常用单词,例如and或者the ,它们对相关性没有多少影响),它们会被删除。
测试分析器
使用analyze API来看文本是如何被分析的。在消息体里,指定分析器和要分析的文本。
GET http://localhost:1001/_analyze
{
"analyzer": "standard", // 标准分析器
"text": "Text to analyze" // 要保存的文档数据
}
# 返回响应:
{
"tokens": [
{
"token": "text", // 将Text转成小写text
"start_offset": 0, // 开始的下标
"end_offset": 4, // 结束的下标
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "to", // 拆分的第2个词:to
"start_offset": 5,
"end_offset": 7,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "analyze", // 拆分的第3个词:analyze
"start_offset": 8,
"end_offset": 15,
"type": "<ALPHANUM>",
"position": 3
}
]
}
# token是实际存储到索引中的词条。
# start_ offset 和end_ offset指明字符在原始字符串中的位置。
# position指明词条在原始文本中出现的位置。
IK分词器
需要安装插件,将解压后的后的文件夹放入 ES 根目录下的 plugins 目录下,重启 ES 即可使用。
我们可以通过发送GET请求,查询分词效果:
- ik_max_word 会将文本做最细粒度的拆分
- ik_smart: 会将文本做最粗粒度的拆分
使用中文分词后的结果:
GET http://localhost:1001/_analyze
{
"text":"测试单词", // 要保存的文档数据,下面对该词进行分词
"analyzer":"ik_max_word" // ik_max_word 会将文本做最细粒度的拆分
}
# 返回响应:
{
"tokens": [
{
"token": "测试", // 进行拆分词:测试
"start_offset": 0,
"end_offset": 2,
"type": "CN_WORD",
"position": 0
},
{
"token": "单词", // 进行拆分词:单词
"start_offset": 2,
"end_offset": 4,
"type": "CN_WORD",
"position": 1
}
]
}
# 测试单词,进行拆分成:测试、单词
ES 中也可以进行扩展词汇,首先查询
GET http://localhost:1001/_analyze
{
"text":"弗雷尔卓德",
"analyzer":"ik_max_word"
}
# 返回响应:
# 得到每个字的分词结果,例:弗、雷、尔、卓、德,共拆分成5个词,
# 而我们需要做的就是使分词器识别到弗雷尔卓德也是一个词语。只有1个词
配置扩展词汇:
-
首先进入 ES 根目录中的plugins目录下的 ik 文件夹,进入config目录,创建custom.dic文件,写入“弗雷尔卓德”。
-
同时打开 IKAnalyzer.cfg.xml 文件,将新建的 custom.dic 配置其中。
-
重启 ES 服务器 。
-
扩展后再次查询,返回结果如下:
# 结果:从之前的弗、雷、尔、卓、德,共拆分成5个词,现在成一个词了
{
"tokens": [
{
"token": "弗雷尔卓德",
"start_offset": 0,
"end_offset": 5,
"type": "CN_WORD",
"position": 0
}
]
}
自定义分析器
可以通过在一个适合你的特定数据的设置之中组合字符过滤器、分词器、词汇单元过滤器来创建自定义的分析器。
- 字符过滤器:字符过滤器用来整理一个尚未被分词的字符串
- 分词器:一个分析器必须有一个唯一的分词器
- 词单元过滤器:经过分词,作为结果的词单元流会按照指定的顺序通过指定的词单元过滤器
自定义分析器例子:
# 创建某个索引(my_index)
PUT http://localhost:1001/my_index
Content-Type:application/json
{
"settings": {
// analysis 分词器
"analysis": {
"char_filter": {
"&_to_and": {
"type": "mapping",
"mappings": [ // 字符过滤器,将& 转换成and
"&=> and "
]
}
},
"filter": {
"my_stopwords": {
"type": "stop",
"stopwords": [ // 过滤器,将the、a等词过滤掉
"the",
"a"
]
}
},
"analyzer": {
"my_analyzer": { // my_analyzer 自定义分词器的名称
"type": "custom", // 类型,custom是自定义类型
"char_filter": [ // 字符过滤器
"html_strip",
"&_to_and" // 字符过滤器的名字(&_to_and)上面定义规则
],
"tokenizer": "standard", // standard标准的分词器
"filter": [ // filter过滤器
"lowercase",
"my_stopwords" // 过滤器的名字(my_stopwords)上面定义规则
]
}
}
}
}
}
索引被创建以后,使用 analyze API 来 测试这个新的分析器
# 查看某个索引(my_index)的分词结果
GET http://localhost:1001/my_index/_analyze
{
"text":"The quick & brown fox", // 要保存的文档数据
"analyzer": "my_analyzer" // my_analyzer 自定义分词器的名称
}
# 返回响应:
{
"tokens": [
{
"token": "quick",
"start_offset": 4,
"end_offset": 9,
"type": "<ALPHANUM>",
"position": 1
},
{
"token": "and",
"start_offset": 10,
"end_offset": 11,
"type": "<ALPHANUM>",
"position": 2
},
{
"token": "brown",
"start_offset": 12,
"end_offset": 17,
"type": "<ALPHANUM>",
"position": 3
},
{
"token": "fox",
"start_offset": 18,
"end_offset": 21,
"type": "<ALPHANUM>",
"position": 4
}
]
}
# the被被过滤掉,
# & 转换成 and
文档控制
文档冲突
主要发生在多个客户端或进程尝试同时更新同一个文档时。由于ES是分布式的,并发地处理请求,如果在读取文档和提交更新之间,有其他请求修改了该文档,就可能导致更新失败或数据丢失。
解决方法:
乐观并发控制
Elasticsearch是分布式的。当文档创建、更新或删除时,新版本的文档必须复制到集群中的其他节点。Elasticsearch也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许顺序是乱的。Elasticsearch需要一种方法确保文档的旧版本不会覆盖新的版本。
我们可以利用version号来确保应用中相互冲突的变更不会导致数据丢失。我们通过指定想要修改文档的 version号来达到这个目的。如果该版本不是当前版本号,我们的请求将会失败。
老的版本es使用version,但是新版本不支持了,会报下面的错误,提示我们用if_seq _no和if _primary_term
例子:
# shopping是索引名,在该索引创建文档,1001就是文档唯一标识
PUT http://localhost:1001/shopping/_create/1001
Content-Type:application/json
# 返回响应:
{
"_index": "shopping", //索引
"_type": "_doc", //类型-文档
"_id": "1001", //唯一标识,可以类比为 MySQL中的主键,随机生成
"_version": 1, //版本
"result": "created", //结果,这里的 create 表示创建成功
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0, //乐观锁相关,序列号,全局递增的序列号
"_primary_term": 1 //乐观锁相关,主分片任期
}
对1001文档,进行更新数据
POST http://localhost:1001/shopping/_update/1001
{
"doc":{
"title":"华为手机"
}
}
# 返回响应:
{
"_index": "shopping",
"_type": "_doc",
"_id": "1001",
"_version": 2, //版本,发生变化,变成2
"result": "updated", //结果,这里的 updated 表示修改成功
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 1, //乐观锁相关,发生变化
"_primary_term": 1 //乐观锁相关,发生变化
}
防止冲突更新方法:
POST http://localhost:1001/shopping/_update/1001?if_seq_no=1&if_primary_term=1
{
"doc":{
"title":"华为手机2"
}
}
# 返回响应:
{
"_index": "shopping",
"_type": "_doc",
"_id": "1001",
"_version": 3, //版本,发生变化,变成3
"result": "updated", //结果,这里的 updated 表示修改成功
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 2, //乐观锁相关,发生变化
"_primary_term": 1 //乐观锁相关,发生变化
}
# version:目前该方式已经失效
#POST http://localhost:1001/shopping/_update/1001?version=1
{
"doc":{
"title":"华为手机2"
}
}
外部系统版本控制
一个常见的设置是使用其它数据库作为主要的数据存储,使用Elasticsearch做数据检索,这意味着主数据库的所有更改发生时都需要被复制到Elasticsearch,如果多个进程负责这一数据同步,你可能遇到类似于之前描述的并发问题。
通过增加version_type=extermal到查询字符串的方式重用这些相同的版本号,版本号必须是大于零的整数。
外部版本号的处理方式和我们之前讨论的内部版本号的处理方式有些不同,Elasticsearch不是检查当前version和请求中指定的版本号是否相同,而是检查当前version是否小于指定的版本号。如果请求成功,外部的版本号作为文档的新_version进行存储。
POST http://localhost:1001/shopping/_doc/1001?version=300&version_type=external
{
"title":"华为手机2"
}
# 返回响应:
{
"_index": "shopping",
"_type": "_doc",
"_id": "1001",
"_version": 300, //版本,发生变化,变成3
"result": "updated", //结果,这里的 updated 表示修改成功
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 3,
"_primary_term": 1
}
# 注意:当前version要大于之前的版本值
文档展示-Kibana
Kibana是一个免费且开放的用户界面,能够让你对Elasticsearch 数据进行可视化,并让你在Elastic Stack 中进行导航。你可以进行各种操作,从跟踪查询负载,到理解请求如何流经你的整个应用,都能轻松完成。
一、解压缩下载的 zip 文件。
二、修改 config/kibana.yml 文件。
# 默认端口
server.port: 5601
# ES 服务器的地址,具体的ES的服务器地址
elasticsearch.hosts: ["http://localhost:1001"]
# 索引名
kibana.index: ".kibana"
# 支持中文
i18n.locale: "zh-CN"
三、Windows 环境下执行 bin/kibana.bat 文件。(首次启动有点耗时)
四、通过浏览器访问:http://localhost:5601,就可以