1. 环境准备与项目搭建
想要玩转SpringBoot和ElasticSearch的整合,首先得把环境准备妥当。我刚开始接触这个组合时,就被版本兼容问题坑过好几次,后来才发现这俩的版本匹配特别重要。就像手机和充电器的关系,不是随便拿个充电器就能用的。
先说说ElasticSearch的安装。你可以选择直接下载官方压缩包,也可以使用Docker快速部署。我推荐后者,特别是对于新手来说,一条命令就能搞定:
docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.17.15
接下来创建SpringBoot项目。我习惯用Spring Initializr(https://start.spring.io/)快速生成项目骨架。记得勾选Web和Elasticsearch两个依赖。不过这里有个坑要注意:SpringBoot和ElasticSearch的版本必须匹配。比如你用SpringBoot 2.7.x,对应的ElasticSearch应该是7.17.x。我在pom.xml里是这么配置的:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
配置文件application.yml也很关键。第一次配置时我忘了加端口号,结果死活连不上ES。正确的配置应该是:
spring:
elasticsearch:
uris: http://localhost:9200
connection-timeout: 10s
socket-timeout: 30s
2. 实体映射与索引创建
实体类映射是连接Java对象和ES文档的桥梁。刚开始我总搞混@Id和@Field注解的用法,后来才明白它们的区别。来看个商品实体的例子:
@Data
@Document(indexName = "products")
public class Product {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String name;
@Field(type = FieldType.Double)
private Double price;
@Field(type = FieldType.Date,
format = DateFormat.custom,
pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
这里有几个实用技巧:
- @Document的indexName要小写,ES默认不支持大写索引名
- 中文搜索记得用ik分词器,需要提前在ES中安装
- 日期字段的pattern要和实际数据格式一致,否则会解析失败
索引的自动创建是个很贴心的功能。第一次启动项目时,SpringData会根据实体类自动创建索引和映射。不过在生产环境,我建议还是手动创建索引,这样可以更精细地控制分片和副本数:
@Autowired
private ElasticsearchRestTemplate template;
public void createIndex() {
IndexOperations ops = template.indexOps(Product.class);
if(!ops.exists()){
ops.create();
ops.putMapping();
}
}
3. 基础CRUD操作
掌握了基础CRUD,就能应付大部分日常需求了。SpringData提供了两种操作方式:Repository和ElasticsearchTemplate。我更喜欢前者,因为写起来更简洁。
先定义一个Repository接口:
public interface ProductRepository extends
ElasticsearchRepository<Product, String> {
// 自定义查询方法
List<Product> findByName(String name);
@Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
Page<Product> searchByName(String name, Pageable pageable);
}
增删改查的示例代码:
// 新增/更新
Product product = new Product();
product.setId("1");
product.setName("iPhone 15");
product.setPrice(7999.00);
product.setCreateTime(new Date());
productRepository.save(product);
// 查询
Optional<Product> result = productRepository.findById("1");
List<Product> list = productRepository.findByName("iPhone");
// 删除
productRepository.deleteById("1");
这里有个小技巧:save方法既是新增也是更新。当ID存在时执行更新,不存在时执行新增。我遇到过批量插入性能问题,后来改用bulk操作才解决:
List<Product> products = new ArrayList<>();
// 添加多个product
productRepository.saveAll(products);
4. 高级查询与分页
基础CRUD满足不了复杂业务需求时,就需要用到高级查询了。BoolQueryBuilder是我的最爱,它能组合各种查询条件。
先看一个多条件查询的例子:
public Page<Product> searchProducts(String keyword, Double minPrice,
Double maxPrice, int page, int size) {
// 构建布尔查询
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
if(StringUtils.isNotBlank(keyword)){
boolQuery.must(QueryBuilders.matchQuery("name", keyword));
}
if(minPrice != null || maxPrice != null){
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
if(minPrice != null) rangeQuery.gte(minPrice);
if(maxPrice != null) rangeQuery.lte(maxPrice);
boolQuery.filter(rangeQuery);
}
// 分页和排序
PageRequest pageable = PageRequest.of(page, size,
Sort.by(Sort.Direction.DESC, "createTime"));
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(boolQuery)
.withPageable(pageable)
.build();
SearchHits<Product> hits = template.search(query, Product.class);
// 转换为Spring分页对象
List<Product> content = hits.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
return new PageImpl<>(content, pageable, hits.getTotalHits());
}
高亮显示能让搜索结果更醒目,配置起来也不复杂:
HighlightBuilder highlightBuilder = new HighlightBuilder()
.field("name")
.preTags("<em>")
.postTags("</em>");
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("name", keyword))
.withHighlightBuilder(highlightBuilder)
.build();
聚合查询适合做数据统计。比如统计各个价格区间的商品数量:
TermsAggregationBuilder aggregation = AggregationBuilders
.terms("price_ranges")
.field("price")
.subAggregation(AggregationBuilders
.range("range")
.addRange(0, 1000)
.addRange(1000, 5000)
.addRange(5000, 10000));
SearchQuery query = new NativeSearchQueryBuilder()
.addAggregation(aggregation)
.build();
SearchHits<Product> hits = template.search(query, Product.class);
5. 性能优化与实战技巧
在实际项目中,我总结了一些性能优化的经验。首先是索引设计,分片数要根据数据量来定。我一般用这个公式估算:
分片数 = 节点数 × 最大堆内存(GB) / 30
批量操作能显著提升性能。下面是个批量插入的优化方案:
@Autowired
private ElasticsearchRestTemplate template;
public void bulkInsert(List<Product> products) {
List<IndexQuery> queries = products.stream()
.map(product -> new IndexQueryBuilder()
.withId(product.getId())
.withObject(product)
.build())
.collect(Collectors.toList());
template.bulkIndex(queries, IndexCoordinates.of("products"));
}
查询优化也很重要。我常用的技巧包括:
- 合理使用filter代替query,filter结果会被缓存
- 限制返回字段,减少网络传输
- 使用scroll API处理大数据量查询
// 只返回必要字段
SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchAllQuery())
.withSourceFilter(new FetchSourceFilter(
new String[]{"id", "name"}, null))
.build();
最后说说监控。SpringBoot Actuator可以监控ES健康状态:
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always
访问/actuator/health就能看到ES的连接状态。遇到性能问题时,可以开启慢查询日志:
logging:
level:
org.elasticsearch.client.WIRE: trace
6. 常见问题排查
版本冲突是最常见的问题。有一次我的项目启动报错,折腾半天才发现是SpringBoot和ES版本不匹配。现在我会先查官方版本对照表:
| Spring Boot | Elasticsearch |
|---|---|
| 2.4.x | 7.9.x |
| 2.5.x | 7.12.x |
| 2.6.x | 7.15.x |
| 2.7.x | 7.17.x |
连接超时问题也很让人头疼。我现在的解决方案是:
spring:
elasticsearch:
rest:
connection-timeout: 10s
socket-timeout: 30s
max-conn-total: 100
max-conn-per-route: 10
映射异常经常发生在字段类型变更时。比如把String改成Integer,ES会直接报错。我的处理流程是:
- 创建新索引
- 使用reindex API迁移数据
- 别名切换
template.indexOps(Product.class).putMapping();
ReindexRequest request = new ReindexRequest()
.setSourceIndices("products_v1")
.setDestIndex("products_v2");
template.getClient().reindex(request, RequestOptions.DEFAULT);
7. 实际业务场景应用
最后分享几个实战中的典型应用场景。首先是搜索建议功能,可以用completion类型实现:
@Data
@Document(indexName = "products")
public class Product {
@CompletionField
private Completion suggest;
}
public void buildSuggestions() {
Product product = new Product();
product.setId("1");
product.setName("iPhone 15 Pro Max");
CompletionBuilder builder = new CompletionBuilder()
.input("iPhone 15 Pro Max")
.weight(10);
product.setSuggest(builder.build());
productRepository.save(product);
}
public List<String> getSuggestions(String prefix) {
CompletionSuggestionBuilder suggestion = SuggestBuilders
.completionSuggestion("suggest")
.prefix(prefix);
SearchRequest request = new SearchRequest("products")
.suggest(new SuggestBuilder().addSuggestion("product-suggest", suggestion));
SearchResponse response = template.getClient()
.search(request, RequestOptions.DEFAULT);
return response.getSuggest()
.getSuggestion("product-suggest")
.getEntries().get(0)
.getOptions().stream()
.map(CompletionSuggestion.Entry.Option::getText)
.collect(Collectors.toList());
}
另一个实用场景是日志分析。我们可以用Date Histogram聚合分析日志趋势:
@Document(indexName = "app_logs")
@Data
public class AppLog {
@Id
private String id;
@Field(type = FieldType.Text)
private String message;
@Field(type = FieldType.Date)
private Date timestamp;
@Field(type = FieldType.Keyword)
private String level;
}
public Map<String, Long> analyzeLogs(Date from, Date to) {
RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("timestamp")
.gte(from.getTime())
.lte(to.getTime());
DateHistogramAggregationBuilder aggregation = AggregationBuilders
.dateHistogram("log_count")
.field("timestamp")
.calendarInterval(DateHistogramInterval.DAY)
.format("yyyy-MM-dd");
SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(rangeQuery)
.addAggregation(aggregation)
.build();
SearchHits<AppLog> hits = template.search(query, AppLog.class);
ParsedDateHistogram histogram = hits.getAggregations().get("log_count");
return histogram.getBuckets().stream()
.collect(Collectors.toMap(
bucket -> bucket.getKeyAsString(),
bucket -> bucket.getDocCount()));
}
地理空间查询也很有意思。比如实现附近门店搜索:
@Data
@Document(indexName = "stores")
public class Store {
@Id
private String id;
@Field(type = FieldType.Text)
private String name;
@GeoPointField
private GeoPoint location;
}
public List<Store> findNearbyStores(double lat, double lon, double distance) {
GeoDistanceQueryBuilder query = QueryBuilders
.geoDistanceQuery("location")
.point(lat, lon)
.distance(distance, DistanceUnit.KILOMETERS);
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
.withFilter(query)
.build();
return template.search(searchQuery, Store.class)
.stream()
.map(SearchHit::getContent)
.collect(Collectors.toList());
}

601

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



