目录
介绍
elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容,例如:在GitHub搜索代码,在电商网站搜索商品,在百度搜索答案,在打车软件搜索附近的车。elasticsearch结合kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域;而elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。 elasticsearch具备下列优势:
- 支持分布式,可水平扩展
- 提供Restful接口,可被任何语言调用
倒排索引
在mysql正向索引中根据文本搜索,只能模糊查询全表扫描数据,效率低下;
倒排索引则根据词条进行搜索,每个词条都建立了索引而不是字段:
- 将每一个文档的数据利用算法分词,得到一个个词条
- 创建表,每行数据包括词条、词条所在文档id、位置等信息
- 因为词条唯一性,可以给词条创建索引,例如hash表结构索引
elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。 文档数据会被序列化为json格式后存储在elasticsearch中。
- 索引(index):相同类型的文档的集合
- 映射(mapping):索引中文档的字段约束信息,类似表的结构约束
Mysql:擅长事务类型操作,可以确保数据的安全和一致性
Elasticsearch:擅长海量数据的搜索、分析、计算
安装
部署单点es,在浏览器输入地址访问:http://10.0.0.31:5601,即可看到结果
#为了让es和kibana容器互联。先创建一个网络
docker network create es-net
#加载镜像
docker load -i es.tar
docker load -i kibana.tar
#创建容器
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \ #非集群模式
-v es-data:/usr/share/elasticsearch/data \ #挂载逻辑卷,绑定es的数据目录
-v es-plugins:/usr/share/elasticsearch/plugins \ #挂载逻辑卷,绑定es的插件目录
--privileged \ #授予逻辑卷访问权
--network es-net \ #加入一个名为es-net的网络中
-p 9200:9200 \ #端口映射配置
-p 9300:9300 \
elasticsearch:7.12.1
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \ #设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch
--network=es-net \ #加入一个名为es-net的网络中,与elasticsearch在同一个网络中
-p 5601:5601 \
kibana:7.12.1
#离线安装ik插件
#查看elasticsearch的数据卷目录
docker volume inspect es-plugins
#解压缩分词器安装包,上传到es容器的插件数据卷中
/var/lib/docker/volumes/es-plugins/_data
#在分词器config目录下IKAnalyzer.cfg.xml配置文件配置自己的扩展字典和停用词词典
#重启容器
docker restart es
docker restart kibana
部署es集群可以直接使用docker-compose来完成,不过要求Linux虚拟机至少有4G的内存空间
#docker-compose文件,启动命令docker-compose up
version: '2.2'
services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es01
environment:
- node.name=es01
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data01:/usr/share/elasticsearch/data
ports:
- 9200:9200
networks:
- elastic
es02:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es02
environment:
- node.name=es02
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data02:/usr/share/elasticsearch/data
networks:
- elastic
es03:
image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
container_name: es03
environment:
- node.name=es03
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data03:/usr/share/elasticsearch/data
networks:
- elastic
volumes:
data01:
driver: local
data02:
driver: local
data03:
driver: local
networks:
elastic:
driver: bridge
索引库操作
索引库就类似数据库表,mapping映射就类似表的结构。我们要向es中存储数据,必须先创建“库”和“表”。
常见的mapping属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
- 数值:long、integer、short、byte、double、float、
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为true
- analyzer:使用哪种分词器
- properties:该字段的子字段
创建索引库和映射
PUT /heima
{
"mappings": {
"properties": {
"info":{
"type": "text",
"analyzer": "ik_smart"
},
"email":{
"type": "keyword",
"index": "falsae"
},
"name":{
"properties": {
"firstName": {
"type": "keyword"
}
}
},
// ... 略
}
}
}
查询索引库
修改索引库
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
删除索引库
文档操作
新增文档
查询文档
删除文档
修改文档
全量修改:根据指定的id删除文档,新增一个相同id的文档,如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
增量修改:修改文档中的部分字段
PUT /{索引库名}/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
// ... 略
}
POST /{索引库名}/_update/文档id
{
"doc": {
"字段名": "新的值",
}
}
RestAPI
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。其中的Java Rest Client又包括两种:Java Low Level Rest Client, Java High Level Rest Client
在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。分为三步:
1、引入es的RestHighLevelClient依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
2、因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
3、初始化RestHighLevelClient
@BeforeEach
void setUp() {
client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://10.0.0.31:9200")
));
}
@AfterEach
void tearDown() throws IOException {
client.close();
}
4、创建索引库
5、判断索引库是否存在
@Test
void testExistsHotelIndex() throws IOException {
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
// 3.输出
System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}
6、操作文档
文档操作的基本步骤:
- 初始化RestHighLevelClient
- 创建XxxRequest。XXX是Index、Get、Update、Delete、Bulk
- 准备参数(Index、Update、Bulk时需要)
- 发送请求。调用RestHighLevelClient#.xxx()方法,xxx是index、get、update、delete、bulk
- 解析结果(Get时需要)
新增
查询
删除
@Test
void testDeleteDocumentById() throws IOException {
// 1.准备Request // DELETE /hotel/_doc/{id}
DeleteRequest request = new DeleteRequest("hotel", "61083");
// 2.发送请求
client.delete(request, RequestOptions.DEFAULT);
}
更新
批量导入:批量处理BulkRequest,其本质就是将多个普通的CRUD请求组合在一起发送
@Test
void testBulkRequest() throws IOException {
// 查询所有的酒店数据
List<Hotel> list = hotelService.list();
// 1.准备Request
BulkRequest request = new BulkRequest();
// 2.准备参数
for (Hotel hotel : list) {
// 2.1.转为HotelDoc
HotelDoc hotelDoc = new HotelDoc(hotel);
// 2.2.转json
String json = JSON.toJSONString(hotelDoc);
// 2.3.添加请求
request.add(new IndexRequest("hotel").id(hotel.getId().toString()).source(json, XContentType.JSON));
}
// 3.发送请求
client.bulk(request, RequestOptions.DEFAULT);
}
查询与检索
除了存储,elasticsearch最擅长的还是搜索和数据分析,Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
- 查询所有:查询出所有数据,一般测试用。例如:match_all
- 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:match_query,multi_match_query
- 精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。例如: ids, range, term
- 地理(geo)查询:根据经纬度查询。例如: geo_distance, geo_bounding_box
- 复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。例如: bool, function_score
GET /indexName/_search
{
"query": {
"查询类型": {
"查询条件": "条件值"
}
}
}
DSL查询语法
// 查询所有
GET /indexName/_search
{
"query": {
"match_all": {
}
}
}
全文检索查询
精确查询
算分函数查询
布尔查询
搜索结果处理
搜索的结果可以按照用户指定的方式去处理或展示
- 排序:es默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索结果排序
- 分页:默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了,elasticsearch中通过修改from、size参数来控制要返回的分页结果
- 高亮:高亮查询必须使用全文检索查询,并且要有搜索关键字,将来才可以对关键字高亮
GET /hotel/_search
{
"query": {
"match_all": {}
},
"from": 990, // 分页开始的位置,默认为0
"size": 10, // 期望获取的文档总数
"sort": [
{"price": "asc"}
]
}
#elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条
深度分页问题,ES提供了两种解决方案 :
- search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
- scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。
高亮:
数据聚合
数据聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。参加聚合的字段必须是keyword、日期、数值、布尔类型。
- 桶(Bucket)聚合:用来对文档做分组
- TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
- Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
- 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
- Avg:求平均值
- Max:求最大值
- Min:求最小值
- Stats:同时求max、min、avg、sum等
- 管道(pipeline)聚合:其它聚合的结果为基础做聚合
自动补全
elasticsearch中分词器(analyzer)的组成包含三部分:
- character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
- tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
- tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
声明自定义分词器的语法如下:
PUT /test
{
"settings": {
"analysis": {
"analyzer": { // 自定义分词器
"my_analyzer": { // 分词器名称
"tokenizer": "ik_max_word",
"filter": "py"
}
},
"filter": { // 自定义tokenizer filter
"py": { // 过滤器名称
"type": "pinyin", // 过滤器类型,这里是pinyin
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16,
"remove_duplicated_term": true,
"none_chinese_pinyin_tokenize": false
}
}
}
},
"mappings": {
"properties": {
"name": {
"type": "text",
"analyzer": "my_analyzer",
"search_analyzer": "ik_smart"//拼音分词器适合在创建倒排索引的时候使用,但不能在搜索的时候使用
}
}
}
}
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
- 参与补全查询的字段必须是completion类型。
- 字段的内容一般是用来补全的多个词条形成的数组。
查询的dsl语句及api如下:
// 自动补全查询
GET /test/_search
{
"suggest": {
"title_suggest": {
"text": "s", // 关键字
"completion": {
"field": "title", // 补全查询的字段
"skip_duplicates": true, // 跳过重复的
"size": 10 // 获取前10条结果
}
}
}
}
数据同步
elasticsearch中的数据来自于mysql数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是elasticsearch与mysql之间的数据同步
代码示例:
//生产者
@PutMapping()
public void updateById(@RequestBody Hotel hotel){
if (hotel.getId() == null) {
throw new InvalidParameterException("id不能为空");
}
hotelService.updateById(hotel);
// 发送MQ消息
rabbitTemplate.convertAndSend(HotelMqConstants.EXCHANGE_NAME, HotelMqConstants.INSERT_KEY, hotel.getId());
}
//消费者
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = HotelMqConstants.INSERT_QUEUE_NAME),
exchange = @Exchange(name = HotelMqConstants.EXCHANGE_NAME, type = ExchangeTypes.TOPIC),
key = HotelMqConstants.INSERT_KEY
))
public void listenHotelInsert(Long hotelId){
// 新增
hotelService.saveById(hotelId);
}
ES集群
单机的elasticsearch做数据存储,必然面临两个问题:海量数据存储问题、单点故障问题。
- 海量数据存储问题:将索引库从逻辑上拆分为N个分片(shard),存储到多个节点
- 单点故障问题:将分片数据在不同节点备份(replica )
集群(cluster):一组拥有共同的 cluster name 的 节点。
节点 :集群中的一个 Elasticearch 实例
分片(shard):索引可以被拆分为不同的部分进行存储,称为分片。在集群环境下,一个索引的不同分片可以拆分到不同的节点中
主分片(Primary shard):相对于副本分片的定义。
副本分片(Replica shard)每个主分片可以有一个或者多个副本,数据和主分片一样。
每个索引库的分片数量、副本数量都是在创建索引库时指定的,并且分片数量一旦设置以后无法修改。语法如下:
PUT /itcast
{
"settings": {
"number_of_shards": 3, // 分片数量
"number_of_replicas": 1 // 副本数量
},
"mappings": {
"properties": {
// mapping映射定义 ...
}
}
}
集群节点有不同的职责划分,默认情况下,集群中的任何一个节点都同时具备上述四种角色,但是真实的集群一定要将集群职责分离:
- master节点:对CPU要求高,但是内存要求低
- data节点:对CPU和内存要求都高
- coordinating节点:对网络带宽、CPU要求高
职责分离可以让我们根据不同节点的需求分配不同的硬件去部署。而且避免业务之间的互相干扰。
默认情况下,每个节点都是master eligible节点,因此一旦master节点宕机,其它候选节点会选举一个成为主节点。当主节点与其他节点网络故障时,可能发生脑裂问题。 为了避免脑裂,需要要求选票超过 ( eligible节点数量 + 1 )/ 2 才能当选为主,因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes,在es7.0以后,已经成为默认配置,因此一般不会发生脑裂问题
ES集群的分布式存储:
当新增文档时,应该保存到不同分片,保证数据均衡,那么coordinating node如何确定数据该存储到哪个分片呢? elasticsearch会通过hash算法来计算文档应该存储到哪个分片(_routing默认是文档的id,算法与分片数量有关,因此索引库一旦创建,分片数量不能修改!):
ES集群的分布式查询:
elasticsearch的查询分成两个阶段:
- scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
- gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户
ES集群的故障转移:
集群的master节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到其它节点,确保数据安全,这个叫做故障转移。master宕机后,EligibleMaster选举为新的主节点。
node1发生故障后的第一件事,就是重新选主,选中了node2,node2成为主节点后,会检测集群监控状态,发现:shard-1、shard-0没有副本节点。因此需要将node1上的数据迁移到node2、node3: