4.1. 配置Java client
前边我们都是在Kibana中使用DSL语法直接请求Elasticsearch的HTTP 接口进行测试,DSL是Elasticsearch的领域特定语言(Domain Specific Language, DSL)是一种基于JSON的查询语言。
在项目开发中为了提高开发效率ES官方提供了各种不同语言的客户端,这些客户端的本质就是组装DSL语句,通过http请求发送给ES。官方文档地址
由于ES目前最新版本是8.x,提供了全新版本的客户端Java Client,老版本的客户端Java REST Client已经被标记为过时。
我们使用的是7.17.x版本,新版本和老版本都支持,将来的版本会全面抛弃老版本的Java REST Client ,所以本教程使用新版本的Java Client。
Java Client要求:
- Java 8 或更高版本。
- JSON 对象映射库,可将您的应用程序类与 Elasticsearch API 无缝集成。Java 客户端支持Jackson或 JSON-B库(如 Eclipse Yasson)。
如何集成Java Client,可参考文档:地址链接
在hmall-parent中添加依赖管理:
<properties> <es.version>7.17.7</es.version> <jackson.version>2.13.0</jackson.version> <jakarta.json-ai.version>2.0.1</jakarta.json-ai.version> </properties> <!-- 对依赖包进行管理 --> <dependencyManagement> <dependencies> <!--es--> <dependency> <groupId>co.elastic.clients</groupId> <artifactId>elasticsearch-java</artifactId> <version>${es.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>jakarta.json</groupId> <artifactId>jakarta.json-api</artifactId> <version>${jakarta.json-ai.version}</version> </dependency> </dependencies> </dependencyManagement>
在hmall-item添加如下依赖:
<dependency> <groupId>co.elastic.clients</groupId> <artifactId>elasticsearch-java</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <dependency> <groupId>jakarta.json</groupId> <artifactId>jakarta.json-api</artifactId> </dependency>
这里为了单元测试方便,我们创建一个测试类IndexTest,然后将初始化的代码编写在@BeforeEach方法中:参考:链接
package com.hmall.item.es; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.rest_client.RestClientTransport; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; public class IndexTest { private ElasticsearchClient esClient; private RestClient restClient; @BeforeEach void setUp() { // Create the low-level client this.restClient = RestClient.builder( new HttpHost("192.168.101.68", 9200)).build(); // Create the transport with a Jackson mapper ElasticsearchTransport transport = new RestClientTransport( restClient, new JacksonJsonpMapper()); // And create the API client this.esClient = new ElasticsearchClient(transport); } @AfterEach void tearDown() throws IOException { this.restClient.close(); } }
4.2. 创建索引
下边我们以商城项目为例,使用Java Client维护索引数据。搜索页面的效果如图所示:
最终我们使用Elaticsearch实现搜索接口。
4.2.1 分析索引的映射
以下分析过程非常重要,强烈建议:自己根据上面的页面完成独立分析、落地工作
首先我们需要创建索引,配置映射,首先针对上图去分析映射结构,包括哪些字段以及字段的类型等属性。
实现搜索功能需要的字段包括三大部分:
- 搜索关键字字段:
- 商品名称
- 过滤字段
- 分类
- 品牌
- 价格
- 排序字段
- 默认:按照更新时间降序排序
- 销量
- 价格
- 展示字段
- 商品id:用于点击后跳转
- 图片地址
- 是否是广告推广商品
- 名称
- 价格
- 评价数量
- 销量
对应的商品表结构如下,索引库无关字段已经划掉:
结合数据库表结构,以上字段对应的mapping映射属性如下:
字段名 |
字段类型 |
类型说明 |
是否 参与搜索 |
是否 参与分词 |
分词器 |
|
id |
|
长整数 |
—— |
|||
name |
|
字符串,参与分词搜索 |
IK |
|||
price |
|
以分为单位,所以是整数 |
—— |
|||
stock |
|
字符串,但是不分词 |
—— |
|||
image |
|
字符串,但是不分词 |
—— |
|||
category |
|
字符串,但是不分词 |
—— |
|||
brand |
|
字符串,但是不分词 |
—— |
|||
sold |
|
销量,整数 |
—— |
|||
commentCount |
|
评价,整数 |
—— |
|||
isAD |
|
布尔类型 |
—— |
|||
updateTime |
|
更新时间 |
—— |
|||
4.2.2 创建索引
根据分析我们使用下边的语句创建items索引:
PUT /items { "mappings": { "properties": { "id": { "type": "keyword" }, "name":{ "type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart" }, "price":{ "type": "integer" }, "stock":{ "type": "integer" }, "image":{ "type": "keyword", "index": false }, "category":{ "type": "keyword" }, "brand":{ "type": "keyword" }, "sold":{ "type": "integer" }, "commentCount":{ "type": "integer", "index": false }, "isAD":{ "type": "boolean" }, "updateTime":{ "type": "date" } } } }
我们为什么不用Java Client去创建索引呢?
这就好比在MySQL中创建表,通常我们使用DDL语句通过MySQL客户端执行,而对于数据的CRUD及复杂的SQL语句我们会通过jdbc 去访问mysql数据库一样。
所以,使用Elasticsearch通常我们使用Kibana通过DSL语句去创建索引,而不会使用Java Client去创建索引,使用Java Client主要是为了向索引中添加文档、从索引中搜索文档。
4.3. 新增文档
接下来我们使用Java Client向索引中添加文档。
Java Client的使用方法可以参考 文档 学习
4.3.1 创建模型类
就和使用MyBatis一样操作数据库需要一个模型对象,使用Elasticsearch向索引执行CRUD操作也需要模型类。
依据索引映射创建模型类:
小技巧:创建模型类可以提供索引映射由AI去生成模型类。
package com.hmall.item.domain.po; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.time.LocalDateTime; @Data @ApiModel(description = "索引库实体") public class ItemDoc { @ApiModelProperty("商品id") private String id; @ApiModelProperty("商品名称") private String name; @ApiModelProperty("价格(分)") private Integer price; @ApiModelProperty("库存") private Integer stock; @ApiModelProperty("商品图片") private String image; @ApiModelProperty("类目名称") private String category; @ApiModelProperty("品牌名称") private String brand; @ApiModelProperty("销量") private Integer sold; @ApiModelProperty("评论数") private Integer commentCount; @ApiModelProperty("是否是推广广告,true/false") private Boolean isAD; @ApiModelProperty("更新时间") private LocalDateTime updateTime; }
4.3.2 编写客户端代码
下边参考ES文档编写客户端代码。
@SpringBootTest @Slf4j public class IndexTest { ... @Autowired private IItemService itemService; @Test void testAddDocument() throws IOException { //商品id Long id = 100002644680L; // 1.根据id查询商品数据 Item item = itemService.getById(id); // 2.转换为文档类型 ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class); IndexResponse response = esClient.index(i -> i .index("items")//指定索引名称 .id(itemDoc.getId())//指定主键 .document(itemDoc)//指定文档对象 ); //结果 String s = response.result().jsonValue(); log.info("result:"+s); } }
4.3.3 测试
下边进行运行测试方法。报错:
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.hmall.item.domain.po.ItemDoc["updateTime"])
根据提示猜测是数据绑定出问题,现在是要把Java 对象的信息映射为ES的索引文档,jackson-datatype-jsr310对于LocalDateTime不支持。
使用AI解决:
elasticsearch使用的是7.17.7,使用co.elastic.clients.elasticsearch.ElasticsearchClient 向索引新增文档报错如下: Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.hmall.item.domain.po.ItemDoc["updateTime"])
根据AI提示修改如下:
添加 JavaTimeModule 以支持 LocalDateTime 类型。
这个类引包别整错了:package com.fasterxml.jackson.databind;
完整代码如下:
package com.hmall.item.es; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.date.DatePattern; import cn.hutool.json.JSONUtil; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.AcknowledgedResponse; import co.elastic.clients.elasticsearch.core.IndexResponse; import co.elastic.clients.elasticsearch.core.InfoRequest; import co.elastic.clients.elasticsearch.indices.CreateIndexResponse; import co.elastic.clients.elasticsearch.indices.PutMappingResponse; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.rest_client.RestClientTransport; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; import com.hmall.item.domain.po.Item; import com.hmall.item.domain.po.ItemDoc; import com.hmall.item.service.IItemService; import lombok.extern.slf4j.Slf4j; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; 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.time.LocalDateTime; import java.time.format.DateTimeFormatter; @SpringBootTest @Slf4j public class IndexTest { private ElasticsearchClient esClient; private RestClient restClient; @Autowired private IItemService itemService; @BeforeEach void setUp() { // Create the low-level client this.restClient = RestClient.builder( new HttpHost("192.168.101.68", 9200)).build(); // 创建 ObjectMapper 实例 ObjectMapper objectMapper = new ObjectMapper(); // 添加 JavaTimeModule 以支持 LocalDateTime 类型 objectMapper.registerModule(new JavaTimeModule()); // Create the transport with a Jackson mapper ElasticsearchTransport transport = new RestClientTransport( restClient, new JacksonJsonpMapper(objectMapper)); // And create the API client this.esClient = new ElasticsearchClient(transport); } //创建文档 @Test void testAddDocument() throws IOException { // 1.根据id查询商品数据 Item item = itemService.getById(100002644680L); // 2.转换为文档类型 ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class); IndexResponse response = esClient.index(i -> i .index("items")//指定索引名称 .id(itemDoc.getId())//指定主键 .document(itemDoc)//指定文档对象 ); //结果 String s = response.result().jsonValue(); log.info("result:"+s); } @AfterEach void tearDown() throws IOException { this.restClient.close(); } }
执行成功进行验证
我们使用DSL查询:
GET /items/_search
结果:
确定ES的索引中存在刚才添加的文档。
4.3.4 错误修复
这里我运行单测,提示seata相关错误,如下
解决方案:
- 注释pom文件中seata依赖,重启测试即可
4.4 查询文档
参考文档:https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/reading.html
通过阅读文档可知,这里实现的是根据ID查询文档,编写代码:
@Test void testGetDocumentById() throws IOException { GetResponse<ItemDoc> response = esClient.get(g -> g .index("items") .id("100002644680"), ItemDoc.class ); if (response.found()) { ItemDoc itemDoc = response.source(); log.info("itemDoc: " + itemDoc); } else { log.info ("itemDoc not found"); } }
4.5 删除文档
通过两个API方法的学习:
- 新增文档:esClient.index()方法
- 查询文档:esClient.get()方法
对于删除文档的方式可以根据代码提示自行编写,如下:
@Test void testDeleteDocumentById() throws IOException { DeleteResponse response = esClient.delete(d -> d .index("items") .id("100002644680") ); String s = response.result().jsonValue(); log.info("result:"+s); }
4.6 修改文档
- 局部修改
如果要更新的文档不存在会报错。
@Test void testUpdateDocumentById() throws IOException { //更新对象 ItemDoc itemDoc = new ItemDoc(); itemDoc.setName("更新名称"); UpdateResponse<ItemDoc> response = esClient.update(u -> u .index("items") .id("100002644680") .doc(itemDoc), ItemDoc.class); String s = response.result().jsonValue(); log.info("result:"+s); }
- 有则更新,没有则添加。
@Test void testUpdateDocumentById2() throws IOException { //更新对象 ItemDoc itemDoc = new ItemDoc(); itemDoc.setName("更新名称"); UpdateResponse<ItemDoc> response = esClient.update(u -> u .index("items") .id("100002644680aa") .doc(itemDoc) .docAsUpsert(true), ItemDoc.class); String s = response.result().jsonValue(); log.info("result:"+s); }
通过docAsUpsert(true)控制,如果没有该文档则添加新文档。
4.7. 批量导入
文档地址:https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/7.17/indexing-bulk.html
测试代码如下:
引包注意
@Test void testBatchAddDocment() throws Exception { //取第一页10条数据 Page<Item> page = Page.of(0, 10); //查询所有商品信息 Page<Item> itemPage = itemService.page(page, new LambdaQueryWrapper<Item>()); //获取商品集合 List<Item> items = itemPage.getRecords(); //拷贝属性 List<ItemDoc> itemDocs = BeanUtils.copyList(items, ItemDoc.class); //批量添加请求 BulkRequest.Builder br = new BulkRequest.Builder(); itemDocs.forEach(itemDoc -> br.operations(op -> op .index(i -> i .index("items") .id(itemDoc.getId().toString()) .document(itemDoc)))); //构建请求 BulkRequest build = br.build(); //批量添加 BulkResponse bulkResponse = esClient.bulk(build); //遍历结果 bulkResponse.items().forEach(item -> log.info("添加结果:{}",item.result().toString())); //如果有错误 if(bulkResponse.errors()){ log.error("批量添加失败"); //遍历错误 bulkResponse.items().forEach(item -> log.error("添加失败:{}",item.error().reason())); } }
此时去Kibana查询,发现多了刚才新增的10条,一共12条