01. ElasticSearch简介
1.1 数据库存在的问题
目标:能够说出数据库查询存在的问题
问题一:查询title中包含 “手机” 的信息?
SELECT * FROM goods WHERE title LIKE '%手机%';
结论:如果使用模糊查询,左边有通配符,不会走索引,会全表扫描,性能低
问题二:查询title中包含‘华为手机’的信息?
SELECT * FROM goods WHERE title LIKE ‘%华为手机%';
结论:关系型数据库提供的查询,功能太弱
1.2 倒排索引
目标:了解倒排索引的存储原理
使用新华字典查找汉字,先找到汉字的偏旁部首,再根据偏旁部首对应的目录(索引)找到目标汉字。
ES样例:
- 文档0(编号0): we like java java java
- 文档1(编号1): we like lucene lucene lucene
建立倒排索引的流程:
1)首先对所有数据的内容进行拆分,拆分成唯一的一个个词语(词条)
2)然后建立词条和每条数据的对应关系
(Term 词条) | (Doc ID,Freq 频率) | (Pos 位置) |
---|---|---|
we | (0,1) (1,1) | (0)(0) |
like | (0,1) (1,1) | (1)(1) |
java | (0,3) | (2,3,4) |
lucene | (1,3) | (2,3,4) |
小结
倒排索引:将每条数据中的内容进行分词,形成词条。然后记录词条和数据的唯一标识(id)的对应关系,形成的产物。
1.3 ElasticSearch存储和搜索原理
目标:能够说出ElasticSearch存储和搜索原理
假设数据库存在以下几条数据:
ES中存储及搜索原理图:
说明:ES的数据库我们称之为 index(索引库),每条数据我们称之为 document(文档),ES在存储文档的时候,会对它需要分词的字段内容进行切分,切分成一个个词条,再建立每个词条与文档唯一标识(id)的对应关系,即倒排索引。
我们再回过头看之前数据库存在的两个问题,通过ES是否能够解决:
-
性能低:使用模糊查询,左边有通配符,不会走索引,会全表扫描,性能低
ES解决方案:如果使用"手机"作为关键字查询,ES生成的倒排索引中,词条会排序,形成一颗树形结构,提升词条的查询速度。
-
功能弱:如果以"华为手机" 作为条件,查询不出来数据
ES解决方案:如果使用"华为手机"作为关键字查询,ES也可以对搜索的关键字进行分词,比如将华为手机拆分成"华为"、“手机”,然后根据两个词分词去倒排索引中进行查询,然后取结果的并集。
1.4 ElasticSearch概念
1.4.1 介绍
ElasticSearch是Java语言开发的,并作为Apache许可条款下的开放源码发布,基于Lucene实现,是一款分布式、高扩展、近实时的搜索服务,可以基于RESTful web接口进行操作。官网:https://www.elastic.co/
1.4.2 应用场景
-
海量数据的查询
-
日志数据分析
-
实时数据分析
1.4.3 与数据库的区别
1)结构上的区别
索引库/索引(index)
ElasticSearch存储数据的地方,可以理解成关系型数据库中的数据库概念。
映射(mapping)
mapping定义了每个字段的类型、字段所使用的分词器等。相当于关系型数据库中的表结构。
类型(type)
相当于关系型数据库中的表。在Elasticsearch7.X默认type为 _doc,
ES 5.x中一个index可以有多种type。
ES 6.x中一个index只能有一种type。
ES 7.x以后,将逐步移除type这个概念,现在的操作已经不再使用,默认_doc
文档(document)
Elasticsearch中的最小数据单元,常以json格式显示。相当于关系型数据库中的一行数据。
倒排索引
一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,对应一个包含它的文档id列表。
2)功能上的区别
- MySQL有事务性,而ElasticSearch没有事务性,所以数据安全性、一致性较低。
- MySQL使用方便,DML语法丰富,ES相对较弱
ElasticSearch和MySql分工不同,MySQL负责存储数据,ElasticSearch负责海量数据的搜索。
1.5 小结
02. 安装ElasticSearch
2.1 安装ElasticSearch
请参考 资料\elasticsearch安装.md
进行安装
2.2 安装Kibana及Postman
请参考 资料\elasticsearch安装.md
进行安装
03. 脚本操作ES
3.1 RESTful风格介绍
REST(Representational State Transfer 表述性状态转移),是一组架构约束条件和原则,满足这些约束条件和原则的应用程序或设计就是RESTful。就是一种定义接口的规范。有以下特征:
-
基于HTTP
-
可以使用XML格式定义或JSON格式定义参数和返回值。
-
每一个URI代表1种资源。
-
客户端使用GET、POST、PUT、DELETE 4个表示操作方式的动词对服务端资源进行操作:
- GET:用来获取资源
- POST:用来新建资源(也可以用于更新资源)
- PUT:用来新建资源
- DELETE:用来删除资源
如果有一个/user资源
get: /user/1 ---查询
delete: /user/1 ---删除
post: /user ---添加、修改
put: /user ---添加
3.2 操作索引
- 添加索引
PUT http://ip:端口/索引名称
- 查询索引
# 查询单个索引信息
GET http://ip:端口/索引名称
# 查询多个索引信息
GET http://ip:端口/索引名称1,索引名称2...
# 查询所有索引信息
GET http://ip:端口/_all
- 删除索引
DELETE http://ip:端口/索引名称
- 关闭索引
POST http://ip:端口/索引名称/_close
说明:当索引进入关闭状态,是不能添加文档的
- 打开索引
POST http://ip:端口/索引名称/_open
3.3 操作映射
3.3.1 数据类型
ES中包含两类数据类型:简单数据类型和复杂数据类型
1)简单数据类型
-
字符串
-
text:会分词,不支持聚合
-
keyword:不会分词,将全部内容作为一个词条,支持聚合
例如:有个文档(相当于数据库一条数据),其中一个字段的值是华为手机 text: 华为、手机 keyword: 华为手机
-
-
数值
-
布尔(boolean)
-
二进制(binary)
-
范围类型(integer_range, float_range, long_range, double_range, date_range)(了解即可)
-
日期(date)
2)复杂数据类型
-
数组 []:没有专用的array数据类型,任何一个字段的值,都可以被添加0个到多个,但要求他们的类型必须一致,当多个值存储到ES的某条数据的某字段时,数据类型会自动转化成数组类型
-
对象 {}
3.3.2 kibana操作映射
- 添加映射
# 创建索引
PUT person
# 查询索引
GET person
# 添加映射
PUT person/_doc/_mapping
{
"properties": {
"name": {
"type": "keyword"
},
"age": {
"type": "integer"
}
}
}
- 查询映射
# 查询映射
GET person/_doc/_mapping
- 创建索引并添加映射
# 创建索引并添加映射
PUT person2
{
"mappings": {
"_doc": {
"properties": {
"age": {
"type": "integer"
},
"name": {
"type": "keyword"
}
}
}
}
}
- 添加字段(同添加映射),不支持修改字段
# 添加字段
PUT person/_doc/_mapping
{
"properties": {
"address": {
"type": "text"
}
}
}
说明:在ES7.0之后,脚本操作不需要携带_doc
3.4 操作文档
目标:能够在kibana中操作文档
- 添加文档,指定id
# 添加文档,指定id
PUT person/_doc/1
{
"name": "张三",
"age": 20,
"address": "广东天河区"
}
- 添加文档,不指定id(自动分配一个UUID作为id)
# 添加文档,不指定id
POST person/_doc
{
"name": "李四",
"age": 19,
"address": "广东黄埔区"
}
- 查询文档
# 根据id查询文档
GET person/_doc/1
# 查询所有文档
GET person/_doc/_search
或
GET person/_search
- 修改文档(同指定id添加文档,如果id已存在就是修改文档)
# 修改文档
PUT person/_doc/1
{
"name": "张三222",
"age": 22,
"address": "广东天河区"
}
- 删除文档
# 删除文档
DELETE person/_doc/0bB_yHcByJaCSNWY-GM1
说明:在ES7.0之后,脚本操作不需要携带_doc
04. 分词器
4.1 分词器介绍
分词器(Analyzer)是将一段文本,按照一定逻辑,拆分成多个词语的一种工具,如:华为手机 —> 华为、手、手机,ElasticSearch 内置分词器有以下几种:
- Standard Analyzer - 默认分词器,按词切分,小写处理
- Simple Analyzer - 按照非字母切分(符号被过滤),小写处理
- Stop Analyzer - 小写处理,停用词过滤(the,a,is)
- Whitespace Analyzer - 按照空格切分,不转小写
- Keyword Analyzer - 不分词,直接将输入当作输出
- Patter Analyzer - 正则表达式,默认\W+(非字符分割)
- Language - 提供了30多种常见语言的分词器
ES提供了一个接口给我们来验证分词效果,如下所示:
# 分词效果验证,若不指定analyzer默认使用standard分词器
GET _analyze
{
"text": "我爱黑马程序员",
"analyzer": "standard"
}
分词结果:
ElasticSearch 可以建立倒排索引时对字段内容进行分词、也可以在搜索时会搜索关键字进行分词。
ElasticSearch 内置分词器对中文很不友好,处理方式为:一个字一个词。
4.2 IK分词器介绍及安装
IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包,是一个基于Maven构建的项目,具有60万字/秒的高速处理能力,并且支持用户词典扩展定义。下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases
安装请参考 资料\ik分词器安装.md
4.3 查询文档-使用IK分词器
目标:能够在kibana中使用term查询文档
4.3.1 高级查询介绍
- 词条查询:term
词条查询不会对查询条件进行分词,只有当查询字符串和倒排索引中的词条完全匹配时才可以搜索到
4.3.2 term搜索
# term查询,查询的关键字必须和词条完全匹配
GET person/_search
{
"query": {
"term": {
"address": {
"value": "广东"
}
}
}
}
查询结果如下:
可以发现查询结果为空,因为我们之前在添加文档时,并没有指定字段使用哪种分词器,它会默认使用standard单字分词器,将address的内容分成一个个字来和文档建立倒排索引。因此如果我们查询的关键字如果是"广"、"东"这样一个个字就可以查询到结果
# term查询,查询的关键字必须和词条完全匹配
GET person/_search
{
"query": {
"term": {
"address": {
"value": "广"
}
}
}
}
实际项目中,我们查询一般是按照一个个词来进行查询,所以如果使用standard分词器,查询效果并不好,因此我们在创建索引之初一般会指定需要分词的字段的分词器是ik分词器,我们删除person索引库并进行重建,脚本如下:
# 删除索引库
DELETE person
# 创建索引库并指定映射
PUT person
{
"mappings": {
"_doc": {
"properties": {
"age": {
"type": "integer"
},
"name": {
"type": "keyword"
},
"address":{
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
}
# 再重新添加文档
PUT person/_doc/1
{
"name": "张三",
"age": 20,
"address": "广东天河区"
}
这个时候我们再来搜索关键字"广东" 就搜索到结果了,我们可以根据ES提供的分词验证接口,验证广东天河区的分出了哪些词,如下所示:
可以看出在存储数据的时候,广东天河区可以分出"广东"这个词,因此查询广东可以查询到结果。
05. ElasticSearch HighLevelAPI
5.1 springboot整合ES
1)搭建springboot工程,并添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
</parent>
<groupId>cn.itcast</groupId>
<artifactId>springboot_es</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!--test启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.6.2</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>6.6.2</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.6.2</version>
</dependency>
</dependencies>
</project>
2)添加启动类和yml配置文件如下:
3)编写elasticsearch配置类
package cn.itcast.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* ES配置类
*
* @Author LK
* @Date 2021/2/22
*/
@Configuration
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticSearchConfig {
private String host;
private int port;
public void setHost(String host) {
this.host = host;
}
public void setPort(int port) {
this.port = port;
}
@Bean
public RestHighLevelClient restHighLevelClient() {
RestHighLevelClient restHighLevelClient = new RestHighLevelClient(RestClient.builder(
new HttpHost(
"192.168.211.129",
9200,
"http"
)
));
return restHighLevelClient;
}
}
4)编写yml配置文件
elasticsearch:
host: 192.168.211.129
port: 9200
5)编写单元测试类
package cn.itcast;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ElasticTest {
@Autowired
private RestHighLevelClient client;
@Test
public void test(){
System.out.println(client);
}
}
6)项目结构
5.2 添加索引
1) 添加空索引
/**
* 添加索引
*/
@Test
public void addIndex() throws Exception{
// 1.使用client获取操作索引的对象
IndicesClient indices = client.indices();
// 2.执行操作,获取返回值
CreateIndexRequest request = new CreateIndexRequest("itheima");
CreateIndexResponse response = indices.create(request, RequestOptions.DEFAULT);
// 3.判断结果
System.out.println(response.isAcknowledged());
}
运行结果如下:
2) 添加索引并添加映射
/**
* 创建索引并添加映射
*/
@Test
public void addIndexAndMapping() throws Exception{
// 1.使用client获取操作索引的对象
IndicesClient indices = client.indices();
// 2.执行操作,获取返回值
CreateIndexRequest request = new CreateIndexRequest("itcast");
// 2.1 设置映射信息
String mapping = "{\n" +
" \"_doc\" : {\n" +
" \"properties\" : {\n" +
" \"address\" : {\n" +
" \"type\" : \"text\",\n" +
" \"analyzer\" : \"ik_max_word\"\n" +
" },\n" +
" \"age\" : {\n" +
" \"type\" : \"integer\"\n" +
" },\n" +
" \"name\" : {\n" +
" \"type\" : \"keyword\"\n" +
" }\n" +
" }\n" +
" }\n" +
" }";
// 指定表名,以及映射的格式
request.mapping("_doc", mapping, XContentType.JSON);
CreateIndexResponse response = indices.create(request, RequestOptions.DEFAULT);
// 3.判断结果
System.out.println(response.isAcknowledged());
}
运行结果如下:
5.3 删除索引
/**
* 删除索引
*/
@Test
public void deleteIndex() throws Exception{
// 1.使用client获取操作索引的对象
IndicesClient indices = client.indices();
// 2.执行操作,获取返回值
DeleteIndexRequest request = new DeleteIndexRequest("itheima");
AcknowledgedResponse response = indices.delete(request, RequestOptions.DEFAULT);
// 3.打印结果
System.out.println(response.isAcknowledged());
}
5.4 添加文档
应用场景:添加、修改、删除文档作用就是同步数据库的数据到ES的索引库中
方式一
/**
* 添加文档
*/
@Test
public void addDoc() throws Exception {
// 1.构建文档数据
Map<String, Object> data = new HashMap<String, Object>();
data.put("name", "小kk");
data.put("age", 20);
data.put("address", "广州天河区");
// 2.构建请求对象
// 参数1-索引库名称,参数2-类型名称,参数3-主键id(不指定使用uuid)
IndexRequest request = new IndexRequest("itcast", "_doc", "1").source(data);
// 3.执行,获取返回结果
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
// 4.打印查询结果
System.out.println(response.getId());
}
运行结果:
方式二
第一步:创建实体类
package cn.itcast.pojo;
public class Person {
private String id;
private String name;
private int age;
private String address;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
第二步:pom文件添加jackson-databind依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.2</version>
</dependency>
第三步:编写测试用例
/**
* 添加文档,通过对象构建
*/
@Test
public void addDoc2() throws Exception{
// 1.构建文档数据
Person data = new Person();
data.setId("2");
data.setName("小zz");
data.setAge(21);
data.setAddress("广州黄埔区");
// 1.1 将对象转为json
String jsonStr = new ObjectMapper().writeValueAsString(data);
// 2.构建请求对象
// 参数1-索引库名称,参数2-类型名称,参数3-主键id(不指定使用uuid)
IndexRequest request = new IndexRequest("itcast", "_doc", data.getId()).source(jsonStr, XContentType.JSON);
// 3.执行,获取返回结果
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
// 4.打印结果
System.out.println(response.getId());
}
运行结果:
5.5 修改、查询、删除文档
1)修改文档
/**
* 修改文档,如果id存在便修改,如果不存在就添加
*/
@Test
public void updateDoc() throws Exception{
// 1.构建文档数据
Person data = new Person();
data.setId("2");
data.setName("小zzzz");
data.setAge(22);
data.setAddress("广州黄埔区");
// 1.1 将对象转为json
String jsonStr = new ObjectMapper().writeValueAsString(data);
// 2.构建请求对象
// 参数1-索引库名称,参数2-类型名称,参数3-主键id
IndexRequest request = new IndexRequest("itcast", "_doc", data.getId()).source(jsonStr, XContentType.JSON);
// 3.执行,获取返回结果
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
// 4.打印结果
System.out.println(response.getId());
}
以上执行完毕可将id为2的数据修改成data的数据
2)查询文档
/**
* 根据id查询文档
* @throws Exception
*/
@Test
public void findDocById() throws Exception {
// 1.构建请求对象
// 参数1-索引库名称,参数2-类型名称,参数3-主键id
GetRequest request = new GetRequest("itcast", "_doc", "1");
// 2.执行,获取返回结果
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.打印结果
System.out.println(response.getSourceAsString());
}
运行结果:
3)删除文档
/**
* 根据id删除
* @throws Exception
*/
@Test
public void deleteDocById() throws Exception {
// 1.构建请求对象
// 参数1-索引库名称,参数2-类型名称,参数3-主键id
DeleteRequest request = new DeleteRequest("itcast", "_doc", "1");
// 2.执行结果,获取返回结果
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
// 3.打印结果
System.out.println(response.getId());
}
以上执行完毕可将id为1的数据进行删除
5.6 批量操作
Bulk 批量操作是将文档的增删改查一些列操作,通过一次请求全都做完。减少网络传输次数。
**应用场景:**ES索引库数据初始化的时候,可以将数据库的数据查询出来通过批量操作导入到索引库中
脚本操作
语法:
POST _bulk
{"action": {"metadata"}}
{"data"}
示例:
# 1.删除id为5的文档
# 2.添加id为6的文档
# 3.修改id为2的文档
POST _bulk
{"delete":{"_index":"person","_type":"_doc","_id":"5"}} # 指定删除person索引库中_doc表的id为5的文档
{"create":{"_index":"person","_type":"_doc","_id":"6"}} # 指定新增文档到person索引库的_doc表中,id为6
{"name":"六号","age":60,"address":"广州越秀区"} # 新增文档的内容
{"update":{"_index":"person","_type":"_doc","_id":"2"}} # 指定修改person索引库中_doc表的id为2的文档
{"doc": {"name":"二号"}} # 修改的内容
代码操作
1)编写单元测试用例
/**
* 批量操作
*/
@Test
public void bulk() throws Exception{
// 1.创建批量请求对象
BulkRequest bulkRequest = new BulkRequest();
// 2.循环添加请求对象
for (int i = 10; i < 20; i++) {
IndexRequest indexRequest = new IndexRequest("itcast", "_doc");
// 2.1.构建文档数据
Person data = new Person();
data.setId(i + "");
data.setName("张" + i);
data.setAge(20);
data.setAddress("广州黄埔区");
// 2.2 将对象转为json
String jsonStr = new ObjectMapper().writeValueAsString(data);
indexRequest.source(jsonStr, XContentType.JSON);
// 2.3 添加请求对象
bulkRequest.add(indexRequest);
}
// 3.执行请求,获取返回结果
BulkResponse response = client.bulk(bulkRequest, RequestOptions.DEFAULT);
// 4.打印执行状态,返回200代表执行成功
System.out.println(response.status().getStatus());
}