Spring Boot 使用 Elasticsearch
·
1. 依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2. application.yml 中配置了 ES 连接信息:
# Spring配置
spring:
elasticsearch:
uris: http://localhost:9201
socket-timeout: 10s
username: elastic
password: qD+=4WRHuM7JgmJVo-ZD
ssl:
verification-mode: none # 忽略证书验证
3. Elasticsearch模板引擎接口
import java.util.List;
import java.util.Map;
/**
* Elasticsearch模板引擎接口
* 基于 Spring Data Elasticsearch 5.x
* 适用于 Spring Boot 3.3.x
*
* @param <T> 泛型
* @author jz
*/
public interface ElasticsearchTemplateEngine<T> {
// ==================== 索引操作 ====================
/**
* 创建索引
*
* @param clazz 索引实体类(需要@Document注解)
* @return 是否创建成功
*/
boolean createIndex(Class<T> clazz);
/**
* 判断索引库是否存在
*
* @param index 索引名
* @return 是否存在
*/
boolean ifExist(String index);
/**
* 根据索引库名删除索引
*
* @param index 索引库名
* @return 是否删除成功
*/
boolean deteleIndex(String index);
// ==================== 新增操作 ====================
/**
* 新增单条数据
*
* @param t 实体对象
* @param indexName 索引名
*/
void saveOne(T t, String indexName);
/**
* 批量新增数据
*
* @param list 实体集合
* @param indexName 索引名
* @return 成功数量
*/
int saveBatch(List<T> list, String indexName);
// ==================== 更新操作 ====================
/**
* 批量更新数据
*
* @param list 实体集合
* @param indexName 索引名
* @return 成功数量
*/
int updateBatch(List<T> list, String indexName);
/**
* 更新单条数据
*
* @param t 实体对象
* @param indexName 索引名
*/
void updateOne(T t, String indexName);
/**
* 根据脚本更新单条数据
*
* @param id 文档ID
* @param params 脚本参数
* @param script Painless脚本,如:ctx._source.name = params.newName
* @param indexName 索引名
*/
void updateOneByScript(String id, Map<String, Object> params, String script, String indexName);
/**
* 根据脚本批量更新数据
*
* @param idLists ID列表
* @param params 脚本参数Map,key为文档ID
* @param script Painless脚本
* @param indexName 索引名
*/
void updateBatchByScript(List<String> idLists, Map<String, Map<String, Object>> params, String script, String indexName);
// ==================== 查询操作 ====================
/**
* 查询单条记录
*
* @param id 文档ID
* @param clazz 返回类型
* @param indexName 索引名
* @return 实体对象
*/
T findOne(String id, Class<T> clazz, String indexName);
/**
* 查询统计数量
*
* @param query 查询条件对象
* @param indexName 索引名
* @return 数量
*/
long findCount(T query, String indexName);
/**
* 查询列表
*
* @param query 查询条件对象
* @param clazz 返回类型
* @param indexName 索引名
* @return 结果列表
*/
<R> List<R> findList(T query, Class<R> clazz, String indexName);
/**
* 查询列表(带bool查询条件)
*
* @param query 查询条件对象
* @param clazz 返回类型
* @param indexName 索引名称
* @param boolQueryConsumer bool查询条件构建器
* @return 结果列表
*/
<R> List<R> findList(T query, Class<R> clazz, String indexName, BoolQuery.Builder boolQueryConsumer);
/**
* 查询列表(带bool查询条件和过滤条件)
*
* @param query 查询条件对象
* @param clazz 返回类型
* @param indexName 索引名称
* @param boolQueryConsumer bool查询条件构建器
* @param boolFilterConsumer bool过滤条件构建器
* @return 结果列表
*/
<R> List<R> findList(T query, Class<R> clazz, String indexName, BoolQuery.Builder boolQueryConsumer, BoolQuery.Builder boolFilterConsumer);
/**
* 分页查询
*
* @param query 查询条件对象(需包含pageNum、pageSize字段)
* @param clazz 返回类型
* @param indexName 索引名称
* @return 分页结果
*/
<R> EsPageVO<R> findPage(T query, Class<R> clazz, String indexName);
/**
* 分页查询(带bool查询条件)
*
* @param query 查询条件对象
* @param clazz 返回类型
* @param indexName 索引名称
* @param boolQueryConsumer bool查询条件构建器
* @return 分页结果
*/
<R> EsPageVO<R> findPage(T query, Class<R> clazz, String indexName, BoolQuery.Builder boolQueryConsumer);
/**
* 分页查询(带bool查询条件和过滤条件)
*
* @param query 查询条件对象
* @param clazz 返回类型
* @param indexName 索引名称
* @param boolQueryConsumer bool查询条件构建器
* @param boolFilterConsumer bool过滤条件构建器
* @return 分页结果
*/
<R> EsPageVO<R> findPage(T query, Class<R> clazz, String indexName, BoolQuery.Builder boolQueryConsumer, BoolQuery.Builder boolFilterConsumer);
/**
* 聚合查询
*
* @param query 查询条件对象
* @param clazz 返回类型
* @param indexName 索引名
* @return 聚合结果
*/
<R> List<R> aggregation(T query, Class<R> clazz, String indexName) throws Exception;
/**
* 查询全部列表
*
* @param clazz 返回类型
* @param indexName 索引名
* @return 结果列表
*/
<R> List<R> seachList(Class<R> clazz, String indexName);
/**
* 自动补全建议
*
* @param indexName 索引名
* @param prefix 前缀
* @param category 分类(可选)
* @return 建议列表
*/
List<String> suggest(String indexName, String prefix, String category) throws Exception;
// ==================== 删除操作 ====================
/**
* 删除单条数据
*
* @param id 文档ID
* @param indexName 索引名称
*/
void delete(String id, String indexName);
/**
* 批量删除数据
*
* @param list ID集合
* @param clazz 文档类型
* @param indexName 索引名称
*/
<D> void deleteBatch(List<String> list, Class<D> clazz, String indexName);
/**
* 根据ID列表删除数据
*
* @param list ID集合
* @param clazz 文档类型
* @param indexName 索引名称
*/
<D> void deleteSeach(List<String> list, Class<D> clazz, String indexName);
}
4. Elasticsearch模板引擎实现类
import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.client.elc.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
import org.springframework.data.elasticsearch.client.elc.NativeQueryBuilder;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates;
import org.springframework.data.elasticsearch.core.query.IndexQuery;
import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder;
import org.springframework.data.elasticsearch.core.query.ScriptType;
import org.springframework.data.elasticsearch.core.query.UpdateQuery;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Elasticsearch模板引擎实现类
* 基于 Spring Data Elasticsearch 5.x (ElasticsearchTemplate)
* 适用于 Spring Boot 3.3.x
*
* @param <T> 泛型
*/
@Component
public class ElasticsearchTemplateEngineImpl<T> implements ElasticsearchTemplateEngine<T> {
private static final Logger log = LoggerFactory.getLogger(ElasticsearchTemplateEngineImpl.class);
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;
// ==================== 索引操作 ====================
@Override
public boolean createIndex(Class<T> clazz) {
try {
IndexOperations indexOps = elasticsearchTemplate.indexOps(clazz);
if (!indexOps.exists()) {
indexOps.create();
Document mapping = indexOps.createMapping(clazz);
indexOps.putMapping(mapping);
return true;
}
return false;
} catch (Exception e) {
log.error("创建索引失败", e);
return false;
}
}
@Override
public boolean ifExist(String index) {
try {
IndexOperations indexOps = elasticsearchTemplate.indexOps(IndexCoordinates.of(index));
return indexOps.exists();
} catch (Exception e) {
log.error("判断索引是否存在失败", e);
return false;
}
}
@Override
public boolean deteleIndex(String index) {
try {
IndexOperations indexOps = elasticsearchTemplate.indexOps(IndexCoordinates.of(index));
return indexOps.delete();
} catch (Exception e) {
log.error("删除索引失败", e);
return false;
}
}
// ==================== 新增操作 ====================
@Override
public void saveOne(T t, String indexName) {
try {
IndexQuery indexQuery = new IndexQueryBuilder()
.withId(getIdValue(t))
.withObject(t)
.build();
elasticsearchTemplate.index(indexQuery, IndexCoordinates.of(indexName));
} catch (Exception e) {
log.error("新增单条数据失败", e);
throw new RuntimeException("新增单条数据失败", e);
}
}
@Override
public int saveBatch(List<T> list, String indexName) {
if (list == null || list.isEmpty()) {
return 0;
}
try {
List<IndexQuery> queries = new ArrayList<>();
for (T t : list) {
IndexQuery indexQuery = new IndexQueryBuilder()
.withId(getIdValue(t))
.withObject(t)
.build();
queries.add(indexQuery);
}
elasticsearchTemplate.bulkIndex(queries, IndexCoordinates.of(indexName));
return list.size();
} catch (Exception e) {
log.error("批量新增数据失败", e);
throw new RuntimeException("批量新增数据失败", e);
}
}
// ==================== 更新操作 ====================
@Override
public int updateBatch(List<T> list, String indexName) {
if (list == null || list.isEmpty()) {
return 0;
}
try {
List<UpdateQuery> updateQueries = new ArrayList<>();
for (T t : list) {
String id = getIdValue(t);
if (id == null) {
continue;
}
Document document = Document.create();
Map<String, Object> objectMap = objectToMap(t);
objectMap.forEach(document::put);
UpdateQuery updateQuery = UpdateQuery.builder(id)
.withDocument(document)
.withDocAsUpsert(true)
.build();
updateQueries.add(updateQuery);
}
elasticsearchTemplate.bulkUpdate(updateQueries, IndexCoordinates.of(indexName));
return updateQueries.size();
} catch (Exception e) {
log.error("批量更新数据失败", e);
throw new RuntimeException("批量更新数据失败", e);
}
}
@Override
public void updateOne(T t, String indexName) {
try {
String id = getIdValue(t);
if (id == null) {
throw new IllegalArgumentException("实体ID不能为空");
}
Document document = Document.create();
Map<String, Object> objectMap = objectToMap(t);
objectMap.forEach(document::put);
UpdateQuery updateQuery = UpdateQuery.builder(id)
.withDocument(document)
.withDocAsUpsert(true)
.build();
elasticsearchTemplate.update(updateQuery, IndexCoordinates.of(indexName));
} catch (Exception e) {
log.error("更新单条数据失败", e);
throw new RuntimeException("更新单条数据失败", e);
}
}
@Override
public void updateOneByScript(String id, Map<String, Object> params, String script, String indexName) {
try {
if (id == null || id.isEmpty()) {
throw new IllegalArgumentException("ID不能为空");
}
Map<String, Object> scriptParams = params != null ? new HashMap<>(params) : new HashMap<>();
UpdateQuery updateQuery = UpdateQuery.builder(id)
.withScript(script)
.withScriptType(ScriptType.INLINE)
.withLang("painless")
.withParams(scriptParams)
.build();
elasticsearchTemplate.update(updateQuery, IndexCoordinates.of(indexName));
} catch (Exception e) {
log.error("根据脚本更新单条数据失败", e);
throw new RuntimeException("根据脚本更新单条数据失败", e);
}
}
@Override
public void updateBatchByScript(List<String> idLists, Map<String, Map<String, Object>> params, String script, String indexName) {
if (idLists == null || idLists.isEmpty()) {
return;
}
try {
List<UpdateQuery> updateQueries = new ArrayList<>();
for (String id : idLists) {
if (id == null || id.isEmpty()) {
continue;
}
Map<String, Object> scriptParams = new HashMap<>();
if (params != null && params.containsKey(id)) {
scriptParams.putAll(params.get(id));
}
UpdateQuery updateQuery = UpdateQuery.builder(id)
.withScript(script)
.withScriptType(ScriptType.INLINE)
.withLang("painless")
.withParams(scriptParams)
.build();
updateQueries.add(updateQuery);
}
if (!updateQueries.isEmpty()) {
elasticsearchTemplate.bulkUpdate(updateQueries, IndexCoordinates.of(indexName));
}
} catch (Exception e) {
log.error("根据脚本批量更新数据失败", e);
throw new RuntimeException("根据脚本批量更新数据失败", e);
}
}
// ==================== 查询操作 ====================
@Override
public T findOne(String id, Class<T> clazz, String indexName) {
try {
return elasticsearchTemplate.get(id, clazz, IndexCoordinates.of(indexName));
} catch (Exception e) {
log.error("查询单条数据失败", e);
return null;
}
}
@Override
public long findCount(T query, String indexName) {
try {
NativeQuery nativeQuery = buildNativeQuery(query, null, null);
return elasticsearchTemplate.count(nativeQuery, Object.class, IndexCoordinates.of(indexName));
} catch (Exception e) {
log.error("查询统计失败", e);
return 0;
}
}
@Override
public <R> List<R> findList(T query, Class<R> clazz, String indexName) {
return findList(query, clazz, indexName, null, null);
}
@Override
public <R> List<R> findList(T query, Class<R> clazz, String indexName, BoolQuery.Builder boolQueryConsumer) {
return findList(query, clazz, indexName, boolQueryConsumer, null);
}
@Override
public <R> List<R> findList(T query, Class<R> clazz, String indexName,
BoolQuery.Builder boolQueryConsumer, BoolQuery.Builder boolFilterConsumer) {
try {
NativeQuery nativeQuery = buildNativeQuery(query, boolQueryConsumer, boolFilterConsumer);
SearchHits<R> searchHits = elasticsearchTemplate.search(nativeQuery, clazz, IndexCoordinates.of(indexName));
return searchHits.getSearchHits().stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
} catch (Exception e) {
log.error("查询列表失败", e);
return new ArrayList<>();
}
}
@Override
public <R> EsPageVO<R> findPage(T query, Class<R> clazz, String indexName) {
return findPage(query, clazz, indexName, null, null);
}
@Override
public <R> EsPageVO<R> findPage(T query, Class<R> clazz, String indexName, BoolQuery.Builder boolQueryConsumer) {
return findPage(query, clazz, indexName, boolQueryConsumer, null);
}
@Override
public <R> EsPageVO<R> findPage(T query, Class<R> clazz, String indexName,
BoolQuery.Builder boolQueryConsumer, BoolQuery.Builder boolFilterConsumer) {
try {
int pageNum = getPageNum(query);
int pageSize = getPageSize(query);
NativeQueryBuilder queryBuilder = new NativeQueryBuilder();
BoolQuery.Builder mainBoolQuery = new BoolQuery.Builder();
// 构建基础查询条件
if (query != null) {
Map<String, Object> queryMap = objectToMap(query);
for (Map.Entry<String, Object> entry : queryMap.entrySet()) {
String key = entry.getKey();
Object val = entry.getValue();
if (val != null && !isPageField(key)) {
String value = val.toString();
if (!value.isEmpty()) {
mainBoolQuery.must(Query.of(q -> q.match(m -> m.field(key).query(value))));
}
}
}
}
// 合并外部bool查询条件
if (boolQueryConsumer != null) {
BoolQuery externalBool = boolQueryConsumer.build();
externalBool.must().forEach(mainBoolQuery::must);
externalBool.should().forEach(mainBoolQuery::should);
externalBool.mustNot().forEach(mainBoolQuery::mustNot);
}
// 合并外部bool过滤条件
if (boolFilterConsumer != null) {
BoolQuery filterBool = boolFilterConsumer.build();
filterBool.must().forEach(mainBoolQuery::filter);
}
queryBuilder.withQuery(Query.of(q -> q.bool(mainBoolQuery.build())));
queryBuilder.withPageable(PageRequest.of(Math.max(0, pageNum - 1), pageSize));
NativeQuery nativeQuery = queryBuilder.build();
SearchHits<R> searchHits = elasticsearchTemplate.search(nativeQuery, clazz, IndexCoordinates.of(indexName));
List<R> list = searchHits.getSearchHits().stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
return new EsPageVO<>(searchHits.getTotalHits(), pageNum, pageSize, list);
} catch (Exception e) {
log.error("分页查询失败", e);
return new EsPageVO<>(0, 1, 10, new ArrayList<>());
}
}
@Override
public <R> List<R> aggregation(T query, Class<R> clazz, String indexName) throws Exception {
// 聚合查询需要根据具体业务场景实现
log.warn("聚合查询需要根据具体业务场景实现");
return new ArrayList<>();
}
@Override
public <R> List<R> seachList(Class<R> clazz, String indexName) {
try {
NativeQuery nativeQuery = new NativeQueryBuilder()
.withQuery(Query.of(q -> q.matchAll(m -> m)))
.build();
SearchHits<R> searchHits = elasticsearchTemplate.search(nativeQuery, clazz, IndexCoordinates.of(indexName));
return searchHits.getSearchHits().stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
} catch (Exception e) {
log.error("查询全部列表失败", e);
return new ArrayList<>();
}
}
@Override
public List<String> suggest(String indexName, String prefix, String category) throws Exception {
// 自动补全功能 - 基于前缀匹配的简单实现
try {
String fieldName = "suggestion";
NativeQuery nativeQuery = new NativeQueryBuilder()
.withQuery(Query.of(q -> q.prefix(p -> p.field(fieldName).value(prefix))))
.withMaxResults(10)
.build();
SearchHits<?> searchHits = elasticsearchTemplate.search(nativeQuery, Object.class, IndexCoordinates.of(indexName));
List<String> suggestions = new ArrayList<>();
searchHits.getSearchHits().forEach(hit -> {
Object content = hit.getContent();
if (content != null) {
suggestions.add(content.toString());
}
});
return suggestions;
} catch (Exception e) {
log.error("自动补全查询失败", e);
return new ArrayList<>();
}
}
// ==================== 删除操作 ====================
@Override
public void delete(String id, String indexName) {
try {
elasticsearchTemplate.delete(id, IndexCoordinates.of(indexName));
} catch (Exception e) {
log.error("删除数据失败", e);
throw new RuntimeException("删除数据失败", e);
}
}
@Override
public <D> void deleteBatch(List<String> list, Class<D> clazz, String indexName) {
if (list == null || list.isEmpty()) {
return;
}
try {
for (String id : list) {
if (id != null && !id.isEmpty()) {
elasticsearchTemplate.delete(id, IndexCoordinates.of(indexName));
}
}
} catch (Exception e) {
log.error("批量删除数据失败", e);
throw new RuntimeException("批量删除数据失败", e);
}
}
@Override
public <D> void deleteSeach(List<String> list, Class<D> clazz, String indexName) {
if (list == null || list.isEmpty()) {
return;
}
try {
for (String id : list) {
if (id != null && !id.isEmpty()) {
elasticsearchTemplate.delete(id, IndexCoordinates.of(indexName));
}
}
} catch (Exception e) {
log.error("根据条件删除数据失败", e);
throw new RuntimeException("根据条件删除数据失败", e);
}
}
// ==================== 私有辅助方法 ====================
/**
* 构建NativeQuery
*/
private NativeQuery buildNativeQuery(T query, BoolQuery.Builder boolQueryConsumer, BoolQuery.Builder boolFilterConsumer) {
NativeQueryBuilder queryBuilder = new NativeQueryBuilder();
BoolQuery.Builder mainBoolQuery = new BoolQuery.Builder();
if (query != null) {
Map<String, Object> queryMap = objectToMap(query);
for (Map.Entry<String, Object> entry : queryMap.entrySet()) {
String key = entry.getKey();
Object val = entry.getValue();
if (val != null && !isPageField(key)) {
String value = val.toString();
if (!value.isEmpty()) {
mainBoolQuery.must(Query.of(q -> q.match(m -> m.field(key).query(value))));
}
}
}
}
if (boolQueryConsumer != null) {
BoolQuery externalBool = boolQueryConsumer.build();
externalBool.must().forEach(mainBoolQuery::must);
externalBool.should().forEach(mainBoolQuery::should);
externalBool.mustNot().forEach(mainBoolQuery::mustNot);
}
if (boolFilterConsumer != null) {
BoolQuery filterBool = boolFilterConsumer.build();
filterBool.must().forEach(mainBoolQuery::filter);
}
queryBuilder.withQuery(Query.of(q -> q.bool(mainBoolQuery.build())));
return queryBuilder.build();
}
/**
* 获取对象的ID值
*/
private String getIdValue(Object obj) {
if (obj == null) {
return null;
}
try {
// 先查找当前类
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
if ("id".equalsIgnoreCase(field.getName())) {
field.setAccessible(true);
Object value = field.get(obj);
return value != null ? value.toString() : null;
}
}
// 再查找父类
Class<?> superClass = obj.getClass().getSuperclass();
while (superClass != null && superClass != Object.class) {
Field[] superFields = superClass.getDeclaredFields();
for (Field field : superFields) {
if ("id".equalsIgnoreCase(field.getName())) {
field.setAccessible(true);
Object value = field.get(obj);
return value != null ? value.toString() : null;
}
}
superClass = superClass.getSuperclass();
}
} catch (Exception e) {
log.error("获取ID值失败", e);
}
return null;
}
/**
* 对象转Map
*/
private Map<String, Object> objectToMap(Object obj) {
Map<String, Object> map = new HashMap<>();
if (obj == null) {
return map;
}
try {
Class<?> clazz = obj.getClass();
while (clazz != null && clazz != Object.class) {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Object value = field.get(obj);
if (value != null) {
map.put(field.getName(), value);
}
}
clazz = clazz.getSuperclass();
}
} catch (Exception e) {
log.error("对象转Map失败", e);
}
return map;
}
/**
* 判断是否为分页字段
*/
private boolean isPageField(String fieldName) {
return "pageNum".equals(fieldName) || "pageSize".equals(fieldName)
|| "page".equals(fieldName) || "size".equals(fieldName)
|| "orderByColumn".equals(fieldName) || "isAsc".equals(fieldName)
|| "serialVersionUID".equals(fieldName);
}
/**
* 获取分页页码
*/
private int getPageNum(Object query) {
if (query == null) {
return 1;
}
try {
Field field = getField(query.getClass(), "pageNum");
if (field == null) {
field = getField(query.getClass(), "page");
}
if (field != null) {
field.setAccessible(true);
Object value = field.get(query);
if (value != null) {
int num = Integer.parseInt(value.toString());
return num > 0 ? num : 1;
}
}
} catch (Exception e) {
log.debug("获取pageNum失败,使用默认值1");
}
return 1;
}
/**
* 获取分页大小
*/
private int getPageSize(Object query) {
if (query == null) {
return 10;
}
try {
Field field = getField(query.getClass(), "pageSize");
if (field == null) {
field = getField(query.getClass(), "size");
}
if (field != null) {
field.setAccessible(true);
Object value = field.get(query);
if (value != null) {
int size = Integer.parseInt(value.toString());
return size > 0 ? size : 10;
}
}
} catch (Exception e) {
log.debug("获取pageSize失败,使用默认值10");
}
return 10;
}
/**
* 获取字段(包括父类)
*/
private Field getField(Class<?> clazz, String fieldName) {
while (clazz != null && clazz != Object.class) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
return null;
}
}
5. ES分页返回对象
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* ES分页返回对象
*
* @param <T> 泛型
* @author jz
*/
public class EsPageVO<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 总记录数
*/
private long total;
/**
* 当前页码
*/
private int pageNum;
/**
* 每页大小
*/
private int pageSize;
/**
* 总页数
*/
private int pages;
/**
* 数据列表
*/
private List<T> list;
public EsPageVO() {
this.list = new ArrayList<>();
}
public EsPageVO(long total, int pageNum, int pageSize, List<T> list) {
this.total = total;
this.pageNum = pageNum;
this.pageSize = pageSize;
this.pages = pageSize > 0 ? (int) Math.ceil((double) total / pageSize) : 0;
this.list = list != null ? list : new ArrayList<>();
}
/**
* 是否有下一页
*/
public boolean hasNext() {
return pageNum < pages;
}
/**
* 是否有上一页
*/
public boolean hasPrevious() {
return pageNum > 1;
}
/**
* 是否为空
*/
public boolean isEmpty() {
return list == null || list.isEmpty();
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getPages() {
return pages;
}
public void setPages(int pages) {
this.pages = pages;
}
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
@Override
public String toString() {
return "EsPageVO{" +
"total=" + total +
", pageNum=" + pageNum +
", pageSize=" + pageSize +
", pages=" + pages +
", listSize=" + (list != null ? list.size() : 0) +
'}';
}
}
6. Elasticsearch 索引实体示例
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.CompletionField;
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.Setting;
import org.springframework.data.elasticsearch.core.suggest.Completion;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* Elasticsearch 索引实体示例
*
* <p>该类展示了如何定义一个完整的ES索引实体,包含:</p>
* <ul>
* <li>索引配置(分片、副本)</li>
* <li>字段映射(各种字段类型)</li>
* <li>分词器配置</li>
* <li>自动补全功能</li>
* </ul>
*
* <p>使用前请确保ES已安装IK分词器插件</p>
*
* @author jz
*/
@Document(indexName = "example_doc") // 定义索引名称,ES中会创建名为 example_doc 的索引
@Setting(
shards = 3, // 主分片数量:数据会被分成3份存储在不同节点,提高并行处理能力
replicas = 1 // 副本数量:每个主分片有1个副本,提高可用性和读取性能
)
public class EsDocumentExample implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 文档ID
*
* @Id - Spring Data 标识主键
* FieldType.Keyword - 不分词,精确匹配,适用于ID、状态码、枚举值等
*/
@Id
@Field(type = FieldType.Keyword)
private String id;
/**
* 用户ID
*
* name = "user_id" - ES中的字段名(下划线命名)
* FieldType.Long - 长整型,适用于ID、数量等数值
*/
@Field(name = "user_id", type = FieldType.Long)
private Long userId;
/**
* 标题 - 全文检索字段
*
* FieldType.Text - 会进行分词,适用于需要全文检索的字段
* analyzer = "ik_max_word" - 索引时使用IK最细粒度分词(拆分出尽可能多的词)
* searchAnalyzer = "ik_smart" - 搜索时使用IK智能分词(拆分出最合理的词)
* copyTo = "suggestion" - 将该字段内容复制到suggestion字段,用于自动补全
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart", copyTo = "suggestion")
private String title;
/**
* 内容 - 全文检索字段
*
* 同样使用IK分词器进行中文分词
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String content;
/**
* 分类ID
*
* FieldType.Keyword - 不分词,用于精确过滤和聚合统计
*/
@Field(name = "category_id", type = FieldType.Keyword)
private String categoryId;
/**
* 状态
*
* FieldType.Integer - 整型,适用于状态码、数量等
*/
@Field(type = FieldType.Integer)
private Integer status;
/**
* 价格
*
* FieldType.Double - 双精度浮点型,适用于价格、评分等
*/
@Field(type = FieldType.Double)
private Double price;
/**
* 是否删除
*
* FieldType.Boolean - 布尔型
*/
@Field(name = "is_deleted", type = FieldType.Boolean)
private Boolean deleted;
/**
* 创建时间
*
* FieldType.Date - 日期类型
* format - 日期格式,支持多种格式
* pattern - 自定义日期格式
*/
@Field(name = "create_time", type = FieldType.Date, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
/**
* 更新时间
*/
@Field(name = "update_time", type = FieldType.Date, pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
/**
* 自动补全字段
*
* @CompletionField - 用于实现搜索建议/自动补全功能
* maxInputLength = 100 - 最大输入长度
* analyzer - 索引分词器
* searchAnalyzer - 搜索分词器
*
* <p>使用方式:</p>
* <pre>
* // 设置自动补全内容
* Completion completion = new Completion(new String[]{"关键词1", "关键词2"});
* entity.setSuggestion(completion);
* </pre>
*/
@CompletionField(maxInputLength = 100, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private Completion suggestion;
// ==================== 构造方法 ====================
public EsDocumentExample() {
}
public EsDocumentExample(String id, Long userId, String title, String content) {
this.id = id;
this.userId = userId;
this.title = title;
this.content = content;
}
// ==================== Getter/Setter ====================
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getCategoryId() {
return categoryId;
}
public void setCategoryId(String categoryId) {
this.categoryId = categoryId;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public Boolean getDeleted() {
return deleted;
}
public void setDeleted(Boolean deleted) {
this.deleted = deleted;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
public Completion getSuggestion() {
return suggestion;
}
public void setSuggestion(Completion suggestion) {
this.suggestion = suggestion;
}
@Override
public String toString() {
return "EsDocumentExample{" +
"id='" + id + '\'' +
", userId=" + userId +
", title='" + title + '\'' +
", categoryId='" + categoryId + '\'' +
", status=" + status +
'}';
}
}
7. 调用示例
public class ElasticsearchTemplateEngineTest {
private static final String INDEX_NAME = "example_doc";
/**
* 注入 ElasticsearchTemplateEngine 接口
* Spring 会自动注入 ElasticsearchTemplateEngineImpl 实现类
*/
@Autowired
private ElasticsearchTemplateEngine<EsDocumentExample> esEngine;
// ==================== 1. 索引操作测试 ====================
/**
* 测试创建索引
*/
@Test
public void testCreateIndex() {
// 先删除已存在的索引
if (esEngine.ifExist(INDEX_NAME)) {
boolean deleted = esEngine.deteleIndex(INDEX_NAME);
System.out.println("删除已存在索引: " + deleted);
}
// 创建索引
boolean created = esEngine.createIndex(EsDocumentExample.class);
System.out.println("创建索引结果: " + created);
}
/**
* 测试判断索引是否存在
*/
@Test
public void testIndexExists() {
boolean exists = esEngine.ifExist(INDEX_NAME);
System.out.println("索引是否存在: " + exists);
}
// ==================== 2. 新增操作测试 ====================
/**
* 测试新增单条数据
*/
@Test
public void testSaveOne() {
EsDocumentExample doc = new EsDocumentExample();
doc.setId("1");
doc.setUserId(1001L);
doc.setTitle("苹果iPhone 15 Pro Max 手机");
doc.setContent("苹果最新款旗舰手机,A17芯片,钛金属边框");
doc.setCategoryId("phone");
doc.setStatus(1);
doc.setPrice(9999.0);
doc.setDeleted(false);
doc.setCreateTime(LocalDateTime.now());
esEngine.saveOne(doc, INDEX_NAME);
System.out.println("新增单条数据成功, id: " + doc.getId());
}
/**
* 测试批量新增数据
*/
@Test
public void testSaveBatch() {
List<EsDocumentExample> docs = new ArrayList<>();
EsDocumentExample doc2 = new EsDocumentExample();
doc2.setId("2");
doc2.setUserId(1002L);
doc2.setTitle("华为Mate 60 Pro 手机");
doc2.setContent("华为旗舰手机,麒麟芯片,卫星通话");
doc2.setCategoryId("phone");
doc2.setStatus(1);
doc2.setPrice(6999.0);
doc2.setDeleted(false);
doc2.setCreateTime(LocalDateTime.now());
docs.add(doc2);
EsDocumentExample doc3 = new EsDocumentExample();
doc3.setId("3");
doc3.setUserId(1001L);
doc3.setTitle("小米14 Ultra 手机");
doc3.setContent("小米旗舰手机,徕卡影像,骁龙8 Gen3");
doc3.setCategoryId("phone");
doc3.setStatus(1);
doc3.setPrice(5999.0);
doc3.setDeleted(false);
doc3.setCreateTime(LocalDateTime.now());
docs.add(doc3);
EsDocumentExample doc4 = new EsDocumentExample();
doc4.setId("4");
doc4.setUserId(1003L);
doc4.setTitle("苹果MacBook Pro 笔记本电脑");
doc4.setContent("苹果笔记本电脑,M3芯片,16寸屏幕");
doc4.setCategoryId("laptop");
doc4.setStatus(1);
doc4.setPrice(19999.0);
doc4.setDeleted(false);
doc4.setCreateTime(LocalDateTime.now());
docs.add(doc4);
EsDocumentExample doc5 = new EsDocumentExample();
doc5.setId("5");
doc5.setUserId(1002L);
doc5.setTitle("华为MatePad Pro 平板电脑");
doc5.setContent("华为平板电脑,鸿蒙系统,120Hz高刷");
doc5.setCategoryId("tablet");
doc5.setStatus(0);
doc5.setPrice(4999.0);
doc5.setDeleted(false);
doc5.setCreateTime(LocalDateTime.now());
docs.add(doc5);
int count = esEngine.saveBatch(docs, INDEX_NAME);
System.out.println("批量新增数据成功, 数量: " + count);
}
// ==================== 3. 查询操作测试 ====================
/**
* 测试根据ID查询
*/
@Test
public void testFindOne() {
EsDocumentExample doc = esEngine.findOne("1", EsDocumentExample.class, INDEX_NAME);
System.out.println("根据ID查询结果: " + doc);
}
/**
* 测试查询全部
*/
@Test
public void testSearchAll() {
List<EsDocumentExample> list = esEngine.seachList(EsDocumentExample.class, INDEX_NAME);
System.out.println("查询全部, 共 " + list.size() + " 条");
list.forEach(doc -> System.out.println(" - " + doc.getTitle()));
}
/**
* 测试关键词搜索
*/
@Test
public void testSearchByKeyword() {
EsDocumentExample query = new EsDocumentExample();
BoolQuery.Builder boolBuilder = new BoolQuery.Builder();
// 关键词匹配
boolBuilder.must(Query.of(q -> q.match(m -> m.field("title").query("手机"))));
// 排除已删除
boolBuilder.mustNot(Query.of(q -> q.term(t -> t.field("is_deleted").value(true))));
List<EsDocumentExample> list = esEngine.findList(query, EsDocumentExample.class, INDEX_NAME, boolBuilder);
System.out.println("关键词搜索'手机', 共 " + list.size() + " 条");
list.forEach(doc -> System.out.println(" - " + doc.getTitle() + " | 价格: " + doc.getPrice()));
}
/**
* 测试复杂条件查询
*/
@Test
public void testSearchByCondition() {
EsDocumentExample query = new EsDocumentExample();
BoolQuery.Builder boolBuilder = new BoolQuery.Builder();
// 关键词匹配
boolBuilder.must(Query.of(q -> q.match(m -> m.field("title").query("手机"))));
// 状态过滤
boolBuilder.filter(Query.of(q -> q.term(t -> t.field("status").value(1))));
// 价格范围
boolBuilder.filter(Query.of(q -> q.range(r -> r.field("price").gte(JsonData.of(5000)).lte(JsonData.of(10000)))));
// 排除已删除
boolBuilder.mustNot(Query.of(q -> q.term(t -> t.field("is_deleted").value(true))));
List<EsDocumentExample> list = esEngine.findList(query, EsDocumentExample.class, INDEX_NAME, boolBuilder);
System.out.println("复杂条件查询(手机+上架+价格5000-10000), 共 " + list.size() + " 条");
list.forEach(doc -> System.out.println(" - " + doc.getTitle() + " | 价格: " + doc.getPrice()));
}
/**
* 测试分页查询
*/
@Test
public void testSearchPage() {
PageQuery query = new PageQuery();
query.setPageNum(1);
query.setPageSize(2);
BoolQuery.Builder boolBuilder = new BoolQuery.Builder();
boolBuilder.filter(Query.of(q -> q.term(t -> t.field("status").value(1))));
boolBuilder.mustNot(Query.of(q -> q.term(t -> t.field("is_deleted").value(true))));
// 使用原始类型的引擎进行分页查询
@SuppressWarnings("unchecked")
ElasticsearchTemplateEngine<PageQuery> pageEngine = (ElasticsearchTemplateEngine<PageQuery>) (ElasticsearchTemplateEngine<?>) esEngine;
EsPageVO<EsDocumentExample> page = pageEngine.findPage(query, EsDocumentExample.class, INDEX_NAME, boolBuilder);
System.out.println("分页查询结果:");
System.out.println(" 总数: " + page.getTotal());
System.out.println(" 总页数: " + page.getPages());
System.out.println(" 当前页: " + page.getPageNum());
System.out.println(" 每页大小: " + page.getPageSize());
System.out.println(" 数据:");
page.getList().forEach(doc -> System.out.println(" - " + doc.getTitle()));
}
/**
* 测试统计数量
*/
@Test
public void testCount() {
EsDocumentExample query = new EsDocumentExample();
long count = esEngine.findCount(query, INDEX_NAME);
System.out.println("统计数量: " + count);
}
// ==================== 4. 更新操作测试 ====================
/**
* 测试更新单条数据
*/
@Test
public void testUpdateOne() {
EsDocumentExample doc = new EsDocumentExample();
doc.setId("1");
doc.setPrice(8999.0); // 修改价格
doc.setTitle("苹果iPhone 15 Pro Max 手机 (降价促销)");
esEngine.updateOne(doc, INDEX_NAME);
System.out.println("更新单条数据成功");
// 查询验证
EsDocumentExample updated = esEngine.findOne("1", EsDocumentExample.class, INDEX_NAME);
System.out.println("更新后: " + updated.getTitle() + " | 价格: " + updated.getPrice());
}
/**
* 测试脚本更新
*/
@Test
public void testUpdateByScript() {
Map<String, Object> params = new HashMap<>();
params.put("newPrice", 7999.0);
esEngine.updateOneByScript("2", params, "ctx._source.price = params.newPrice", INDEX_NAME);
System.out.println("脚本更新成功");
// 查询验证
EsDocumentExample updated = esEngine.findOne("2", EsDocumentExample.class, INDEX_NAME);
System.out.println("脚本更新后: " + updated.getTitle() + " | 价格: " + updated.getPrice());
}
/**
* 测试批量脚本更新
*/
@Test
public void testBatchUpdateByScript() {
List<String> ids = Arrays.asList("3", "4");
Map<String, Map<String, Object>> params = new HashMap<>();
Map<String, Object> param3 = new HashMap<>();
param3.put("newStatus", 0);
params.put("3", param3);
Map<String, Object> param4 = new HashMap<>();
param4.put("newStatus", 0);
params.put("4", param4);
esEngine.updateBatchByScript(ids, params, "ctx._source.status = params.newStatus", INDEX_NAME);
System.out.println("批量脚本更新成功");
}
// ==================== 5. 删除操作测试 ====================
/**
* 测试删除单条数据
*/
@Test
public void testDelete() {
esEngine.delete("5", INDEX_NAME);
System.out.println("删除单条数据成功");
// 查询验证
EsDocumentExample doc = esEngine.findOne("5", EsDocumentExample.class, INDEX_NAME);
System.out.println("删除后查询结果: " + doc);
}
/**
* 测试批量删除
*/
@Test
public void testDeleteBatch() {
List<String> ids = Arrays.asList("3", "4");
esEngine.deleteBatch(ids, EsDocumentExample.class, INDEX_NAME);
System.out.println("批量删除成功");
// 查询验证
List<EsDocumentExample> list = esEngine.seachList(EsDocumentExample.class, INDEX_NAME);
System.out.println("删除后剩余: " + list.size() + " 条");
}
// ==================== 6. 删除索引测试 ====================
/**
* 测试删除索引(最后执行)
*/
@Test
public void testDeleteIndex() {
boolean deleted = esEngine.deteleIndex(INDEX_NAME);
System.out.println("删除索引结果: " + deleted);
}
// ==================== 分页查询对象 ====================
/**
* 分页查询对象
*/
public static class PageQuery {
private Integer pageNum = 1;
private Integer pageSize = 10;
public Integer getPageNum() {
return pageNum;
}
public void setPageNum(Integer pageNum) {
this.pageNum = pageNum;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
}
}
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)