一、前言
在前一篇文章中,我们学习了ES中的一部分的搜索功能,作为一名Java工程师,更多时候我们是用代码去操作ES,同时对于Java而言时下最流行的就是Springboot了,所以这里我们将ES和Springboot整合将上一篇文章中的所有DSL操作,使用Java代码来操作一遍从而加深记忆。
二、创建一个Springboot项目
1、POM文件
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>springboot-es</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-es</name>
<description>springboot-es</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.6</spring-boot.version>
</properties>
<dependencies>
<!--Spring Data ES-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!--Spring Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Spring Test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--Lombok:简化开发-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>com.example.SpringbootEsApplication</mainClass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
依赖解释:
(1)首先我们是用的是Springboot2.7.6的版本,JDK则使用的是17,当然用JDK8也是一样的。
(2)我们是用SpringData作为操作数据的框架,当然你也可以用原生的客户端。
(3)添加了Web依赖,我们直接使用接口的形式可以更方便的看出效果
(4)lombok:则是为了简化开发,可以简单的认为是用于生成get/set方法
2、配置文件
server:
port: 8088
spring:
elasticsearch:
uris: http://localhost:9200
这里没啥好说的,就是定义了程序运行在8088端口,同时es的地址为localhost:9200。至此Springboot和ES的整合就完成了,是不是很简单。当然这背后可一点也不简单,之所以我们可以这么简单的配置就能做好整合,是因为Springboot为我们提供的强大的自动装配,这个不在本文的范畴,暂时放下不表。
3、编写代码
在上一篇文章中,我们已经定义了索引的结构
PUT /hotel
{
"mappings": {
"properties": {
"title":{
"type": "text"
},
"city":{
"type": "keyword"
},
"price":{
"type": "double"
},
"create_time":{
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss"
},
"amenities":{
"type": "text"
},
"full_room":{
"type": "boolean"
},
"location":{
"type": "geo_point"
},
"praise":{
"type": "integer"
}
}
}
3.1、编写实体类
按照这个索引的结构,我们创建对应的实体类,这个和我们是用Mybatis时很像,也要定义对应的实体类。
package com.example.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.math.BigDecimal;
import java.util.Date;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.GeoPointField;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
/**
* @Author hardy(叶阳华)
* @Description
* @Date 2024/11/26 14:06
*/
@Data
@Document(indexName = "hotel", createIndex = false)
public class Hotel {
/**
* ID
*/
@Id
private String id;
/**
* 标题:text类型,使用ik_max_word作为分词器
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
private String title;
/**
* 所在城市:所在城市没必要分词
*/
@Field(type = FieldType.Keyword)
private String city;
/**
* 价格
*/
@Field(type = FieldType.Double)
private BigDecimal price;
/**
* 便利措施
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
private String amenities;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Field(value = "create_time", type = FieldType.Date, format = {}, pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 是否满员
*/
@Field(value = "full_room", type = FieldType.Boolean)
private Boolean fullRoom;
/**
* 位置
*/
@GeoPointField
private GeoPoint location;
@Field(type = FieldType.Integer)
private Integer praise;
}
说一下笔者编写过程中碰到的小坑
(1)Date类型,如果要自定义日期的格式,需要将format设置为custom(不建议,即将被废弃),或者设置为{},否则就会提示日期转换异常
(2)地理位置,一开始笔者也以为可以直接使用@Field(type = FieldType.Point) 然后,并没有这个类型,官网上则说使用 @GeoPoint 然而还是没有,最终发现有一个@GeoPointField尝试了一下发现可以。
(3)@GeoPointField 并没有任何属性,如果我们的字段名和索引中的字段名不一致,则可以使用 @Field去标记,如
3.2、查询索引是否存在
@Resource
private ElasticsearchOperations elasticsearchOperations;
/**
* 校验索引是否存在
*/
@GetMapping("checkIndexExists")
public Boolean checkIndexExists() {
return elasticsearchOperations.indexOps(Hotel.class).exists();
}
tips:ElasticsearchOperations是SpringData为我们提供的一个工具类,用于简化开发。
执行结果
3.3、批量写入数据
/**
* 批量写入
*/
@PostMapping("/batchCreate")
public Boolean batchCreate(@RequestBody List<Hotel> hotelList) {
if (CollectionUtils.isEmpty(hotelList)) {
throw new IllegalArgumentException("参数不能为空");
}
final Iterable<Hotel> hotels = elasticsearchOperations.save(hotelList);
hotels.iterator().forEachRemaining(System.out::println);
return true;
}
请求参数:
[
{
"id": "001",
"title": "文雅酒店",
"city": "青岛",
"price": 556,
"createTime": "2020-04-18 12:00:00",
"amenities": "浴池,普通停车场/充电停车场",
"fullRoom": false,
"location": {
"lat": 36.083078,
"lon": 120.37566
},
"praise": 10
},
{
"id": "002",
"title": "金都嘉怡假日酒店",
"city": "北京",
"price": 337,
"createTime": "2021-03-15 20:00:00",
"amenities": "wifi,充电停车场/可升降停车场",
"fullRoom": false,
"location": {
"lat": 39.915153,
"lon": 116.403
},
"praise": 60
},
{
"id": "003",
"itle": "金都欣欣酒店",
"city": "天津",
"price": 200,
"createTime": "2021-05-09 16:00:00",
"amenities": "提供假日party,免费早餐,可充电停车场",
"fullRoom": true,
"location": {
"lat": 39.186555,
"lon": 117.162007
},
"praise": 30
},
{
"id": "004",
"title": "金都酒店",
"city": "北京",
"price": 500,
"createTime": "2021-02-18 08:00:00",
"amenities": "浴池(假日需预定),室内游泳池,普通停车场",
"fullRoom": true,
"location": {
"lat": 39.915343,
"lon": 116.4239
},
"praise": 20
},
{
"id": "005",
"title": "文雅精选酒店",
"city": "北京",
"price": 800,
"createTime": "2021-01-01 08:00:00",
"amenities": "浴池(假日需预定),wifi,室内游泳池,普通停车场",
"fullRoom": true,
"location": {
"lat": 39.918229,
"lon": 116.422011
},
"praise": 20
}
]
结果:
3.4、查询所有数据
@GetMapping("/queryAll")
public List<Hotel> queryAll() {
Criteria criteria = new Criteria();
Query query = new CriteriaQuery(criteria);
final SearchHits<Hotel> searchHits = elasticsearchOperations.search(query, Hotel.class);
//没有命中任何文档
if (!searchHits.hasSearchHits()) {
return new ArrayList<>();
}
return searchHits.getSearchHits().stream().map(SearchHit::getContent).collect(Collectors.toList());
}
结果:
3.5、分页查询
@GetMapping("/pageQuery")
public Page<Hotel> pageQuery(String city, int pageNo, int pageSize) {
Criteria criteria = Criteria.where("city").is(city);
PageRequest pageRequest = PageRequest.of(pageNo, pageSize);
Query query = new CriteriaQuery(criteria);
query.setPageable(pageRequest);
final SearchHits<Hotel> searchHits = elasticsearchOperations.search(query, Hotel.class);
if (!searchHits.hasSearchHits()) {
return new PageImpl<>(new ArrayList<>());
}
List<Hotel> hotels = searchHits.getSearchHits().stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
//总记录数
final long totalHits = searchHits.getTotalHits();
// 返回分页结果
return new PageImpl<>(hotels, pageRequest, totalHits);
}
结果:
{
"content": [
{
"id": "002",
"title": "金都嘉怡假日酒店",
"city": "北京",
"price": 337,
"amenities": "wifi,充电停车场/可升降停车场",
"createTime": "2021-03-15 20:00:00",
"fullRoom": false,
"location": {
"lat": 39.915153,
"lon": 116.403
},
"praise": 60
},
{
"id": "004",
"title": "金都酒店",
"city": "北京",
"price": 500,
"amenities": "浴池(假日需预定),室内游泳池,普通停车场",
"createTime": "2021-02-18 08:00:00",
"fullRoom": true,
"location": {
"lat": 39.915343,
"lon": 116.4239
},
"praise": 20
},
{
"id": "005",
"title": "文雅精选酒店",
"city": "北京",
"price": 800,
"amenities": "浴池(假日需预定),wifi,室内游泳池,普通停车场",
"createTime": "2021-01-01 08:00:00",
"fullRoom": true,
"location": {
"lat": 39.918229,
"lon": 116.422011
},
"praise": 20
}
],
"pageable": {
"sort": {
"empty": true,
"unsorted": true,
"sorted": false
},
"offset": 0,
"pageNumber": 0,
"pageSize": 3,
"paged": true,
"unpaged": false
},
"last": true,
"totalPages": 1,
"totalElements": 3,
"first": true,
"size": 3,
"number": 0,
"sort": {
"empty": true,
"unsorted": true,
"sorted": false
},
"numberOfElements": 3,
"empty": false
}
和上面直接返回所有数据不同,分页查询还会返回分页相关信息
三、结束语
至此Springboot和ES已经整合,并且做了一些基础的查询操作,后续我们会继续审核ES的查询,更多的复杂的查询在后面的文章中,希望对你有所帮助。