Elasticsearch是一个基于Lucene的分布式搜索和分析引擎,广泛应用于全文搜索、日志分析等场景。结合Spring Boot可以快速构建强大的搜索应用。本文将介绍如何在Spring Boot项目中集成和使用Elasticsearch。
ES9.0.1目前支持的包只有
elasticsearch-rest-client/ - - elasticsearch-rest-client-sniffer/
注意:9.0版本暂不支持elasticsearch-rest-high-level-client
所以我们要用elasticsearch-rest-client来进行开发。
一、环境准备
1. 添加依赖
首先,在Spring Boot项目的`pom.xml`中添加必要的依赖:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>EsExample</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<mybatisplus.version>3.5.12</mybatisplus.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<!-- <!– MyBatis集成 –>-->
<!-- <dependency>-->
<!-- <groupId>org.mybatis.spring.boot</groupId>-->
<!-- <artifactId>mybatis-spring-boot-starter</artifactId>-->
<!-- <version>${mybatis.version}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.23</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>3.3.4</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.6</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.2.5.Final</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>9.0.1</version>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>9.0.1</version>
</dependency>
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.16.0</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>elasticsearch</artifactId>
<version>1.21.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.21</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.21.0</version>
</dependency>
<dependency>
<groupId>com.github.slugify</groupId>
<artifactId>slugify</artifactId>
<version>3.0.6</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.apache.httpcomponents</groupId>-->
<!-- <artifactId>httpclient</artifactId>-->
<!-- <version>4.5.13</version>-->
<!-- </dependency>-->
<dependency>
<groupId>jakarta.json.bind</groupId>
<artifactId>jakarta.json.bind-api</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.eclipse</groupId>
<artifactId>yasson</artifactId>
<version>3.0.4</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatisplus.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>aliyun</id>
<url>https://maven.aliyun.com/nexus/content/groups/public</url>
</repository>
<repository>
<id>oss-public</id>
<url>https://oss.sonatype.org/content/repositories/public</url>
</repository>
<repository>
<id>snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</project>
2. 配置Elasticsearch连接
在 application.yml 中配置Elasticsearch连接,pgsql连接:
Elasticsearch配置
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/postgres?stringtype=unspecified
username: postgres
password: 123456
jpa:
hibernate:
ddl-auto: update #自动生成数据库表
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
show-sql: true # jpa配置,在控制台显示hibernate的sql
server:
port: 8904
servlet:
encoding:
charset: UTF-8
elasticsearch:
server:
url: https://localhost:9200
api:
key: YL5i65YB6ixG4DLbphIW
userName: elastic
password: _XRuyepioTeZGzQOhUfk
host: localhost
port: 9200
logging:
level:
org.example.mapper: debug
file:
path: D://testaa/img/
二、基本用法
1. 定义实体类
使用 Table 来注解标记表名称以及es的索引:
@Id注解用于es的id
/**
* 测试日期
*
* @author lyl
* @version v1.0
* @since 2025/5/26
*/
@Data
@Table(name="sys_base_data")
@TableName("sys_base_data")
public class StockBaseEntity implements Serializable {
@Id
private String id;
private String name;
@TableField(fill= FieldFill.INSERT)
@JsonFormat(shape= JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd",timezone = "GMT+8")
private String createDate;
@TableField(fill= FieldFill.INSERT)
@JsonFormat(shape=JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
private Date updateDate;
}
2. 创建ES连接对象
Spring Data Elasticsearch提供了类似于JPA的Repository接口:
package org.example.es.service;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.message.BasicHeader;
import org.elasticsearch.client.RestClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
@Configuration
public class ElasticClient {
@Value("${elasticsearch.server.url}")
private String serverUrl;
@Value("${elasticsearch.api.key}")
private String apiKey;
@Value("${elasticsearch.host}")
private String host;
@Value("${elasticsearch.port}")
private int port;
@Value("${elasticsearch.userName}")
private String userName;
@Value("${elasticsearch.password}")
private String password;
@Autowired
ResourceLoader resourceLoader;
@Bean
public ElasticsearchClient elasticRestClient() throws IOException {
// ES服务器URL
// Connection settings
try {
HttpHost httphost = new HttpHost(host, port, "https");
SSLContext sslContext = createInsecureSSLContext();
BasicCredentialsProvider credsProv = new BasicCredentialsProvider();
credsProv.setCredentials(
AuthScope.ANY, new UsernamePasswordCredentials(userName, password)
);
// Building the rest client
RestClient restClient = RestClient.builder(httphost)
.setHttpClientConfigCallback(hc -> hc
.setDefaultCredentialsProvider(credsProv)
.setSSLContext(sslContext)
)
.build();
ObjectMapper mapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.build();
ElasticsearchTransport transport = new RestClientTransport(restClient,
new JacksonJsonpMapper(mapper));
ElasticsearchClient esClient = new ElasticsearchClient(transport);
// Creating the indexes
// createSimpleIndex(esClient, USERS);
return esClient;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (KeyManagementException e) {
throw new RuntimeException(e);
}
}
/**
* 禁用ssl验证
*
* @return
* @throws NoSuchAlgorithmException
* @throws KeyManagementException
*/
public SSLContext createInsecureSSLContext() throws NoSuchAlgorithmException, KeyManagementException {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}}, new SecureRandom());
return sslContext;
}
private void createSimpleIndex(ElasticsearchClient esClient, String index) throws IOException {
BooleanResponse indexRes = esClient.indices().exists(ex -> ex.index(index));
if (!indexRes.value()) {
esClient.indices().create(c -> c
.index(index));
}
}
private void createIndexWithDateMapping(ElasticsearchClient esClient, String index) throws IOException {
BooleanResponse indexRes = esClient.indices().exists(ex -> ex.index(index));
if (!indexRes.value()) {
esClient.indices().create(c -> c
.index(index)
.mappings(m -> m
.properties("createdAt", p -> p
.date(d -> d))
.properties("updatedAt", p -> p
.date(d -> d))));
}
}
}
3. 创建通用的操作,让所有Reposity继承此类
package org.example.es.service;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.Refresh;
import co.elastic.clients.elasticsearch._types.SortOrder;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.reflect.GenericTypeUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.example.es.util.TableInfoParamUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 业务实现类封装
*
* @author lyl
* @version v1.0
* @since 2025/5/21
*/
@Slf4j
public class EsService<T> {
public final ElasticsearchClient esClient;
/**
* 索引字段,自动获取T对象里@Table注解字段名
*/
public String index;
private Class<T> clazz;
/**
* 获取当前类对象
*
* @return
*/
// private Class<T> getClazz() {
// return (Class<T>) GenericTypeUtils.resolveTypeArguments(getClass(), EsService.class)[0];
// }
public EsService(ElasticsearchClient esClient, Class<T> clazz) {
this.esClient = esClient;
// 获取当前类对应的数据库表名
TableInfoParamUtil tableInfoParamUtil = new TableInfoParamUtil();
//Class<T> clazz = getClazz();
this.clazz = clazz;
this.index = tableInfoParamUtil.getTableName(clazz);
}
/**
* 列表
*
* @param params 参数列表,key为数据库字段名
* @return
*/
public List<T> list(Map<String, Object> params) {
Class<T> clazz = this.clazz;
try {
List<Query> conditions = TableInfoParamUtil.changeMapParam(params);
Query query = new Query.Builder().bool(b -> b.should(conditions)).build();
SearchResponse<T> response = esClient.search(s -> s
.index(index)
.query(query)
, clazz);
return response.hits().hits().stream()
.map(hit -> hit.source())
.collect(Collectors.toList());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 分页查询
*
* @param params 查询参数
* @param pageNum 当前页码
* @param pageSize 每页条数
* @param sortField 排序字段
* @return
*/
public IPage<T> page(Map<String, Object> params, int pageNum, int pageSize, String sortField) {
int offset = (pageNum - 1) * pageSize;
List<Query> conditions = TableInfoParamUtil.changeMapParam(params);
Query query = new Query.Builder().bool(b -> b.should(conditions)).build();
try {
SearchResponse<T> getArticle = esClient.search(ss -> ss
.index(index)
.size(pageSize) // how many results to return
.from(offset) // starting point
.query(query)
.sort(srt -> srt
.field(fld -> fld
.field(sortField)
.order(SortOrder.Desc))) // last updated first
, this.clazz);
if (getArticle.hits().hits().size() > 0) {
List<T> collect = getArticle.hits().hits().stream()
.map(hit -> hit.source())
.collect(Collectors.toList());
IPage<T> page = new Page<>(pageNum, pageSize);
page.setRecords(collect);
page.setTotal(getArticle.hits().total().value());
return page;
}
} catch (IOException e) {
log.error(e.getMessage());
}
return null;
}
/**
* 保存
*
* @param nodeDocument
*/
public void save(T nodeDocument) {
IndexRequest<T> articleReq = IndexRequest.of((id -> id
.index(index)
.id(TableInfoParamUtil.getIdValue(nodeDocument))
.refresh(Refresh.WaitFor)
.document(nodeDocument)));
try {
esClient.index(articleReq).id();
} catch (IOException e) {
log.error(e.getMessage());
}
}
/**
* 批量保存
*
* @param nodeList
*/
public boolean saveBatch(List<T> nodeList) {
try {
BulkRequest.Builder brBuilder = new BulkRequest.Builder();
for (T product : nodeList) {
brBuilder.operations(b -> b
.index(idx -> idx.index(index)
.id(TableInfoParamUtil.getIdValue(product))
.document(product)));
}
BulkRequest bulkRequest = brBuilder.build();
BulkResponse response = esClient.bulk(bulkRequest);
if (!response.errors()) {
return true;
}
} catch (IOException e) {
log.error(e.getMessage());
}
return false;
}
/**
* 根据id删除数据
*
* @param id
*/
public void delete(String id) {
try {
DeleteResponse deleteArticle = esClient.delete(d -> d
.index(index)
.id(id));
if (!deleteArticle.result().name().equals("deleted")) {
throw new RuntimeException("Failed to delete article");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 更新
*
* @param nodeDocument
*/
public void update(T nodeDocument, String id) {
try {
Class<T> cla = (Class<T>) nodeDocument.getClass();
boolean exists = esClient.exists(b -> b
.index(index)
.id(id)
).value();
if (exists) {
System.out.println("exists");
UpdateResponse<T> upArticle = esClient.update(up -> up
.index(index)
.id(id)
.doc(nodeDocument), cla);
if (!upArticle.result().name().toLowerCase().equals("updated")) {
throw new RuntimeException("Article update failed");
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 根据id查询
*
* @param id 对象id
* @return
*/
public T getById(String id) {
try {
GetResponse<T> searchResponse = esClient.get(ss -> ss
.index(index)
.id(id)
, this.clazz);
if (searchResponse.source() == null) {
return null;
}
return searchResponse.source();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 获取总数
*
* @return
*/
public long getCount(Map<String, Object> params) {
try {
SearchResponse<T> searchResponse = null;
if (null != params && params.size() > 0) {
List<Query> conditions = TableInfoParamUtil.changeMapParam(params);
Query query = new Query.Builder().bool(b -> b.should(conditions)).build();
searchResponse = esClient.search(ss -> ss
.index(index)
.size(0)
.query(query)
, this.clazz);
} else {
//查全部
searchResponse = esClient.search(ss -> ss
.index(index)
.size(0)
.query(q -> q
.matchAll(m -> m))
, this.clazz);
}
return searchResponse.hits().total().value();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void createIndex(){
try {
BooleanResponse indexRes = esClient.indices().exists(ex -> ex.index(index));
if (!indexRes.value()) {
esClient.indices().create(c -> c
.index(index));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void deleteIndex(){
try {
BooleanResponse indexRes = esClient.indices().exists(ex -> ex.index(index));
if (indexRes.value()) {
esClient.indices().delete(d -> d
.index(index));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void createMapping() {
try {
BooleanResponse indexRes = esClient.indices().exists(ex -> ex.index(index));
if (indexRes.value()) {
esClient.indices().delete(d->d.index(index));
esClient.indices().create(p -> p
.index(index)
.mappings(m -> m
.properties("createDate", pp -> pp
.date(d -> d.format("yyyy-MM-dd")))
.properties("updateDate",pp->pp.date(d->d.format("yyyy-MM-dd HH:mm:ss")))));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
从实体对象分析es的索引和id自动注入。
package org.example.es.util;
import co.elastic.clients.elasticsearch._types.query_dsl.MatchQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 参数转换工具类
*
* @author lyl
* @version v1.0
* @since 2025/5/21
*/
public class TableInfoParamUtil {
/**
* 查询参数封装
* @param params
* @return
*/
public static List<Query> changeMapParam(Map<String, Object> params) {
List<Query> conditions = new ArrayList<>();
params.forEach((k, v) -> {
Query query = null;
if (v instanceof String) {
String value = v.toString();
query = new MatchQuery.Builder()
.field(k)
.query(value).build()._toQuery();
} else if (v instanceof Number) {
query = new MatchQuery.Builder()
.field(k)
.query(Long.valueOf(v.toString())).build()._toQuery();
} else if (v instanceof Boolean) {
query = new MatchQuery.Builder()
.field(k)
.query(Boolean.valueOf(v.toString())).build()._toQuery();
} else if (v instanceof Integer) {
query = new MatchQuery.Builder()
.field(k)
.query(Integer.valueOf(v.toString())).build()._toQuery();
} else if (v instanceof Float) {
query = new MatchQuery.Builder()
.field(k)
.query(Float.valueOf(v.toString())).build()._toQuery();
} else if (v instanceof Double) {
query = new MatchQuery.Builder()
.field(k)
.query(Double.valueOf(v.toString())).build()._toQuery();
}
if (null != query) {
conditions.add(query);
}
});
return conditions;
}
/**
* 获取表名,分析@Table注解
* @param clazz
* @return
*/
public String getTableName(Class<?> clazz) {
// 检查类是否有 Table 注解
if (clazz.isAnnotationPresent(Table.class)) {
// 获取注解实例
Table tableAnnotation = clazz.getAnnotation(Table.class);
// 读取注解的属性值
String tableName = tableAnnotation.name();
return tableName;
}
return null;
}
/**
* 获取主键值
* @param entity
* @return
*/
public static <T> String getIdValue(T entity) {
Class<?> clazz = entity.getClass();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Id.class)) {
field.setAccessible(true); // 允许访问私有字段
try {
String id=String.valueOf(field.get(entity));
return id; // 获取字段值
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
return null;
}
}
业务类实现EsService接口,除了基本增加,修改,删除,查询外,编写自定义查询方法。
package org.example.es.service;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.Time;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch._types.query_dsl.RangeQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.RangeRelation;
import co.elastic.clients.elasticsearch.core.SearchResponse;
import org.example.es.entity.StockBaseEntity;
import org.example.es.util.TableInfoParamUtil;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
/**
* 业务实现类
*
* @author lyl
* @version v1.0
* @since 2025/5/26
*/
@Service
public class StockBaseResposity extends EsService<StockBaseEntity> {
/**
* 获取当前类对象
*
* @param esClient
* @param clazz
* @return
*/
@Autowired
public StockBaseResposity(ElasticsearchClient esClient) {
super(esClient, StockBaseEntity.class);
}
/**
* 把某一年的数据按7天分组
* @return
*/
public List<StockBaseEntity> search(){
try {
Query byMaxPrice = RangeQuery.of(r -> r
.date(n -> n
.field("createDate")
.gte("2024-09-01")
.lte("2025-04-01")
.format("yyyy-MM-dd"))
)._toQuery();
Query dateDistanceFeatureQuery = Query.of(q -> q.bool(b -> b
.must(m -> m.matchAll(mm -> mm))
.should(sh -> sh.distanceFeature(df -> df
.date(d -> d
.field("createDate")
.pivot(Time.of(t -> t.time("7d")))
.origin("now"))))));
//根据条件查询相应的数据
Query query1 = Query.of(q->q.bool(b->b.must(byMaxPrice)));
SearchResponse<StockBaseEntity> response = esClient.search(s -> s
.index(index)
.query(dateDistanceFeatureQuery)
.aggregations("dd",t->t.aggregations("day",
a->a.dateHistogram(d->d.field("createDate").format("yyyy"))))
.size(100)
, StockBaseEntity.class);
return response.hits().hits().stream()
.map(hit -> hit.source())
.collect(Collectors.toList());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
三、高级查询
1. 使用
// 聚合查询示例
public List<StockBaseEntity> search(){
try {
Query dateDistanceFeatureQuery = Query.of(q -> q.bool(b -> b
.must(m -> m.matchAll(mm -> mm))
.should(sh -> sh.distanceFeature(df -> df
.date(d -> d
.field("createDate")
.pivot(Time.of(t -> t.time("7d")))
.origin("now"))))));
//查某一年的数据
SearchResponse<StockBaseEntity> response = esClient.search(s -> s
.index(index)
.query(dateDistanceFeatureQuery)
.aggregations("dd",t->t.aggregations("day",
a->a.dateHistogram(d->d.field("createDate").format("yyyy"))))
.size(100)
, StockBaseEntity.class);
return response.hits().hits().stream()
.map(hit -> hit.source())
.collect(Collectors.toList());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
四、索引管理
1. 创建索引
public void createIndex(){
try {
BooleanResponse indexRes = esClient.indices().exists(ex -> ex.index(index));
if (!indexRes.value()) {
esClient.indices().create(c -> c
.index(index));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
2. 索引映射管理
可以在实体类中使用注解定义更详细的映射:
Elasticsearch 默认支持的日期格式是 strict_date_optional_time,它支持以下形式的日期时间字符串:
yyyy-MM-dd
yyyy-MM-dd HH:mm:ss
yyyy-MM-dd HH:mm:ss.SSS
例如:
2023-10-01
2023-10-01 12:30:45
2023-10-01 12:30:45.123
如果你需要使用其他日期格式,可以在字段映射(mapping)中自定义日期格式。例如:
public void createMapping() {
try {
BooleanResponse indexRes = esClient.indices().exists(ex -> ex.index(index));
if (indexRes.value()) {
// esClient.indices().delete(d->d.index(index));
esClient.indices().create(p -> p
.index(index)
.mappings(m -> m
.properties("createDate", pp -> pp
.date(d -> d.format("yyyy-MM-dd")))
.properties("updateDate",pp->pp.date(d->d.format("yyyy-MM-dd HH:mm:ss")))));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
然后在resources目录下创建对应的settings和mappings文件。
五、性能优化
1. 批量操作:使用`bulk`API进行批量索引
/**
* 批量保存
*
* @param nodeList
*/
public boolean saveBatch(List<T> nodeList) {
try {
BulkRequest.Builder brBuilder = new BulkRequest.Builder();
for (T product : nodeList) {
brBuilder.operations(b -> b
.index(idx -> idx.index(index)
.id(TableInfoParamUtil.getIdValue(product))
.document(product)));
}
BulkRequest bulkRequest = brBuilder.build();
BulkResponse response = esClient.bulk(bulkRequest);
if (!response.errors()) {
return true;
}
} catch (IOException e) {
log.error(e.getMessage());
}
return false;
}
七、总结
Spring Boot与Elasticsearch的集成为开发强大的搜索功能提供了便利。通过elasticsearch-rest-client,我们可以:
1. 使用熟悉的Repository模式进行基本操作
2. 利用ElasticsearchTemplate实现复杂查询
3. 轻松管理索引和映射
4. 实现全文搜索、聚合分析等高级功能
在实际项目中,应根据业务需求合理设计索引结构,优化查询性能,并注意数据同步策略,确保Elasticsearch中的数据与主数据源保持一致。
通过本文介绍的方法,您可以快速在Spring Boot项目中集成Elasticsearch,构建高效的搜索和分析功能。