本片博客是本人通过观看
狂神说
的视频记录的笔记,以此记录方便需要时查阅。
视频地址:https://www.bilibili.com/video/BV17a4y1x7zq
ElasticSearch,简称ES。ES是一个开源的
高扩展
的分布式全文检索引擎
,它可以近乎实时的存储、检索数据
;本身的扩展性很好,可以扩展到上百台服务器,处理BP(大数据)级别的数据。ES也使用Java开发并使用Lucene作为其核心来实现所有缩影和搜索功能,但是它的目的是通过简单的RESTFul API来隐藏Lucene的复杂性,从而让全文搜索变得简单。
# 下载7.6.1版本的ElasticSearch镜像 docker pull elasticsearch:7.6.1 # 通过ElasticSearch镜像创建并容器 # ps:9200对外访问端口,9300通信端口 docker run -d --name es -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch
# 进入elasticsearch容器内部 docker exec -it es /bin/bash # 进入config目录,找到elasticsearch.yml文件 cd config # 编辑elasticsearch.yml文件 vi elasticsearch.yml
详细如下图:
cluster.name:自定义集群名称
network.host:当前es节点绑定的ip地址,默认127.0.0.1,如果需要开放对外访问这个属性必须设置以上两个是默认就有的
http.cors.enabled:是否支持跨域,默认事false
http.cors.allow-origin:当设置允许跨域,默认为*,表示支持所有域名,如果我们只是允许某些网站能访问,那么可以使用正则表达式。
cluster.name: "docker-cluster" network.host: 0.0.0.0 http.cors.enabled: true http.cors.allow-origin: "*"
# 推出容器 exit # 重启容器 docker restart es
上图描述的就是ES的一些基本信息。包括名称、集群名称、集群ID、版本、打开方式、以及依赖程序的版本信息。
# 通过docker命令进入到容器内部 docker exec -it es /bin/bash # 查看目录结构 ls -ll
详细如下图所示:
- LICENSE.txt:证书描述文件
- NOTICE.txt:产品使用注意事项
- README.asciidoc:描述或使用文件
- bin:启动文件
- config:配置文件
- log4j2:日志配置文件
- jvm.options Java虚拟机相关的配置
- elasticsearch.yml elasticsearch的配置文件
- lib:相关依赖包
- logs:日志
- modules:功能模块
- plugins:插件
安装:elasticsearch-head
下载地址:https://github.com/mobz/elasticsearch-head
Kibana是一个针对ElasticSearch的开源分析急可视化平台,用来搜索、查看交互存储在ElasticSearch索引中的数据,使用Kibana可以通过各种图标进行高级数据分析及展示。Kibana让海量数据更容易理解。它操作简单,给予浏览器的用户界面可以快速创建仪表板实时显示Elasticsearch查询动态。设置Kibana非常简单。无需编码或者额外的基础架构,几分钟内就可以完成Kibana安装并启动Elasticsearch缩影监测
注意:Kibana的版本要和ElasticSearch的版本要保持一致
# 下载7.6.1版本的Kibana镜像 docker pull kibana:7.6.1 # 通过Kibana镜像创建容器,需要注意的是ELASTICSEARCH_URL=http://IP:9200 中的IP是elasticsearch所在服务器的IP # 可以先通过如下命令获取elasticsearch所在服务器的IP docker inspect --format '{{ .NetworkSettings.IPAddress }}' es # 我这里得到的IP地址为:172.17.0.2 docker run --name kibana -d -p 5601:5601 --link es -e "ELASTICSEARCH_URL=http://172.17.0.2:9200" kibana:7.6.1
上述操作完成后,在浏览器中访问地址:localhost:5601。如果可以看到如下界面说明Kibana就已经安装好了
如果您访问地址看到的界面如下所示,说明创建的Kibana容器的指向IP没有elasticsearch容器IP。
此时我们可以通过修改Kibana的配置文件来解决这个问题# 进入到Kibana容器内部 docker exec -it kibana /bin/bash # 进入配置文件夹 cd config # 修改kibana.yml配置文件 vi kibana.yml如下图所示,需要修改
elasticsearch.hosts
这项配置,将访问的IP修改为elasticsearch服务地址
Elasticsearch是面向文档的非关系型数据库。
关系型数据库和Elasticsearch的客观比较如下:
关系型数据库 | Elasticsearch |
---|---|
数据库(database) | 索引(indices) |
表(table) | 类型(types,即将被弃用) |
行数据(row) | 文档(documents) |
字段(columns) | 字段(fields) |
elasticsearch(集群)中可以包含多个索引(数据库),每一个索引中可以包含多个类型(表),每个类型下又包含多个文档(行),每个文档中又包含多个字段(列)
ES物理设计
elasticsearch在后台吧每个索引分成多个分片,每份分片可以在集群中的不同服务器间迁移
逻辑设计
一个索引类型中,包含多个文档,比如说文档1,文档2.当我们搜索一篇文档时,可以通过这样的一个顺序找到它:
索引 》类型 》文档ID 》通过这个组合我们就能检索到某个具体的文档。注意:ID不必是整数,实际上它是个字符串
Elasticserach是面向文档的,那么就意味着索引和搜索数据的最小单位是文档,Elasticsearch,文档有几个重要的属性:
- 自我包含:一篇文档同时包含字段和对应的值,也就是同时包含key:value
- 可以是层次型的,一个文档中包含子文档,浮渣的逻辑实体就是这么来的
- 灵活的结构,文档不依赖预先模式。在关系型数据库中,要提前定义字段才能使用,在elasticsearch中,对于字段是非常灵活的。有时候可以忽略某些字段,或者动态的添加一个新的字段
尽管我们可以随意的增加或者忽略某个字段,但是每个字段的类型非常重要。比如一个年龄字段类型,可以是字符串也可以是整数类型。因为Elasticsearch会保存字段和类型之间的映射及其他的设置。这种映射具体到每一个映射的每种类型,这也是为什么在elasticsearch会保存字段和类型之间的映射及其他的设置。这种映射具体到每个映射的每种类型,这也是为什么在elasticsearch中类型有时候也成为
映射类型
。
类型是文档的逻辑容器,就像关系型数据库一样,表格是行的容器。类型中对于字段的定义称为映射,比如
name
映射为字符串类型。我们说文档是无模式的,它不需要拥有映射中所定义的所有字段,比如新增一个字段,那么elasticsearch会自动将新字段加入映射,但是elasticsearch也不知道这个字段是什么类型,于是就会对此进行猜测。如果这个值是18,那么elasticsearch就会认为它是一个整数类型。但是elasticsearch也可能猜不对,所有最安全的方式就是提前的钱定义好所需要的映射。
索引就是映射关系的容器,elasticsearch中的索引是一个非常大的文档集合。索引存储了映射类型的字段和其他设置,然后它们会被存储到各个分片上。
一个集群至少有一个节点,而一个节点就是一个elasticsearch进程,节点可以有多个索引(默认的)。如果您创建索引,那么索引将会有5个分片(primary shard,又称主分片)构成的,每一个主分片会有一个副本(replica shard,又称复制分片)
如上图为一个3节点的集群,可以看到主分片对于的复制分片都不会在同一个节点内,这样有利于某个节点意外down机,数据也不会丢失。实际上,一个分片是一个Lucene索引,一个包含到排索引的文件目录,倒排索引的结构使得elasticsearch在不扫描全部文档的情况下,就能告诉你那些文档包含特定的关键字
Elasticsearch使用的是一种称为倒排索引的结构,采用Lucene倒排索引做为底层。这种结构适用于快速的全文搜索,一个索引由文档中所有不重复的列表构成,对于每一个词,都有一个它的文档列表。例如,现在有两个文档,每个文档包含如下内容:
study every day,good good up to forever # 文档1包含的内容 To forever,study every day,good good up # 文档2包含的内容为了创建倒排索引,我们首先要每个文档拆分成独立的词(或者称为词条或者tokens),然后创建一个包含所有不重复的词条的排序列表,然后列出每个词条出现在那个文档中
Term doc_1 doc_2 study V X To X V every V V forever V V day V V study X V good V V every V V to V X up V V 现在,我们来搜索一个 to forever,只需要查看包含每个词条的文档
Term doc_1 doc_2 to V X forever V V total 2 1 两个文档都匹配,但是第一个文档比第二个匹配程度高。如果没有别的条件,现在,同时包含这两个包含关键字的文档将被返回
倒排索引理解示例:
比如我们通过博客的标签来搜索博客文章,详细如下:
博客文章ID 标签 1 python 2 python 3 Linux python 4 Linux 那么倒排索引列表就是这样的一个结构
标签 博客文章ID python 1,2,3 Linux 3,4 如果要搜索含有python标签的文章,那相对于查找所有原始数据而言,查找倒排索引的数据将会快很多。只需要查看标签这一栏,然后取到相关的文章ID即可,完全过滤无关的文档。
elasticsearch的索引和Lucene的索引对比
在elasticsearch中,索引这个词被频繁使用,这就是术语的使用,在elasticsearch中,索引被分为多个分片,每份是一个Lucene的索引。所以一个elasticsearch索引是由多个Lucene索引组成的。
什么是IK分词器?
分词:即把一段中文或者别的划分成一个个的关键字,我们在搜索的时候会把自己的信息进行分词,会把数据库中的或者索引库中的数据进行分词,然后进行一个匹配操作。默认的中文分词是将每一个字看成一个词。
IK提供两个分词算法:ik_smart
和ik_max_word
,其中ik_smart为最少切分,ik_max_word为最细粒度划分!
# 进入到elasticsearch容器里面 docker exec -it es /bin/bash # 进入到插件目录 cd /usr/share/elasticsearch/plugins # 下载IK分词器插件(我这里的版本是对应的es版本) elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.6.1/elasticsearch-analysis-ik-7.6.1.zip # 下载好后退出es容器,重启es容器
使用Kibana测试
IK分词器会根据算法去拆分,拆分的依据是根据它默认的一个词库。可能有些自己想连起来的词IK默认的词库达不到要求,此时我们就可以自己去定义一些词库字典。
首先第一步我们先要找到在哪里配置:
# 进入es容器 docker exec -it es /bin/bash # 进入插件目录 cd /usr/share/elasticsearch/plugins # 进入IK分词器插件 cd analysis-ik/ # 进入分词器配置目录 cd config/ # 查看分词配置文件 cat IKAnalyzer.cfg.xml详细操作如下图
现在我们可以自己去创建一个
dic
字典# 创建一个自定义分词字典文件 touch myTest.dic先不着急编写内容,可以先通过Kibana来验证一下。
在kibana中对"text":"good man"
进行默认的内容拆分测试,详细如下图所示:现在我想得到的分词中有一项为
good ma
,现在可以编辑myTest.dic文件在文件中输入内容:代码ok
配置:
重启elasticsearch和kibana
Rest是一种软件风格,而不是一种标准,指数一个一组设计原理和约束条件。它主要用于客户端和服务器交互类的软件。给予这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。
基本Rest命令说明:
Method | url地址 | 描述 |
---|---|---|
PUT | localhost:9200/索引名称/类型名称/文档ID | 创建文档(指定文档ID) |
POST | localhost:9200/索引名称/类型名称 | 创建文档(随机文档ID) |
POST | localhost:9200/索引名称/类型名称/文档ID/_update | 修改文档 |
DELETE | localhost:9200/索引名称/类型名称/文档ID | 删除文档 |
GET | localhost:9200/索引名称/类型名称/文档ID | 查询文档通过文档ID |
POST | localhost:9200/索引名称/类型名称/_search | 查询所有数据 |
创建索引
PUT /test1/type1/1 { "name":"jimmy", "age":"21" }
ES中的字段类型
字符串类型
text(text类型会被分词器解析/拆分)、keyword(keyword类型不会被分词器解析)
数值类型
long、integer、short、byte、double、float、half、float、scaled、float
日期类型
date
布尔类型
boolean
二进制类型
binary
等等…
创建规则:创建索引时指定字段的类型。
PUT /test2 { "mappings": { "properties": { "name":{ "type":"text" }, "age":{ "type":"integer" } } } }
获取test2索引的规则信息,通过GET请求获取
GET /test2
获取test1索引的规则信息(创建test1索引时没有设置字段类型规则,es会帮我们自动配置)
# 查询索引信息 GET /test1
拓展命令
# 获取ES健康值 GET _cat/health # 查看ES的所有信息 GET _cat/indices?v
修改文档
修改文档有两种方式:
- 1、通过再次提交去覆盖原有的值
PUT /test1/type1/1 { "name":"邪恶的张三", "age":23 }
- 2、通过POST:localhost:9200/索引名称/类型名称/文档ID/_update的方式去更新文档中指定字段的值。灵活性更高
POST /test1/type1/1/_update { "doc":{ "name":"好人张三" } }
删除索引
DELETE test1
# 查询准备数据 # 1.创建索引及字段映射规则 PUT /search_data_list { "mappings": { "properties": { "name":{"type": "text"}, "age":{"type":"integer"}, "birthday":{"type":"date"}, "tags":{"type": "text"} } } } # 2.添加数据 PUT /search_data_list/_doc/1 { "name":"张三", "age":23, "birthday":"2021-04-13", "tags":["抽烟","喝酒","汤头"] } PUT /search_data_list/_doc/2 { "name":"李四", "age":24, "birthday":"2021-04-13", "tags":["抽烟","喝酒","汤头"] } PUT /search_data_list/_doc/3 { "name":"王五", "age":25, "birthday":"2021-04-13", "tags":["抽烟","喝酒","汤头"] } PUT /search_data_list/_doc/4 { "name":"王五均", "age":26, "birthday":"2021-04-13", "tags":["抽烟","喝酒","汤头"] } # 简单查询 # 1.查看索引结构 GET search_data_list # 2.查看_doc文档下文档ID为2的数据 GET search_data_list/_doc/2 # 3.查询 name=王五 的数据,右边查询出来的_score字段表示数据匹配度,值越大匹配度越高 GET search_data_list/_doc/_search?q=name:王五 # 复杂查询 # query 查询条件结果集 # match 相当于mysql中的and # _source 结果过滤只取需要的字段 GET search_data_list/_doc/_search { "query":{ "match": { "name": "王五" } }, "_source":["name","desc"] } # 排序 GET search_data_list/_doc/_search { "query":{ "match": { "name": "王五" } }, "sort":{ "age":{ "order":"asc" } } } # 分页 # from 起始值(从0开始) # size 偏移量 GET search_data_list/_doc/_search { "query":{ "match": { "name": "王五" } }, "sort":{ "age":{ "order":"asc" } }, "from":0, "size":1 } # 多条件查询 # match > and # should > or # must_not > != GET search_data_list/_doc/_search { "query":{ "bool":{ "must_not":[ { "match":{"name":"张三"} }, { "match":{"age":23} } ] } } } # 过滤器 # 使用 filter 对数据进行过滤 # gt大于 gte大于等于 lt小于 lte小于等于 GET search_data_list/_doc/_search { "query":{ "bool":{ "must":[ { "match":{"name":"王五"} } ], "filter":{ "range":{ "age":{ "lt":26 } } } } } } # 精确查询,通过倒排索引指定的词条进行精确查找 # term 直接查询精确的,match 会使用分词器解析(先分析文档,然后再通过分析文档进行查询) GET search_data_list/_doc/_search { "query":{ "term":{ "name":"王" } } } # 多条件精确查询 GET search_data_list/_doc/_search { "query":{ "bool":{ "should":[ { "term":{"age":13} }, { "term":{"age":25} } ] } } # 高亮查询 GET search_data_list/_doc/_search { "query":{ "match":{ "name":"张三" } }, "highlight":{ "fields":{ "name":{} } } } # 自定义高亮显示 GET search_data_list/_doc/_search { "query":{ "match":{ "name":"张三" } }, "highlight":{ "pre_tags":"<p>", "post_tags":"</p>", "fields":{ "name":{} } } }
导入依赖的时候需要注意,es客户端的版本需要和本地es服务的版本一致。
SpringBoot整合的es默认时使用父工程的版本,所以我们在编写代码前需要去看一下高级的Rest风格的es客户端版本。详细操作如下图:
我本地的es服务的版本是7.6.1的,这里依赖父工程的版本是7.6.2。保险请见我需要调整一下es客户端的版本
<properties> <java.version>1.8</java.version> <elasticsearch.version>7.6.1</elasticsearch.version> </properties> <!--EsClient的版本要和ES的版本一致--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>到这一步就可以开始写代码了…
package com.scholartang.config; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @Author ScholarTang * @Date 2021/4/13 2:02 下午 * @Desc ES 配置类 */ @Configuration public class ElasticSearchConfig { /** * 构建一个高级的Rest风格的es客户端实例 * 一个RestHighLevelClient实例需要一个REST低级别的客户端生成器 来构建如下 * @return */ @Bean public RestHighLevelClient restHighLevelClient(){ return new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200, "http"))); } }
package com.scholartang; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.indices.GetIndexRequest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.IOException; /** * es 7.6.1 高级客户端测试API(索引相关) */ @Slf4j @SpringBootTest class EsDemoApplicationTests { @Autowired private RestHighLevelClient restHighLevelClient; /** * 创建索引 */ @Test void testCreateIndex() throws IOException { //创建索引请求 CreateIndexRequest createIndexRequest = new CreateIndexRequest("create_index_01"); //客户端执行请求 restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT); } /** * 获取索引 */ @Test public void testGetIndex() throws IOException { //创建获取索引请求 GetIndexRequest getIndexRequest = new GetIndexRequest("create_index_01"); //客户端执行请求 boolean exists = restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT); log.info("索引" + (exists ? "存在" : "不存在")); } /** * 删除索引 */ @Test public void testDeleteIndex() throws IOException { //创建删除索引请求 DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("create_index_01"); //客户端执行请求 restHighLevelClient.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT); } }
package com.scholartang; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.indices.GetIndexRequest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.io.IOException; /** * es 7.6.1 高级客户端测试API(文档相关) */ @Slf4j @SpringBootTest class EsDemoApplicationTests { @Autowired private RestHighLevelClient restHighLevelClient; /** 添加文档 */ @Test public void testAddDocument() throws IOException { // 创建一个索引请求 IndexRequest indexRequest = new IndexRequest("create_index_01"); // 文档ID indexRequest.id("1"); // 设置请求超时时间 indexRequest.timeout(TimeValue.timeValueMillis(1)); // 也可以这么写 indexRequest.timeout("60s"); // 设置文档数据 indexRequest.source(JSON.toJSONString(new User(1, "张三", "男", 23)), XContentType.JSON); // 客户端发送添加文档请求 IndexResponse index = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT); log.info("IndexResponse:{}", JSON.toJSONString(index)); } /** 获取文档信息 */ @Test public void testGetDocument() throws IOException { // 创建一个获取请求 get /index/_doc/documentId GetRequest getRequest = new GetRequest("create_index_01", "5"); GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT); if (getResponse.isExists()) { String source = getResponse.getSourceAsString(); log.info("source:{}", source); } } /** 更新文档信息 */ @Test public void testUpdateDocument() throws IOException { // 创建一个更新请求 UpdateRequest updateRequest = new UpdateRequest("create_index_01", "1"); updateRequest.timeout("60s"); updateRequest.doc(JSON.toJSONString(new User(2, "kuang", "女", 23)), XContentType.JSON); UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT); log.info("updateStatus:{}", updateResponse.status()); } /** 删除文档 */ @Test public void testDeleteDocument() throws IOException { // 创建一个删除请求 DeleteRequest deleteRequest = new DeleteRequest("create_index_01", "1"); deleteRequest.timeout("60s"); DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT); log.info("deleteResponse:{}", deleteResponse.status()); } /** 批量插入文档 */ @Test public void testBathAddDocument() throws IOException { BulkRequest bulkRequest = new BulkRequest(); bulkRequest.timeout("60s"); ArrayList<User> users = new ArrayList<>(); Collections.addAll( users, new User(1, "kuang1", "女", 23), new User(2, "kuang2", "女", 23), new User(3, "kuang3", "女", 23), new User(4, "kuang4", "女", 23)); for (int i = 0; i < users.size(); i++) { bulkRequest.add( // 批量的更新操作和删除操作,在这里修改请求方式即可 new IndexRequest("create_index_01") .id((i + 1) + "") .source(JSON.toJSONString(users.get(i)), XContentType.JSON)); } BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT); // 操作是否失败,返回false则为成功 log.info("bulkResponseStatus:{}", bulkResponse.isFragment()); } /** 批量查询操作 */ @Test public void testBathGetDocument() throws IOException { // 创建批量查询请求 SearchRequest searchRequest = new SearchRequest("create_index_01"); // 构建查询条件 SearchSourceBuilder builder = new SearchSourceBuilder(); // QueryBuilders 条件工具类 // termQuery 精确查询 TermQueryBuilder termQuery = QueryBuilders.termQuery("name", "kuang1"); // matchAllQuery 匹配所有 // MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery(); // builder.query(termQuery); // 根据文档ID批量查询 IdsQueryBuilder idsQueryBuilder = new IdsQueryBuilder().addIds("1", "2", "3", "100"); builder.query(idsQueryBuilder); // 设置请求超时时间 builder.timeout(new TimeValue(60, TimeUnit.SECONDS)); searchRequest.source(builder); // 客户端发起请求 SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT); RestStatus status = searchResponse.status(); if ("ok".equalsIgnoreCase(status.toString())) { // 所有的结果都是在 SearchHits 里面 List<Map> maps = new ArrayList<>(); SearchHits responseHits = searchResponse.getHits(); SearchHit[] hits = responseHits.getHits(); for (int i = 0; i < hits.length; i++) { maps.add(hits[i].getSourceAsMap()); } log.info(JSON.toJSONString(maps)); } } }