Elastic Search ​
1 引例 ​
现在假设我们有如下的一张数据库表

现在假设我们有一个需求是从这张表中搜索“手机”或者“华为手机”的相关信息,我们会怎么做?
select * from product where name like "%手机%";
select * from product where name like "%华为手机%";但是针对这张表,我们如果直接通过以上SQL语句来查询,会存在两个问题:
- 针对单表的全表扫描效率低
- 关系数据库中提供的查询功能弱
思考一下,我们的一个电商网站中,有上百万商品数据,如果我们采用like的方式,去实现商品搜索的功能,其效率是非常低下的,同时由于用户输入的商品关键字是比较随意的,使用like的方式往往也很难真正查询到用户想要的商品。
2 ES介绍及基本概念 ​
在很多场景下,比如电商网站的商品搜索,我们都需要使用全文检索的功能查询所需的内容,那么如何高效的实现全文检索的功能呢?解决之道就在于Elastic Search:
ElasticSearch是一个基于Lucene 的分布式、高扩展、高实时的基于RESTful 风格API的搜索与数据分析引擎
为什么Elastic Search能够实现大规模数据场景下的高效全文检索呢?主要是因为在ES中,数据的存储和组织方式与关系数据库不同。
我们首先需要了解ES中的基本概念:
字段(Field): 一个字段表示一个属性,类比于数据库表中的属性,数据库中的一行数据通常是多个属性值组成
文档(document):在ES中数据的存储和关系数据库不同,所有的数据都是以文档的JSON document的形式存在,document是ES中索引和搜索的最小的数据单位,类比于数据库中的一行数据,通常有多个字段值组成
映射(mapping): mapping定义了document中每个字段的类型、字段所使用的分词器等。相当于关系型数据库中的表结构。
索引(index): ElasticSearch存储数据的地方,可以理解成关系型数据库中的数据库概念,存放一类相同或者类似的document,比如一个员工索引,商品索引。
类型(Type):逻辑上的数据分类,一种type就像一张表。如用户表、角色表等。在Elasticsearch6.X默认type为_doc,es 7.x中删除了type的概念
在了解了ES的基本概念之后,接下来,我们可以大致解释下,为何ES可以实现高效的全文检索功能,其中一个很重要的原因是ES使用了倒排索引(这里要注意的一点是,倒排索引和文档的存储本身没有关系,只是为了快速的全文检索,针对document的一个或者多个字段,所创建的索引)

那么什么是倒排索引呢?为了理解什么是倒排索引,我们先来理解正排索引。针对以下3条document文档数据所创建的正排索引如下:

- 凡是索引其本质都是在建立一种映射关系
- 无论是正排索引还是倒排索引,在创建索引时都是要对目标字段的值进行分词的。
- 正排索引,建立的是文档的唯一标识Id(内容所在位置) ——> 文档目标字段内容的映射
如果我们是基于正向索引来查找和“华为手机”相关的商品信息(根据商品的title字段的值来匹配),首先对搜索的关键字也会做分词处理,比如分解为"华为"和"手机"两个关键词,然后遍历,每一个文档,和将关键词和文档中的分词内容进行匹配,此时,我们可以查找到我们所需要的华为手机的相关商品信息。
但是,使用正向索引我们仍然无法避免,遍历每一个商品对应的文档("类似全表扫描"),这种匹配方式,效率比较低下。接下来,我们换种方式,基于倒排索引,来实现搜索。

- 反向索引是以文段目标字段所有可能的分词结果为key,其value表达的是,包含改词的文档Id(即文档位置)
- 通过对比可知,反向索引表达的是文档字段内容 ——> 包含该内容的文档位置(文档Id)的映射
如果我们,基于倒排索引,来查找和关键字“华为手机”相关的商品,那么很显然,我们很容易就可以查询出想要的结果,并且还不需要,遍历每一个商品信息。同理,ES在搜索时就是基于倒排索引,所以它的搜索性能很好。

3 ES及可视化客户端安装 ​
参考 [环境搭建.md](..\08_ 环境搭建\环境搭建.md)
以下内容为Linux环境上的安装方式参考,在项目中不使用。
3.1 ElasticSearch安装 ​
利用winscp上传ElasticSearch安装包
执行解压操作 ,如下图
# 将elasticsearch-7.4.0-linux-x86_64.tar.gz解压到opt文件夹下. -C 大写
tar -zxvf elasticsearch-7.4.0-linux-x86_64.tar.gz -C /opt/es- 修改elasticsearch.yml文件
vim /opt/es/elasticsearch-7.4.0/config/elasticsearch.yml# ======================== Elasticsearch Configuration =========================
cluster.name: my-application
node.name: node-1
network.host: 0.0.0.0
http.port: 9200
cluster.initial_master_nodes: ["node-1"]cluster.name:配置elasticsearch的集群名称,默认是elasticsearch。建议修改成一个有意义的名称
node.name:节点名,elasticsearch会默认随机指定一个名字,建议指定一个有意义的名称,方便管理
network.host:设置为0.0.0.0允许外网访问
http.port:Elasticsearch的http访问端口
cluster.initial_master_nodes:初始化新的集群时需要此配置来选举master
- 修改配置文件
系统默认用户最大可创建文件数太小,最大虚拟内存太小,要让es正常运行,需要修改操作系统配置如下:
#1. ===最大可创建文件数太小=======
sudo vim /etc/systemd/system.conf
# 在文件末尾添加如下内容
DefaultLimitNOFILE=65536
sudo vim /etc/systemd/user.conf
# 在文件末尾添加如下内容
DefaultLimitNOFILE=65536
#2. ===最大虚拟内存太小=======
vim /etc/sysctl.conf
# 在文件中增加下面内容
vm.max_map_count=655360
# 重新加载,输入下面命令:
sysctl -p
# 输入如下命令重启系统
sudo reboot ubuntu8、启动elasticsearch
cd /opt/elasticsearch-7.4.0/bin
./elasticsearch #启动
通过上图我们可以看到elasticsearch已经成功启动
浏览器输入http://192.168.149.135:9200/,如下图

此时elasticsearch已成功启动:
重点几个关注下即可:
number" : "7.4.0" 表示elasticsearch版本
lucene_version" : "8.2.0" 表示lucene版本
name : 默认启动的时候指定了 ES 实例名称
cluster_name : 默认名为 elasticsearch这里需要注意一点,有的同学虚拟机或者云服务器的内存较小,测试我们需要限制下es的内存大小。打开config目录下的jvm.options文件

3.2 Kibana安装 ​
Kibana是一个针对Elasticsearch的开源分析及可视化平台,用来搜索、查看交互存储在Elasticsearch索引中的数据。使用Kibana,可以通过各种图表进行高级数据分析及展示。
- 通过winscp上传Kibana安装包
- 执行解压操作
tar -xzf kibana-7.4.0-linux-x86_64.tar.gz -C /opt/es- 打开kibana配置文件
vim /opt/kibana-7.4.0-linux-x86_64/config/kibana.yml- 添加如下配置
server.port: 5601
server.host: "0.0.0.0"
server.name: "kibana"
elasticsearch.hosts: ["http://127.0.0.1:9200"]
elasticsearch.requestTimeout: 99999server.port:http访问端口
server.host:ip地址,0.0.0.0表示可远程访问
server.name:kibana服务名
elasticsearch.hosts:elasticsearch地址
elasticsearch.requestTimeout:请求elasticsearch超时时间,默认为30000,此处可根据情况设置
- 启动kibana
# 切换到kibana的bin目录
cd /opt/kibana-7.4.0-linux-x86_64/bin
# 启动
./kibana
访问kibana

在启动kibana之前,我们也设置下kibana的内存:

NODE_OPTIONS="$NODE_OPTIONS --max-old-space-size=200"4 Restful API操作ES ​
ES虽然是基于Java语言开发的,但它的使用却不仅仅局限于Java语言,因为ES对外提供了Restful风格的API,我们可以通过这些Restful风格的API向ES发送请求,从而操作ES。
4.1 操作索引 ​
- 创建索引
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:端口/索引名称/_open4.2 数据类型 ​
4.2.1 简单数据类型 ​
我们先来看一个简单的映射定义:
PUT teacher/_mapping
{
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "text"
},
"isMale": {
"type": "boolean"
}
}
}在定义映射的时候,类比于定义数据库中的表结构,我们需要指明每一个字段的名称,数据类型等等信息,所以我们先得了解映射中包含的数据类型。
- 字符串
text:会分词,不支持聚合
keyword:不会分词,将全部内容作为一个词条,支持聚合数值:long, integer, short, byte, double, float, half_float, scaled_float
布尔:boolean
二进制:binary
范围类型
integer_range, float_range, long_range, double_range, date_range- 日期:date
4.2.2 复杂数据类型 ​
•数组:[ ] 没有专门的数组类型,ES会自动处理数组类型数据
比如说 给某一个字段 定义为 integer类型,那么这个字段是可以存储 integer数组的,ES会自动处理
•对象:{ } Object: object(for single JSON objects 单个JSON对象)
4.3 操作映射 ​
- 添加映射
#添加映射
PUT student/_mapping
{
"properties":{
"name":{
"type":"text"
},
"age":{
"type":"integer"
}
}
}
#查询映射
GET studnt/_mapping- 创建索引并添加映射
#创建索引并添加映射
PUT teacher
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"age": {
"type": "integer"
}
}
}
}
# 查询映射
GET teacher/_mapping- 添加字段
#添加字段
PUT teacher/_mapping
{
"properties": {
"name": {
"type": "text"
},
"age": {
"type": "integer"
}
}
}
# 查询映射
GET teacher/_mapping4.4 操作文档 ​
•添加文档,指定id
POST teacher/_doc/2
{
"name":"张三",
"age":18,
"address":"北京"
}
# 根据文档id 查询文档
GET /teacher/_doc/2•添加文档,不指定id
#添加文档,不指定id,自动生成
POST teacher/_doc/
{
"name":"张三",
"age":18,
"address":"北京"
}
#查询所有文档
GET /teacher/_search修改文档(可以只修改部分)
jsonPOST teacher/_update/2 { "doc": { "name": "李四" } }全部覆盖修改
jsonPOST /user/_doc/1 { "id": 2001, "name":"刘姥姥", "age":80 }删除文档
#删除指定id文档
DELETE teacher/_doc/15 分词器 ​
对于Elastic Search而言,在生成倒排索引时,需要对文档字段分词,在搜索时,还需要对搜索关键字进行分词,分词的工作是由分词器来完成的,但是很遗憾,ES中默认使用的分词器,对中文支持的并不好,会对中文逐字拆分。所以对于中文内容的分词,我们通常会采用,对中文支持比较好的IK分词器。
IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包,基于Maven构建,具有60万字/秒的高速处理能力,并且支持用户词典扩展定义。
5.1 IK分词器的安装 ​
参考 [环境搭建.md](..\08_ 环境搭建\环境搭建.md)
- 安装JDK
- 下载IK分词器的压缩包,并解压:https://github.com/medcl/elasticsearch-analysis-ik
- 进入IK分词器的解压目录,利用mvn命令编译打包
# 切换到 elasticsearch-analysis-ik-7.4.0目录
#打包
mvn package- 将arget/releases的elasticsearch-analysis-ik-7.4.0.zip,移动到es安装目录的指定目录下
#进入es安装目录
cd /opt/es/elasticsearch-7.4.0/plugins/
#新建目录(ik分词器jar包就放在这个目录)
mkdir analysis-ik
# 将jar包上传到analysis-ik目录并解压(可能要先安装unzip命令)
unzip /opt/es/elasticsearch-7.4.0/plugins/analysis-ik/elasticsearch-analysis-ik-7.4.0.zip- 将elasticsearch-analysis-ik-7.4.0目录下的config目录中的所有文件 拷贝到elasticsearch的config目录
cp -R /opt/elasticsearch-analysis-ik-7.4.0/config/* /opt/elasticsearch-7.4.0/config- 重启es
5.2 IK分词器的使用 ​
IK分词器有两种分词模式:ik_max_word和ik_smart模式。
1、ik_max_word
会将文本做最细粒度的拆分,比如会将“好好学习, 天天向上”拆分为“好好学习,好好学、好好、好学、学习、天天向上、天天,向上。
#方式一ik_max_word
GET _analyze
{
"analyzer": "ik_max_word",
"text": "好好学习, 天天向上"
}ik_max_word分词器执行如下:
{
"tokens" : [
{
"token" : "好好学习",
"start_offset" : 0,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "好好学",
"start_offset" : 0,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "好好",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "好学",
"start_offset" : 1,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "学习",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "天天向上",
"start_offset" : 6,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "天天",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "向上",
"start_offset" : 8,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 7
}
]
}2、ik_smart 会做最粗粒度的拆分,比如会将“好好学习, 天天向上”拆分为好好学习、天天向上。
#方式二ik_smart
GET _analyze
{
"analyzer": "ik_smart",
"text": "好好学习, 天天向上"
}ik_smart分词器执行如下:
{
"tokens" : [
{
"token" : "好好学习",
"start_offset" : 0,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "天天向上",
"start_offset" : 5,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 1
}
]
}5.3 指定分词器查询文档 ​
文档的查询可以分为两种查询方式:
•词条查询(term):词条查询不会分析查询条件,只有当词条和查询字符串完全匹配时才匹配搜索
•全文查询(match):全文查询会分析查询条件,先将查询条件进行分词,然后查询,求并集
准备工作如下:
- 创建索引,添加映射,并指定分词器为ik分词器
PUT member
{
"mappings": {
"properties": {
"name": {
"type": "keyword"
},
"address": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}- 添加文档
POST member/_doc/1
{
"name":"zs",
"age":18,
"address":"武汉市洪山区"
}
POST member/_doc/2
{
"name":"lisi",
"age":18,
"address":"武汉市"
}
POST /member/_doc/3
{
"name":"ww",
"age":18,
"address":"武汉黄陂"
}- 查询映射
GET member/_search4.查看分词效果
GET _analyze
{
"analyzer": "ik_max_word",
"text": "武汉市洪山区"
}下面分别采用两种方式查询,分词都是用IK分词器:
- 词条查询:term
查询member中匹配到"武汉"两字的词条
GET member/_search
{
"query": {
"term": {
"address": {
"value": "武汉"
}
}
}
}全文查询:match
全文查询会分析查询条件,先将查询条件进行分词,然后查询,求并集
GET member/_search
{
"query": {
"match": {
"address":"武汉黄陂"
}
}
}6 Java 操作ES ​
我们仍然基于SpringBoot工程,首先引入依赖
<!--引入es的坐标-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.8.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>7.8.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.8.0</version>
</dependency>定义ES配置类
@Configuration
@ConfigurationProperties(prefix="elasticsearch")
public class ElasticSearchConfig {
private String host;
private int port;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
@Bean
public RestHighLevelClient client(){
return new RestHighLevelClient(RestClient.builder(
new HttpHost(host,port,"http")
));
}
}准备好测试类
@SpringBootTest
@RunWith(SpringRunner.class)
class ElasticsearchDay01ApplicationTests {
@Autowired
RestHighLevelClient client;
@Test
public void test() {
System.out.println(client);
}
}6.1 创建索引 ​
1.添加索引
/**
* 添加索引
* @throws IOException
*/
@Test
public void addIndex() throws IOException {
//1.使用client获取操作索引对象
IndicesClient indices = client.indices();
//2.具体操作获取返回值
//2.1 设置索引名称
CreateIndexRequest createIndexRequest=new CreateIndexRequest("member");
CreateIndexResponse createIndexResponse = indices.create(createIndexRequest, RequestOptions.DEFAULT);
//3.根据返回值判断结果
System.out.println(createIndexResponse.isAcknowledged());
}2.添加索引,并添加映射
/**
* 添加索引,并添加映射
*/
@Test
public void addIndexAndMapping() throws IOException {
//1.使用client获取操作索引对象
IndicesClient indices = client.indices();
//2.具体操作获取返回值
//2.具体操作,获取返回值
CreateIndexRequest createIndexRequest = new CreateIndexRequest("member");
//2.1 设置mappings
String mapping = "{\n" +
" \"properties\" : {\n" +
" \"address\" : {\n" +
" \"type\" : \"text\",\n" +
" \"analyzer\" : \"ik_max_word\"\n" +
" },\n" +
" \"age\" : {\n" +
" \"type\" : \"long\"\n" +
" },\n" +
" \"name\" : {\n" +
" \"type\" : \"keyword\"\n" +
" }\n" +
" }\n" +
" }";
createIndexRequest.mapping(mapping,XContentType.JSON);
CreateIndexResponse createIndexResponse = indices.create(createIndexRequest, RequestOptions.DEFAULT);
//3.根据返回值判断结果
System.out.println(createIndexResponse.isAcknowledged());
}6.2 查询、删除、判断索引 ​
查询索引
/**
* 查询索引
*/
@Test
public void queryIndex() throws IOException {
IndicesClient indices = client.indices();
GetIndexRequest getRequest=new GetIndexRequest("member");
GetIndexResponse response = indices.get(getRequest, RequestOptions.DEFAULT);
Map<String, MappingMetaData> mappings = response.getMappings();
for (String key : mappings.keySet()) {
System.out.println(key+":"+mappings.get(key).getSourceAsMap());
}
}删除索引
/**
* 删除索引
*/
@Test
public void deleteIndex() throws IOException {
IndicesClient indices = client.indices();
DeleteIndexRequest deleteRequest=new DeleteIndexRequest("member");
AcknowledgedResponse delete = indices.delete(deleteRequest, RequestOptions.DEFAULT);
System.out.println(delete.isAcknowledged());
}索引是否存在
/**
* 索引是否存在
*/
@Test
public void existIndex() throws IOException {
IndicesClient indices = client.indices();
GetIndexRequest getIndexRequest=new GetIndexRequest("member");
boolean exists = indices.exists(getIndexRequest, RequestOptions.DEFAULT);
System.out.println(exists);
}6.3 添加文档 ​
1.添加文档,使用map作为数据
@Test
public void addDoc1() throws IOException {
Map<String, Object> map=new HashMap<>();
map.put("name","zs");
map.put("age","18");
map.put("address","武汉市洪山区");
IndexRequest request=new IndexRequest("member").id("1").source(map);
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(response.getId());
}2.添加文档,使用对象作为数据
@Test
public void addDoc2() throws IOException {
Person person=new Person();
person.setId("2");
person.setName("lisi");
person.setAge(18);
person.setAddress("武汉");
String data = JSON.toJSONString(person);
IndexRequest request=new IndexRequest("member").id(person.getId()).source(data,XContentType.JSON);
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(response.getId());
}6.4 修改、查询、删除文档 ​
1.修改文档:添加文档时,如果id存在则修改(全量修改),或者做增量修改
/**
* 修改文档:添加文档时,如果id存在则修改,id不存在则添加
*/
@Test
public void UpdateDoc() throws IOException {
Person person=new Person();
person.setId("2");
person.setName("ww");
person.setAge(20);
person.setAddress("长城");
String data = JSON.toJSONString(person);
IndexRequest request=new IndexRequest("member").id(person.getId()).source(data,XContentType.JSON);
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(response.getId());
}
/*
增量修改,只修改文档的一部分
*/
@Test
public void update() throws IOException {
// 构造修改请求,修改member索引中,_id值为1的文档
UpdateRequest updateRequest = new UpdateRequest("member", "100023501");
// 准备修改的字段以及字段值
HashMap<String, Object> fieldMap = new HashMap<>();
// 假设只修改age字段的值
fieldMap.put("age", 100);
updateRequest.doc(fieldMap);
UpdateResponse update = client.update(updateRequest, RequestOptions.DEFAULT);
System.out.println(update.status());
}3.根据id查询文档
/**
* 根据id查询文档
*/
@Test
public void getDoc() throws IOException {
//设置查询的索引、文档
GetRequest indexRequest=new GetRequest("member","1");
GetResponse response = client.get(indexRequest, RequestOptions.DEFAULT);
System.out.println(response.getSourceAsString());
}4.根据id删除文档
/**
* 根据id删除文档
*/
@Test
public void delDoc() throws IOException {
//设置要删除的索引、文档
DeleteRequest deleteRequest=new DeleteRequest("member","1");
DeleteResponse response = client.delete(deleteRequest, RequestOptions.DEFAULT);
System.out.println(response.getId());
}笔记 ​
- 分布式意味着可以部署多个节点
- Restful风格API特点
- 协议,接收HTTP请求,返回HTTP响应
- 请求参数是json,返回内容也是json
- 请求类型(get,post,put,delete)代表对资源的操作类型
- 对实现客户端的语言没有要求
- 9300端口,集群内部通信。9200端口java客户端连接
- 倒排索引就是目标字段的分词结果,对文档id的映射
- es的查询语言叫DSL,data structured language
- es数据结构对应mysql
- 映射-->表
- 文档-->一行数据
- 索引-->数据库
- Field-->每一列字段
- 关闭索引后,里面的文档就查不到了
- text会分词,keyword不会分词
- scaled_float用来做压缩存储
- java操作es是使用rest-high-level-clien帮我们发送请求,它可以快速构建es请求,我们只需要构建请求对象
- 修改文档:全部覆盖,部分修改
// 修改文档---- 修改指定的字段
/**
*
* POST /user/_update/1
* {
* "doc": {
* "name":"贾琏",
* "age": 30
* }
* }
*/
/**
* 修改全部
* POST /user/_doc/1
* {
* "id": 2001,
* "name":"刘姥姥",
* "age":80
* }
*/
#批量修改文档,修改全部,修改部分
POST /_bulk
{"post":{"_index":"teacher","_id":"102"}}
{"id":"10","name":"白骨精","age":"12"}
{"update":{"_index":"teacher","_id":"103"}}
{"doc":{"age":"88"}}
GET /teacher/_search