1. ES核心概念、环境搭建

第一部分:ES 核心概念

Elasticsearch 是一个分布式、RESTful 风格的搜索和分析引擎。理解其核心概念是掌握它的基础。

核心数据概念(与关系型数据库类比)

关系型数据库

ELASTICSEARCH

说明

Database

Index

索引,是文档的集合。类似于数据库中的“表”。

Table

Type(已废弃)

在 7.x 之后已废弃,一个索引通常只包含一种文档类型。

Row

Document

文档,是 JSON 格式的基本数据单元。相当于一条“记录”。

Column

Field

字段,文档的 JSON 键值对。相当于一个“列”。

Schema

Mapping

映射,定义索引中字段的名称、数据类型、分词方式等。相当于“表结构”。

SQL

Query DSL

ES 使用基于 JSON 的查询领域特定语言来查询数据。

关键点 :

  • 文档(Document) :ES 中可被索引的最小信息单元。必须属于一个索引,是 JSON 格式。
  • 索引(Index) :具有相似特征的文档集合。是进行 CRUD 和搜索操作的主要对象。
  • 映射(Mapping) :定义文档的结构和字段属性(如是否为文本、数字、日期,以及使用何种分词器)。动态映射允许自动推断类型,但生产环境建议明确定义。
核心架构概念
  • 集群(Cluster) :一个或多个节点的集合,共同持有全部数据并提供跨节点联合索引和搜索的能力。每个集群有唯一名称(默认 elasticsearch)。
  • 节点(Node) :集群中的一个服务器,存储数据并参与集群的索引和搜索。节点类型可配置:
  • 主节点(Master-eligible) :负责集群管理(创建/删除索引,跟踪节点状态,分配分片)。
  • 数据节点(Data) :存储数据,执行数据相关操作(CRUD、搜索、聚合)。生产环境需要大量此类节点。
  • 协调节点(Coordinating) :处理客户端请求,将请求路由到相应节点,并汇总结果。任何节点默认都是协调节点。
  • 摄取节点(Ingest) :用于在索引前对文档进行预处理(如数据转换)。
  • 分片(Shard) :
  • 主分片(Primary Shard) :索引数据的子集。每个文档属于一个主分片。索引创建时指定,后续不可更改(除非重建)。数据水平拆分和并行化的基础。
  • 副本分片(Replica Shard) :主分片的拷贝。提供高可用性(主分片故障时,副本可提升为主分片)和提升读取性能(搜索可并行在所有副本上执行)。创建后可以动态调整数量。
  • 近实时(NRT) :文档索引后,通常在 1秒 内即可被搜索到,因为需要从内存缓冲区刷新到段(Segment)。
  • 段(Segment) :一个倒排索引的最小存储单元。索引由多个不可变的段组成,段会定期合并。
倒排索引(Inverted Index)

这是 ES 实现快速全文搜索的核心数据结构。

  • 正排索引 :文档 -> 关键词列表(像书的目录)。
  • 倒排索引 :关键词 -> 文档 ID 列表(像书的索引)。
  • 过程 :对文档内容进行分词(Analysis) ,得到一系列词条(Term),建立词条到文档 ID 的映射。搜索时,先查找词条,再获取文档 ID。
分析与分词器(Analysis & Analyzer)

文本字段在索引和搜索前都会经过分析过程。

  • 分析器(Analyzer) :由三部分组成。
  • 字符过滤器(Character Filters) :预处理原始文本(如去除 HTML 标签)。
  • 分词器(Tokenizer) :将文本切分为独立的词条(如按空格切分)。
  • 词条过滤器(Token Filters) :对词条进行加工(如转小写、删除停用词、添加同义词)。
  • 内置分析器 :如 standard(标准)、simplewhitespacekeyword(不分词)等。

第二部分:环境搭建

这里提供两种最常用的搭建方式:单节点本地运行 (用于学习开发)和 使用 Docker Compose 运行集群 (更接近生产环境)。

方式一:单节点本地运行(快速开始)
  1. 下载
  • 访问 ​Elastic 官网下载页面 ​。
  • 选择与您系统对应的版本(如 Linux/Mac 的 tar.gz, Windows 的 zip)。
  • 解压到目录,如 /opt/elasticsearch-8.12.0
  1. 启动(Linux/Mac)
# 进入解压目录
cd /opt/elasticsearch-8.12.0

# 启动(前台运行,-d 参数可后台运行)
./bin/elasticsearch

首次启动 8.x 版本,会:

  • 自动生成集群和节点的 TLS 证书。
  • 在控制台输出 elastic 用户的默认密码。
  • 启用安全特性(用户名/密码)。
  1. 验证
# 使用自动生成的密码访问(将密码替换为你的)
curl -k -u elastic:your_password https://localhost:9200

或打开浏览器访问 ​​https://localhost:9200​​,输入用户名 ​​elastic​​ 和密码。

  1. 关键文件路径
  • ​config/elasticsearch.yml​​ :主配置文件。
  • ​config/jvm.options​​ :JVM 堆内存设置(建议设为机器内存的一半,不超过 32GB)。
  • ​logs/​​ :日志文件。
  • ​data/​​ :数据文件(默认在安装目录下)。
方式二:使用 Docker Compose 搭建集群(推荐)
  1. 准备工作 确保系统已安装 ​Docker ​和 ​Docker Compose ​。
  2. 创建目录结构
~/es-docker/
├── docker-compose.yml
├── config/
│   └── elasticsearch.yml (可选,覆盖默认配置)
└── .env (用于设置环境变量)
  1. 编写 docker-compose.yml
version: '3.8'
services:
  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    container_name: es01
    environment:
      - node.name=es01
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es02,es03 # 其他节点地址
      - cluster.initial_master_nodes=es01,es02,es03 # 初始主节点
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g" # JVM 堆大小
      - xpack.security.enabled=false # 为方便学习,先关闭安全认证。生产环境必须开启!
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data01:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
    networks:
      - elastic

  es02:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    container_name: es02
    environment:
      - node.name=es02
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es01,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
      - xpack.security.enabled=false
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data02:/usr/share/elasticsearch/data
    networks:
      - elastic

  es03:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
    container_name: es03
    environment:
      - node.name=es03
      - cluster.name=es-docker-cluster
      - discovery.seed_hosts=es01,es02
      - cluster.initial_master_nodes=es01,es02,es03
      - bootstrap.memory_lock=true
      - "ES_JAVA_OPTS=-Xms1g -Xmx1g"
      - xpack.security.enabled=false
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - data03:/usr/share/elasticsearch/data
    networks:
      - elastic

  kibana: # 可选,可视化工具
    image: docker.elastic.co/kibana/kibana:8.12.0
    container_name: kibana
    ports:
      - 5601:5601
    environment:
      ELASTICSEARCH_HOSTS: http://es01:9200
    networks:
      - elastic

volumes:
  data01:
    driver: local
  data02:
    driver: local
  data03:
    driver: local

networks:
  elastic:
    driver: bridge
  1. 启动集群
# 进入项目目录
cd ~/es-docker

# 后台启动所有服务
docker-compose up -d

# 查看日志
docker-compose logs -f es01

# 检查集群健康状态
curl http://localhost:9200/_cluster/health?pretty

输出中 ​​"status" : "green"​​ 表示集群健康。

  1. 访问
  • Elasticsearch : http://localhost:9200
  • Kibana (如果部署了): http://localhost:5601

下一步建议
  1. 使用 Kibana Dev Tools :在 Kibana 的 Dev Tools 中练习 REST API,这是学习和操作 ES 的最佳界面。
  2. 执行第一个操作 :
// 创建索引
PUT /my-first-index

// 插入文档
POST /my-first-index/_doc/1
{
  "title": "Hello Elasticsearch",
  "content": "This is my first document."
}

// 搜索
GET /my-first-index/_search
{
  "query": {
    "match": {
      "content": "first"
    }
  }
}
  1. 学习核心 API :掌握 _cat_search_bulk_mapping_cluster 等 API。
  2. 开启安全认证 :在生产环境中,务必在 elasticsearch.yml 中设置 xpack.security.enabled: true,并为用户设置强密码。

通过理解这些核心概念并成功搭建环境,你就为深入学习 Elasticsearch 的搜索、聚合、性能调优等高级特性打下了坚实的基础。

2. SpringBoot整合ES、索引创建与管理

Spring Boot 整合 Elasticsearch 主要可以通过两种方式:Spring Data Elasticsearch (官方推荐)和 Rest High Level Client 。下面我将详细介绍两种方式的整合步骤、索引创建与管理。

一、Spring Data Elasticsearch(推荐)

添加依赖
<!-- Spring Boot 2.7.x -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

<!-- 或 Spring Boot 3.x -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
配置连接
# application.yml
spring:
  elasticsearch:
    uris: http://localhost:9200
    username: elastic  # 如果启用安全认证
    password: your_password
    connection-timeout: 5s
    socket-timeout: 30s
实体类定义(自动创建索引)
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.util.Date;

@Document(indexName = "product_index")
@Setting(shards = 3, replicas = 2)  // 分片和副本设置
public class Product {
    
    @Id
    private String id;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String name;
    
    @Field(type = FieldType.Double)
    private Double price;
    
    @Field(type = FieldType.Integer)
    private Integer stock;
    
    @Field(type = FieldType.Keyword)
    private String category;
    
    @Field(type = FieldType.Date, format = DateFormat.date_optional_time)
    private Date createTime;
    
    // 嵌套类型
    @Field(type = FieldType.Nested)
    private List<Tag> tags;
    
    // getters and setters
}

// 嵌套对象
public class Tag {
    @Field(type = FieldType.Keyword)
    private String name;
    
    @Field(type = FieldType.Integer)
    private Integer weight;
}
Repository 接口
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
    
    // 自定义查询方法
    List<Product> findByName(String name);
    
    List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
    
    // 使用 @Query 注解
    @Query("{\"match\": {\"name\": \"?0\"}}")
    Page<Product> findByNameCustom(String name, Pageable pageable);
}
索引管理服务
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.IndexOperations;
import org.springframework.data.elasticsearch.core.document.Document;
import org.springframework.stereotype.Service;

@Service
public class IndexManagementService {
    
    private final ElasticsearchOperations elasticsearchOperations;
    private final ProductRepository productRepository;
    
    public IndexManagementService(ElasticsearchOperations elasticsearchOperations,
                                  ProductRepository productRepository) {
        this.elasticsearchOperations = elasticsearchOperations;
        this.productRepository = productRepository;
    }
    
    /**
     * 创建索引(如果不存在)
     */
    public boolean createProductIndex() {
        IndexOperations indexOps = elasticsearchOperations.indexOps(Product.class);
        
        if (!indexOps.exists()) {
            // 创建索引
            boolean created = indexOps.create();
            
            // 创建映射
            Document mapping = indexOps.createMapping();
            indexOps.putMapping(mapping);
            
            // 设置索引设置
            Document settings = indexOps.createSettings();
            indexOps.putSettings(settings);
            
            return created;
        }
        return false;
    }
    
    /**
     * 删除索引
     */
    public boolean deleteProductIndex() {
        IndexOperations indexOps = elasticsearchOperations.indexOps(Product.class);
        if (indexOps.exists()) {
            return indexOps.delete();
        }
        return false;
    }
    
    /**
     * 刷新索引
     */
    public void refreshProductIndex() {
        IndexOperations indexOps = elasticsearchOperations.indexOps(Product.class);
        indexOps.refresh();
    }
    
    /**
     * 获取索引信息
     */
    public Map<String, Object> getIndexInfo() {
        IndexOperations indexOps = elasticsearchOperations.indexOps(Product.class);
        Map<String, Object> info = new HashMap<>();
        
        info.put("exists", indexOps.exists());
        info.put("settings", indexOps.getSettings());
        info.put("mapping", indexOps.getMapping());
        info.put("alias", indexOps.getAliases());
        
        return info;
    }
    
    /**
     * 创建索引别名
     */
    public boolean createAlias(String aliasName) {
        IndexOperations indexOps = elasticsearchOperations.indexOps(Product.class);
        return indexOps.alias(new AliasActions(
            new AliasAction.Add(AliasActionParameters.builder()
                .withIndices("product_index")
                .withAliases(aliasName)
                .build())
        ));
    }
}
索引模板管理
@Service
public class IndexTemplateService {
    
    private final ElasticsearchOperations elasticsearchOperations;
    
    /**
     * 创建索引模板
     */
    public void createIndexTemplate() {
        IndexOperations indexOps = elasticsearchOperations.indexOps(Product.class);
        
        PutTemplateRequest request = PutTemplateRequest.builder("product_template")
            .withPatterns(Collections.singletonList("product_*"))
            .withSettings("""
                {
                  "number_of_shards": 3,
                  "number_of_replicas": 1
                }
                """)
            .withMappings("""
                {
                  "properties": {
                    "name": {
                      "type": "text",
                      "analyzer": "ik_max_word"
                    },
                    "price": {
                      "type": "double"
                    }
                  }
                }
                """)
            .build();
        
        indexOps.putTemplate(request);
    }
}

二、使用 Rest High Level Client(更灵活)

添加依赖
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.17.9</version> <!-- 与ES服务器版本保持一致 -->
</dependency>
配置 Client
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.client.ClientConfiguration;
import org.springframework.data.elasticsearch.client.RestClients;

@Configuration
public class ElasticsearchConfig {
    
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        ClientConfiguration clientConfiguration = ClientConfiguration.builder()
            .connectedTo("localhost:9200")
            .withBasicAuth("elastic", "password") // 如果需要认证
            .withConnectTimeout(Duration.ofSeconds(5))
            .withSocketTimeout(Duration.ofSeconds(30))
            .build();
        
        return RestClients.create(clientConfiguration).rest();
    }
}
索引管理服务
@Service
public class ElasticsearchIndexService {
    
    private final RestHighLevelClient client;
    
    /**
     * 创建索引
     */
    public boolean createIndex(String indexName) throws IOException {
        CreateIndexRequest request = new CreateIndexRequest(indexName);
        
        // 设置分片和副本
        request.settings(Settings.builder()
            .put("index.number_of_shards", 3)
            .put("index.number_of_replicas", 1)
            .put("analysis.analyzer.default.type", "ik_max_word")
        );
        
        // 定义映射
        XContentBuilder mappingBuilder = XContentFactory.jsonBuilder()
            .startObject()
                .startObject("properties")
                    .startObject("name")
                        .field("type", "text")
                        .field("analyzer", "ik_max_word")
                        .field("search_analyzer", "ik_smart")
                    .endObject()
                    .startObject("price")
                        .field("type", "double")
                    .endObject()
                    .startObject("createTime")
                        .field("type", "date")
                        .field("format", "yyyy-MM-dd HH:mm:ss||epoch_millis")
                    .endObject()
                .endObject()
            .endObject();
        
        request.mapping(mappingBuilder);
        
        CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
        return response.isAcknowledged();
    }
    
    /**
     * 删除索引
     */
    public boolean deleteIndex(String indexName) throws IOException {
        DeleteIndexRequest request = new DeleteIndexRequest(indexName);
        DeleteIndexResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
        return response.isAcknowledged();
    }
    
    /**
     * 判断索引是否存在
     */
    public boolean indexExists(String indexName) throws IOException {
        GetIndexRequest request = new GetIndexRequest(indexName);
        return client.indices().exists(request, RequestOptions.DEFAULT);
    }
    
    /**
     * 更新索引设置
     */
    public boolean updateIndexSettings(String indexName) throws IOException {
        UpdateSettingsRequest request = new UpdateSettingsRequest(indexName);
        
        Settings settings = Settings.builder()
            .put("index.number_of_replicas", 2)
            .put("index.refresh_interval", "30s")
            .build();
        
        request.settings(settings);
        UpdateSettingsResponse response = client.indices().putSettings(request, RequestOptions.DEFAULT);
        return response.isAcknowledged();
    }
    
    /**
     * 创建索引别名
     */
    public boolean createAlias(String indexName, String aliasName) throws IOException {
        IndicesAliasesRequest request = new IndicesAliasesRequest();
        IndicesAliasesRequest.AliasActions aliasAction = 
            new IndicesAliasesRequest.AliasActions(IndicesAliasesRequest.AliasActions.Type.ADD)
                .index(indexName)
                .alias(aliasName);
        
        request.addAliasAction(aliasAction);
        AcknowledgedResponse response = client.indices().updateAliases(request, RequestOptions.DEFAULT);
        return response.isAcknowledged();
    }
    
    /**
     * 获取索引统计信息
     */
    public Map<String, Object> getIndexStats(String indexName) throws IOException {
        IndicesStatsRequest request = new IndicesStatsRequest();
        request.indices(indexName);
        
        IndicesStatsResponse response = client.indices().stats(request, RequestOptions.DEFAULT);
        
        Map<String, Object> stats = new HashMap<>();
        stats.put("docsCount", response.getTotal().getDocs().getCount());
        stats.put("size", response.getTotal().getStore().getSize());
        stats.put("shards", response.getTotal().getShards().getTotal());
        
        return stats;
    }
}

三、索引生命周期管理(ILM)

@Service
public class IndexLifecycleService {
    
    private final RestHighLevelClient client;
    
    /**
     * 创建生命周期策略
     */
    public void createLifecyclePolicy() throws IOException {
        // 定义生命周期策略
        String policyJson = """
            {
              "policy": {
                "phases": {
                  "hot": {
                    "min_age": "0ms",
                    "actions": {
                      "rollover": {
                        "max_size": "50gb",
                        "max_age": "30d"
                      }
                    }
                  },
                  "warm": {
                    "min_age": "30d",
                    "actions": {
                      "shrink": {
                        "number_of_shards": 1
                      }
                    }
                  },
                  "delete": {
                    "min_age": "90d",
                    "actions": {
                      "delete": {}
                    }
                  }
                }
              }
            }
            """;
        
        PutLifecyclePolicyRequest request = new PutLifecyclePolicyRequest(
            RequestOptions.DEFAULT,
            new PutLifecyclePolicyRequest.Factory().fromJson(policyJson)
        );
        
        client.indexLifecycle().putLifecyclePolicy(request, RequestOptions.DEFAULT);
    }
    
    /**
     * 应用生命周期策略到索引模板
     */
    public void applyLifecycleToTemplate() throws IOException {
        PutIndexTemplateRequest request = new PutIndexTemplateRequest("product_template");
        
        Settings settings = Settings.builder()
            .put("index.lifecycle.name", "product_policy")
            .put("index.lifecycle.rollover_alias", "products")
            .build();
        
        request.settings(settings);
        client.indices().putTemplate(request, RequestOptions.DEFAULT);
    }
}

四、最佳实践建议

索引命名规范
// 使用时间戳或版本号
public String generateIndexName(String prefix) {
    return prefix + "_" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy_MM_dd"));
    // 或使用版本控制: product_v1, product_v2
}
索引监控
@Component
public class IndexMonitor {
    
    @Scheduled(fixedDelay = 60000) // 每分钟检查一次
    public void monitorIndices() throws IOException {
        // 检查索引健康状态
        // 监控索引大小
        // 发送告警等
    }
}
配置类完整示例
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.example.repository")
public class ElasticsearchConfiguration {
    
    @Value("${spring.elasticsearch.uris}")
    private String[] uris;
    
    @Bean
    public RestHighLevelClient client() {
        ClientConfiguration clientConfiguration = ClientConfiguration.builder()
            .connectedTo(uris)
            .withConnectTimeout(Duration.ofSeconds(10))
            .withSocketTimeout(Duration.ofSeconds(30))
            .withBasicAuth("elastic", "password")
            .build();
        
        return RestClients.create(clientConfiguration).rest();
    }
    
    @Bean
    public ElasticsearchOperations elasticsearchTemplate() {
        return new ElasticsearchRestTemplate(client());
    }
}

五、常见问题解决

版本兼容性问题

确保 Spring Data Elasticsearch 版本与 Elasticsearch 服务器版本兼容。

连接池配置
spring:
  elasticsearch:
    rest:
      connection-timeout: 5s
      read-timeout: 30s
      max-connections: 100
      max-connections-per-route: 10
索引重建策略
public class IndexRebuildService {
    
    public void reindex(String sourceIndex, String destIndex) throws IOException {
        ReindexRequest request = new ReindexRequest();
        request.setSourceIndices(sourceIndex);
        request.setDestIndex(destIndex);
        
        // 设置版本冲突处理
        request.setDestOpType("create");
        request.setConflicts("proceed");
        
        // 异步执行
        TaskSubmissionResponse response = client.submitReindexTask(request, RequestOptions.DEFAULT);
    }
}

选择建议

  1. Spring Data Elasticsearch :适合简单CRUD,与Spring生态集成好
  2. Rest High Level Client :需要更细粒度控制,复杂查询场景
  3. 混合使用 :Repository处理简单查询,ElasticsearchOperations/RestClient处理复杂操作

根据实际需求选择合适的整合方式,Spring Data Elasticsearch 提供了更简洁的API,而 Rest High Level Client 提供了更全面的功能控制。

3. 文档CRUD、复杂条件检索、分页排序

一、项目依赖配置

Maven依赖
<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Spring Data Elasticsearch -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
配置文件 application.yml
spring:
  elasticsearch:
    uris: http://localhost:9200
    username: elastic  # 如果有认证
    password: password  # 如果有认证
    
  data:
    elasticsearch:
      repositories:
        enabled: true

二、实体类定义

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@Data
@Document(indexName = "product_index")
public class Product {
    
    @Id
    private String id;
    
    @Field(type = FieldType.Keyword)
    private String productCode;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String productName;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String description;
    
    @Field(type = FieldType.Double)
    private BigDecimal price;
    
    @Field(type = FieldType.Integer)
    private Integer stock;
    
    @Field(type = FieldType.Keyword)
    private String category;
    
    @Field(type = FieldType.Keyword)
    private String brand;
    
    @Field(type = FieldType.Date)
    private LocalDateTime createTime;
    
    @Field(type = FieldType.Date)
    private LocalDateTime updateTime;
    
    @Field(type = FieldType.Boolean)
    private Boolean status;
    
    @Field(type = FieldType.Nested)
    private List<Specification> specifications;
    
    @Field(type = FieldType.Object)
    private Map<String, Object> attributes;
}

@Data
public class Specification {
    @Field(type = FieldType.Keyword)
    private String key;
    
    @Field(type = FieldType.Text)
    private String value;
}

三、Repository接口

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
    
    // 1. 基本查询方法
    List<Product> findByProductName(String productName);
    
    List<Product> findByCategoryAndStatus(String category, Boolean status);
    
    List<Product> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice);
    
    // 2. 分页查询
    Page<Product> findByCategory(String category, Pageable pageable);
    
    // 3. 自定义查询语句
    @Query("{\"match\": {\"productName\": \"?0\"}}")
    List<Product> findByProductNameCustom(String productName);
    
    // 4. 使用Query注解进行复杂查询
    @Query("{\"bool\": {\"must\": [{\"match\": {\"productName\": \"?0\"}}, {\"range\": {\"price\": {\"gte\": ?1, \"lte\": ?2}}}]}}")
    List<Product> findByNameAndPriceRange(String name, BigDecimal minPrice, BigDecimal maxPrice);
}

四、Service层实现

基础CRUD Service
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHit;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.*;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class ProductService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    // ================ CRUD操作 ================
    
    /**
     * 创建/更新文档
     */
    public Product save(Product product) {
        if (product.getId() == null) {
            product.setCreateTime(LocalDateTime.now());
        }
        product.setUpdateTime(LocalDateTime.now());
        return productRepository.save(product);
    }
    
    /**
     * 批量保存
     */
    public Iterable<Product> saveAll(List<Product> products) {
        products.forEach(product -> {
            if (product.getId() == null) {
                product.setCreateTime(LocalDateTime.now());
            }
            product.setUpdateTime(LocalDateTime.now());
        });
        return productRepository.saveAll(products);
    }
    
    /**
     * 根据ID查询
     */
    public Optional<Product> findById(String id) {
        return productRepository.findById(id);
    }
    
    /**
     * 查询所有
     */
    public Iterable<Product> findAll() {
        return productRepository.findAll();
    }
    
    /**
     * 根据ID删除
     */
    public void deleteById(String id) {
        productRepository.deleteById(id);
    }
    
    /**
     * 删除所有
     */
    public void deleteAll() {
        productRepository.deleteAll();
    }
    
    // ================ 复杂查询 ================
    
    /**
     * 多条件组合查询 + 分页 + 排序
     */
    public Page<Product> searchProducts(ProductSearchRequest request) {
        // 构建NativeSearchQuery
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        
        // 1. 关键词查询(商品名称或描述)
        if (StringUtils.hasText(request.getKeyword())) {
            boolQuery.must(QueryBuilders.multiMatchQuery(request.getKeyword())
                    .field("productName", 3.0f)  // 权重
                    .field("description", 1.0f)
                    .type(MultiMatchQueryBuilder.Type.BEST_FIELDS));
        }
        
        // 2. 分类过滤
        if (StringUtils.hasText(request.getCategory())) {
            boolQuery.filter(QueryBuilders.termQuery("category", request.getCategory()));
        }
        
        // 3. 品牌过滤(多选)
        if (request.getBrands() != null && !request.getBrands().isEmpty()) {
            boolQuery.filter(QueryBuilders.termsQuery("brand", request.getBrands()));
        }
        
        // 4. 价格范围过滤
        if (request.getMinPrice() != null || request.getMaxPrice() != null) {
            RangeQueryBuilder priceRange = QueryBuilders.rangeQuery("price");
            if (request.getMinPrice() != null) {
                priceRange.gte(request.getMinPrice());
            }
            if (request.getMaxPrice() != null) {
                priceRange.lte(request.getMaxPrice());
            }
            boolQuery.filter(priceRange);
        }
        
        // 5. 库存过滤
        if (request.getInStock() != null && request.getInStock()) {
            boolQuery.filter(QueryBuilders.rangeQuery("stock").gt(0));
        }
        
        // 6. 状态过滤
        if (request.getStatus() != null) {
            boolQuery.filter(QueryBuilders.termQuery("status", request.getStatus()));
        }
        
        // 7. 时间范围查询
        if (request.getStartTime() != null || request.getEndTime() != null) {
            RangeQueryBuilder timeRange = QueryBuilders.rangeQuery("createTime");
            if (request.getStartTime() != null) {
                timeRange.gte(request.getStartTime());
            }
            if (request.getEndTime() != null) {
                timeRange.lte(request.getEndTime());
            }
            boolQuery.filter(timeRange);
        }
        
        // 8. 嵌套对象查询(规格参数)
        if (request.getSpecKey() != null && request.getSpecValue() != null) {
            boolQuery.filter(QueryBuilders.nestedQuery("specifications",
                    QueryBuilders.boolQuery()
                            .must(QueryBuilders.termQuery("specifications.key", request.getSpecKey()))
                            .must(QueryBuilders.matchQuery("specifications.value", request.getSpecValue())),
                    ScoreMode.None));
        }
        
        // 构建分页和排序
        Pageable pageable = PageRequest.of(
                request.getPage(), 
                request.getSize(),
                buildSort(request.getSortField(), request.getSortOrder())
        );
        
        // 构建NativeSearchQuery
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQuery)
                .withPageable(pageable)
                .build();
        
        // 执行查询
        return productRepository.search(searchQuery);
    }
    
    /**
     * 构建排序
     */
    private Sort buildSort(String sortField, String sortOrder) {
        if (!StringUtils.hasText(sortField)) {
            return Sort.unsorted();
        }
        
        Sort.Direction direction = "desc".equalsIgnoreCase(sortOrder) 
                ? Sort.Direction.DESC 
                : Sort.Direction.ASC;
        
        return Sort.by(direction, sortField);
    }
    
    /**
     * 聚合查询 - 按品牌统计
     */
    public Map<String, Long> aggregateByBrand() {
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .addAggregation(AggregationBuilders.terms("brand_agg").field("brand"))
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
        
        return searchHits.getAggregations()
                .<Terms>get("brand_agg")
                .getBuckets()
                .stream()
                .collect(Collectors.toMap(
                        Terms.Bucket::getKeyAsString,
                        Terms.Bucket::getDocCount
                ));
    }
    
    /**
     * 高亮查询
     */
    public List<Product> searchWithHighlight(String keyword) {
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery(keyword, "productName", "description"))
                .withHighlightFields(
                        new HighlightBuilder.Field("productName").preTags("<em>").postTags("</em>"),
                        new HighlightBuilder.Field("description").preTags("<em>").postTags("</em>")
                )
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
        
        return searchHits.getSearchHits()
                .stream()
                .map(this::convertToProductWithHighlight)
                .collect(Collectors.toList());
    }
    
    /**
     * 处理高亮结果
     */
    private Product convertToProductWithHighlight(SearchHit<Product> searchHit) {
        Product product = searchHit.getContent();
        
        // 处理商品名称高亮
        if (searchHit.getHighlightFields().containsKey("productName")) {
            product.setProductName(searchHit.getHighlightFields().get("productName").get(0));
        }
        
        // 处理描述高亮
        if (searchHit.getHighlightFields().containsKey("description")) {
            product.setDescription(searchHit.getHighlightFields().get("description").get(0));
        }
        
        return product;
    }
    
    /**
     * 使用Scroll API进行深度分页
     */
    public List<Product> scrollSearch(String category, int batchSize) {
        List<Product> allProducts = new ArrayList<>();
        
        // 创建初始查询
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.termQuery("category", category))
                .withPageable(PageRequest.of(0, batchSize))
                .build();
        
        // 开启Scroll
        SearchScrollHits<Product> scrollHits = elasticsearchRestTemplate.searchScrollStart(
                60000L, // Scroll保持时间
                searchQuery,
                Product.class,
                IndexCoordinates.of("product_index")
        );
        
        String scrollId = scrollHits.getScrollId();
        
        try {
            // 处理第一批结果
            scrollHits.getSearchHits().forEach(hit -> allProducts.add(hit.getContent()));
            
            // 继续获取后续批次
            while (scrollHits.hasSearchHits()) {
                scrollHits = elasticsearchRestTemplate.searchScrollContinue(
                        scrollId,
                        60000L,
                        Product.class,
                        IndexCoordinates.of("product_index")
                );
                scrollHits.getSearchHits().forEach(hit -> allProducts.add(hit.getContent()));
            }
        } finally {
            // 清理Scroll
            elasticsearchRestTemplate.searchScrollClear(Collections.singletonList(scrollId));
        }
        
        return allProducts;
    }
    
    /**
     * 批量操作 - Bulk API
     */
    public void bulkOperations(List<Product> productsToSave, List<String> idsToDelete) {
        List<IndexQuery> indexQueries = productsToSave.stream()
                .map(product -> {
                    IndexQuery indexQuery = new IndexQuery();
                    indexQuery.setId(product.getId());
                    indexQuery.setObject(product);
                    return indexQuery;
                })
                .collect(Collectors.toList());
        
        List<String> deleteQueries = idsToDelete.stream()
                .map(id -> new DeleteQuery(null, null, id))
                .map(query -> query.getId())
                .collect(Collectors.toList());
        
        // 批量索引
        if (!indexQueries.isEmpty()) {
            elasticsearchRestTemplate.bulkIndex(indexQueries, IndexCoordinates.of("product_index"));
        }
        
        // 批量删除
        if (!deleteQueries.isEmpty()) {
            elasticsearchRestTemplate.bulkDelete(deleteQueries, IndexCoordinates.of("product_index"));
        }
    }
}
查询请求参数类
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@Data
public class ProductSearchRequest {
    private String keyword;           // 关键词
    private String category;          // 分类
    private List<String> brands;      // 品牌列表
    private BigDecimal minPrice;      // 最低价格
    private BigDecimal maxPrice;      // 最高价格
    private Boolean inStock;          // 是否有库存
    private Boolean status;           // 状态
    private LocalDateTime startTime;  // 开始时间
    private LocalDateTime endTime;    // 结束时间
    private String specKey;           // 规格键
    private String specValue;         // 规格值
    
    // 分页参数
    private Integer page = 0;
    private Integer size = 10;
    
    // 排序参数
    private String sortField = "createTime";
    private String sortOrder = "desc";
}

五、Controller层

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;
import java.util.Optional;

@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @Autowired
    private ProductService productService;
    
    // ================ CRUD接口 ================
    
    @PostMapping
    public ResponseEntity<Product> create(@RequestBody Product product) {
        return ResponseEntity.ok(productService.save(product));
    }
    
    @PutMapping("/{id}")
    public ResponseEntity<Product> update(@PathVariable String id, @RequestBody Product product) {
        product.setId(id);
        return ResponseEntity.ok(productService.save(product));
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<Product> getById(@PathVariable String id) {
        Optional<Product> product = productService.findById(id);
        return product.map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
    
    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable String id) {
        productService.deleteById(id);
        return ResponseEntity.ok().build();
    }
    
    // ================ 查询接口 ================
    
    @GetMapping("/search")
    public ResponseEntity<Page<Product>> search(ProductSearchRequest request) {
        return ResponseEntity.ok(productService.searchProducts(request));
    }
    
    @GetMapping("/highlight")
    public ResponseEntity<List<Product>> searchWithHighlight(@RequestParam String keyword) {
        return ResponseEntity.ok(productService.searchWithHighlight(keyword));
    }
    
    @GetMapping("/aggregate/brand")
    public ResponseEntity<Map<String, Long>> aggregateByBrand() {
        return ResponseEntity.ok(productService.aggregateByBrand());
    }
    
    @GetMapping("/scroll")
    public ResponseEntity<List<Product>> scrollSearch(
            @RequestParam String category,
            @RequestParam(defaultValue = "100") int batchSize) {
        return ResponseEntity.ok(productService.scrollSearch(category, batchSize));
    }
    
    // ================ 批量操作 ================
    
    @PostMapping("/batch")
    public ResponseEntity<Iterable<Product>> batchSave(@RequestBody List<Product> products) {
        return ResponseEntity.ok(productService.saveAll(products));
    }
    
    @PostMapping("/bulk")
    public ResponseEntity<Void> bulkOperations(
            @RequestBody BulkRequest request) {
        productService.bulkOperations(request.getProductsToSave(), request.getIdsToDelete());
        return ResponseEntity.ok().build();
    }
}

@Data
class BulkRequest {
    private List<Product> productsToSave;
    private List<String> idsToDelete;
}

六、高级查询示例

复杂布尔查询
@Service
public class AdvancedSearchService {
    
    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    /**
     * 复杂布尔查询示例
     */
    public List<Product> complexBoolQuery() {
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                // must: 必须满足的条件(AND)
                .must(QueryBuilders.matchQuery("productName", "手机"))
                // should: 应该满足的条件(OR),可设置最小匹配数
                .should(QueryBuilders.termQuery("brand", "华为"))
                .should(QueryBuilders.termQuery("brand", "小米"))
                .minimumShouldMatch(1)  // 至少匹配一个should条件
                
                // filter: 过滤条件,不参与评分
                .filter(QueryBuilders.rangeQuery("price").gte(1000).lte(5000))
                .filter(QueryBuilders.termQuery("status", true))
                
                // must_not: 必须不满足的条件(NOT)
                .mustNot(QueryBuilders.termQuery("category", "二手"));
        
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQuery)
                .withPageable(PageRequest.of(0, 10))
                .build();
        
        return elasticsearchRestTemplate.search(searchQuery, Product.class)
                .getSearchHits()
                .stream()
                .map(SearchHit::getContent)
                .collect(Collectors.toList());
    }
    
    /**
     * 多字段模糊查询 + 权重
     */
    public List<Product> multiFieldSearch(String keyword) {
        MultiMatchQueryBuilder query = QueryBuilders.multiMatchQuery(keyword)
                .field("productName", 3.0f)     // 权重3倍
                .field("description", 2.0f)     // 权重2倍
                .field("brand", 1.0f)          // 权重1倍
                .type(MultiMatchQueryBuilder.Type.BEST_FIELDS)  // 最佳字段匹配
                .fuzziness("AUTO");            // 模糊匹配
        
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(query)
                .withPageable(PageRequest.of(0, 10))
                .build();
        
        return elasticsearchRestTemplate.search(searchQuery, Product.class)
                .getSearchHits()
                .stream()
                .map(SearchHit::getContent)
                .collect(Collectors.toList());
    }
    
    /**
     * 前缀查询 + 通配符查询
     */
    public List<Product> prefixAndWildcardSearch() {
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
                .should(QueryBuilders.prefixQuery("productName", "智能"))  // 前缀查询
                .should(QueryBuilders.wildcardQuery("productName", "*手机*"))  // 通配符查询
                .minimumShouldMatch(1);
        
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQuery)
                .withPageable(PageRequest.of(0, 10))
                .build();
        
        return elasticsearchRestTemplate.search(searchQuery, Product.class)
                .getSearchHits()
                .stream()
                .map(SearchHit::getContent)
                .collect(Collectors.toList());
    }
    
    /**
     * 嵌套查询 - 查询规格参数
     */
    public List<Product> nestedQuery() {
        // 查询规格参数中颜色为黑色且内存为8G的商品
        BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery()
                .must(QueryBuilders.termQuery("specifications.key", "颜色"))
                .must(QueryBuilders.matchQuery("specifications.value", "黑色"))
                .must(QueryBuilders.termQuery("specifications.key", "内存"))
                .must(QueryBuilders.matchQuery("specifications.value", "8G"));
        
        NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("specifications", 
                nestedBoolQuery, ScoreMode.None);
        
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(nestedQuery)
                .withPageable(PageRequest.of(0, 10))
                .build();
        
        return elasticsearchRestTemplate.search(searchQuery, Product.class)
                .getSearchHits()
                .stream()
                .map(SearchHit::getContent)
                .collect(Collectors.toList());
    }
    
    /**
     * 聚合统计 - 多维度分析
     */
    public Map<String, Object> multiAggregation() {
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.matchAllQuery())
                .addAggregation(AggregationBuilders.terms("category_agg").field("category"))
                .addAggregation(AggregationBuilders.avg("avg_price").field("price"))
                .addAggregation(AggregationBuilders.sum("total_stock").field("stock"))
                .addAggregation(
                        AggregationBuilders.terms("brand_agg").field("brand")
                                .subAggregation(AggregationBuilders.avg("brand_avg_price").field("price"))
                                .subAggregation(AggregationBuilders.sum("brand_total_stock").field("stock"))
                )
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
        Aggregations aggregations = searchHits.getAggregations();
        
        Map<String, Object> result = new HashMap<>();
        
        // 分类统计
        Terms categoryAgg = aggregations.get("category_agg");
        Map<String, Long> categoryMap = categoryAgg.getBuckets()
                .stream()
                .collect(Collectors.toMap(
                        Terms.Bucket::getKeyAsString,
                        Terms.Bucket::getDocCount
                ));
        result.put("categoryDistribution", categoryMap);
        
        // 平均价格
        Avg avgPrice = aggregations.get("avg_price");
        result.put("averagePrice", avgPrice.getValue());
        
        // 总库存
        Sum totalStock = aggregations.get("total_stock");
        result.put("totalStock", totalStock.getValue());
        
        // 品牌详细统计
        Terms brandAgg = aggregations.get("brand_agg");
        List<Map<String, Object>> brandStats = brandAgg.getBuckets()
                .stream()
                .map(bucket -> {
                    Map<String, Object> brandInfo = new HashMap<>();
                    brandInfo.put("brand", bucket.getKeyAsString());
                    brandInfo.put("count", bucket.getDocCount());
                    
                    Avg brandAvgPrice = bucket.getAggregations().get("brand_avg_price");
                    brandInfo.put("avgPrice", brandAvgPrice.getValue());
                    
                    Sum brandTotalStock = bucket.getAggregations().get("brand_total_stock");
                    brandInfo.put("totalStock", brandTotalStock.getValue());
                    
                    return brandInfo;
                })
                .collect(Collectors.toList());
        result.put("brandStatistics", brandStats);
        
        return result;
    }
    
    /**
     * 地理位置查询(如果包含位置信息)
     */
    public List<Product> geoDistanceSearch(double lat, double lon, String distance) {
        GeoDistanceQueryBuilder geoQuery = QueryBuilders.geoDistanceQuery("location")
                .point(lat, lon)
                .distance(distance);
        
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(geoQuery)
                .withPageable(PageRequest.of(0, 10))
                .build();
        
        return elasticsearchRestTemplate.search(searchQuery, Product.class)
                .getSearchHits()
                .stream()
                .map(SearchHit::getContent)
                .collect(Collectors.toList());
    }
    
    /**
     * 脚本字段查询
     */
    public List<Map<String, Object>> scriptFieldSearch() {
        Script script = new Script(ScriptType.INLINE, "painless",
                "doc['price'].value * doc['stock'].value", new HashMap<>());
        
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.matchAllQuery())
                .withScriptField("totalValue", script)
                .withPageable(PageRequest.of(0, 10))
                .build();
        
        return elasticsearchRestTemplate.search(searchQuery, Product.class)
                .getSearchHits()
                .stream()
                .map(hit -> {
                    Map<String, Object> result = new HashMap<>();
                    result.put("product", hit.getContent());
                    result.put("totalValue", hit.getFields().get("totalValue").getValue());
                    return result;
                })
                .collect(Collectors.toList());
    }
}
自定义Repository实现
public interface CustomProductRepository {
    List<Product> findProductsByComplexCriteria(ProductSearchCriteria criteria);
    Page<Product> findProductsWithAggregation(ProductSearchCriteria criteria, Pageable pageable);
}

@Repository
public class CustomProductRepositoryImpl implements CustomProductRepository {
    
    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    @Override
    public List<Product> findProductsByComplexCriteria(ProductSearchCriteria criteria) {
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        
        // 动态构建查询条件
        if (StringUtils.hasText(criteria.getKeyword())) {
            boolQuery.must(QueryBuilders.multiMatchQuery(criteria.getKeyword(), 
                    "productName", "description", "brand"));
        }
        
        if (criteria.getCategoryIds() != null && !criteria.getCategoryIds().isEmpty()) {
            boolQuery.filter(QueryBuilders.termsQuery("categoryId", criteria.getCategoryIds()));
        }
        
        if (criteria.getMinPrice() != null || criteria.getMaxPrice() != null) {
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
            if (criteria.getMinPrice() != null) {
                rangeQuery.gte(criteria.getMinPrice());
            }
            if (criteria.getMaxPrice() != null) {
                rangeQuery.lte(criteria.getMaxPrice());
            }
            boolQuery.filter(rangeQuery);
        }
        
        // 添加排序
        Sort sort = Sort.by(Sort.Direction.DESC, "createTime");
        
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(boolQuery)
                .withSort(sort)
                .withPageable(PageRequest.of(0, criteria.getSize()))
                .build();
        
        return elasticsearchRestTemplate.search(searchQuery, Product.class)
                .getSearchHits()
                .stream()
                .map(SearchHit::getContent)
                .collect(Collectors.toList());
    }
    
    @Override
    public Page<Product> findProductsWithAggregation(ProductSearchCriteria criteria, Pageable pageable) {
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.matchAllQuery())
                .withPageable(pageable);
        
        // 添加聚合
        queryBuilder.addAggregation(
                AggregationBuilders.terms("category_agg").field("category")
                        .subAggregation(AggregationBuilders.avg("avg_price").field("price"))
        );
        
        NativeSearchQuery searchQuery = queryBuilder.build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
        
        // 处理聚合结果
        Aggregations aggregations = searchHits.getAggregations();
        if (aggregations != null) {
            Terms categoryAgg = aggregations.get("category_agg");
            // 处理聚合数据...
        }
        
        // 转换为Page对象
        List<Product> products = searchHits.getSearchHits()
                .stream()
                .map(SearchHit::getContent)
                .collect(Collectors.toList());
        
        return new PageImpl<>(products, pageable, searchHits.getTotalHits());
    }
}

// 扩展主Repository
public interface ProductRepository extends ElasticsearchRepository<Product, String>, CustomProductRepository {
    // 原有的方法...
}
异步操作
@Service
public class AsyncProductService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    /**
     * 异步保存
     */
    @Async
    public CompletableFuture<Product> saveAsync(Product product) {
        return CompletableFuture.completedFuture(productRepository.save(product));
    }
    
    /**
     * 异步批量保存
     */
    @Async
    public CompletableFuture<Iterable<Product>> saveAllAsync(List<Product> products) {
        return CompletableFuture.completedFuture(productRepository.saveAll(products));
    }
    
    /**
     * 异步查询
     */
    @Async
    public CompletableFuture<List<Product>> searchAsync(String keyword) {
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery(keyword, "productName", "description"))
                .withPageable(PageRequest.of(0, 10))
                .build();
        
        List<Product> products = elasticsearchRestTemplate.search(searchQuery, Product.class)
                .getSearchHits()
                .stream()
                .map(SearchHit::getContent)
                .collect(Collectors.toList());
        
        return CompletableFuture.completedFuture(products);
    }
}

七、配置类

@Configuration
@EnableElasticsearchRepositories(basePackages = "com.example.repository")
@EnableAsync
public class ElasticsearchConfig {
    
    /**
     * 配置RestHighLevelClient
     */
    @Bean
    public RestHighLevelClient restHighLevelClient() {
        ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo("localhost:9200")
                .withBasicAuth("elastic", "password")  // 如果有认证
                .withConnectTimeout(Duration.ofSeconds(5))
                .withSocketTimeout(Duration.ofSeconds(30))
                .build();
        
        return RestClients.create(clientConfiguration).rest();
    }
    
    /**
     * 配置ElasticsearchRestTemplate
     */
    @Bean
    public ElasticsearchRestTemplate elasticsearchRestTemplate(
            RestHighLevelClient restHighLevelClient) {
        return new ElasticsearchRestTemplate(restHighLevelClient);
    }
    
    /**
     * 配置异步任务执行器
     */
    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(500);
        executor.setThreadNamePrefix("Elasticsearch-Async-");
        executor.initialize();
        return executor;
    }
}

八、异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(ElasticsearchException.class)
    public ResponseEntity<ErrorResponse> handleElasticsearchException(ElasticsearchException e) {
        ErrorResponse error = new ErrorResponse(
                "ELASTICSEARCH_ERROR",
                e.getMessage(),
                LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }
    
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException e) {
        ErrorResponse error = new ErrorResponse(
                "RESOURCE_NOT_FOUND",
                e.getMessage(),
                LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
    }
    
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidationException(ValidationException e) {
        ErrorResponse error = new ErrorResponse(
                "VALIDATION_ERROR",
                e.getMessage(),
                LocalDateTime.now()
        );
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
    }
}

@Data
@AllArgsConstructor
class ErrorResponse {
    private String code;
    private String message;
    private LocalDateTime timestamp;
}

class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

class ValidationException extends RuntimeException {
    public ValidationException(String message) {
        super(message);
    }
}

4. 模糊查询、高亮显示、聚合查询

项目依赖配置

pom.xml

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- Elasticsearch -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    
    <!-- Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

application.yml

spring:
  elasticsearch:
    uris: http://localhost:9200
    username: ${ES_USERNAME:elastic}
    password: ${ES_PASSWORD:password}
    
  data:
    elasticsearch:
      repositories:
        enabled: true

实体类定义

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Data
@Document(indexName = "product")
public class Product {
    
    @Id
    private String id;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String name;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
    private String description;
    
    @Field(type = FieldType.Double)
    private Double price;
    
    @Field(type = FieldType.Keyword)
    private String category;
    
    @Field(type = FieldType.Integer)
    private Integer stock;
    
    @Field(type = FieldType.Date)
    private Date createTime;
}

Repository接口

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends ElasticsearchRepository<Product, String> {
    
    // 自定义查询方法
    List<Product> findByNameContaining(String name);
    
    List<Product> findByDescriptionContaining(String description);
}

Service层实现

ProductService接口

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.List;
import java.util.Map;

public interface ProductService {
    
    // 模糊查询
    Page<Product> fuzzySearch(String keyword, Pageable pageable);
    
    // 高亮显示查询
    Page<Product> highlightSearch(String keyword, String[] fields, Pageable pageable);
    
    // 聚合查询
    Map<String, Long> categoryAggregation();
    
    Map<String, Double> priceRangeAggregation();
    
    // 组合查询:模糊+高亮+聚合
    SearchResult combinedSearch(String keyword, Pageable pageable);
}

ProductServiceImpl实现类

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.common.lucene.search.function.FunctionScoreQuery;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
import org.elasticsearch.index.query.functionscore.ScoreFunctionBuilders;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.range.Range;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.elasticsearch.core.SearchHits;
import org.springframework.data.elasticsearch.core.query.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
    
    private final ProductRepository productRepository;
    private final ElasticsearchRestTemplate elasticsearchRestTemplate;
    
    /**
     * 模糊查询 - 使用Wildcard Query
     */
    @Override
    public Page<Product> fuzzySearch(String keyword, Pageable pageable) {
        if (!StringUtils.hasText(keyword)) {
            return productRepository.findAll(pageable);
        }
        
        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.wildcardQuery("name", "*" + keyword + "*"))
                .withPageable(pageable)
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(query, Product.class);
        
        List<Product> products = searchHits.getSearchHits().stream()
                .map(hit -> hit.getContent())
                .collect(Collectors.toList());
        
        return new PageImpl<>(products, pageable, searchHits.getTotalHits());
    }
    
    /**
     * 高亮显示查询
     */
    @Override
    public Page<Product> highlightSearch(String keyword, String[] fields, Pageable pageable) {
        if (!StringUtils.hasText(keyword)) {
            return productRepository.findAll(pageable);
        }
        
        // 构建高亮
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        for (String field : fields) {
            highlightBuilder.field(field)
                    .preTags("<span style='color:red'>")
                    .postTags("</span>");
        }
        
        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(QueryBuilders.multiMatchQuery(keyword, fields))
                .withHighlightBuilder(highlightBuilder)
                .withPageable(pageable)
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(query, Product.class);
        
        List<Product> products = searchHits.getSearchHits().stream()
                .map(hit -> {
                    Product product = hit.getContent();
                    
                    // 处理高亮字段
                    Map<String, List<String>> highlightFields = hit.getHighlightFields();
                    if (highlightFields.containsKey("name")) {
                        product.setName(highlightFields.get("name").get(0));
                    }
                    if (highlightFields.containsKey("description")) {
                        product.setDescription(highlightFields.get("description").get(0));
                    }
                    
                    return product;
                })
                .collect(Collectors.toList());
        
        return new PageImpl<>(products, pageable, searchHits.getTotalHits());
    }
    
    /**
     * 高级模糊查询 - 支持多种模糊匹配方式
     */
    public Page<Product> advancedFuzzySearch(String keyword, Pageable pageable) {
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        
        // 1. 通配符查询
        boolQuery.should(QueryBuilders.wildcardQuery("name", "*" + keyword + "*"));
        boolQuery.should(QueryBuilders.wildcardQuery("description", "*" + keyword + "*"));
        
        // 2. 模糊查询(编辑距离)
        boolQuery.should(QueryBuilders.fuzzyQuery("name", keyword).fuzziness("AUTO"));
        
        // 3. 前缀查询
        boolQuery.should(QueryBuilders.prefixQuery("name", keyword));
        
        // 4. 正则表达式查询
        boolQuery.should(QueryBuilders.regexpQuery("name", ".*" + keyword + ".*"));
        
        // 5. Match Query(分词查询)
        boolQuery.should(QueryBuilders.matchQuery("name", keyword)
                .fuzziness("AUTO")
                .prefixLength(0)
                .maxExpansions(10));
        
        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(boolQuery)
                .withPageable(pageable)
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(query, Product.class);
        
        List<Product> products = searchHits.getSearchHits().stream()
                .map(hit -> hit.getContent())
                .collect(Collectors.toList());
        
        return new PageImpl<>(products, pageable, searchHits.getTotalHits());
    }
    
    /**
     * 分类聚合查询
     */
    @Override
    public Map<String, Long> categoryAggregation() {
        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .addAggregation(
                        AggregationBuilders.terms("category_agg")
                                .field("category")
                                .size(10)
                )
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(query, Product.class);
        
        Terms terms = searchHits.getAggregations().get("category_agg");
        Map<String, Long> result = new HashMap<>();
        
        for (Terms.Bucket bucket : terms.getBuckets()) {
            result.put(bucket.getKeyAsString(), bucket.getDocCount());
        }
        
        return result;
    }
    
    /**
     * 价格区间聚合查询
     */
    @Override
    public Map<String, Double> priceRangeAggregation() {
        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .addAggregation(
                        AggregationBuilders.range("price_ranges")
                                .field("price")
                                .addRange(0, 100)
                                .addRange(100, 500)
                                .addRange(500, 1000)
                                .addRange(1000, Double.MAX_VALUE)
                )
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(query, Product.class);
        
        Range rangeAgg = searchHits.getAggregations().get("price_ranges");
        Map<String, Double> result = new HashMap<>();
        
        for (Range.Bucket bucket : rangeAgg.getBuckets()) {
            result.put(bucket.getKeyAsString(), (double) bucket.getDocCount());
        }
        
        return result;
    }
    
    /**
     * 多字段聚合查询
     */
    public Map<String, Object> multiFieldAggregation() {
        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .addAggregation(
                        AggregationBuilders.terms("category_agg")
                                .field("category")
                                .subAggregation(
                                        AggregationBuilders.avg("avg_price")
                                                .field("price")
                                )
                                .subAggregation(
                                        AggregationBuilders.sum("total_stock")
                                                .field("stock")
                                )
                )
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(query, Product.class);
        
        Terms terms = searchHits.getAggregations().get("category_agg");
        Map<String, Object> result = new HashMap<>();
        
        for (Terms.Bucket bucket : terms.getBuckets()) {
            Map<String, Object> categoryData = new HashMap<>();
            categoryData.put("count", bucket.getDocCount());
            
            // 获取子聚合结果
            categoryData.put("avgPrice", ((Terms) bucket.getAggregations().getAsMap().get("avg_price")).getBuckets());
            categoryData.put("totalStock", ((Terms) bucket.getAggregations().getAsMap().get("total_stock")).getBuckets());
            
            result.put(bucket.getKeyAsString(), categoryData);
        }
        
        return result;
    }
    
    /**
     * 组合查询:模糊查询 + 高亮 + 聚合
     */
    @Override
    public SearchResult combinedSearch(String keyword, Pageable pageable) {
        SearchResult result = new SearchResult();
        
        // 1. 执行高亮查询
        String[] highlightFields = {"name", "description"};
        Page<Product> productPage = highlightSearch(keyword, highlightFields, pageable);
        result.setProducts(productPage.getContent());
        result.setTotal(productPage.getTotalElements());
        
        // 2. 执行聚合查询
        result.setCategoryAggregation(categoryAggregation());
        result.setPriceAggregation(priceRangeAggregation());
        
        return result;
    }
    
    /**
     * 函数评分查询(相关性排序)
     */
    public Page<Product> functionScoreSearch(String keyword, Pageable pageable) {
        FunctionScoreQueryBuilder.FilterFunctionBuilder[] functions = {
                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                        QueryBuilders.matchQuery("name", keyword),
                        ScoreFunctionBuilders.weightFactorFunction(2.0f)
                ),
                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                        QueryBuilders.matchQuery("description", keyword),
                        ScoreFunctionBuilders.weightFactorFunction(1.0f)
                ),
                // 库存越多,评分越高
                new FunctionScoreQueryBuilder.FilterFunctionBuilder(
                        QueryBuilders.rangeQuery("stock").gte(0),
                        ScoreFunctionBuilders.fieldValueFactorFunction("stock")
                                .modifier(FieldValueFactorFunction.Modifier.LN1P)
                                .factor(0.1f)
                )
        };
        
        FunctionScoreQueryBuilder functionScoreQuery = QueryBuilders.functionScoreQuery(functions)
                .scoreMode(FunctionScoreQuery.ScoreMode.SUM)
                .setMinScore(1.0f);
        
        NativeSearchQuery query = new NativeSearchQueryBuilder()
                .withQuery(functionScoreQuery)
                .withPageable(pageable)
                .build();
        
        SearchHits<Product> searchHits = elasticsearchRestTemplate.search(query, Product.class);
        
        List<Product> products = searchHits.getSearchHits().stream()
                .map(hit -> hit.getContent())
                .collect(Collectors.toList());
        
        return new PageImpl<>(products, pageable, searchHits.getTotalHits());
    }
}

/**
 * 搜索结果封装类
 */
@Data
class SearchResult {
    private List<Product> products;
    private long total;
    private Map<String, Long> categoryAggregation;
    private Map<String, Double> priceAggregation;
}

Controller层

import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {
    
    private final ProductService productService;
    
    /**
     * 模糊查询
     */
    @GetMapping("/fuzzy")
    public Page<Product> fuzzySearch(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        Pageable pageable = PageRequest.of(page, size, Sort.by("createTime").descending());
        return productService.fuzzySearch(keyword, pageable);
    }
    
    /**
     * 高亮查询
     */
    @GetMapping("/highlight")
    public Page<Product> highlightSearch(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        String[] fields = {"name", "description"};
        return productService.highlightSearch(keyword, fields, pageable);
    }
    
    /**
     * 分类聚合
     */
    @GetMapping("/aggregation/category")
    public Map<String, Long> categoryAggregation() {
        return productService.categoryAggregation();
    }
    
    /**
     * 价格区间聚合
     */
    @GetMapping("/aggregation/price")
    public Map<String, Double> priceAggregation() {
        return productService.priceRangeAggregation();
    }
    
    /**
     * 组合查询
     */
    @GetMapping("/combined")
    public SearchResult combinedSearch(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        return productService.combinedSearch(keyword, pageable);
    }
    
    /**
     * 高级模糊查询
     */
    @GetMapping("/advanced-fuzzy")
    public Page<Product> advancedFuzzySearch(
            @RequestParam String keyword,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        
        Pageable pageable = PageRequest.of(page, size);
        return ((ProductServiceImpl) productService).advancedFuzzySearch(keyword, pageable);
    }
}

初始化数据(可选)

import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Date;

@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {
    
    private final ProductRepository productRepository;
    
    @Override
    public void run(String... args) {
        // 清空索引
        productRepository.deleteAll();
        
        // 初始化测试数据
        Product p1 = new Product();
        p1.setName("Apple iPhone 14 Pro Max");
        p1.setDescription("最新款苹果手机,搭载A16芯片,4800万像素摄像头");
        p1.setPrice(8999.0);
        p1.setCategory("手机");
        p1.setStock(100);
        p1.setCreateTime(new Date());
        
        Product p2 = new Product();
        p2.setName("Huawei Mate 50 Pro");
        p2.setDescription("华为旗舰手机,XMAGE影像系统,鸿蒙操作系统");
        p2.setPrice(6999.0);
        p2.setCategory("手机");
        p2.setStock(80);
        p2.setCreateTime(new Date());
        
        Product p3 = new Product();
        p3.setName("联想拯救者Y9000P游戏本");
        p3.setDescription("高性能游戏笔记本电脑,RTX 4060显卡,16GB内存");
        p3.setPrice(9999.0);
        p3.setCategory("电脑");
        p3.setStock(50);
        p3.setCreateTime(new Date());
        
        productRepository.saveAll(Arrays.asList(p1, p2, p3));
    }
}

更多推荐