目录

我们统一的把mysql与elasticsearch的概念做一下对比:
| MySQL | Elasticsearch | 说明 |
| Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
| Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
| Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
| Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
| SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
是不是说,我们学习了elasticsearch就不再需要mysql了呢?
并不是如此,两者各自有自己的擅长支出:
因此在企业中,往往是两者结合使用:
mapping是对索引库中文档的约束,常见的mapping属性包括:
例如下面的json文档:
- {
- "age": 21,
- "weight": 52.1,
- "isMarried": false,
- "info": "黑马程序员Java讲师",
- "email": "zy@itcast.cn",
- "score": [99.1, 99.5, 98.9],
- "name": {
- "firstName": "云",
- "lastName": "赵"
- }
- }
对应的每个字段映射(mapping):
这里我们统一使用Kibana编写DSL的方式来演示。
基本语法:
格式:
- 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"
- }
- }
- }
- }
- }
- }
基本语法:
格式:
GET /索引库名
示例:GET /heima
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
语法说明:
- PUT /索引库名/_mapping
- {
- "properties": {
- "新字段名":{
- "type": "integer"
- }
- }
- }
语法:
格式:
DELETE /索引库名
在kibana中测试:
索引库操作有哪些?
语法:
- POST /索引库名/_doc/文档id
- {
- "字段1": "值1",
- "字段2": "值2",
- "字段3": {
- "子属性1": "值3",
- "子属性2": "值4"
- },
- // ...
- }
示例:
- POST /heima/_doc/1
- {
- "info":"黑马程序员Java讲师",
- "email":"zy@itcast.cn",
- "name":{
- "firstName":"云",
- "lastName":"赵"
- }
- }
响应:
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
语法:
GET /{索引库名称}/_doc/{id}
通过kibana查看数据:
GET /heima/_doc/1
删除使用DELETE请求,同样,需要根据id进行删除:
语法:
DELETE /{索引库名}/_doc/id值
示例:
# 根据id删除数据 DELETE /heima/_doc/1
修改有两种方式:
全量修改是覆盖原来的文档,其本质是:
注意:如果根据id删除时,id不存在,第二步的新增也会执行,也就从修改变成了新增操作了。
语法:
- PUT /{索引库名}/_doc/文档id
- {
- "字段1": "值1",
- "字段2": "值2",
- // ... 略
- }
示例:
- PUT /heima/_doc/1
- {
- "info": "黑马程序员高级Java讲师",
- "email": "zy@itcast.cn",
- "name": {
- "firstName": "云",
- "lastName": "赵"
- }
- }
增量修改是只修改指定id匹配的文档中的部分字段。
语法:
- POST /{索引库名}/_update/文档id
- {
- "doc": {
- "字段名": "新的值",
- }
- }
示例:
- POST /heima/_update/1
- {
- "doc": {
- "email": "ZhaoYun@itcast.cn"
- }
- }
文档操作有哪些?
ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址:Elasticsearch Clients | Elastic
其中的Java Rest Client又包括两种:
首先导入课前资料提供的数据库数据:
数据结构如下:
- CREATE TABLE `tb_hotel` (
- `id` bigint(20) NOT NULL COMMENT '酒店id',
- `name` varchar(255) NOT NULL COMMENT '酒店名称;例:7天酒店',
- `address` varchar(255) NOT NULL COMMENT '酒店地址;例:航头路',
- `price` int(10) NOT NULL COMMENT '酒店价格;例:329',
- `score` int(2) NOT NULL COMMENT '酒店评分;例:45,就是4.5分',
- `brand` varchar(32) NOT NULL COMMENT '酒店品牌;例:如家',
- `city` varchar(32) NOT NULL COMMENT '所在城市;例:上海',
- `star_name` varchar(16) DEFAULT NULL COMMENT '酒店星级,从低到高分别是:1星到5星,1钻到5钻',
- `business` varchar(255) DEFAULT NULL COMMENT '商圈;例:虹桥',
- `latitude` varchar(32) NOT NULL COMMENT '纬度;例:31.2497',
- `longitude` varchar(32) NOT NULL COMMENT '经度;例:120.3925',
- `pic` varchar(255) DEFAULT NULL COMMENT '酒店图片;例:/img/1.jpg',
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
然后导入课前资料提供的项目:
创建索引库,最关键的是mapping映射,而mapping映射要考虑的信息包括:
其中:
来看下酒店数据的索引库结构:
- PUT /hotel
- {
- "mappings": {
- "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",
- "copy_to": "all"
- },
- "starName":{
- "type": "keyword"
- },
- "business":{
- "type": "keyword"
- },
- "location":{
- "type": "geo_point"
- },
- "pic":{
- "type": "keyword",
- "index": false
- },
- "all":{
- "type": "text",
- "analyzer": "ik_max_word"
- }
- }
- }
- }
几个特殊字段说明:
地理坐标说明:
copy_to说明:

在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。
分为三步:
1)引入es的RestHighLevelClient依赖:
-
org.elasticsearch.client -
elasticsearch-rest-high-level-client
2)因为SpringBoot默认的ES版本是7.6.2,所以我们需要覆盖默认的ES版本:
-
1.8 -
7.12.1
3)初始化RestHighLevelClient:
初始化的代码如下:
- RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
- HttpHost.create("http://192.168.150.101:9200")
- ));
这里为了单元测试方便,我们创建一个测试类HotelIndexTest,然后将初始化的代码编写在@BeforeEach方法中:
- package cn.itcast.hotel;
-
- import org.apache.http.HttpHost;
- import org.elasticsearch.client.RestClient;
- import org.elasticsearch.client.RestHighLevelClient;
- import org.junit.jupiter.api.AfterEach;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.Test;
-
- import java.io.IOException;
-
- public class HotelIndexTest {
- private RestHighLevelClient client;
-
- @BeforeEach
- void setUp() {
- this.client = new RestHighLevelClient(RestClient.builder(
- HttpHost.create("http://192.168.150.101:9200")
- ));
- }
-
- @AfterEach
- void tearDown() throws IOException {
- this.client.close();
- }
- }
4.1.1.代码解读
创建索引库的API如下:
代码分为三步:
在hotel-demo的cn.itcast.hotel.constants包下,创建一个类,定义mapping映射的JSON字符串常量:
-
- package cn.itcast.hotel.constants;
-
- public class HotelConstants {
- public static final String MAPPING_TEMPLATE = "{\n" +
- " \"mappings\": {\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" +
- " \"copy_to\": \"all\"\n" +
- " },\n" +
- " \"starName\":{\n" +
- " \"type\": \"keyword\"\n" +
- " },\n" +
- " \"business\":{\n" +
- " \"type\": \"keyword\"\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" +
- "}";
- }
在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现创建索引:
- @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);
- }
删除索引库的DSL语句非常简单:
DELETE /hotel
与创建索引库相比:
所以代码的差异,注意体现在Request对象上。依然是三步走:
在hotel-demo中的HotelIndexTest测试类中,编写单元测试,实现删除索引:
- @Test
- void testDeleteHotelIndex() throws IOException {
- // 1.创建Request对象
- DeleteIndexRequest request = new DeleteIndexRequest("hotel");
- // 2.发送请求
- client.indices().delete(request, RequestOptions.DEFAULT);
- }
判断索引库是否存在,本质就是查询,对应的DSL是:
GET /hotel
因此与删除的Java代码流程是类似的。依然是三步走:
- @Test
- void testExistsHotelIndex() throws IOException {
- // 1.创建Request对象
- GetIndexRequest request = new GetIndexRequest("hotel");
- // 2.发送请求
- boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
- // 3.输出
- System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
- }
JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。
索引库操作的基本步骤:
为了与索引库操作分离,我们再次参加一个测试类,做两件事情:
- package cn.itcast.hotel;
-
- import cn.itcast.hotel.pojo.Hotel;
- import cn.itcast.hotel.service.IHotelService;
- import org.junit.jupiter.api.AfterEach;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.Test;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.boot.test.context.SpringBootTest;
-
- import java.io.IOException;
- import java.util.List;
-
- @SpringBootTest
- public class HotelDocumentTest {
- @Autowired
- private IHotelService hotelService;
-
- private RestHighLevelClient client;
-
- @BeforeEach
- void setUp() {
- this.client = new RestHighLevelClient(RestClient.builder(
- HttpHost.create("http://192.168.150.101:9200")
- ));
- }
-
- @AfterEach
- void tearDown() throws IOException {
- this.client.close();
- }
- }
-
我们要将数据库的酒店数据查询出来,写入elasticsearch中。
数据库查询后的结果是一个Hotel类型的对象。结构如下:
- @Data
- @TableName("tb_hotel")
- public class Hotel {
- @TableId(type = IdType.INPUT)
- private Long id;
- private String name;
- private String address;
- private Integer price;
- private Integer score;
- private String brand;
- private String city;
- private String starName;
- private String business;
- private String longitude;
- private String latitude;
- private String pic;
- }
与我们的索引库结构存在差异:
因此,我们需要定义一个新的类型,与索引库结构吻合:
- package cn.itcast.hotel.pojo;
-
- import lombok.Data;
- import lombok.NoArgsConstructor;
-
- @Data
- @NoArgsConstructor
- public class HotelDoc {
- private Long id;
- private String name;
- private String address;
- private Integer price;
- private Integer score;
- private String brand;
- private String city;
- private String starName;
- private String business;
- private String location;
- private String pic;
-
- public HotelDoc(Hotel hotel) {
- this.id = hotel.getId();
- this.name = hotel.getName();
- this.address = hotel.getAddress();
- this.price = hotel.getPrice();
- this.score = hotel.getScore();
- this.brand = hotel.getBrand();
- this.city = hotel.getCity();
- this.starName = hotel.getStarName();
- this.business = hotel.getBusiness();
- this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
- this.pic = hotel.getPic();
- }
- }
-
新增文档的DSL语句如下:
- POST /{索引库名}/_doc/1
- {
- "name": "Jack",
- "age": 21
- }
对应的java代码如图:
可以看到与创建索引库类似,同样是三步走:
变化的地方在于,这里直接使用client.xxx()的API,不再需要client.indices()了。
我们导入酒店数据,基本流程一致,但是需要考虑几点变化:
因此,代码整体步骤如下:
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
- @Test
- void testAddDocument() throws IOException {
- // 1.根据id查询酒店数据
- Hotel hotel = hotelService.getById(61083L);
- // 2.转换为文档类型
- HotelDoc hotelDoc = new HotelDoc(hotel);
- // 3.将HotelDoc转json
- String json = JSON.toJSONString(hotelDoc);
-
- // 1.准备Request对象
- IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
- // 2.准备Json文档
- request.source(json, XContentType.JSON);
- // 3.发送请求
- client.index(request, RequestOptions.DEFAULT);
- }
查询的DSL语句如下:
GET /hotel/_doc/{id}
非常简单,因此代码大概分两步:
不过查询的目的是得到结果,解析为HotelDoc,因此难点是结果的解析。完整代码如下:

可以看到,结果是一个JSON,其中文档放在一个_source属性中,因此解析就是拿到_source,反序列化为Java对象即可。
与之前类似,也是三步走:
5.2.2.完整代码
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
- @Test
- void testGetDocumentById() throws IOException {
- // 1.准备Request
- GetRequest request = new GetRequest("hotel", "61082");
- // 2.发送请求,得到响应
- GetResponse response = client.get(request, RequestOptions.DEFAULT);
- // 3.解析响应结果
- String json = response.getSourceAsString();
-
- HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
- System.out.println(hotelDoc);
- }
删除的DSL为是这样的:
DELETE /hotel/_doc/{id}
与查询相比,仅仅是请求方式从DELETE变成GET,可以想象Java代码应该依然是三步走:
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
- @Test
- void testDeleteDocument() throws IOException {
- // 1.准备Request
- DeleteRequest request = new DeleteRequest("hotel", "61083");
- // 2.发送请求
- client.delete(request, RequestOptions.DEFAULT);
- }
修改我们讲过两种方式:
在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:
这里不再赘述,我们主要关注增量修改。
代码示例如图:
与之前类似,也是三步走:
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
- @Test
- void testUpdateDocument() throws IOException {
- // 1.准备Request
- UpdateRequest request = new UpdateRequest("hotel", "61083");
- // 2.准备请求参数
- request.doc(
- "price", "952",
- "starName", "四钻"
- );
- // 3.发送请求
- client.update(request, RequestOptions.DEFAULT);
- }
这里也可以传Map
- @Test
- void testUpdateDocument() throws IOException {
- // 1.准备Request
- UpdateRequest request = new UpdateRequest("hotel", "61083");
- // 2.准备请求参数
- Map
docMap = new HashMap<>(); - docMap.put("price", "952");
- docMap.put("starName", "四钻");
- request.doc(docMap);
- client.update(request, RequestOptions.DEFAULT);
- }
案例需求:利用BulkRequest批量将数据库数据导入到索引库中。
步骤如下:
批量处理BulkRequest,其本质就是将多个普通的CRUD请求组合在一起发送。
其中提供了一个add方法,用来添加其他请求:
可以看到,能添加的请求包括:
因此Bulk中添加了多个IndexRequest,就是批量新增功能了。示例:
其实还是三步走:
我们在导入酒店数据时,将上述代码改造成for循环处理即可。
在hotel-demo的HotelDocumentTest测试类中,编写单元测试:
- @Test
- void testBulkRequest() throws IOException {
- // 批量查询酒店数据
- List
hotels = hotelService.list(); -
- // 1.创建Request
- BulkRequest request = new BulkRequest();
- // 2.准备参数,添加多个新增的Request
- for (Hotel hotel : hotels) {
- // 2.1.转换为文档类型HotelDoc
- HotelDoc hotelDoc = new HotelDoc(hotel);
- // 2.2.创建新增文档的Request对象
- request.add(new IndexRequest("hotel")
- .id(hotelDoc.getId().toString())
- .source(JSON.toJSONString(hotelDoc), XContentType.JSON));
- }
- // 3.发送请求
- client.bulk(request, RequestOptions.DEFAULT);
- }
文档操作的基本步骤: