本片博客是本人通过观看
狂神说
的视频记录的笔记,以此记录方便需要时查阅。
视频地址: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容器
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":""
,
"post_tags":"",
"fields":{
"name":{
}
}
}
}
导入依赖的时候需要注意,es客户端的版本需要和本地es服务的版本一致。
SpringBoot整合的es默认时使用父工程的版本,所以我们在编写代码前需要去看一下高级的Rest风格的es客户端版本。详细操作如下图:
我本地的es服务的版本是7.6.1的,这里依赖父工程的版本是7.6.2。保险请见我需要调整一下es客户端的版本
<properties> <java.version>1.8java.version> <elasticsearch.version>7.6.1elasticsearch.version> properties> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-data-elasticsearchartifactId> 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));
}
}
}