提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 1.ElasticSearch 概述
- 1.1 ElasticSearch介绍
- 1.2 全文搜索引擎
- 1.3 lucene介绍
- 1.4 倒排索引
- 1.5 elasticsearch、solr对比
- 2.ElasticSearch安装
- 2.1 下载软件
- 2.2 windows环境安装
- 2.3 linux环境安装
- 2.3.1 前提条件
- 2.3.2 修改配置
- 2.3.3 启动elasticsearch
- 2.4 docker安装
- 2.5 kibana安装
- 2.6 ik分词器安装
- 3. elasticsearch核心概念
- 3.1 es对照数据库
- 3.2 索引(Index)
- 3.3 类型(Type)
- 3.4 文档(Document)
- 3.5 字段(Field)
- 3.6 映射(Mapping)
- 4. elasticsearch基本操作
- 4.1 分词器
- 4.2 索引操作
- 4.2.1 创建索引
- 4.2.2 查看所有索引
- 4.2.3 删除索引
- 4.3 文档操作
- 4.3.1 添加文档
- 4.3.2 查看文档
- 4.3.3 修改文档
- 4.3.4 修改局部属性
- 4.3.5 删除文档
- 4.3.6 批量操作
- 4.4 映射mapping
- 4.4.1 查看映射
- 4.4.2 动态映射
- 4.4.3 静态映射
- 5. DSL高级查询
- 5.1 DSL概述
- 5.2 查询所有文档-match_all
- 5.3 匹配查询(match)
- 5.4 条件删除
- 5.5 前缀匹配(prefix)
- 5.6 关键字精确查询(term)
- 5.7 多关键字精确查询(terms)
- 5.8 范围查询(range)
- 5.9 组合查询
- 5.10 聚合查询(aggs)
- 6.进阶查询
- 6.1 排序查询
- 6.2 分页查询
- 6.2.1 from、size分页查询
- 6.2.2 scroll分页
- 6.3 高亮查询(highlight)
- 6.4 近似查询(fuzzy)
- 7.Java api操作ElectricSearch
- 7.1 导入坐标
- 7.2 索引相关操作
- 7.3 文档相关操作
- 7.5 DSL查询
- 8.spring data elasticsearch
- 8.1 spring data 框架介绍
- 8.2 spring data elasticsearch
- 8.3 spring data elasticsearch版本
- 8.4 框架集成
- 8.4.1 导入依赖
- 8.4.2 yml配置
- 8.4.3 配置Document映射
- 8.4.4 dao 数据访问层
- 8.4.5 创建启动类
- 8.4.6 接口测试
- 9.cap定理
- 9.1 CAP定理的证明
- 9.2 取舍策略
- 10.elasticsearch集群
- 10.1 集群安装
- 10.1.1 环境搭建
- 10.1.2 head插件
- nodejs安装
- elasticsearch-head插件安装
- 10.2 elasticsearch中的集群核心概念
- 10.2.1 集群Cluster
- 10.2.2 节点Node
- 10.2.3 分片(Shards)
- 10.2.4 副本(Replicas)
- 10.2.5 分配(Allocation)
- 10.2.6 节点类型
- 10.3 系统架构
- 10.4 elasticsearch分片
- 10.5 分片控制
- 10.5.1 写流程
- 10.5.2 读流程
- 11.分片原理
- 11.1 倒排索引
- 11.2 文档搜索
- 11.3 动态更新索引
- 11.4 近实时搜索
- 11.5 段合并
- 12.优化建议
- 12.1 硬件选择
- 12.2 分片策略
- 12.2.1 合理设置分片数
- 12.2.2 推迟分片分配
- 12.3 路由选择
- 12.4 写入速度优化
- 12.4.1 批量数据提交
- 12.4.2 优化存储设备
- 12.4.3 减少Refresh的次数
- 12.4.4 加大Flush设置
- 12.4.5 减少副本的数量
- 12.4.6 内存设置
- 12.5 重要配置
1.ElasticSearch 概述
1.1 ElasticSearch介绍
Elasticsearch 是位于 Elastic Stack 核心的分布式搜索和分析引擎。Logstash 和 Beats 有助于收集、聚合和丰富您的数据并将其存储在 Elasticsearch 中。Kibana 使您能够以交互方式探索、可视化和分享对数据的见解,并管理和监控堆栈。Elasticsearch 是索引、搜索和分析魔法发生的地方。
Elasticsearch 为所有类型的数据提供近乎实时的搜索和分析。无论您拥有结构化或非结构化文本、数字数据还是地理空间数据,Elasticsearch 都能以支持快速搜索的方式高效地存储和索引它。您可以超越简单的数据检索和聚合信息来发现数据中的趋势和模式。随着您的数据和查询量的增长,Elasticsearch 的分布式特性使您的部署能够随之无缝增长。
查询和分析
- 可以自定义搜索方式: 通过 Elasticsearch,您能够执行及合并多种类型的搜索(结构化数据、非结构化数据、地理位置、指标),搜索方式随心而变。先从一个简单的问题出发,试试看能够从中发现些什么。
- 分析大规模数据: 找到与查询最匹配的 10 个文档并不困难。但如果面对的是十亿行日志,又该如何解读呢?Elasticsearch 聚合让您能够从大处着眼,探索数据的趋势和规律。
查询速度
- 近实时搜索(数据1s之内可见)
- 通过有限状态转换器实现了用于全文检索的倒排索引,实现了用于存储数值数据和地理位置数据的 BKD 树,以及用于分析的列存储。
- 每个数据都被编入了索引,因此您再也不用因为某些数据没有索引而烦心。您可以用快到令人惊叹的速度使用和访问您的所有数据。
可扩展性
- 无论 Elasticsearch 是在一个节点上运行,还是在一个包含 300 个节点的集群上运行,您都能够以相同的方式与 Elasticsearch 进行通信。
- 它能够水平扩展,每秒钟可处理海量事件,同时能够自动管理索引和查询在集群中的分布方式,以实现极其流畅的操作。
内容相关度
- 基于各项元素(从词频或新近度到热门度等)对搜索结果进行排序。将这些内容与功能进行混搭,以优化向用户显示结果的方式。
而且,由于我们的大部分用户都是真实的人,Elasticsearch 具备齐全功能,可以处理包括各种复杂情况(例如拼写错误)在内的人为错误。
弹性设计
- 硬件故障。网络分割。Elasticsearch 为您检测这些故障并确保您的集群(和数据)的安全性和可用性。通过跨集群复制功能,辅助集群可以作为热备份随时投入使用。Elasticsearch 运行在一个分布式的环境中,从设计之初就考虑到了这一点,目的只有一个,让您永远高枕无忧
创始人:Shay Banon(谢巴农)
搜索引擎典型应用场景:
1.2 全文搜索引擎
Google,百度类的网站搜索,它们都是根据网页中的关键字生成索引,我们在搜索的时候输入关键字,它们会将该关键字即索引匹配到的所有网页返回;还有常见的项目中应用日志的搜索等等。对于这些非结构化的数据文本,关系型数据库搜索不是能很好的支持。
一般传统数据库,全文检索都实现的很鸡肋,因为一般也没人用数据库存文本字段。进行全文检索需要扫描整个表,如果数据量大的话即使对SQL的语法优化,也收效甚微。建立了索引,但是维护起来也很麻烦,对于 insert 和 update 操作都会重新构建索引。
这里说到的全文搜索引擎指的是目前广泛应用的主流搜索引擎。它的工作原理是计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。
1.3 lucene介绍
Lucene是Apache软件基金会Jakarta项目组的一个子项目,提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具。就其本身而言,Lucene是当前以及最近几年最受欢迎的免费Java信息检索程序库。但Lucene只是一个提供全文搜索功能类库的核心工具包,而真正使用它还需要一个完善的服务框架搭建起来进行应用。
目前市面上流行的搜索引擎软件,主流的就两款:Elasticsearch和Solr,这两款都是基于Lucene搭建的,可以独立部署启动的搜索引擎服务软件。由于内核相同,所以两者除了服务器安装、部署、管理、集群以外,对于数据的操作 修改、添加、保存、查询等等都十分类似。
1.4 倒排索引
倒排索引源于实际应用中需要根据属性的值来查找记录。这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。带有倒排索引的文件我们称为倒排索引文件,简称倒排文件(inverted file)。
倒排索引步骤:
- 数据根据词条进行分词,同时记录文档索引位置
- 将词条相同的数据化进行合并
- 对词条进行排序
搜索过程:
先将搜索词语进行分词,分词后在倒排索引列表查询文档位置(docId)。根据docId查询文档数据。
1.5 elasticsearch、solr对比
1.Elasticsearch的优点
优点
-
Elasticsearch是分布式的。不需要其他组件,分发是实时的,被叫做”Push replication”。
-
Elasticsearch 完全支持 Apache Lucene 的接近实时的搜索。
-
处理多租户(multitenancy)不需要特殊配置,而Solr则需要更多的高级设置。
-
Elasticsearch 采用 Gateway 的概念,使得完备份更加简单。
-
各节点组成对等的网络结构,某些节点出现故障时会自动分配其他节点代替其进行工作。
2.es与solr的区别
- Solr建立索引时候, Solr会产生io阻塞,查询性能较差, 搜索效率下降,实时搜索效率不高,es实时搜索效率高
- 随着数据量的增加,Solr的搜索效率会变得更低,而Elasticsearch却没有明显的变化
- Solr利用Zookeeper进行分布式管理,而Elasticsearch自身带有分布式协调管理功能。
- Solr支持更多格式的数据,比如JSON、XML、CSV,而Elasticsearch仅支持json文件格式。
- Solr官方提供的功能更多,而Elasticsearch本身更注重于核心功能,高级功能多有第三方插件提供
- Solr在传统的搜索应用中表现好于Elasticsearch,但在处理实时搜索应用时效率明显低于Elasticsearch。
- Solr是传统搜索应用的有力解决方案,但Elasticsearch更适用于新兴的实时搜索应用。
2.ElasticSearch安装
2.1 下载软件
https://www.elastic.co/cn/downloads/past-releases#elasticsearch
选择7.8版本即可,其它elastic stack也可以在这里下载。
2.2 windows环境安装
解压elasticsearch-7.8.0-windows-x86_64.zip,目录结构:
目录 | 说明 |
---|---|
bin | 可执行脚本目录 |
config | 配置目录 |
jdk | 内置jdk目录 |
lib | 类库 |
logs | 日志目录 |
modules | 模块目录 |
plugins | 插件目录 |
解压完成后进入bin目录,双击运行elasticsearch.bat
测试访问: http://localhost:9200/
2.3 linux环境安装
将下载好的elasticsearch-7.8.0-linux-x86_64.tar.gz上传至服务器并解压。先将jdk环境搭建好,jdk环境配置略。
2.3.1 前提条件
elasticsearch不能直接通过root用户启动,所以要先创建一个普通用户。
#创建用户
useradd es;
#设置密码
passwd es;
给普通用户授权:
chown -R es /opt/es
给用户设置sudo权限:
#使用root用户执行
visudo
#在root ALL=(ALL) ALL下面新增
es ALL=(ALL) ALL
普通用户在启动elasticsearch时会出现下面错误:
解决办法:
普通用户打开文件最大数限制修改
#编辑limits.conf文件
vi /etc/security/limits.conf
#添加以下内容
* soft nofile 65536
* hard nofile 131072
* soft nproc 2048
* hard nproc 4096
普通用户启动线程数限制
# Centos6
vi /etc/security/limits.d/90‐nproc.conf
# Centos7
vi /etc/security/limits.d/20‐nproc.conf
#添加以下内容
* soft nproc 4096
普通用户增大虚拟内存
vi /etc/sysctl.conf
#添加以下内容:
vm.max_map_count=262144
#保存后执行sysctl -p让配置生效
sysctl -p
全部步骤完成后需要重新打开终端,重新登入。
2.3.2 修改配置
切换普通用户(es)登入,进入到${解压目录}/config目录下,修改elasticsearch.yml配置文件:
#集群名称
cluster.name: my-application
#节点名称
node.name: node-1
#数据文件目录
path.data: ./data
#日志文件目录
path.logs: ./logs
#运行访问的网络,0.0.0.0表示任意ip都匹配,这样可以远程访问
network.host: 0.0.0.0
#http rest服务端口
http.port: 9200
#集群初始master选举节点
cluster.initial_master_nodes: ["node-1"]
2.3.3 启动elasticsearch
#进入到bin目录(注意,不能使用root账号启动)
./elasticsearch -d
浏览器测试访问:
2.4 docker安装
docker run -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -d --name elasticsearch docker.elastic.co/elasticsearch/elasticsearch:7.8.0
2.5 kibana安装
elasticsearch服务是一个restful风格的http服务。我们可以采用postman作为客户端来进行操作,elastic stack官方也给我们提供了kibana来进行客户端操作,这个相比postman要友好一点,因为里面有些自动补全的代码提示。
下载地址:https://www.elastic.co/cn/downloads/past-releases/kibana-7-8-0
上传tar并解压文件:
进入到config目录,修改kibana.yml文件:
#服务端口
server.port: 5601
#运行访问的IP设置,0.0.0.0可以远程访问
server.host: "0.0.0.0"
进入bin目录,后台启动kibana:
nohup ./kibana &
开始访问:http://192.168.6.100:5601/
2.6 ik分词器安装
下载地址: https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.8.0/elasticsearch-analysis-ik-7.8.0.zip
进入到**${es安装目录}/plugins**目录,新建ik目录。
#在ik目录下解压elasticsearch-analysis-ik-7.8.0.zip文件
unzip elasticsearch-analysis-ik-7.8.0.zip
#删除zip文件
rm -f elasticsearch-analysis-ik-7.8.0.zip
重启es:
测试分词器:
POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}
3. elasticsearch核心概念
3.1 es对照数据库
3.2 索引(Index)
一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母),并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。在一个集群中,可以定义任意多的索引。
能搜索的数据必须索引,这样的好处是可以提高查询速度,比如:新华字典前面的目录就是索引的意思,目录可以提高查询速度。
Elasticsearch索引的精髓:一切设计都是为了提高搜索的性能。
3.3 类型(Type)
在一个索引中,。
一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。通常,会为具有一组共同字段的文档定义一个类型。不同的版本,类型发生了不同的变化
版本 | Type |
---|---|
5.x | 支持多种type |
6.x | 只能有一种type |
7.x | 默认不再支持自定义索引类型(默认类型为:_doc) |
3.4 文档(Document)
一个文档是一个可被索引的基础信息单元,也就是一条数据
比如:你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。文档以JSON(Javascript Object Notation)格式来表示,而JSON是一个到处存在的互联网数据交互格式。
在一个index/type里面,你可以存储任意多的文档。
3.5 字段(Field)
相当于是数据表的字段,对文档数据根据不同属性进行的分类标识。
3.6 映射(Mapping)
mapping是处理数据的方式和规则方面做一些限制,如:某个字段的数据类型、默认值、分析器、是否被索引等等。这些都是映射里面可以设置的,其它就是处理ES里面数据的一些使用规则设置也叫做映射,按着最优规则处理数据对性能提高很大,因此才需要建立映射,并且需要思考如何建立映射才能对性能更好。
4. elasticsearch基本操作
4.1 分词器
官方提供的分词器有这么几种: Standard、Letter、Lowercase、Whitespace、UAX URL Email、Classic、Thai等,中文分词器可以使用第三方的比如IK分词器。前面我们已经安装过了。
IK分词器核心配置:
- main.dic:单词词典
- stopword.dic: 停用词,这里只记录了英文的一部分单词,比如: a、an、and、are、as、at、be、but、by等。
语法格式:
POST _analyze
{
"analyzer": "类型",
"text": "内容"
}
类型:ik_smart、Standard、Letter、Lowercase、Whitespace、UAX URL Email、Classic、Thai
代码示例:
POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}
POST _analyze
{
"analyzer": "standard",
"text": "我是中国人"
}
4.2 索引操作
4.2.1 创建索引
语法
PUT /索引名称
代码示例
PUT /my_index
运行结果
4.2.2 查看所有索引
语法格式-查看所有
GET /_cat/indices?v
运行结果
语法格式-查看单个
GET /索引名称
示例
GET /my_index
运行结果
4.2.3 删除索引
语法格式
DELETE /索引名称
代码示例
DELETE /my_index
运行结果
4.3 文档操作
4.3.1 添加文档
语法格式
PUT /索引名称/类型/id
{
JSON内容
}
代码示例
PUT /my_index/_doc/1
{
"id":"1",
"name":"小米",
"price":"1999"
}
运行结果
4.3.2 查看文档
语法格式
GET /索引名称/类型/id
代码示例
GET /my_index/_doc/1
运行结果
4.3.3 修改文档
语法格式
PUT /索引名称/类型/id
{
JSON内容
}
代码示例
PUT /my_index/_doc/1
{
"id":"1",
"name":"小米",
"price":"2999"
}
4.3.4 修改局部属性
语法格式
POST /{索引名称}/_update/{docId}
{
"doc": {
"属性": "值"
}
}
代码示例
POST /my_index/_update/1
{
"doc": {
"name": "华为"
}
}
运行结果
4.3.5 删除文档
语法格式
DELETE /索引名称/类型/id
代码示例
DELETE /my_index/_doc/1
运行结果
4.3.6 批量操作
1.批量创建
语法格式
POST _bulk
{"create":{"_index":"索引名称","_id":id值1}}
{JSON内容1}
{"create":{"_index":"索引名称","_id":id值2}}
{JSON内容2}
...
{"create":{"_index":"索引名称","_id":id值n}}
{JSON内容n}
代码示例
POST _bulk
{"create":{"_index":"my_index","_id":1}}
{"id":"1","name":"小米","price":"1999"}
{"create":{"_index":"my_index","_id":2}}
{"id":"2","name":"华为","price":"5999"}
运行结果
2.批量删除
语法格式
POST _bulk
{"delete":{"_index":"索引名称","_id":id值1}}
...
{"delete":{"_index":"索引名称","_id":id值n}}
代码演示
POST _bulk
{"delete":{"_index":"my_index","_id":1}}
{"delete":{"_index":"my_index","_id":2}}
4.4 映射mapping
有了索引库,等于有了数据库中的database。
接下来就需要建索引库(index)中的映射了,类似于数据库(database)中的表结构(table)。创建数据库表需要设置字段名称,类型,长度,约束等;索引库也一样,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做映射(mapping)。
4.4.1 查看映射
语法格式
GET /索引名称/_mapping
代码示例
GET /my_index/_mapping
运行结果
4.4.2 动态映射
在关系数据库中,需要事先创建数据库,然后在该数据库下创建数据表,并创建 表字段、类型、长度、主键等,最后才能基于表插入数据。而Elasticsearch中不需要定义Mapping映射(即关系型数据库的表、字段等),在文档写入 Elasticsearch时,会根据文档字段自动识别类型,这种机制称之为动态映射。
映射规则对应:
数据 | 对应的类型 |
---|---|
null | 字段不添加 |
true|flase | boolean |
字符串 | text |
数值 | long |
小数 | float |
日期 | date |
4.4.3 静态映射
静态映射是在Elasticsearch中也可以事先定义好映射,包含文档的各字段类型、分词器等,这种方式称之为静态映射。
语法格式
PUT /my_index
{
"mappings": {
"properties": {
"title": {
"type": "标题类型",
"index": true,
"store": true,
"analyzer": "存储分词器类型",
"search_analyzer": "查询分词器类型"
},
"字段名称": {
"type": "字段类型",
"index": true,
"store": true
},
...
"字段名称": {
"type": "字段类型",
"index": true,
"store": true
}
}
}
}
代码示例
#删除原创建的索引
DELETE /my_index
#创建索引,并同时指定映射关系和分词器等。
PUT /my_index
{
"mappings": {
"properties": {
"title": {
"type": "text",
"index": true,
"store": true,
"analyzer": "ik_max_word",
"search_analyzer": "ik_max_word"
},
"category": {
"type": "keyword",
"index": true,
"store": true
},
"images": {
"type": "keyword",
"index": true,
"store": true
},
"price": {
"type": "integer",
"index": true,
"store": true
}
}
}
}
运行结果
type分类如下:
- 字符串:string,string类型包含 text 和 keyword。
- text:该类型被用来索引长文本,在创建索引前会将这些文本进行分词,转化为词的组合,建立索引;允许es来检索这些词,text类型不能用来排序和聚合。
- keyword:该类型不能分词,可以被用来检索过滤、排序和聚合,keyword类型不可用text进行分词模糊检索。
- 数值型:long、integer、short、byte、double、float
- 日期型:date
- 布尔型:boolean
5. DSL高级查询
5.1 DSL概述
Query DSL概述: Domain Specific Language(领域专用语言),Elasticsearch提供了基于JSON的DSL来定义查询。
DSL概览:
数据准备
POST _bulk
{"create":{"_index":"my_index","_id":1}}
{"id":1,"title":"华为笔记本电脑","category":"华为","images":"http://www.gulixueyuan.com/xm.jpg","price":5388}
{"create":{"_index":"my_index","_id":2}}
{"id":2,"title":"华为手机","category":"华为","images":"http://www.gulixueyuan.com/xm.jpg","price":5500}
{"create":{"_index":"my_index","_id":3}}
{"id":3,"title":"VIVO手机","category":"vivo","images":"http://www.gulixueyuan.com/xm.jpg","price":3600}
5.2 查询所有文档-match_all
语法格式
POST /索引名称/_search
{
"query": {
"match_all": {}
}
}
代码演示
POST /my_index/_search
{
"query":
{
"match_all": {}
}
}
运行结果
5.3 匹配查询(match)
语法格式
POST /索引名称/_search
{
"query": {
"match": {
"字段名称": "值"
}
}
}
代码示例
POST /my_index/_search
{
"query": {
"match": {
"title": "华为智能手机"
}
}
}
运行结果
5.4 条件删除
语法格式
POST /索引名称/_delete_by_query
{
"query": {
"match": {
"字段名": "值"
}
}
}
代码示例
POST /my_index/_delete_by_query
{
"query": {
"match": {
"title": "vivo"
}
}
}
运行结果
{
"took" : 51,
"timed_out" : false,
"total" : 1,
"deleted" : 1,
"batches" : 1,
"version_conflicts" : 0,
"noops" : 0,
"retries" : {
"bulk" : 0,
"search" : 0
},
"throttled_millis" : 0,
"requests_per_second" : -1.0,
"throttled_until_millis" : 0,
"failures" : [ ]
}
5.5 前缀匹配(prefix)
语法格式
POST /索引名称/_search
{
"query": {
"prefix": {
"字段名称": {
"value": "值"
}
}
}
}
代码示例
POST /my_index/_search
{
"query": {
"prefix": {
"title": {
"value": "vivo智能"
}
}
}
}
运行结果
5.6 关键字精确查询(term)
语法格式
POST /索引名称/_search
{
"query": {
"term": {
"字段名称": {
"value": "值"
}
}
}
}
代码示例
POST /my_index/_search
{
"query": {
"prefix": {
"title": {
"value": "vivo智能"
}
}
}
}
运行结果
5.7 多关键字精确查询(terms)
语法格式
POST /索引名称/_search
{
"query": {
"terms": {
"字段名称": [
"值1",
"值2"
]
}
}
}
代码示例
POST /my_index/_search
{
"query": {
"terms": {
"title": [
"华为手机",
"华为"
]
}
}
}
运行结果
5.8 范围查询(range)
范围查询使用range。
- gte: 大于等于
- lte: 小于等于
- gt: 大于
- lt: 小于
语法格式
POST /索引名称/_search
{
"query": {
"range": {
"字段名称": {
"符号1": 值1,
"符号2": 值2
}
}
}
}
代码示例
POST /my_index/_search
{
"query": {
"range": {
"price": {
"gte": 3000,
"lte": 5000
}
}
}
}
代码示例
5.9 组合查询
bool 各条件之间有and,or或not的关系
- must: 各个条件都必须满足,所有条件是and的关系
- should: 各个条件有一个满足即可,即各条件是or的关系
- must_not: 不满足所有条件,即各条件是not的关系
- filter: 与must效果等同,但是它不计算得分,效率更高点。
msut
POST /my_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "华为"
}
},
{
"range": {
"price": {
"gte": 2000,
"lte": 10000
}
}
}
]
}
}
}
运行结果
should
POST /my_index/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"title": "华为"
}
},
{
"range": {
"price": {
"gte": 2000,
"lte": 10000
}
}
}
]
}
}
}
must_not
POST /my_index/_search
{
"query": {
"bool": {
"must_not": [
{
"match": {
"title": "华为"
}
},
{
"range": {
"price": {
"gte": 2000,
"lte": 10000
}
}
}
]
}
}
}
filter
POST /my_index/_search
{
"query": {
"bool": {
"filter": [
{
"match": {
"title": "华为"
}
},
{
"range": {
"price": {
"gte": 2000,
"lte": 10000
}
}
}
]
}
}
}
5.10 聚合查询(aggs)
聚合允许使用者对es文档进行统计分析,类似与关系型数据库中的group by,当然还有很多其他的聚合,例如取最大值、平均值等等。
max、min、avg、sum、stats、terms…
语法格式
POST /my_index/_search
{
"query": {
"match_all": {}
},
"size": 0,
"aggs": {
"设置名称": {
"聚合查询名称": {
"field": "字段名称"
}
}
}
}
代码示例-max
POST /my_index/_search
{
"query": {
"match_all": {}
},
"size": 0,
"aggs": {
"max_price": {
"max": {
"field": "price"
}
}
}
}
运行结果
代码示例二-min
POST /my_index/_search
{
"query": {
"match_all": {}
},
"size": 0,
"aggs": {
"min_price": {
"min": {
"field": "price"
}
}
}
}
6.进阶查询
6.1 排序查询
语法格式
POST /索引名称/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"字段名称": "值"
}
}
]
}
},
"sort": [
{
"字段名称": {
"order": "排序规则"
}
},
{
"_score": {
"order": "排序规则"
}
}
]
}
代码示例
POST /my_index/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "华为"
}
}
]
}
},
"sort": [
{
"price": {
"order": "asc"
}
},
{
"_score": {
"order": "desc"
}
}
]
}
运行结果
6.2 分页查询
6.2.1 from、size分页查询
分页的两个关键属性:from、size。
- from: 当前页的起始索引,默认从0开始。 from = (pageNum - 1) * size
- size: 每页显示多少条
语法格式
POST /索引名称/_search
{
"query": {
"match_all": {}
},
"from": 起始索引,
"size": 每页展示条数
}
代码示例
POST /my_index/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 2
}
运行结果
6.2.2 scroll分页
第一次查询
POST /my_index/_search?scroll=1m
{
"query": {
"match_all": {}
},
"size": 1
}
第二次查询
GET /_search/scroll?scroll=1m
{
"scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFHNKV2VWWHdCeUZ2WWVjeDZYbXNGAAAAAAAAGwUWTERWbzhrWFZTdFd3WnVoOV9EaGV0dw=="
}
6.3 高亮查询(highlight)
在进行关键字搜索时,搜索出的内容中的关键字会显示不同的颜色,称之为高亮。
语法格式
POST /索引名称/_search
{
"query": {
"match": {
"字段名称": "值"
}
},
"highlight": {
"pre_tags": "<html开始标签 style='属性名称:值'>",
"post_tags": "</html结束标签>",
"fields": {
"字段名称": {}
}
}
}
代码示例
POST /my_index/_search
{
"query": {
"match": {
"title": "华为"
}
},
"highlight": {
"pre_tags": "<b style='color:red'>",
"post_tags": "</b>",
"fields": {
"title": {}
}
}
}
运行结果
6.4 近似查询(fuzzy)
返回包含与搜索字词相似的字词的文档。编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括:
- 更改字符(box → fox)
- 删除字符(black → lack)
- 插入字符(sic → sick)
- 转置两个相邻字符(act → cat)
为了找到相似的术语,fuzzy查询会在指定的编辑距离内创建一组搜索词的所有可能的变体或扩展。然后查询返回每个扩展的完全匹配。通过fuzziness修改编辑距离。一般使用默认值AUTO,根据术语的长度生成编辑距离。
语法格式
GET /索引名称/_search
{
"query": {
"fuzzy": {
"字段名称": {
"value": "值",
"fuzziness": "偏移距离"
}
}
}
}
代码示例
POST /my_index/_search
{
"query": {
"fuzzy": {
"title": {
"value": "华未",
"fuzziness": 1
}
}
}
}
运行结果
7.Java api操作ElectricSearch
官方参考:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.x/java-rest-high-getting-started.html
7.1 导入坐标
<dependencies>
<!--ES官方提供的Java客户端 high-level-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.8.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.19</version>
</dependency>
</dependencies>
Student.java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private Integer id;
private String name;
private String age;
}
7.2 索引相关操作
/**
* Created with IntelliJ IDEA.
*
* @Author: TLC
* @Date: 2022/11/22/18:13
* @Description: 索引相关操作
*/
public class IndexElectricalSearchTest {
RestHighLevelClient client = new RestHighLevelClient(RestClient
.builder(new HttpHost("localhost", 9200, "http")));
private String INDEX_NAME = "my_index02";
/**
* @Description:创建索引
* @Author: TCL
* @Date: 2022/11/22
* @Time: 18:26
*/
@Test
public void createIndex() throws IOException {
// 创建索引请求对象
CreateIndexRequest createIndexRequest = new CreateIndexRequest(INDEX_NAME);
/**
* 设置mapping映射类型
* String source 数据源
* XContentType xContentType 内容类型 XContentType.JSON JSON类型格式
*/
String source="{\n" +
" \"properties\": {\n" +
" \"title\": {\n" +
" \"type\": \"text\",\n" +
" \"index\": true,\n" +
" \"store\": true,\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_max_word\"\n" +
" },\n" +
" \"category\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true,\n" +
" \"store\": true\n" +
" },\n" +
" \"images\": {\n" +
" \"type\": \"keyword\",\n" +
" \"index\": true,\n" +
" \"store\": true\n" +
" },\n" +
" \"price\": {\n" +
" \"type\": \"integer\",\n" +
" \"index\": true,\n" +
" \"store\": true\n" +
" }\n" +
" }\n" +
" }";
createIndexRequest.mapping(source, XContentType.JSON);
// 创建响应对象
CreateIndexResponse createIndexResponse = client.indices().create(createIndexRequest, RequestOptions.DEFAULT);
System.out.println("createIndexResponse = " + createIndexResponse.isAcknowledged());
}
/**
* @Description: 查看索引
* @Author: TCL
* @Date: 2022/11/22
* @Time: 18:51
*/
@Test
public void GetIndex() throws IOException {
GetIndexRequest getIndexRequest = new GetIndexRequest(INDEX_NAME);
GetIndexResponse getIndexResponse = client.indices().get(getIndexRequest, RequestOptions.DEFAULT);
Map<String, MappingMetadata> mappings = getIndexResponse.getMappings();
System.out.println("mappings = " + mappings);
Map<String, Settings> settings = getIndexResponse.getSettings();
System.out.println("settings = " + settings);
}
/**
* @Description: 删除索引
* @Author: TCL
* @Date: 2022/11/22
* @Time: 19:02
*/
@Test
public void deleteIndex() throws IOException {
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(INDEX_NAME);
AcknowledgedResponse delete = client.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
boolean acknowledged = delete.isAcknowledged();
System.out.println("acknowledged = " + acknowledged);
}
}
7.3 文档相关操作
/**
* Created with IntelliJ IDEA.
*
* @Author: TLC
* @Date: 2022/11/22/19:05
* @Description: 文档相关操作
*/
public class DocumentElectricSearchTest {
RestHighLevelClient client = new RestHighLevelClient(RestClient
.builder(new HttpHost("localhost", 9200, "http")));
private String INDEX_NAME = "my_index02";
/**
* @Description: 创建文档
* @Author: TCL
* @Date: 2022/11/22
* @Time: 19:06
*/
@Test
public void createDocument() throws IOException {
// 创建请求对象
IndexRequest request = new IndexRequest(INDEX_NAME);
// 设置id
request.id("1");
// 创建学生对象
Student student = new Student(1001, "张三", "18");
request.source(JSON.toJSONString(student),XContentType.JSON);
// 使用默认设置
IndexResponse index = client.index(request, RequestOptions.DEFAULT);
System.out.println("index = " + index);
}
/**
* 更新指定字段
*/
@Test
public void update() throws IOException {
//更新的请求
UpdateRequest request = new UpdateRequest(INDEX_NAME,"1");
//doc参数, 声明为JSON类型
Student student = new Student(null, null, "132");
request.doc(JSON.toJSONString(student),XContentType.JSON);
//使用客户端执行请求,默认参数
UpdateResponse response = client.update(request, RequestOptions.DEFAULT);
//解析返回值
String result = response.getResult().getLowercase();
System.out.println("result = " + result);
}
/**
* 根据文档ID查询
*/
@Test
public void getById() throws IOException {
//查询文档的请求
GetRequest request = new GetRequest(INDEX_NAME, "1");
//使用客户端执行请求,默认参数
GetResponse response = client.get(request, RequestOptions.DEFAULT);
//解析返回值
System.out.println("_source = " + response.getSourceAsMap());
}
/**
* 根据文档ID删除
*/
@Test
public void deleteById() throws IOException {
//删除文档的请求
DeleteRequest request = new DeleteRequest(INDEX_NAME, "1");
//使用客户端执行请求,默认参数
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
//解析返回值
String result = response.getResult().getLowercase();
System.out.println("result = " + result);
}
/**
* 批量创建文档
*/
@Test
public void bulk() throws IOException {
//创建批量请求
BulkRequest bulkRequest = new BulkRequest();
for (int i = 1; i <= 3; i++) {
//创建文档的请求
IndexRequest request = new IndexRequest(INDEX_NAME);
//设置文档ID
request.id(10+i+"");
//设置文档内容, 声明为json格式
Student student = new Student(2000+i, "衬衫"+i, 80+i+"");
request.source(JSON.toJSONString(student), XContentType.JSON);
//在批量请求中 添加创建请求
bulkRequest.add(request);
}
//使用客户端执行请求,默认参数
BulkResponse response = client.bulk(bulkRequest, RequestOptions.DEFAULT);
//解析响应的数据
for (BulkItemResponse item : response.getItems()) {
String result = item.getResponse().getResult().getLowercase();
System.out.println("result = " + result);
}
}
}
7.5 DSL查询
/**
* Created with IntelliJ IDEA.
*
* @Author: TLC
* @Date: 2022/11/22/19:35
* @Description:
*/
public class DslElectricSearchTest {
// 连接ES客户端
RestHighLevelClient client = new RestHighLevelClient(RestClient
.builder(new HttpHost("localhost", 9200, "http")));
// 名称
private String INDEX_NAME = "my_index";
/**
* @Description: match匹配
* 例子:查询 title 为 小米电脑 的文档
* @Author: TCL
* @Date: 2022/11/22
* @Time: 19:39
*/
@Test
public void match() throws IOException {
// 创建搜索请求
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
// 创建搜索参数
SearchSourceBuilder builder = new SearchSourceBuilder();
// 添加query参数, query中再添加match
builder.query(QueryBuilders.matchQuery("title","小米电脑"));
//组装,在请求中添加参数
searchRequest.source(builder);
// 执行客户端默认请求行为
SearchResponse search = client.search(searchRequest, RequestOptions.DEFAULT);
// 解析响应数据
SearchHit[] hits = search.getHits().getHits();
for (SearchHit hit : hits) {
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
System.out.println("sourceAsMap = " + sourceAsMap);
}
}
/**
* @Description: 高亮查询
* @Author: TCL
* @Date: 2022/11/22
* @Time: 19:47
*/
@Test
public void hightLightSearch() throws IOException {
// 创建请求对象
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
// 创建搜索参数
SearchSourceBuilder builder = new SearchSourceBuilder();
// 添加query参数, query中再添加match
builder.query(QueryBuilders.matchQuery("title","小米电脑"));
// 创建高亮参数
HighlightBuilder highlightBuilder = new HighlightBuilder();
// 高亮的前台代码
highlightBuilder.field("title")
.preTags("<front color='red'>")
.postTags("</front>");
// 添加高亮请求
builder.highlighter(highlightBuilder);
// 组装
searchRequest.source(builder);
// 执行客户端默认行为
SearchResponse search = client.search(searchRequest, RequestOptions.DEFAULT);
// 解析
SearchHit[] hits = search.getHits().getHits();
for (SearchHit hit : hits) {
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
System.out.println("sourceAsMap = " + sourceAsMap);
// 高亮结果
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField title = highlightFields.get("title");
System.out.println("title = " + title);
}
}
/**
* @Description: terms桶聚(分组统计)
* @Author: TCL
* @Date: 2022/11/22
* @Time: 19:59
*/
@Test
public void terms() throws IOException {
//创建搜索的请求_search
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
//创建搜索的参数
SearchSourceBuilder builder = new SearchSourceBuilder();
//添加query参数, query中再添加match
builder.query(QueryBuilders.matchAllQuery());
//aggs参数
TermsAggregationBuilder aggs = AggregationBuilders
.terms("category_count")
.field("category");
//添加aggs参数
builder.aggregation(aggs);
//组装,在请求中添加参数
searchRequest.source(builder);
//使用客户端执行请求,默认参数
SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
//解析响应的数据,获取聚合结果
Aggregations aggregations = response.getAggregations();
//因为使用的是terms聚合,所以可以使用Terms子接口接收数据
Terms terms = aggregations.get("category_count");
for (Terms.Bucket bucket : terms.getBuckets()) {
System.out.println("key = " + bucket.getKey());
System.out.println("doc_count = " + bucket.getDocCount());
}
}
}
8.spring data elasticsearch
8.1 spring data 框架介绍
Spring Data是一个用于简化数据库、非关系型数据库、索引库访问,并支持云服务的开源框架。其主要目标是使得对数据的访问变得方便快捷,并支持map-reduce框架和云计算数据服务。 Spring Data可以极大的简化JPA(Elasticsearch…)的写法,可以在几乎不用写实现的情况下,实现对数据的访问和操作。除了CRUD外,还包括如分页、排序等一些常用的功能。
Spring Data的官网:https://spring.io/projects/spring-data
8.2 spring data elasticsearch
Spring Data Elasticsearch 基于 spring data API 简化 Elasticsearch操作,将原始操作Elasticsearch的客户端API 进行封装 。Spring Data为Elasticsearch项目提供集成搜索引擎。Spring Data Elasticsearch POJO的关键功能区域为中心的模型与Elastichsearch交互文档和轻松地编写一个存储索引库数据访问层。
8.3 spring data elasticsearch版本
springboot2.3.x版本可以兼容elasticsearch7.x版本。
8.4 框架集成
8.4.1 导入依赖
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.3.6.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
8.4.2 yml配置
spring:
elasticsearch:
rest:
uris: http://localhost:9200
8.4.3 配置Document映射
/**
* Created with IntelliJ IDEA.
*
* @Author: TLC
* @Date: 2022/11/22/20:13
* @Description: 绑定文档
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
@Document(indexName = "product-index") //绑定的索引
public class Product {
//绑定的字段,字段名与属性名一致时,name可忽略, type映射的数据类型
@Field(name = "id", type = FieldType.Keyword)
private String id; //让文档ID(类型为text字符串) 与 字段ID 一致
@Field(name = "name", type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
private String name;
@Field(name = "price", type = FieldType.Double)
private double price;
}
8.4.4 dao 数据访问层
@Repository
public interface GoodsDao extends ElasticsearchRepository<Goods, String> {
}
8.4.5 创建启动类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
8.4.6 接口测试
/**
* Created with IntelliJ IDEA.
*
* @Author: TLC
* @Date: 2022/11/22/20:49
* @Description:
*/
@SpringBootTest
public class EsTest {
@Autowired
private ProductRepository productRepository;
/**
* 创建、更新文档
* 文档ID不存在,则创建文档
* 文档ID存在,则删除原文档,创建新文档
*/
@Test
public void create(){
Product product = new Product("2", "衬衫2", 200.9);
Product save = productRepository.save(product);
System.out.println("save = " + save);
}
/**
* 查询所有
*/
@Test
public void get(){
Iterable<Product> all = productRepository.findAll();
for (Product product : all) {
System.out.println(product);
}
}
/**
* 根据文档ID查询
*/
@Test
public void getById(){
Optional<Product> optional = productRepository.findById("2");
Product product = optional.get(); //获取查询到的文档数据
System.out.println("product = " + product);
}
/**
* 删除文档
* 不论文档ID存在与否,方法都会正常执行结束
*/
@Test
public void deleteById(){
productRepository.deleteById("123");
System.out.println("删除执行完成......");
}
}
9.cap定理
- CAP 定理,又被叫作布鲁尔定理。对于设计分布式系统(不仅仅是分布式事务)的架构师来说,CAP 就是你的入门理论。
- 分布式系统(distributed system)正变得越来越重要,大型网站几乎都是分布式的。
- 分布式系统的最大难点,就是各个节点的状态
C (一致性):指数据在多个副本之间能够保持一致的特性(严格的一致性)在分布式系统中的所有数据备份,在同一时刻是否同样的值。(所有节点在同一时间具有相同的数据)
一致性(Consistency)是指多副本(Replications)问题中的数据一致性。可以分为强一致性、与弱一致性。
① 强一致性
*简言之,在任意时刻,所有节点中的数据是一样的。*
例如,对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。
② 弱一致性
数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。
最终一致性就属于弱一致性。
A (可用性):指系统提供的服务必须一直处于可用的状态,每次只要收到用户的请求,服务器就必须给出回应。在合理的时间内返回合理的响应(不是错误和超时的响应)
只有非故障节点才能满足业务正常;只有在合理的时间内,用户才能接受;只有返回合理的响应,用户才能接受。
P (网络分区容错性):网络节点之间无法通信的情况下,节点被隔离,产生了网络分区, 整个系统仍然是可以工作的 . 大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
- 什么是分区?
在分布式系统中,不同的节点分布在不同的子网络中,由于一些特殊的原因,这些子节点之间出现了网络不通的状态,但他们的内部子网络是正常的。从而导致了整个系统的环境被切分成了若干个孤立的区域。这就是分区。
- CAP原则的精髓就是要么AP,要么CP,要么AC,但是不存在CAP。
9.1 CAP定理的证明
现在我们就来证明一下,为什么不能同时满足三个特性?
假设有两台服务器,一台放着应用A和数据库V,一台放着应用B和数据库V,他们之间的网络可以互通,也就相当于分布式系统的两个部分。
在满足一致性的时候,两台服务器 N1和N2,一开始两台服务器的数据是一样的,DB0=DB0。在满足可用性的时候,用户不管是请求N1或者N2,都会得到立即响应。在满足分区容错性的情况下,N1和N2有任何一方宕机,或者网络不通的时候,都不会影响N1和N2彼此之间的正常运作。
当用户通过N1中的A应用请求数据更新到服务器DB0后,这时N1中的服务器DB0变为DB1,通过分布式系统的数据同步更新操作,N2服务器中的数据库V0也更新为了DB1,这时,用户通过B向数据库发起请求得到的数据就是即时更新后的数据DB1。
上面是正常运作的情况,但分布式系统中,最大的问题就是网络传输问题,现在假设一种极端情况,N1和N2之间的网络断开了,但我们仍要支持这种网络异常,也就是满足分区容错性,那么这样能不能同时满足一致性和可用性呢?
假设N1和N2之间通信的时候网络突然出现故障,有用户向N1发送数据更新请求,那N1中的数据DB0将被更新为DB1,由于网络是断开的,N2中的数据库仍旧是DB0;
如果这个时候,有用户向N2发送数据读取请求,由于数据还没有进行同步,应用程序没办法立即给用户返回最新的数据DB1,怎么办呢?有二种选择,第一,牺牲数据一致性,响应旧的数据DB0给用户;第二,牺牲可用性,阻塞等待,直到网络连接恢复,数据更新操作完成之后,再给用户响应最新的数据DB1。
上面的过程比较简单,但也说明了要满足分区容错性的分布式系统,只能在一致性和可用性两者中,选择其中一个。也就是说分布式系统不可能同时满足三个特性。这就需要我们在搭建系统时进行取舍了,那么,怎么取舍才是更好的策略呢?
9.2 取舍策略
现如今,对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,节点只会越来越多,所以节点故障、网络故障是常态,因此分区容错性也就成为了一个分布式系统必然要面对的问题。那么就只能在C和A之间进行取舍。
*原因是*
因为,在分布式系统中,网络无法 100% 可靠,分区其实是一个必然现象,随着网络节点出现问题,产生了分区, 这时候其他节点和出错节点的数据必然会不一致,这时候就要面临选择,
是选择停掉所有的服务,等网络节点修复后恢复数据,以此来保证一致性(PC),
还是选择继续提供服务,放弃强一致性的要求,以此来保证整体的可用性(PA)。
所以,最多满足两个条件:
组合 | 分析结果 |
---|---|
CA | 满足原子和可用,放弃分区容错。说白了,就是一个整体的应用。 |
CP | 满足原子和分区容错,也就是说,要放弃可用。当系统被分区,为了保证原子性,必须放弃可用性,让服务停用。 |
AP | 满足可用性和分区容错,当出现分区,同时为了保证可用性,必须让节点继续对外服务,这样必然导致失去原子性。 |
在分布式系统设计中AP的应用较多,即保证分区容忍性和可用性,牺牲数据的强一致性(写操作后立刻读取到最新数据),保证数据最终一致性。比如:订单退款,今日退款成功,明日账户到账,只要在预定的用户可以接受的时间内退款事务走完即可。
顺便一提,CAP 理论中是忽略网络延迟,也就是当事务提交时,从节点 A 复制到节点 B 没有延迟,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。
但是,有个特殊情况需要注意:但对于传统的项目就可能有所不同,拿银行的转账系统来说,涉及到金钱的对于数据一致性不能做出一丝的让步,C必须保证,出现网络故障的话,宁可停止服务,可以在A和P之间做取舍。
*总而言之,没有最好的策略,好的系统应该是根据业务场景来进行架构设计的,只有适合的才是最好的。*
10.elasticsearch集群
单台Elasticsearch服务器提供服务,往往都有最大的负载能力,超过这个阈值,服务器性能就会大大降低甚至不可用,所以生产环境中,一般都是运行在指定服务器集群中。
除了负载能力,单点服务器也存在其他问题:
- 单台机器存储容量有限
- 单服务器容易出现单点故障,无法实现高可用
- 单服务的并发处理能力有限
配置服务器集群时,集群中节点数量没有限制,大于等于2个节点就可以看做是集群了。一般出于高性能及高可用方面来考虑集群中节点数量都是3个以上。
10.1 集群安装
10.1.1 环境搭建
一般集群建议3台机器以上,这里我们就使用1台机器来安装集群环境。一般生产环境都是使用linux服务器,所以我们这里就是linux环境下安装es集群。
节点名称 | 节点IP | http服务端口 | transport节点通讯端口 |
---|---|---|---|
node-1 | 192.168.6.100 | 9201 | 9301 |
node-2 | 192.168.6.100 | 9202 | 9302 |
node-3 | 192.168.6.100 | 9203 | 9303 |
各个机器集群环境安装之前,先把之前的data数据目录删除。
node-1:
#集群名称
cluster.name: my-application
#节点名称
node.name: node-1
#配置允许的访问网络
network.host: 0.0.0.0
#http服务端口
http.port: 9201
#配置集群间通信的端口号
transport.tcp.port: 9301
#是否允许为主节点,默认true
node.master: true
#是否为数据节点,默认true
node.data: true
#初始配置选举master节点
cluster.initial_master_nodes: ["node-1"]
#节点发现
discovery.seed_hosts: ["localhost:9301", "localhost:9302","localhost:9303"]
#elasticsearch-head 跨域解决
http.cors.allow-origin: "*"
http.cors.enabled: true
node-2:
#集群名称
cluster.name: my-application
#节点名称
node.name: node-2
#配置允许的访问网络
network.host: 0.0.0.0
#http服务端口
http.port: 9202
#配置集群间通信的端口号
transport.tcp.port: 9302
#是否允许为主节点,默认true
node.master: true
#是否为数据节点,默认true
node.data: true
#初始配置选举master节点
cluster.initial_master_nodes: ["node-1"]
#节点发现
discovery.seed_hosts: ["localhost:9301", "localhost:9302","localhost:9303"]
node-3:
#集群名称
cluster.name: my-application
#节点名称
node.name: node-3
#配置允许的访问网络
network.host: 0.0.0.0
#http服务端口
http.port: 9203
#配置集群间通信的端口号
transport.tcp.port: 9303
#是否允许为主节点,默认true
node.master: true
#是否为数据节点,默认true
node.data: true
#初始配置选举master节点
cluster.initial_master_nodes: ["node-1"]
#节点发现
discovery.seed_hosts: ["localhost:9301", "localhost:9302","localhost:9303"]
依次启动node-1、node-2、node-3节点。
启动完毕后查看节点健康状态:
10.1.2 head插件
kibana中查看集群相关的信息不是那么的直观,这里介绍一款第三方浏览器插件(elasticsearch-head)来查看和管理集群。
elasticsearch-head是一个nodesjs项目,所以要先安装nodejs环境。
nodejs安装
#下载nodejs(可以直接copy资料中的node-v12.18.1-linux-x64.tar.xz):
#注意:我的软件放在/opt目录下。
wget https://nodejs.org/dist/v12.18.1/node-v12.18.1-linux-x64.tar.xz
#解压文件:
tar -Jxf node-v12.18.1-linux-x64.tar.xz
mv node-v12.18.1-linux-x64 nodejs
#配置软链接环境(注意目录地址是否正确):
ln -s /opt/nodejs/bin/node /usr/local/bin/node
ln -s /opt/nodejs/bin/npm /usr/local/bin/npm
验证nodejs环境(node -v):
elasticsearch-head插件安装
#上传资料中的文件(该文件是nodejs编译后的)elasticsearch-head-compile-after.tar.gz
#1.解压文件
tar zxvf elasticsearch-head-compile-after.tar.gz
#2.修改Gruntfile.js(/解压目录/Gruntfile.js)找到代码中的93行[:93],将192.168.100.100修改为自己的IP
server: {
options: {
hostname: '192.168.6.100',
port: 9100,
base: '.',
keepalive: true
}
}
#3.修改app.js(/解压目录/_stie/app.js)找到4354行[:4354],将http://localhost:9200修改为对应的IP
this.base_uri = this.config.base_uri || this.prefs.get("app-base_uri") || "http://192.168.6.100:9200";
#4.启动head(/opt/es/elasticsearch-head为解压目录)
cd /opt/es/elasticsearch-head/node_modules/grunt
#后台启动
nohup ./grunt server >/dev/null 2>&1 &
测试访问(head端口9100):http://192.1686.100:9100/
10.2 elasticsearch中的集群核心概念
10.2.1 集群Cluster
一个集群就是由一个或多个服务器节点组织在一起,共同持有整个的数据,并一起提供索引和搜索功能。一个Elasticsearch集群有一个唯一的名字标识,这个名字默认就是”elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。
10.2.2 节点Node
集群中包含很多服务器,一个节点就是其中的一个服务器。作为集群的一部分,它存储数据,参与集群的索引和搜索功能。
一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说挺重要的,因为在这个管理过程中,你会去确定网络中的哪些服务器对应于Elasticsearch集群中的哪些节点。
一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做“elasticsearch”的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做“elasticsearch”的集群中。
在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何Elasticsearch节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”的集群。
10.2.3 分片(Shards)
一个索引可以存储超出单个节点硬件限制的大量数据。比如,一个具有10亿文档数据的索引占据1TB的磁盘空间,而任一节点都可能没有这样大的磁盘空间。或者单个节点处理搜索请求,响应太慢。为了解决这个问题,Elasticsearch提供了将索引划分成多份的能力,每一份就称之为分片。当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。
分片很重要,主要有两方面的原因:
1)允许你水平分割 / 扩展你的内容容量。
2)允许你在分片之上进行分布式的、并行的操作,进而提高性能/吞吐量。
至于一个分片怎样分布,它的文档怎样聚合和搜索请求,是完全由Elasticsearch管理的,对于作为用户的你来说,这些都是透明的,无需过分关心。
10.2.4 副本(Replicas)
在一个网络 / 云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,Elasticsearch允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片(副本)。
复制分片之所以重要,有两个主要原因:
- 在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的。
- 扩展你的搜索量/吞吐量,因为搜索可以在所有的副本上并行运行。
总之,每个索引可以被分成多个分片。一个索引也可以被复制0次(意思是没有复制)或多次。一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。分片和复制的数量可以在索引创建的时候指定。在索引创建之后,你可以在任何时候动态地改变复制的数量,但是你事后不能改变分片的数量。默认情况下,Elasticsearch中的每个索引被分片1个主分片和1个复制,这意味着,如果你的集群中至少有两个节点,你的索引将会有1个主分片和另外1个复制分片(1个完全拷贝),这样的话每个索引总共就有2个分片,我们需要根据索引需要确定分片个数。
10.2.5 分配(Allocation)
将分片分配给某个节点的过程,包括分配主分片或者副本。如果是副本,还包含从主分片复制数据的过程。这个过程是由master节点完成的。
10.2.6 节点类型
es集群中的节点类型分为:Master、DataNode。
-
master:
Elasticsearch启动时,会选举出来一个Master节点。当某个节点启动后,使用Zen Discovery机制找到集群中的其他节点,并建立连接。
discovery.seed_hosts: [“host1”, “host2”, “host3”]
并从候选主节点中选举出一个主节点。
cluster.initial_master_nodes: [“node-1”, “node-2”,“node-3”]
Master节点主要负责:
- 管理索引(创建索引、删除索引)、分配分片
- 维护元数据
- 管理集群节点状态
一个ElasticSearch集群中,只有一个Master节点。
不负责数据写入和查询,比较轻量级。
在生产环境中,内存可以相对小一点,但要确保机器稳定。 -
DataNode:
在Elasticsearch集群中,会有N个DataNode节点。DataNode节点主要负责:
- 数据写入、数据检索,大部分Elasticsearch的压力都在DataNode节点上
在生产环境中,内存最好配置大一些。
10.3 系统架构
一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。
当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。
作为用户,我们可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。
10.4 elasticsearch分片
我们可以在建立索引的时候创建分片信息:
#number_of_shards:主分片数量(7.x版本之后如果不指定数量默认为1),number_of_replicas:每个主分片对应的副本数量
PUT /users
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
}
}
head中数据查看说明:
★ 代表当前节点为master节点
● 表示DataNode节点
粗线框格子为主分片,细线框为副本分片,主分片与副本分片不能同时在一台机器上。
刚才的例子中,我们创建了3个主分片,然后又为每个主分片配置了两个副本分片,加起来一共9个分片。分片序号分别为0、1、2代表不同的数据段存储。其中0号分片的主分片在node-3机器上,node-1和node-2是它的备份分片。
注意:主分片数量一旦指定后就不允许更改,否则会影响后续的数据操作(分片位置路由是取模主分片数量)。
虽然主分片数量不可用更改,但是副本数量可以修改:
#修改副本数
PUT users/_settings
{
"number_of_replicas": 0
}
Elasticsearch 节点磁盘使用率过高,导致ES集群索引无副本
10.5 分片控制
10.5.1 写流程
新建和删除请求都是写操作, 必须在主分片上面完成之后才能被复制到相关的副本分片
- 第一步: 客户端选择DataNode节点发送请求,如上图架构,假设发送到node-2节点上。此时被选择的node-2节点也称为coordinating node(协调节点)
- 第二步: 协调节点根据路由规则计算分片索引位置。并将请求发送到分片索引对应的主分片机器上(这里假设分片计算后的值为0,那么请求会命中到node-3节点上)。
- 计算分片索引位置: shard = hash(routing) % number_of_primary_shards,routing可以自己设定,一般默认为文档的ID。
- 第三步: 当主分片文档写入完成后,同时将数据推送到与之对应的副本分片进行写入操作
- 第四步: 当分片完成了写入后再由协调节点将操作结果返回给客户端
10.5.2 读流程
- 第一步:客户端选择DataNode节点发送请求,如上图架构,假设发送到node-2节点上。此时被选择的node-2节点也称为coordinating node(协调节点)
- 第二步: 协调节点将从客户端获取到的请求数据转发到其它节点
- 第三步: 其它节点将查询结果文档ID、节点、分片信息返回给协调节点
- 第四步: 协调节点通过文档ID、节点信息等发送get请求给其它节点进行数据获取,最后进行汇总排序将数据返回给客户端
11.分片原理
11.1 倒排索引
Elasticsearch 使用一种称为****倒排索引****的结构,它适用于快速的全文搜索。
见其名,知其意,有倒排索引,肯定会对应有正向索引。正向索引(forward index),反向索引(inverted index)更熟悉的名字是倒排索引。
所谓的正向索引,就是搜索引擎会将待搜索的文件都对应一个文件ID,搜索时将这个ID和搜索关键字进行对应,形成K-V对,然后对关键字进行统计计数
但是互联网上收录在搜索引擎中的文档的数目是个天文数字,这样的索引结构根本无法满足实时返回排名结果的要求。所以,搜索引擎会将正向索引重新构建为倒排索引,即把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。
一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。例如,假设我们有两个文档,每个文档的 content 域包含如下内容:
- The quick brown fox jumped over the lazy dog
- Quick brown foxes leap over lazy dogs in summer
为了创建倒排索引,我们首先将每个文档的 content 域拆分成单独的 词(我们称它为 词条 或 tokens ),创建一个包含所有不重复词条的排序列表,然后列出每个词条出现在哪个文档。结果如下所示:
现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:
两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配词条数量的简单相似性算法,那么我们可以说,对于我们查询的相关性来讲,第一个文档比第二个文档更佳。
11.2 文档搜索
早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 一旦新的索引就绪,旧的就会被其替换,这样最近的变化便可以被检索到。
倒排索引被写入磁盘后是 不可改变 的:它永远不会修改。
不变性有重要的价值:
- 不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题。
- 一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升。
- 其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。
- 写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。
当然,一个不变的索引也有不好的地方。主要事实是它是不可变的! 你不能修改它。如果你需要让一个新的文档可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。
11.3 动态更新索引
如何在保留不变性的前提下实现倒排索引的更新?
答案是: **用更多的索引。**通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。
Elasticsearch 基于 Lucene, 这个java库引入了按段搜索的概念。 每一 段本身都是一个倒排索引。 但索引在 Lucene 中除表示所有段的集合外, 还增加了提交点的概念 — 一个列出了所有已知段的文件。
按段搜索会以如下流程执行:
- 新文档被收集到内存索引缓存
- 不时地, 缓存被 提交
(1) 一个新的段—一个追加的倒排索引—被写入磁盘。
(2) 一个新的包含新段名字的 提交点 被写入磁盘
(3) 磁盘进行同步 — 所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件
- 新的段被开启,让它包含的文档可见以被搜索
- 内存缓存被清空,等待接收新的文档
当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。 这种方式可以用相对较低的成本将新文档添加到索引。
段是不可改变的,所以既不能把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个 .del 文件,文件中会列出这些被删除文档的段信息。
当一个文档被 “删除” 时,它实际上只是在 .del 文件中被标记删除。一个被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前从结果集中移除。
文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。
11.4 近实时搜索
- 分段数据先写入到内存缓存中,同时文档操作也会记录translog日志
- 内存的数据对查询不可见,默认间隔1s将内存中数据写入到文件系统缓存中,这里面的数据对查询可见。
- 文件系统缓存数据间隔30分钟再将数据刷入磁盘中。
- 如果文件系统缓存数据在没有刷新到硬盘时宕机了,可以从translog中恢复数据到磁盘,数据恢复完成后translog数据也会清理。
11.5 段合并
由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。 每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。
Elasticsearch通过在后台进行段合并来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。
段合并的时候会将那些旧的已删除文档从文件系统中清除。被删除的文档(或被更新文档的旧版本)不会被拷贝到新的大段中。
启动段合并不需要你做任何事。进行索引和搜索时会自动进行。
- 当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。
- 合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。
- 一旦合并结束,老的段被删除
- 新的段被刷新(flush)到了磁盘。写入一个包含新段且排除旧的和较小的段的新提交点。
- 新的段被打开用来搜索。
- 老的段被删除。
合并大的段需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行。
12.优化建议
12.1 硬件选择
Elasticsearch的基础是 Lucene,所有的索引和文档数据是存储在本地的磁盘中,具体的路径可在 ES 的配置文件…/config/elasticsearch.yml中配置,如下:
\#----------------------------------- Paths ------------------------------------
\#
\# Path to directory where to store the data (separate multiple locations by comma):
\#
\#path.data: /path/to/data
\#
\# Path to log files:
\#
\#path.logs: /path/to/logs
\#
磁盘在现代服务器上通常都是瓶颈。Elasticsearch 重度使用磁盘,你的磁盘能处理的吞吐量越大,你的节点就越稳定。这里有一些优化磁盘 I/O 的技巧:
l 使用 SSD。就像其他地方提过的, 他们比机械磁盘优秀多了。
l 使用 RAID 0。条带化 RAID 会提高磁盘 I/O,代价显然就是当一块硬盘故障时整个就故障了。不要使用镜像或者奇偶校验 RAID 因为副本已经提供了这个功能。
l 另外,使用多块硬盘,并允许 Elasticsearch 通过多个 path.data 目录配置把数据条带化分配到它们上面。
l 不要使用远程挂载的存储,比如 NFS 或者 SMB/CIFS。这个引入的延迟对性能来说完全是背道而驰的。
12.2 分片策略
12.2.1 合理设置分片数
分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。而且索引的分片完成分配后由于索引的路由机制,我们是不能重新修改分片数的。
可能有人会说,我不知道这个索引将来会变得多大,并且过后我也不能更改索引的大小,所以为了保险起见,还是给它设为 1000 个分片吧。但是需要知道的是,一个分片并不是没有代价的。需要了解:
l 一个分片的底层即为一个 Lucene 索引,会消耗一定文件句柄、内存、以及 CPU 运转。
l 每一个搜索请求都需要命中索引中的每一个分片,如果每一个分片都处于不同的节点还好, 但如果多个分片都需要在同一个节点上竞争使用相同的资源就有些糟糕了。
l 用于计算相关度的词项统计信息是基于分片的。如果有许多分片,每一个都只有很少的数据会导致很低的相关度。
一个业务索引具体需要分配多少分片可能需要架构师和技术人员对业务的增长有个预先的判断,横向扩展应当分阶段进行。为下一阶段准备好足够的资源。 只有当你进入到下一个阶段,你才有时间思考需要作出哪些改变来达到这个阶段。一般来说,我们遵循一些原则:
l 控制每个分片占用的硬盘容量不超过ES的最大JVM的堆空间设置(一般设置不超过32G,参考下文的JVM设置原则),因此,如果索引的总容量在500G左右,那分片大小在16个左右即可;当然,最好同时考虑原则2。
l 考虑一下node数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了1个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以, 一般都设置分片数不超过节点数的3倍。
l 主分片,副本和节点最大数之间数量,我们分配的时候可以参考以下关系:
节点数<=主分片数*(副本数+1)
12.2.2 推迟分片分配
对于节点瞬时中断的问题,默认情况,集群会等待一分钟来查看节点是否会重新加入,如果这个节点在此期间重新加入,重新加入的节点会保持其现有的分片数据,不会触发新的分片分配。这样就可以减少 ES 在自动再平衡可用分片时所带来的极大开销。
通过修改参数 delayed_timeout ,可以延长再均衡的时间,可以全局设置也可以在索引级别进行修改:
PUT /_all/_settings
{
"settings": {
"index.unassigned.node_left.delayed_timeout": "5m"
}
}
12.3 路由选择
当我们查询文档的时候,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?它其实是通过下面这个公式来计算出来:
shard = hash(routing) % number_of_primary_shards
routing 默认值是文档的 id,也可以采用自定义值,比如用户 id。
不带 routing 查询
在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为 2 个步骤
- 分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上。
- 聚合: 协调节点搜集到每个分片上查询结果,在将查询的结果进行排序,之后给用户返回结果。
带 routing 查询
查询的时候,可以直接根据 routing 信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。
向上面自定义的用户查询,如果 routing 设置为 userid 的话,就可以直接查询出数据来,效率提升很多。
12.4 写入速度优化
ES的默认配置,是综合了数据可靠性、写入速度、搜索实时性等因素。实际使用时,我们需要根据公司要求,进行偏向性的优化。
针对于搜索性能要求不高,但是对写入要求较高的场景,我们需要尽可能的选择恰当写优化策略。综合来说,可以考虑以下几个方面来提升写索引的性能:
- 加大 Translog Flush ,目的是降低 Iops、Writeblock。
- 增加 Index Refresh 间隔,目的是减少 Segment Merge 的次数。
- 调整 Bulk 线程池和队列。
- 优化节点间的任务分布。
- 优化 Lucene 层的索引建立,目的是降低 CPU 及 IO。
12.4.1 批量数据提交
ES 提供了 Bulk API 支持批量操作,当我们有大量的写任务时,可以使用 Bulk 来进行批量写入。
通用的策略如下:Bulk 默认设置批量提交的数据量不能超过 100M。数据条数一般是根据文档的大小和服务器性能而定的,但是单次批处理的数据大小应从 5MB~15MB 逐渐增加,当性能没有提升时,把这个数据量作为最大值。
12.4.2 优化存储设备
ES 是一种密集使用磁盘的应用,在段合并的时候会频繁操作磁盘,所以对磁盘要求较高,当磁盘速度提升之后,集群的整体性能会大幅度提高。
12.4.3 减少Refresh的次数
Lucene 在新增数据时,采用了延迟写入的策略,默认情况下索引的 refresh_interval 为 1 秒。
Lucene 将待写入的数据先写到内存中,超过 1 秒(默认)时就会触发一次 Refresh,然后 Refresh 会把内存中的的数据刷新到操作系统的文件缓存系统中。
如果我们对搜索的实效性要求不高,可以将 Refresh 周期延长,例如 30 秒。
这样还可以有效地减少段刷新次数,但这同时意味着需要消耗更多的Heap内存。
12.4.4 加大Flush设置
Flush 的主要目的是把文件缓存系统中的段持久化到硬盘,当 Translog 的数据量达到 512MB 或者 30 分钟时,会触发一次 Flush。
index.translog.flush_threshold_size 参数的默认值是 512MB,我们进行修改。
增加参数值意味着文件缓存系统中可能需要存储更多的数据,所以我们需要为操作系统的文件缓存系统留下足够的空间。
12.4.5 减少副本的数量
ES 为了保证集群的可用性,提供了 Replicas(副本)支持,然而每个副本也会执行分析、索引及可能的合并过程,所以 Replicas 的数量会严重影响写索引的效率。
当写索引时,需要把写入的数据都同步到副本节点,副本节点越多,写索引的效率就越慢。
如果我们需要大批量进行写入操作,可以先禁止 Replica 复制,设置 index.number_of_replicas: 0 关闭副本。在写入完成后,Replica 修改回正常的状态。
12.4.6 内存设置
ES 默认安装后设置的内存是 1GB,对于任何一个现实业务来说,这个设置都太小了。
如果是通过解压安装的 ES,则在 ES 安装文件中包含一个 jvm.option 文件,添加如下命令来设置 ES 的堆大小,Xms 表示堆的初始大小,Xmx 表示可分配的最大内存,都是 1GB。
确保 Xmx 和 Xms 的大小是相同的,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力。
假设你有一个 64G 内存的机器,按照正常思维思考,你可能会认为把 64G 内存都给 ES 比较好,但现实是这样吗, 越大越好?虽然内存对 ES 来说是非常重要的,但是答案是否定的!
因为 ES 堆内存的分配需要满足以下两个原则:
- 不要超过物理内存的 50%:Lucene 的设计目的是把底层 OS 里的数据缓存到内存中。
Lucene 的段是分别存储到单个文件中的,这些文件都是不会变化的,所以很利于缓存,同时操作系 统也会把这些段文件缓存起来,以便更快的访问。
如果我们设置的堆内存过大,Lucene 可用的内存将会减少,就会严重影响降低 Lucene 的全文本查 询性能。
- 堆内存的大小最好不要超过 32GB:在 Java 中,所有对象都分配在堆上,然后有一个 Klass Pointer 指针指向它的类元数据。
这个指针在 64 位的操作系统上为 64 位,64 位的操作系统可以使用更多的内存(2^64)。在 32 位 的系统上为 32 位,32 位的操作系统的最大寻址空间为 4GB(2^32)。
但是 64 位的指针意味着更大的浪费,因为你的指针本身大了。浪费内存不算,更糟糕的是,更大的 指针在主内存和缓存器(例如 LLC, L1等)之间移动数据的时候,会占用更多的带宽。
最终我们都会采用 31 G 设置
-Xms 31g
-Xmx 31g
假设你有个机器有 128 GB 的内存,你可以创建两个节点,每个节点内存分配不超过 32 GB。 也就是说不超过 64 GB 内存给 ES 的堆内存,剩下的超过 64 GB 的内存给 Lucene
12.5 重要配置
参数名 | 参数值 | 说明 |
---|---|---|
cluster.name | elasticsearch | 配置 ES 的集群名称,默认值是ES,建议改成与所存数据相关的名称,ES 会自动发现在同一网段下的集群名称相同的节点 |
node.name | node-1 | 集群中的节点名,在同一个集群中不能重复。节点的名称一旦设置,就不能再改变了。当然,也可以设置成服务器的主机名称,例如 node.name:${HOSTNAME}。 |
node.master | true | 指定该节点是否有资格被选举成为 Master 节点,默认是 True,如果被设置为 True,则只是有资格成为 Master 节点,具体能否成为 Master 节点,需要通过选举产生。 |
node.data | true | 指定该节点是否存储索引数据,默认为 True。数据的增、删、改、查都是在 Data 节点完成的。 |
index.number_of_shards | 1 | 设置都索引分片个数,默认是 1 片。也可以在创建索引时设置该值,具体设置为多大都值要根据数据量的大小来定。如果数据量不大,则设置成 1 时效率最高 |
index.number_of_replicas | 1 | 设置默认的索引副本个数,默认为 1 个。副本数越多,集群的可用性越好,但是写索引时需要同步的数据越多。 |
transport.tcp.compress | true | 设置在节点间传输数据时是否压缩,默认为 False,不压缩 |
discovery.zen.minimum_master_nodes | 1 | 设置在选举 Master 节点时需要参与的最少的候选主节点数,默认为 1。如果使用默认值,则当网络不稳定时有可能会出现脑裂。合理的数值为(master_eligible_nodes/2)+1,其中 master_eligible_nodes 表示集群中的候选主节点数 |
discovery.zen.ping.timeout | 3s | 设置在集群中自动发现其他节点时 Ping 连接的超时时间,默认为 3 秒。在较差的网络环境下需要设置得大一点,防止因误判该节点的存活状态而导致分片的转移 |