在传统数据库中,全文检索、模糊匹配、海量数据快速查询等场景存在性能瓶颈 ------MySQL 模糊查询(LIKE % 关键词 %)无法利用索引,数据量超过百万级后响应缓慢。Elasticsearch(简称 ES)作为分布式全文搜索引擎,凭借「分词检索、近实时查询、高可用集群、聚合分析」的优势,成为电商商品搜索、日志分析、文档检索等场景的首选方案。
本文聚焦 SpringBoot 与 Elasticsearch 的实战落地,从环境搭建、索引设计、基础 CRUD、复杂检索(分词、高亮、过滤),到聚合分析与性能优化,全程嵌入 Java 代码教学,帮你快速掌握 ES 核心技能,打造高效的海量数据检索系统。
一、核心认知:Elasticsearch 核心优势与适用场景
1. 与传统数据库的核心差异
| 特性 | 传统数据库(MySQL) | Elasticsearch |
|---|---|---|
| 检索能力 | 仅支持简单模糊查询,性能差 | 全文检索、分词匹配、高亮显示 |
| 数据量适配 | 百万级以下性能稳定 | 亿级数据可分布式存储与查询 |
| 查询速度 | 依赖索引,复杂查询缓慢 | 倒排索引,毫秒级响应 |
| 聚合分析 | 能力薄弱,效率低 | 强大聚合功能,支持多维度统计 |
2. 核心适用场景
- 电商商品搜索:支持关键词分词、筛选(价格、分类)、排序、高亮显示;
- 日志分析:收集海量日志,按级别、时间、服务名聚合检索;
- 文档检索:PDF、Word 等文档的全文检索与关键词定位;
- 地理位置检索:基于经纬度的附近商家、景点查询。
3. ES 核心概念(类比数据库理解)
- 索引(Index):类比数据库中的「数据库」,存储一类相似数据;
- 文档(Document):类比数据库中的「行」,是 ES 中最小的数据单元(JSON 格式);
- 类型(Type):类比数据库中的「表」,ES 7.x 后已废弃,一个索引仅支持一种类型;
- 字段(Field):类比数据库中的「列」,每个文档包含多个字段;
- 分词器(Analyzer):将文本拆分为关键词(如 "小米手机" 拆分为 "小米""手机"),是全文检索的核心。
二、核心实战一:环境搭建(Docker + SpringBoot 集成)
1. Docker 部署 Elasticsearch 与 Kibana
Kibana 是 ES 的可视化管理工具,用于调试索引、执行 DSL 查询,推荐与 ES 一同部署:
bash
运行
# 1. 部署 Elasticsearch(7.17.0 稳定版,需设置内存限制)
docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \ # 单节点模式(生产环境用集群)
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ # 限制堆内存,避免内存溢出
elasticsearch:7.17.0
# 2. 部署 Kibana(与 ES 版本一致)
docker run -d --name kibana -p 5601:5601 \
-e "ELASTICSEARCH_HOSTS=http://elasticsearch:9200" \ # 关联 ES 地址
kibana:7.17.0
- ES 访问地址:http://localhost:9200(返回 JSON 格式信息即部署成功);
- Kibana 访问地址:http://localhost:5601(通过 Dev Tools 执行 DSL 查询)。
2. SpringBoot 集成 ES 依赖与配置
(1)引入依赖(Maven)
xml
<!-- SpringBoot 集成 ES 依赖(适配 7.x 版本) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- Web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
(2)配置文件(application.yml)
yaml
spring:
elasticsearch:
rest:
uris: http://localhost:9200 # ES 服务地址(集群模式用逗号分隔多个地址)
connection-timeout: 3000ms # 连接超时时间
read-timeout: 5000ms # 读取超时时间
data:
elasticsearch:
repositories:
enabled: true # 开启 ES 仓库支持(用于自定义查询)
cluster-name: elasticsearch # ES 集群名称(默认elasticsearch)
三、核心实战二:索引设计与基础 CRUD 操作
ES 的核心是「索引设计」,合理的字段类型、分词器配置直接影响检索性能与效果,以电商商品索引为例实战。
1. 实体类与索引映射(核心配置)
java
运行
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.*;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 商品实体类(对应 ES 索引:product_index)
*/
@Data
@Document(indexName = "product_index") // 索引名称(小写,不可修改)
@Setting(shards = 3, replicas = 1) // 索引设置:3个主分片,1个副本(集群模式推荐)
public class Product {
@Id // 对应 ES 文档的 _id 字段
private Long id;
// 商品名称:分词检索,中文分词器用 ik_max_word(需安装IK分词器)
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String name;
// 商品标题:不分词,仅精确匹配(如商品编码)
@Field(type = FieldType.Keyword)
private String sku;
// 商品价格:数值类型,支持范围查询
@Field(type = FieldType.Double)
private BigDecimal price;
// 商品分类:支持多值查询(一个商品可属于多个分类)
@Field(type = FieldType.Keyword)
private List<String> categories;
// 商品描述:分词检索,存储压缩
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String description;
// 上架时间:日期类型,支持范围查询
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
private Date createTime;
// 商品库存:整数类型
@Field(type = FieldType.Integer)
private Integer stock;
}
2. 安装 IK 中文分词器(必备)
ES 默认分词器对中文支持差(按单个字分词),需安装 IK 分词器实现中文语义分词:
-
进入 ES 容器:
docker exec -it elasticsearch /bin/bash; -
安装 IK 分词器(版本需与 ES 一致,7.17.0 对应 v7.17.0): bash
运行
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.0/elasticsearch-analysis-ik-7.17.0.zip -
重启 ES 容器:
docker restart elasticsearch。
3. Repository 层(基础 CRUD 接口)
Spring Data Elasticsearch 提供 ElasticsearchRepository,内置基础 CRUD 方法,无需编写实现类:
java
运行
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import com.example.es.entity.Product;
import java.util.List;
/**
* 商品 ES 仓库:继承 ElasticsearchRepository,泛型为(实体类,ID类型)
*/
public interface ProductRepository extends ElasticsearchRepository<Product, Long> {
// 自定义查询方法(Spring Data 自动生成 DSL,无需编写SQL)
// 按分类查询商品(精确匹配)
List<Product> findByCategoriesContaining(String category);
// 按价格区间查询商品
List<Product> findByPriceBetween(Double minPrice, Double maxPrice);
// 按名称分词检索+价格区间过滤
List<Product> findByNameContainingAndPriceBetween(String name, Double minPrice, Double maxPrice);
}
4. 基础 CRUD 实战代码(Service + Controller)
(1)Service 层
java
运行
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import com.example.es.entity.Product;
import com.example.es.repository.ProductRepository;
import javax.annotation.Resource;
import java.util.List;
import java.util.Optional;
@Service
public class ProductService {
@Resource
private ProductRepository productRepository;
// ✅ 新增/修改商品(ID存在则更新,不存在则新增)
public Product saveProduct(Product product) {
Assert.notNull(product.getId(), "商品ID不能为空");
return productRepository.save(product);
}
// ✅ 批量新增商品
public Iterable<Product> batchSaveProduct(List<Product> productList) {
return productRepository.saveAll(productList);
}
// ✅ 根据ID删除商品
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
// ✅ 根据ID查询商品
public Product getProductById(Long id) {
Optional<Product> optional = productRepository.findById(id);
return optional.orElse(null);
}
// ✅ 按分类查询商品
public List<Product> getProductByCategory(String category) {
return productRepository.findByCategoriesContaining(category);
}
}
(2)Controller 层
java
运行
import org.springframework.web.bind.annotation.*;
import com.example.es.entity.Product;
import com.example.es.service.ProductService;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/product")
public class ProductController {
@Resource
private ProductService productService;
@PostMapping
public Product saveProduct(@RequestBody Product product) {
return productService.saveProduct(product);
}
@PostMapping("/batch")
public Iterable<Product> batchSaveProduct(@RequestBody List<Product> productList) {
return productService.batchSaveProduct(productList);
}
@DeleteMapping("/{id}")
public String deleteProduct(@PathVariable Long id) {
productService.deleteProduct(id);
return "商品删除成功";
}
@GetMapping("/{id}")
public Product getProductById(@PathVariable Long id) {
return productService.getProductById(id);
}
@GetMapping("/category/{category}")
public List<Product> getProductByCategory(@PathVariable String category) {
return productService.getProductByCategory(category);
}
}
四、核心实战三:复杂检索与高亮显示(电商商品搜索场景)
基础 CRUD 无法满足复杂场景,需通过 NativeSearchQueryBuilder 构建 DSL 查询,实现分词检索、高亮、过滤、排序等功能。
1. 商品搜索实战(分词 + 高亮 + 过滤 + 排序)
java
运行
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
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.NativeSearchQuery;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;
import com.example.es.entity.Product;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class ProductSearchService {
@Resource
private ElasticsearchRestTemplate elasticsearchRestTemplate;
// 商品搜索:关键词分词检索+分类过滤+价格排序+高亮显示
public List<Product> searchProduct(String keyword, String category, Integer pageNum, Integer pageSize) {
// 1. 构建布尔查询(多条件组合)
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 关键词分词检索(商品名称+描述)
boolQuery.should(QueryBuilders.matchQuery("name", keyword))
.should(QueryBuilders.matchQuery("description", keyword));
// 分类过滤(精确匹配)
if (category != null && !category.isEmpty()) {
boolQuery.filter(QueryBuilders.termsQuery("categories", category));
}
// 2. 高亮配置(高亮商品名称中的关键词)
HighlightBuilder highlightBuilder = new HighlightBuilder();
HighlightBuilder.Field nameHighlight = new HighlightBuilder.Field("name")
.preTags("<em style='color:red'>") // 高亮前缀
.postTags("</em>") // 高亮后缀
.requireFieldMatch(false); // 不强制字段匹配
highlightBuilder.field(nameHighlight);
// 3. 构建查询条件(分页+排序+高亮)
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.withHighlightBuilder(highlightBuilder)
.withPageable(PageRequest.of(pageNum - 1, pageSize)) // 分页(页码从0开始)
.withSort(Sort.by(Sort.Direction.DESC, "price")) // 按价格降序排序
.build();
// 4. 执行查询
SearchHits<Product> searchHits = elasticsearchRestTemplate.search(searchQuery, Product.class);
// 5. 处理高亮结果(替换原始名称为高亮名称)
return searchHits.stream().map(hit -> {
Product product = hit.getContent();
// 获取高亮字段内容
List<String> highlightNames = hit.getHighlightFields().get("name");
if (highlightNames != null && !highlightNames.isEmpty()) {
product.setName(highlightNames.get(0)); // 替换为高亮后的名称
}
return product;
}).collect(Collectors.toList());
}
}
2. 补充 Controller 接口
java
运行
@GetMapping("/search")
public List<Product> searchProduct(
@RequestParam String keyword,
@RequestParam(required = false) String category,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize
) {
return productSearchService.searchProduct(keyword, category, pageNum, pageSize);
}
五、核心实战四:聚合分析(统计场景)
ES 聚合分析可实现多维度数据统计,如按分类统计商品数量、计算价格平均值等,替代传统数据库的分组统计。
1. 商品分类聚合统计(按分类统计商品数量)
java
运行
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
public class ProductAggService {
@Resource
private RestHighLevelClient restHighLevelClient;
// 按分类聚合统计商品数量
public Map<String, Long> aggProductByCategory() throws IOException {
// 1. 构建聚合查询
SearchRequest searchRequest = new SearchRequest("product_index");
org.elasticsearch.search.builder.SearchSourceBuilder sourceBuilder = new org.elasticsearch.search.builder.SearchSourceBuilder();
// 禁用命中数据返回,仅返回聚合结果
sourceBuilder.size(0);
// 按 categories 字段聚合
sourceBuilder.aggregation(AggregationBuilders.terms("category_agg")
.field("categories.keyword") // 关键词字段聚合(不分词)
.size(10)); // 最多返回10个分类
searchRequest.source(sourceBuilder);
// 2. 执行查询
SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
// 3. 解析聚合结果
Terms categoryAgg = response.getAggregations().get("category_agg");
Map<String, Long> resultMap = new HashMap<>();
for (Terms.Bucket bucket : categoryAgg.getBuckets()) {
String category = bucket.getKeyAsString();
long count = bucket.getDocCount();
resultMap.put(category, count);
}
return resultMap;
}
}
六、避坑指南
坑点 1:中文分词效果差,检索不到数据
表现:输入中文关键词无法匹配结果,分词后为单个字;✅ 解决方案:确保安装 IK 分词器,实体类字段指定 analyzer = "ik_max_word",且分词器版本与 ES 一致。
坑点 2:索引字段类型错误,查询失败
表现:范围查询(价格、时间)报错,或精确匹配失效;✅ 解决方案:严格按数据类型定义字段(如价格用 Double,时间用 Date),关键词字段用 FieldType.Keyword,检索字段用 FieldType.Text。
坑点 3:高亮结果未替换原始数据
表现:查询返回结果中,关键词未高亮显示;✅ 解决方案:手动处理高亮结果,将 SearchHit 中的高亮字段替换到实体类对应属性中。
坑点 4:集群模式下数据不一致
表现:主分片与副本分片数据不同步,查询结果不稳定;✅ 解决方案:合理设置分片与副本数量,确保集群节点正常通信,避免单节点故障。
七、终极总结:ES 实战的核心是「索引设计与检索优化」
Elasticsearch 实战的核心并非 API 调用,而是「合理的索引设计」与「高效的检索语句优化」------ 索引设计决定了检索的灵活性与性能上限,检索优化决定了查询响应速度。企业级开发中,需结合业务场景平衡「检索效果」与「性能开销」。
核心原则总结:
- 索引设计优先:根据业务场景选择字段类型、分词器,避免过度分词或字段冗余;
- 检索语句精简:减少不必要的
should条件,合理使用过滤(filter)替代查询(query),过滤条件可缓存; - 集群优化适配:海量数据场景下,合理分配分片与副本,提升并发能力与可用性;
- 避免过度依赖:ES 适合全文检索与统计分析,核心业务数据仍需依赖数据库存储,ES 作为检索辅助。