Elasticsearch 支持检索能力完全指南
一、Elasticsearch 是什么
Elasticsearch(简称 ES)是一个基于 Apache Lucene 构建的分布式全文搜索和分析引擎。它的核心能力:
- 全文检索:对文本内容进行分词、倒排索引,支持模糊搜索、相关性排序
- 结构化查询:精确匹配、范围查询、聚合统计
- 近实时(NRT):数据写入后约 1 秒即可被搜索到
- 分布式:自动分片和副本,水平扩展
二、核心概念
| 概念 | 类比 MySQL | 说明 |
|---|---|---|
| Index(索引) | Database | 一类文档的集合 |
| Document(文档) | Row | 一条数据,JSON 格式 |
| Field(字段) | Column | 文档中的一个属性 |
| Mapping(映射) | Schema | 定义字段类型和分析规则 |
| Shard(分片) | Partition | 数据水平拆分单元 |
| Replica(副本) | Slave | 分片的冗余备份 |
倒排索引原理
文档1: "Java Spring Boot 微服务开发"
文档2: "Spring Cloud 微服务架构"
文档3: "Java 并发编程实战"
倒排索引:
"Java" → [文档1, 文档3]
"Spring" → [文档1, 文档2]
"微服务" → [文档1, 文档2]
"Boot" → [文档1]
"并发" → [文档3]
搜索"Java 微服务"时,取交集或按相关性排序返回结果。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
三、Spring Boot 集成 Elasticsearch
3.1 依赖引入
Spring Boot 3.x + Elasticsearch 8.x:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
Spring Boot 2.x + Elasticsearch 7.x:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- 如需使用 RestHighLevelClient -->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.17.10</version>
</dependency>
3.2 配置
yaml
# application.yml
spring:
elasticsearch:
uris: http://localhost:9200
username: elastic
password: your-password
connection-timeout: 5s
socket-timeout: 30s
3.3 配置类(Spring Boot 3.x + Elasticsearch Java Client)
java
@Configuration
public class ElasticsearchConfig extends ElasticsearchConfiguration {
@Override
public ClientConfiguration clientConfiguration() {
return ClientConfiguration.builder()
.connectedTo("localhost:9200")
.withBasicAuth("elastic", "your-password")
.withConnectTimeout(Duration.ofSeconds(5))
.withSocketTimeout(Duration.ofSeconds(30))
.build();
}
}
四、实体映射(Document Mapping)
java
/**
* 商品搜索文档.
*
*/
@Data
@Document(indexName = "product")
@Setting(shards = 3, replicas = 1)
public class ProductDocument {
@Id
private String id;
/**
* 商品名称 - 使用 ik_max_word 分词器支持中文全文搜索
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String productName;
/**
* 商品描述
*/
@Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart")
private String description;
/**
* 品牌 - keyword 类型,精确匹配
*/
@Field(type = FieldType.Keyword)
private String brand;
/**
* 分类ID - keyword 精确匹配 + 聚合
*/
@Field(type = FieldType.Keyword)
private String categoryId;
/**
* 价格 - 数值类型,支持范围查询
*/
@Field(type = FieldType.Double)
private Double price;
/**
* 销量 - 用于排序
*/
@Field(type = FieldType.Integer)
private Integer salesCount;
/**
* 上架状态
*/
@Field(type = FieldType.Boolean)
private Boolean onSale;
/**
* 标签 - 支持多值精确匹配
*/
@Field(type = FieldType.Keyword)
private List<String> tags;
/**
* 创建时间
*/
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
private LocalDateTime createTime;
/**
* 更新时间
*/
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
private LocalDateTime updateTime;
}
字段类型选择指南
| 字段类型 | 适用场景 | 是否分词 | 是否支持聚合 |
|---|---|---|---|
Text |
全文搜索(名称、描述) | ✅ | ❌(需配置 fielddata) |
Keyword |
精确匹配、过滤、排序、聚合 | ❌ | ✅ |
Integer/Long/Double |
数值范围查询、排序 | ❌ | ✅ |
Date |
时间范围查询 | ❌ | ✅ |
Boolean |
布尔过滤 | ❌ | ✅ |
Nested |
嵌套对象精确匹配 | 取决于子字段 | ✅ |
五、数据访问层(Repository)
5.1 基础 Repository
java
/**
* 商品搜索 Repository.
*/
public interface ProductSearchRepository extends ElasticsearchRepository<ProductDocument, String> {
/**
* 按品牌查询
*/
List<ProductDocument> findByBrand(String brand);
/**
* 按分类和上架状态查询
*/
List<ProductDocument> findByCategoryIdAndOnSale(String categoryId, Boolean onSale);
/**
* 按价格范围查询
*/
List<ProductDocument> findByPriceBetween(Double minPrice, Double maxPrice);
/**
* 按商品名称模糊搜索
*/
List<ProductDocument> findByProductNameContaining(String keyword);
/**
* 按销量降序排列
*/
List<ProductDocument> findByOnSaleTrueOrderBySalesCountDesc();
}
5.2 自定义复杂查询(Service 层)
java
/**
* 商品搜索 Service.
*/
public interface ProductSearchService {
/**
* 综合搜索(关键词 + 过滤 + 排序 + 分页)
*/
SearchResultDto<ProductDocument> search(ProductSearchParamDto param);
/**
* 搜索建议(自动补全)
*/
List<String> suggest(String prefix);
/**
* 聚合统计(按品牌/分类统计数量)
*/
Map<String, Long> aggregateByField(String fieldName);
}
/**
* 商品搜索 Service 实现.
*
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductSearchServiceImpl implements ProductSearchService {
private final ElasticsearchOperations elasticsearchOperations;
@Override
public SearchResultDto<ProductDocument> search(ProductSearchParamDto param) {
// 1. 构建布尔查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 2. 关键词搜索(多字段匹配)
if (StringUtils.isNotBlank(param.getKeyword())) {
boolQuery.must(QueryBuilders.multiMatchQuery(param.getKeyword(),
"productName", "description")
.type(MultiMatchQueryBuilder.Type.BEST_FIELDS)
.fuzziness(Fuzziness.AUTO)
.minimumShouldMatch("75%"));
}
// 3. 过滤条件(不参与打分,可缓存)
if (StringUtils.isNotBlank(param.getBrand())) {
boolQuery.filter(QueryBuilders.termQuery("brand", param.getBrand()));
}
if (StringUtils.isNotBlank(param.getCategoryId())) {
boolQuery.filter(QueryBuilders.termQuery("categoryId", param.getCategoryId()));
}
if (param.getMinPrice() != null || param.getMaxPrice() != null) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if (param.getMinPrice() != null) {
rangeQuery.gte(param.getMinPrice());
}
if (param.getMaxPrice() != null) {
rangeQuery.lte(param.getMaxPrice());
}
boolQuery.filter(rangeQuery);
}
// 只查上架商品
boolQuery.filter(QueryBuilders.termQuery("onSale", true));
// 4. 构建搜索请求
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.withPageable(PageRequest.of(param.getPageNo() - 1, param.getPageSize()));
// 5. 排序
if ("price_asc".equals(param.getSortBy())) {
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.ASC));
} else if ("price_desc".equals(param.getSortBy())) {
queryBuilder.withSort(SortBuilders.fieldSort("price").order(SortOrder.DESC));
} else if ("sales".equals(param.getSortBy())) {
queryBuilder.withSort(SortBuilders.fieldSort("salesCount").order(SortOrder.DESC));
} else {
// 默认按相关性排序
queryBuilder.withSort(SortBuilders.scoreSort().order(SortOrder.DESC));
}
// 6. 高亮
queryBuilder.withHighlightBuilder(new HighlightBuilder()
.field("productName")
.field("description")
.preTags("<em>")
.postTags("</em>"));
// 7. 执行搜索
NativeSearchQuery searchQuery = queryBuilder.build();
SearchHits<ProductDocument> searchHits = elasticsearchOperations.search(
searchQuery, ProductDocument.class);
// 8. 组装结果
List<ProductDocument> results = searchHits.getSearchHits().stream()
.map(hit -> {
ProductDocument doc = hit.getContent();
// 处理高亮
Map<String, List<String>> highlightFields = hit.getHighlightFields();
if (highlightFields.containsKey("productName")) {
doc.setProductName(highlightFields.get("productName").get(0));
}
return doc;
})
.collect(Collectors.toList());
SearchResultDto<ProductDocument> resultDto = new SearchResultDto<>();
resultDto.setList(results);
resultDto.setTotal(searchHits.getTotalHits());
resultDto.setPageNo(param.getPageNo());
resultDto.setPageSize(param.getPageSize());
return resultDto;
}
@Override
public List<String> suggest(String prefix) {
// 前缀匹配建议
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchPhrasePrefixQuery("productName", prefix)
.maxExpansions(10))
.withPageable(PageRequest.of(0, 10))
.build();
SearchHits<ProductDocument> hits = elasticsearchOperations.search(
query, ProductDocument.class);
return hits.getSearchHits().stream()
.map(hit -> hit.getContent().getProductName())
.collect(Collectors.toList());
}
@Override
public Map<String, Long> aggregateByField(String fieldName) {
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchAllQuery())
.withAggregations(AggregationBuilders.terms("agg_" + fieldName)
.field(fieldName)
.size(50))
.withMaxResults(0) // 不需要文档,只要聚合结果
.build();
SearchHits<ProductDocument> hits = elasticsearchOperations.search(
query, ProductDocument.class);
// 解析聚合结果
Map<String, Long> result = new LinkedHashMap<>();
// 根据实际返回的聚合类型解析
// ...
return result;
}
}
六、搜索请求/响应 DTO
java
/**
* 商品搜索请求参数.
*/
@Data
public class ProductSearchParamDto {
/** 搜索关键词 */
private String keyword;
/** 品牌过滤 */
private String brand;
/** 分类过滤 */
private String categoryId;
/** 最低价格 */
private Double minPrice;
/** 最高价格 */
private Double maxPrice;
/** 标签过滤 */
private List<String> tags;
/** 排序方式:price_asc / price_desc / sales / relevance */
private String sortBy;
/** 页码(从1开始) */
@Min(1)
private Integer pageNo = 1;
/** 每页数量 */
@Min(1)
@Max(100)
private Integer pageSize = 20;
}
/**
* 搜索结果通用包装.
*/
@Data
public class SearchResultDto<T> {
/** 结果列表 */
private List<T> list;
/** 总命中数 */
private Long total;
/** 当前页码 */
private Integer pageNo;
/** 每页数量 */
private Integer pageSize;
/** 聚合结果(可选) */
private Map<String, Object> aggregations;
}
七、数据同步策略
ES 不是主数据库,需要将 MySQL 中的数据同步到 ES。常见方案:
方案对比
| 方案 | 实时性 | 一致性 | 侵入性 | 适用场景 |
|---|---|---|---|---|
| 双写(代码同步) | 高 | 弱(可能不一致) | 高 | 简单业务 |
| MQ 异步同步 | 秒级 | 最终一致 | 中 | 推荐方案 |
| Binlog 监听 | 秒级 | 最终一致 | 低 | 大规模数据 |
| 定时全量刷新 | 分钟级 | 弱 | 低 | 对实时性要求不高 |
MQ 异步同步示例(推荐)
java
/**
* 商品变更事件监听器 - 同步数据到 ES.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ProductSyncConsumer {
private final ProductSearchRepository productSearchRepository;
private final ProductMapper productMapper;
@KafkaListener(topics = "product-change-topic", groupId = "es-sync-group")
public void onProductChange(String message) {
ProductChangeEvent event = JSON.parseObject(message, ProductChangeEvent.class);
log.info("收到商品变更事件, productId: {}, action: {}", event.getProductId(), event.getAction());
try {
switch (event.getAction()) {
case "CREATE":
case "UPDATE":
syncToEs(event.getProductId());
break;
case "DELETE":
productSearchRepository.deleteById(event.getProductId());
break;
default:
log.warn("未知的变更动作: {}", event.getAction());
}
} catch (Exception e) {
log.error("同步商品到ES失败, productId: {}", event.getProductId(), e);
// 可发送到死信队列,后续补偿
throw e;
}
}
private void syncToEs(String productId) {
// 从 MySQL 查询最新数据
ProductEntity entity = productMapper.selectById(productId);
if (entity == null) {
productSearchRepository.deleteById(productId);
return;
}
// 转换为 ES 文档并写入
ProductDocument document = convertToDocument(entity);
productSearchRepository.save(document);
log.info("商品同步到ES完成, productId: {}", productId);
}
private ProductDocument convertToDocument(ProductEntity entity) {
ProductDocument doc = new ProductDocument();
doc.setId(entity.getId());
doc.setProductName(entity.getName());
doc.setDescription(entity.getDescription());
doc.setBrand(entity.getBrand());
doc.setCategoryId(entity.getCategoryId());
doc.setPrice(entity.getPrice().doubleValue());
doc.setSalesCount(entity.getSalesCount());
doc.setOnSale(entity.getStatus() == 1);
doc.setCreateTime(entity.getCreateTime());
doc.setUpdateTime(entity.getUpdateTime());
return doc;
}
}
八、索引管理
8.1 索引创建(JSON Mapping)
json
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"ik_smart_pinyin": {
"type": "custom",
"tokenizer": "ik_smart",
"filter": ["lowercase", "pinyin_filter"]
}
},
"filter": {
"pinyin_filter": {
"type": "pinyin",
"keep_full_pinyin": false,
"keep_joined_full_pinyin": true,
"keep_original": true,
"limit_first_letter_length": 16
}
}
}
},
"mappings": {
"properties": {
"productName": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"pinyin": {
"type": "text",
"analyzer": "ik_smart_pinyin"
},
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"description": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"brand": { "type": "keyword" },
"categoryId": { "type": "keyword" },
"price": { "type": "double" },
"salesCount": { "type": "integer" },
"onSale": { "type": "boolean" },
"tags": { "type": "keyword" },
"createTime": { "type": "date", "format": "yyyy-MM-dd'T'HH:mm:ss" }
}
}
}
8.2 索引别名(零停机重建)
bash
# 创建新索引
PUT /product_v2
# 数据迁移
POST /_reindex
{
"source": { "index": "product_v1" },
"dest": { "index": "product_v2" }
}
# 切换别名(原子操作)
POST /_aliases
{
"actions": [
{ "remove": { "index": "product_v1", "alias": "product" } },
{ "add": { "index": "product_v2", "alias": "product" } }
]
}
代码中始终使用别名 product 而非具体版本号,重建索引时业务无感知。
九、中文分词
IK 分词器
ES 中文搜索必备插件,提供两种分词模式:
| 模式 | 说明 | "中华人民共和国" 的分词结果 |
|---|---|---|
ik_max_word |
最细粒度分词(索引时用) | 中华人民共和国、中华人民、中华、华人、人民共和国、人民、共和国、共和、国 |
ik_smart |
最粗粒度分词(搜索时用) | 中华人民共和国 |
安装
bash
# 版本必须与 ES 版本一致
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.11.0/elasticsearch-analysis-ik-8.11.0.zip
自定义词典
# config/analysis-ik/custom.dic
春风得意
人工智能
大数据
微服务
十、性能优化
写入优化
| 策略 | 说明 |
|---|---|
| 批量写入 | 使用 _bulk API,单次 5-15MB |
| 调整刷新间隔 | refresh_interval: 30s(默认1s) |
| 关闭副本后批量导入 | 大量导入时临时设置 replicas: 0 |
| 合理设置分片数 | 单个分片 10-50GB,过多分片影响性能 |
查询优化
| 策略 | 说明 |
|---|---|
| 使用 filter 而非 query | filter 不计算得分,可被缓存 |
| 避免深度分页 | 用 search_after 替代 from + size > 10000 |
| 限制返回字段 | 使用 _source: ["field1", "field2"] |
| 预热缓存 | 高频查询使用 preference: _local |
| 合理使用 keyword | 不需要分词的字段用 keyword 类型 |
深度分页示例(search_after)
java
/**
* 基于 search_after 的深度分页.
*/
public List<ProductDocument> scrollSearch(Object[] searchAfterValues, int size) {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchAllQuery())
.withSort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
.withSort(SortBuilders.fieldSort("id").order(SortOrder.ASC))
.withPageable(PageRequest.of(0, size));
if (searchAfterValues != null) {
queryBuilder.withSearchAfter(List.of(searchAfterValues));
}
SearchHits<ProductDocument> hits = elasticsearchOperations.search(
queryBuilder.build(), ProductDocument.class);
return hits.getSearchHits().stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}
十一、监控与运维
常用运维命令
bash
# 集群健康状态
GET /_cluster/health
# 索引状态
GET /_cat/indices?v&s=store.size:desc
# 查看分片分配
GET /_cat/shards/product?v
# 查看慢查询日志
PUT /product/_settings
{
"index.search.slowlog.threshold.query.warn": "5s",
"index.search.slowlog.threshold.query.info": "2s"
}
# 强制合并段(低峰期执行)
POST /product/_forcemerge?max_num_segments=1
Spring Boot Actuator 集成
yaml
management:
health:
elasticsearch:
enabled: true
endpoint:
health:
show-details: always
十二、常见问题与解决
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 中文搜索无结果 | 未安装 IK 分词器或分析器配置错误 | 安装 IK 插件,确认 mapping 中使用 ik_max_word |
| 搜索结果不准确 | minimum_should_match 过低 | 调整为 75% 以上 |
| 写入后搜不到 | refresh_interval 过长 | 调整为 1s 或手动 refresh |
| 深度分页超时 | from + size > 10000 | 使用 search_after 或 scroll API |
| 聚合结果不全 | terms 聚合默认只返回 10 个 | 设置 size: 50 或更大 |
| 内存溢出 | fielddata 加载了 text 字段 | text 字段不做聚合,使用 keyword 子字段 |
| 集群 Yellow/Red | 副本无法分配 | 检查节点数是否 >= 副本数 + 1 |
十三、项目中的最佳实践总结
| 实践 | 建议 |
|---|---|
| 索引命名 | 使用别名 + 版本号:product_v1 → 别名 product |
| 分词策略 | 索引用 ik_max_word,搜索用 ik_smart |
| 字段设计 | 需要搜索的用 Text,需要过滤/聚合的用 Keyword,两者都需要的用 multi-field |
| 数据同步 | 优先 MQ 异步同步,保证最终一致性 |
| 分页 | 浅分页用 from/size,深度翻页用 search_after |
| 容错 | ES 查询失败时降级到 MySQL 兜底查询 |
| 监控 | 接入 Actuator 健康检查 + 慢查询日志 |
| 索引重建 | 使用别名切换实现零停机 |