当 MySQL 的 LIKE '%keyword%' 越来越慢,或者你需要做全文搜索、高亮展示、聚合统计时,Elasticsearch 就是标配方案了。
一、Elasticsearch 核心概念
| ES 概念 | 类比 MySQL | 说明 |
|---|---|---|
| Index(索引) | 数据库 | 存储同类文档的地方 |
| Type(类型) | 表 | 7.x 以后废弃,一个 Index 下面不再分 Type |
| Document(文档) | 一行记录 | JSON 格式的数据单元 |
| Field(字段) | 一列 | 文档中的字段 |
| Mapping(映射) | 表结构 | 定义字段类型和分析器 |
| Shard(分片) | 分表 | 数据水平拆分到多台机器 |
关键词: ES 的搜索快,靠的是倒排索引——把文档拆成词条,建立"词条→文档"的映射,查的时候直接定位,不走全表扫描。
二、SpringBoot 整合 ES
1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
2. 配置连接
spring:
elasticsearch:
uris: http://localhost:9200
connection-timeout: 3s
socket-timeout: 30s
3. 创建实体类
@Data
@Document(indexName = "products") // 对应 ES 索引名
public class ProductDocument {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title; // 商品标题(中文分词)
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String description; // 商品描述
@Field(type = FieldType.Keyword)
private String brand; // 品牌(不分词,精确匹配)
@Field(type = FieldType.Double)
private Double price; // 价格
@Field(type = FieldType.Integer)
private Integer stock; // 库存
@Field(type = FieldType.Date)
private Date createTime;
}
注意: analyzer = "ik_max_word" 是中文分词器,需要提前安装到 ES 中,否则用默认的 standard 分词器会把中文一个字一个字地分。
4. 编写 Repository
@Repository
public interface ProductRepository
extends ElasticsearchRepository<ProductDocument, Long> {
// 按标题搜索
List<ProductDocument> findByTitle(String title);
// 按标题模糊搜索
List<ProductDocument> findByTitleContaining(String keyword);
// 组合条件:标题包含 + 价格区间
List<ProductDocument> findByTitleContainingAndPriceBetween(
String keyword, Double min, Double max);
// 自定义查询(复杂查询用 @Query)
@Query("{\"match\": {\"description\": \"?0\"}}")
List<ProductDocument> searchByDescription(String keyword);
}
三、CRUD 实战
1. 批量导入数据(从 MySQL 同步到 ES)
@Service
public class ProductIndexService {
@Autowired
private ProductRepository productRepository;
@Autowired
private ProductMapper productMapper; // MyBatis-Plus 的 Mapper
/**
* 全量同步:把 MySQL 的商品数据导入到 ES
*/
public void fullSync() {
List<Product> products = productMapper.selectList(null);
List<ProductDocument> docs = products.stream()
.map(this::convertToDoc)
.collect(Collectors.toList());
productRepository.saveAll(docs);
System.out.println("全量同步完成,共 " + docs.size() + " 条");
}
/**
* 增量同步:单条新增或更新
*/
public void syncById(Long productId) {
Product product = productMapper.selectById(productId);
if (product != null) {
productRepository.save(convertToDoc(product));
}
}
/**
* 从 ES 删除
*/
public void deleteFromIndex(Long productId) {
productRepository.deleteById(productId);
}
private ProductDocument convertToDoc(Product product) {
ProductDocument doc = new ProductDocument();
BeanUtils.copyProperties(product, doc);
return doc;
}
}
2. 搜索接口
@RestController
@RequestMapping("/search")
public class SearchController {
@Autowired
private ProductRepository productRepository;
@Autowired
private ElasticsearchRestTemplate elasticsearchTemplate;
/**
* 简单搜索(使用 Repository 自带方法)
*/
@GetMapping("/simple")
public List<ProductDocument> simpleSearch(String keyword) {
return productRepository.findByTitleContaining(keyword);
}
/**
* 高级搜索(分页 + 高亮 + 排序)
*/
@GetMapping("/advanced")
public Page<ProductDocument> advancedSearch(
@RequestParam String keyword,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
// 构建查询条件
NativeQueryBuilder queryBuilder = new NativeQueryBuilder();
// 多字段匹配:标题和描述都搜
queryBuilder.withQuery(QueryBuilders
.multiMatchQuery(keyword, "title", "description"));
// 分页(ES 页码从 0 开始)
queryBuilder.withPageable(PageRequest.of(page - 1, size));
// 高亮设置
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title").preTags("<em>").postTags("</em>");
highlightBuilder.field("description").preTags("<em>").postTags("</em>");
queryBuilder.withHighlightBuilder(highlightBuilder);
// 排序(按价格升序)
queryBuilder.withSort(Sort.by(Sort.Direction.ASC, "price"));
NativeQuery query = queryBuilder.build();
SearchHits<ProductDocument> searchHits =
elasticsearchTemplate.search(query, ProductDocument.class);
// 处理高亮结果
List<ProductDocument> products = searchHits.stream().map(hit -> {
ProductDocument doc = hit.getContent();
Map<String, List<String>> highlights = hit.getHighlightFields();
// 如果有高亮,替换原标题
if (highlights.containsKey("title")) {
doc.setTitle(highlights.get("title").get(0));
}
if (highlights.containsKey("description")) {
doc.setDescription(highlights.get("description").get(0));
}
return doc;
}).collect(Collectors.toList());
return new PageImpl<>(products);
}
/**
* 聚合搜索(按品牌统计)
*/
@GetMapping("/aggregate")
public List<BrandCount> aggregateByBrand() {
NativeQuery query = NativeQuery.builder()
.withAggregation("brands", AggregationBuilders
.terms("brands").field("brand").size(10))
.build();
SearchHits<ProductDocument> hits =
elasticsearchTemplate.search(query, ProductDocument.class);
// 解析聚合结果
ElasticsearchAggregations aggregations =
(ElasticsearchAggregations) hits.getAggregations();
ParsedStringTerms terms =
aggregations.get("brands");
return terms.getBuckets().stream()
.map(bucket -> new BrandCount(
bucket.getKey().toString(),
bucket.getDocCount()))
.collect(Collectors.toList());
}
}
四、ES 和 MySQL 的双写问题
ES 不是主数据库,它只是搜索引擎。正确的架构是:
写入:MySQL(主) → 同步到 ES(从)
搜索:直接查 ES,速度快
同步方式有三种:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 业务代码双写 | 简单 | 耦合高,容易漏 |
| MQ 异步同步 👍 | 解耦,可靠 | 多一个 MQ 组件 |
| logstash 定时同步 | 不写代码 | 有秒级延迟 |
推荐用 MQ:
// 商品服务:修改商品后发送消息
@PostMapping("/product/update")
public ResultVO<?> updateProduct(@RequestBody Product product) {
productService.updateById(product);
// 发送消息通知 ES 同步
rabbitTemplate.convertAndSend("product.exchange", "product.sync", product.getId());
return ResultVO.success();
}
// 搜索服务:监听消息,同步到 ES
@RabbitListener(queues = "product.sync.queue")
public void handleProductSync(Long productId) {
productIndexService.syncById(productId);
}
五、SpringBoot 版本适配
不同 SpringBoot 版本对应的 ES 客户端不一样:
| SpringBoot 版本 | 推荐的 ES 客户端 |
|---|---|
| 2.3.x | TransportClient(已弃用) |
| 2.4.x - 2.7.x | ElasticsearchRestTemplate |
| 3.x | 最新的 Java Client |
你现在用 SpringBoot 2.7,就用 RestTemplate 方式,上面给的代码都是兼容的。
六、ES 实际开发注意事项
- ES 不是银弹——几十万条数据用 MySQL 的 LIKE 也够用,别什么都往 ES 里扔
- 中文搜索必须装 IK 分词器,否则一个字一个字地搜,体验极差
- ES 不擅长关联查询(比如"查订单 + 商品名 + 用户地址"),关联用 MySQL 做
- ES 写性能不如 MySQL,不适合高频写入场景(比如日志除外)
- 分页别太深:
from + size超过 10000 会慢,深度分页用search_after
💡 觉得有用的话,点赞 + 关注【张老师技术栈】吧!每周更新 Java/Python/爬虫 实战干货,不让你白来。
5829

被折叠的 条评论
为什么被折叠?



