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;
        }
    }
}

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐