一、初识elasticsearch
elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。
例如,搜索商品:

google浏览器搜索:

打车软件搜索附近车辆:

1.1 了解ES
elasticsearch是elastic stack(ELK)的核心,elastic stack还包括kibana、Logstash、Beats,被广泛应用于 日志数据分析、实时监控 等领域。
(1)elasticsearch 负责存储、搜索和分析数据。
(2)logstash和beats 负责数据抓取(可替代,可以写java代码进行搜索)。
(3)kibana 负责数据可视化(可替代,可视化的工具有很多,不一定要用kibana)。

ElasticSearch的底层实现是Lucene技术,Lucene是一个java语言的搜索引擎类库 ,是Apache公司的顶级项目。
官方网址:https://lucene.apache.org/
Lucene的优势:
(1)易扩展
(2)高性能(基于倒排索引)
缺点:
(1)只限于Java语言开发
(2)学习曲线陡峭(API设计复杂)
(3)不支持水平扩展(类库只考虑如何实现搜索,面对高并发场景不支持集群扩展,要想实现需要进行二次开发)
ElasticSearch解决了Lucene的问题,支持分布式,可水平扩展;提供Restful接口,可被任何语言调用。
1.2 倒排索引
正向索引和倒排索引
传统数据库(如MySQL)采用正向索引,例如给tb_goods表的字段id创建索引,而对于没加索引的字段title,根据该字段查询时需要遍历所有记录。

倒排索引:
倒排索引创建时会建立一个新的表,包括文档(document)和词条(term)。
文档:每条数据就是一个文档
词条:文档按照语义分成的词语

词条字段不能重复,给词条创建索引,根据词条查找id,再在原表里根据id查找,速度很快 。

1.3 ES与MySQL对比
(1)文档
elasticsearch是面向文档存储的,文档数据会被序列化为json格式后存储在elasticsearch中。

(2)索引
索引是相同类型的文档的集合

映射:索引中文档的字段约束信息,类似表的结构约束(约束索引中文档的数据格式相同)。

对比:
(1)MySQL擅长事务类型操作,可以确保数据的安全和一致性。
(2)Elasticsearch:擅长海量数据的搜索、分析和计算。

1.4 安装ES和Kibana
1.4.1 创建网络
因为还需要部署kibana容器,因此需要让es和kibana容器互联,先创建一个网络:
bash
docker network create es-net

1.4.2 加载镜像

将资料中的两个jar包导入(镜像太大,docker pull时间会很长)。

bash
docker load -i es.tar
docker load -i kibana.tar

1.4.3 运行(创建容器)
(1)运行elasticsearch

bash
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
命令解释:
-e "cluster.name=es-docker-cluster":设置集群名称-e "http.host=0.0.0.0":监听的地址,可以外网访问-e "ES_JAVA_OPTS=-Xms512m -Xmx512m":内存大小-e "discovery.type=single-node":非集群模式-v es-data:/usr/share/elasticsearch/data:挂载逻辑卷,绑定es的数据目录-v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷,绑定es的日志目录-v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录--privileged:授予逻辑卷访问权--network es-net:加入一个名为es-net的网络中-p 9200:9200:端口映射配置


(2)运行kibana

bash
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.12.1
命令解释:
--network es-net:加入一个名为es-net的网络中,与elasticsearch在同一个网络中-e ELASTICSEARCH_HOSTS=http://es:9200":设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch-p 5601:5601:端口映射配置
注意:kibana启动速度较慢,虽然通过docker ps可以查看到,但启动速度很慢,会产生各种数据和日志。可以通过docker logs -f kibana查看。
bash
docker logs -f kibana

通过浏览器访问:

点击"Explore on my own":



DSL语句的本质就是发送一个Restful请求到ES中。
1.5 安装KI分词器
es在创建倒排索引时 需要对文档分词;在搜索时,需要对用户输入内容分词 。但默认的分词规则对中文处理并不友好。
处理分词时,一般会使用IK分词器。

bash
docker volume inspect es-plugins

将解压的ik文件夹里的东西上传到查询到的插件目录/var/lib/docker/volumes/es-plugins/_data。直接上传可能打不开这个目录,可以先上传到一个目录再用mv指令。


重启docker服务的es:
bash
docker restart es

IK分词器包含两种模式:
ik_smart:最少切分ik_max_word:最细切分
测试:
GET /_analyze { "analyzer": "ik_max_word", "text": "黑马程序员学习java太棒了" }

补充:IK分词器的拓展和停用字典
IK分词器的分词是基于一个词库,词库里有的词就会切分,但有些词词库里没有,无法准确切分,此时需要拓展。
例如:


最新的网络用语"白嫖"、"欧力给"没办法切割出来。
要拓展IK分词器的词库,只需要修改一个IK分词器目录中的config目录中的IKAnalyzer.cfg.xml文件:

也可以加一些停用词(比如"的","地","了"等):


文件里新增地ext.dic和stopword.dic是文件,需要新建并在文件中添加词。





测试:


二、索引库操作
mapping映射是对索引库中文档的约束。
可以查看官方文档:https://www.elastic.co/
2.1 mapping属性
(一)type(字段数据类型)
(1)字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、IP地址)
(2)数值:long、Integer、short、byte、double、float
(3)布尔:boolean
(4)日期:date
(5)对象:object
(二)index(是否创建索引,默认为true)
(三)analyzer(使用哪种分词器)
结合text类型用,值为ik_smart、ik_max_word
(四)properties:该字段的子字段
2.2 创建索引库
ES通过Restful请求操作索引库、文档。请求内容用DSL语句来表示,创建索引库和mapping的DSL语法如下:

一个示例:

操作实例:
创建索引库

bash
PUT /heima
{
"mappings": {
"properties": {
"info":{
"type":"text",
"analyzer": "ik_smart"
},
"email":{
"type":"keyword",
"index":false
},
"name":{
"type":"object",
"properties":{
"firstName":{
"type":"keyword"
},
"lastName":{
"type":"keyword"
}
}
}
}
}
}
2.3 查看、删除索引库
查看索引库语法:
GET /索引库名
删除索引库语法:
DELETE /索引库名
注意:ES中索引库是不允许修改的,但是可以新增新字段。


bash
# 查询
GET /heima

bash
# 修改索引库
PUT /heima/_mapping
{
"properties":{
"age":{
"type":"integer"
}
}
}

bash
# 删除
DELETE /heima
三、文档操作
3.1 新增文档

一个例子:

bash
# 插入一个文档
POST /heima/_doc/1
{
"info":"黑马程序员Java讲师",
"email":"zy@itcast.cn",
"name":{
"firstName":"云",
"lastName":"赵"
}
}
3.2 查询、删除文档




3.3 修改文档
3.3.1 方式一、全量修改

注意:改方式如果文档id没有对应的就文档,则相当于执行"新增"操作。
例如:


bash
PUT /heima/_doc/1
{
"info":"黑马程序员Java讲师",
"email":"ZhaoYun@itcast.cn",
"name":{
"firstName":"云",
"lastName":"赵"
}
}

3.3.2 局部修改(修改指定字段)



四、RestClient
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。
地址:
https://www.elastic.co/guide/en/elasticsearch/client/index.html
通过一个案例学习。

4.1 导入代码和SQL



注意修改数据库连接。
4.2 分析数据结构
mappging要考虑的问题:
字段名、数据类型、是否参与搜索、是否分词、如果分词分词器是什么。
表结构如下:


新增酒店索引的DSL语句如下:
bash
# 酒店的mapping
PUT /hotel
{
"mapping":{
"properties":{
"id":{
"type":"keyword" # id一般是字符串类型
},
"name":{
"type":"text",
"analyzer":"ik_max_word"
},
"address":{
"type":"keyword",
"index":false
},
"price":{
"type":"integer"
},
"score":{
"type":"integer"
},
"brand":{
"type":"keyword"
},
"city":{
"type":"keyword"
},
"starName":{
"type":"keyword"
},
"business":{
"type":"keyword"
},
"location":{
"type":"geo_point"
},
"pic":{
"type":"keyword",
"index":false
}
}
}
}
另外,需要实现多个字段(包括酒店名称、酒店品牌、酒店星级、酒店商圈等)都能搜索的功能,但是分别创建 索引和分词比较麻烦,ES可以把这些字段的值汇总到一个字段,使用copy_to属性。

bash
# 酒店的mapping
PUT /hotel
{
"mapping":{
"properties":{
"id":{
"type":"keyword"
},
"name":{
"type":"text",
"analyzer":"ik_max_word",
"copy_to":"all"
},
"address":{
"type":"keyword",
"index":false
},
"price":{
"type":"integer"
},
"score":{
"type":"integer"
},
"brand":{
"type":"keyword",
"copy_to":"all"
},
"city":{
"type":"keyword"
},
"starName":{
"type":"keyword"
},
"business":{
"type":"keyword",
"copy_to":"all"
},
"location":{
"type":"geo_point"
},
"pic":{
"type":"keyword",
"index":false
},
"all":{
"type":"text",
"analyzer":"ik_max_word"
}
}
}
}
4.3 初始化JavaRestClient

1.引入es的RestHighLevelClient依赖

2.覆盖springboot默认的es版本
见上图。
3.初始化RestHighLevelClient

java
public class HotelIndexTest {
private RestHighLevelClient client;
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://10.38.48.12:9200")));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
@Test
void testInit(){
System.out.println(client);
}
}
测试结果:

4.4 RestClient操作索引库
4.4.1 创建索引库
就是写java代码来组装之前写的DSL语句。


java
@Test
void createHotelIndex() throws IOException {
//1.创建Request对象
CreateIndexRequest request=new CreateIndexRequest("hotel");
//2.准备请求的参数,DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
//3.发送请求
client.indices().create(request, RequestOptions.DEFAULT);
}
注意上图中的MAPPING_TEMPLATEZ字段,它是另一个类的静态字段。

java
public class HotelConstants {
public static final String MAPPING_TEMPLATE="{\n" +
" \"mapping\":{\n" +
" \"properties\":{\n" +
" \"id\":{\n" +
" \"type\":\"keyword\"\n" +
" },\n" +
" \"name\":{\n" +
" \"type\":\"text\",\n" +
" \"analyzer\":\"ik_max_word\",\n" +
" \"copy_to\":\"all\"\n" +
" },\n" +
" \"address\":{\n" +
" \"type\":\"keyword\",\n" +
" \"index\":false\n" +
" },\n" +
" \"price\":{\n" +
" \"type\":\"integer\"\n" +
" },\n" +
" \"score\":{\n" +
" \"type\":\"integer\"\n" +
" },\n" +
" \"brand\":{\n" +
" \"type\":\"keyword\",\n" +
" \"copy_to\":\"all\"\n" +
" },\n" +
" \"city\":{\n" +
" \"type\":\"keyword\"\n" +
" },\n" +
" \"starName\":{\n" +
" \"type\":\"keyword\"\n" +
" },\n" +
" \"business\":{\n" +
" \"type\":\"keyword\",\n" +
" \"copy_to\":\"all\"\n" +
" },\n" +
" \"location\":{\n" +
" \"type\":\"geo_point\"\n" +
" },\n" +
" \"pic\":{\n" +
" \"type\":\"keyword\",\n" +
" \"index\":false\n" +
" },\n" +
" \"all\":{\n" +
" \"type\":\"text\",\n" +
" \"analyzer\":\"ik_max_word\"\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
}


4.4.2 删除和判断索引


注意区分删除和判断是否存在于创建的区别。
java
@Test
void deleteHotelIndex() throws IOException {
//1.创建Request对象
DeleteIndexRequest request=new DeleteIndexRequest("hotel");
//2.发送请求
client.indices().delete(request, RequestOptions.DEFAULT);
}
@Test
void existsHotelIndex() throws IOException {
//1.创建Request对象
GetIndexRequest request=new GetIndexRequest("hotel");
//2.发送请求
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
//3.输出
System.out.println(exists?"索引库已经存在":"索引库不存在");
}


4.5 RestClient操作文档

4.5.1 初始化JavaRestClient


4.5.2 添加酒店数据到数据库


还需要注意一个问题,前面在elastic中创建Hotel索引库时,里面的字段和MySQL数据库中表的字段不完全一致,longitude和latitude合成了location。为了方便,可以再建议一个HotelDoc.java来进行转换:



java
@SpringBootTest
public class HotelDocumentTest {
@Autowired
private IHotelService hotelService;
private RestHighLevelClient client;
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://10.38.48.12:9200")));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
@Test
void addDocument() throws IOException {
//根据id查询酒店数据
Hotel hotel = hotelService.getById(61083L);
//转为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
//1.创建Request对象
IndexRequest request=new IndexRequest("hotel").id(hotel.getId().toString());
//2.准备json文档
request.source(JSON.toJSONString(hotelDoc),XContentType.JSON);
//3.发送请求
client.index(request,RequestOptions.DEFAULT);
}
}


4.5.3 根据ID查询酒店文档

注意,如上图,从elastic中查出的数据除了包括原始值"_source"字段的值,还有其他一些字段。需要使用getSourceAsString()方法获取数据。

java
@Test
void getDocumentById() throws IOException {
//1.创建Request对象
GetRequest request=new GetRequest("hotel","61083");
//2.发送请求得到响应
GetResponse response=client.get(request,RequestOptions.DEFAULT);
//3.解析响应结果
String json = response.getSourceAsString();
HotelDoc hotelDoc = JSON.parseObject(json,HotelDoc.class);
System.out.println(hotelDoc);
}

4.5.4 根据id修改酒店数据

全量更新和新增代码一样。
局部更新代码:


java
@Test
void updateDocumentById() throws IOException {
//1.创建Request对象
UpdateRequest request=new UpdateRequest("hotel","61083");
//2.准备请求参数
request.doc(
"price","952",
"starName","四钻"
);
//3.发送请求
client.update(request,RequestOptions.DEFAULT);
}


4.5.5 根据id删除酒店数据


java
@Test
void deleteDocumentById() throws IOException {
//1.创建Request对象
DeleteRequest request=new DeleteRequest("hotel","61083");
//2.发送请求
client.delete(request,RequestOptions.DEFAULT);
}


4.5.6 批量导入文档
一条一条记录新增效率很慢,需要批量导入。


java
@Test
void testBulkRequest() throws IOException {
//批量查询酒店数据
List<Hotel> hotels = hotelService.list();
//1.创建Request对象
BulkRequest request=new BulkRequest();
for (Hotel hotel : hotels) {
//转化为文档类型
HotelDoc hotelDoc = new HotelDoc(hotel);
//2.准备参数,添加多个新增的Request
request.add(new IndexRequest("hotel")
.id(hotel.getId().toString())
.source(JSON.toJSONString(hotelDoc),XContentType.JSON));
}
// 3.发送请求
client.bulk(request,RequestOptions.DEFAULT);
}

