【SpringBoot 从入门到架构师】第18章:Elasticsearch全文检索整合
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(标准)、simple、whitespace、keyword(不分词)等。
第二部分:环境搭建
这里提供两种最常用的搭建方式:单节点本地运行 (用于学习开发)和 使用 Docker Compose 运行集群 (更接近生产环境)。
方式一:单节点本地运行(快速开始)
- 下载
- 访问 Elastic 官网下载页面 。
- 选择与您系统对应的版本(如 Linux/Mac 的 tar.gz, Windows 的 zip)。
- 解压到目录,如
/opt/elasticsearch-8.12.0。
- 启动(Linux/Mac)
# 进入解压目录
cd /opt/elasticsearch-8.12.0
# 启动(前台运行,-d 参数可后台运行)
./bin/elasticsearch
首次启动 8.x 版本,会:
- 自动生成集群和节点的 TLS 证书。
- 在控制台输出
elastic用户的默认密码。 - 启用安全特性(用户名/密码)。
- 验证
# 使用自动生成的密码访问(将密码替换为你的)
curl -k -u elastic:your_password https://localhost:9200
或打开浏览器访问 https://localhost:9200,输入用户名 elastic 和密码。
- 关键文件路径
-
config/elasticsearch.yml :主配置文件。 -
config/jvm.options :JVM 堆内存设置(建议设为机器内存的一半,不超过 32GB)。 -
logs/ :日志文件。 -
data/ :数据文件(默认在安装目录下)。
方式二:使用 Docker Compose 搭建集群(推荐)
- 准备工作 确保系统已安装 Docker 和 Docker Compose 。
- 创建目录结构
~/es-docker/
├── docker-compose.yml
├── config/
│ └── elasticsearch.yml (可选,覆盖默认配置)
└── .env (用于设置环境变量)
- 编写
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
- 启动集群
# 进入项目目录
cd ~/es-docker
# 后台启动所有服务
docker-compose up -d
# 查看日志
docker-compose logs -f es01
# 检查集群健康状态
curl http://localhost:9200/_cluster/health?pretty
输出中 "status" : "green" 表示集群健康。
- 访问
- Elasticsearch :
http://localhost:9200 - Kibana (如果部署了):
http://localhost:5601
下一步建议
- 使用 Kibana Dev Tools :在 Kibana 的
Dev Tools中练习 REST API,这是学习和操作 ES 的最佳界面。 - 执行第一个操作 :
// 创建索引
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"
}
}
}
- 学习核心 API :掌握
_cat、_search、_bulk、_mapping、_cluster等 API。 - 开启安全认证 :在生产环境中,务必在
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);
}
}
选择建议
- Spring Data Elasticsearch :适合简单CRUD,与Spring生态集成好
- Rest High Level Client :需要更细粒度控制,复杂查询场景
- 混合使用 :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));
}
}
更多推荐
所有评论(0)