尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot项目实战:手把手教你用Elasticsearch Java Client 8.x搞定全文搜索(附完整代码)

SpringBoot项目实战:手把手教你用Elasticsearch Java Client 8.x搞定全文搜索(附完整代码) SpringBoot与Elasticsearch 8.x深度整合构建高性能商品搜索系统实战在电商平台的核心功能中商品搜索系统的响应速度和准确性直接影响用户体验和转化率。传统数据库的LIKE查询在面对海量商品数据时往往力不从心这正是Elasticsearch这类专业搜索引擎大显身手的场景。本文将带你从零开始基于SpringBoot 3.x和Elasticsearch Java Client 8.x构建一个完整的商品搜索解决方案。1. 环境准备与基础配置1.1 依赖引入与版本选择首先确保你的SpringBoot项目使用的是3.x版本与Elasticsearch 8.x的Java客户端保持兼容。在pom.xml中添加以下关键依赖dependency groupIdco.elastic.clients/groupId artifactIdelasticsearch-java/artifactId version8.11.1/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency注意Elasticsearch 8.x默认启用安全配置需要同时配置SSL证书和基础认证。建议开发环境使用自签名证书生产环境务必使用正规CA签发的证书。1.2 客户端配置类实现创建ElasticsearchConfig配置类这里我们采用Builder模式配置客户端Configuration public class ElasticsearchConfig { Value(${spring.elasticsearch.uris}) private String[] hosts; Value(${spring.elasticsearch.username}) private String username; Value(${spring.elasticsearch.password}) private String password; Bean public ElasticsearchClient elasticsearchClient() throws SSLException { // 1. 创建低级客户端 RestClientBuilder builder RestClient.builder( Arrays.stream(hosts).map(HttpHost::create).toArray(HttpHost[]::new) ); // 2. 配置SSLContext SSLContext sslContext SSLContextBuilder .create() .loadTrustMaterial(null, (chain, authType) - true) // 开发环境简化处理 .build(); // 3. 配置认证信息 BasicCredentialsProvider credsProv new BasicCredentialsProvider(); credsProv.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials(username, password) ); // 4. 构建RestClient RestClient restClient builder .setHttpClientConfigCallback(hc - hc .setSSLContext(sslContext) .setDefaultCredentialsProvider(credsProv) ) .build(); // 5. 创建Transport层 ElasticsearchTransport transport new RestClientTransport( restClient, new JacksonJsonpMapper() ); return new ElasticsearchClient(transport); } }2. 商品数据建模与索引设计2.1 商品实体类映射商品搜索的核心在于合理的索引设计。我们先定义商品实体类并添加ES映射注解Data Document(indexName products, createIndex false) public class Product { Id private String id; Field(type FieldType.Text, analyzer ik_max_word) private String name; Field(type FieldType.Keyword) private String category; Field(type FieldType.Double) private Double price; Field(type FieldType.Integer) private Integer stock; Field(type FieldType.Date, format DateFormat.date_hour_minute_second) private Date createTime; Field(type FieldType.Nested) private ListSpecification specifications; Field(type FieldType.Object) private Brand brand; } Data public class Specification { Field(type FieldType.Keyword) private String key; Field(type FieldType.Text, analyzer ik_smart) private String value; } Data public class Brand { Field(type FieldType.Keyword) private String id; Field(type FieldType.Text) private String name; }2.2 索引初始化策略建议在应用启动时检查并创建索引确保映射正确Component RequiredArgsConstructor public class IndexInitializer { private final ElasticsearchClient client; PostConstruct public void init() throws IOException { if (!client.indices().exists(r - r.index(products)).value()) { CreateIndexResponse response client.indices().create(c - c .index(products) .settings(s - s .numberOfShards(3) .numberOfReplicas(1) ) .mappings(m - m .properties(name, p - p.text(t - t.analyzer(ik_max_word))) .properties(specifications, p - p.nested(n - n)) ) ); if (!response.acknowledged()) { throw new RuntimeException(商品索引创建失败); } } } }3. 核心搜索功能实现3.1 基础搜索服务封装创建ProductSearchService作为搜索功能入口Service RequiredArgsConstructor public class ProductSearchService { private final ElasticsearchClient client; public SearchResponseProduct searchProducts(ProductSearchRequest request) { try { return client.search(s - s .index(products) .query(q - buildQuery(request)) .from(request.getPage() * request.getSize()) .size(request.getSize()) .sort(sort - buildSort(request)), Product.class ); } catch (IOException e) { throw new SearchException(商品搜索失败, e); } } private Query buildQuery(ProductSearchRequest request) { ListQuery mustQueries new ArrayList(); // 关键词查询 if (StringUtils.isNotBlank(request.getKeyword())) { mustQueries.add(Query.of(q - q .multiMatch(m - m .query(request.getKeyword()) .fields(name^3, specifications.value) .type(TextQueryType.BestFields) ) )); } // 分类过滤 if (StringUtils.isNotBlank(request.getCategory())) { mustQueries.add(Query.of(q - q .term(t - t .field(category) .value(request.getCategory()) ) )); } // 价格区间 if (request.getMinPrice() ! null || request.getMaxPrice() ! null) { mustQueries.add(Query.of(q - q .range(r - { RangeQuery.Builder range new RangeQuery.Builder(); if (request.getMinPrice() ! null) { range.field(price).gte(JsonData.of(request.getMinPrice())); } if (request.getMaxPrice() ! null) { range.field(price).lte(JsonData.of(request.getMaxPrice())); } return range; }) )); } return Query.of(q - q.bool(b - b.must(mustQueries))); } private ListSortOptions buildSort(ProductSearchRequest request) { ListSortOptions sorts new ArrayList(); if (price_asc.equals(request.getSort())) { sorts.add(SortOptions.of(s - s.field(f - f .field(price).order(SortOrder.Asc) ))); } else if (price_desc.equals(request.getSort())) { sorts.add(SortOptions.of(s - s.field(f - f .field(price).order(SortOrder.Desc) ))); } else if (sales.equals(request.getSort())) { sorts.add(SortOptions.of(s - s.field(f - f .field(sales).order(SortOrder.Desc) ))); } else { // 默认按相关性排序 sorts.add(SortOptions.of(s - s.score(sb - sb .order(SortOrder.Desc) ))); } return sorts; } }3.2 高级搜索功能扩展3.2.1 聚合分析实现商品搜索系统通常需要提供分类统计、价格分布等聚合功能public AggregationResponse analyzeProducts(ProductAnalysisRequest request) { try { SearchResponseVoid response client.search(s - s .index(products) .size(0) .aggregations(category_agg, a - a .terms(t - t.field(category).size(10)) ) .aggregations(price_histogram, a - a .histogram(h - h .field(price) .interval(100.0) ) ), Void.class ); return new AggregationResponse( response.aggregations().get(category_agg).sterms().buckets().array() .stream() .collect(Collectors.toMap( b - b.key().stringValue(), b - b.docCount() )), response.aggregations().get(price_histogram).histogram().buckets().array() .stream() .collect(Collectors.toMap( b - String.valueOf(b.key()), b - b.docCount() )) ); } catch (IOException e) { throw new SearchException(商品分析失败, e); } }3.2.2 搜索建议实现基于Completion Suggester实现搜索词自动补全public ListString suggestKeywords(String prefix) { try { SearchResponseVoid response client.search(s - s .index(product_suggestions) .query(q - q .prefix(p - p .field(suggestion) .value(prefix) ) ) .source(sc - sc .filter(f - f .includes(suggestion) ) ) .size(5), Void.class ); return response.hits().hits().stream() .map(hit - (String) hit.source().get(suggestion)) .collect(Collectors.toList()); } catch (IOException e) { throw new SearchException(搜索建议获取失败, e); } }4. 性能优化与生产实践4.1 查询性能调优Elasticsearch查询性能受多种因素影响以下是一些关键优化点索引设计优化合理设置分片数建议每个分片大小在10-50GB冷热数据分离部署对不需要分词的字段使用keyword类型查询DSL优化使用filter代替query进行不计算相关度的过滤避免使用wildcard等开销大的查询合理使用index_prefixes提升前缀查询效率// 优化后的查询示例 Query optimizedQuery Query.of(q - q .bool(b - b .must(m - m .match(mt - mt .field(name) .query(手机) .operator(Operator.And) ) ) .filter(f - f .range(r - r .field(price) .gte(JsonData.of(1000)) .lte(JsonData.of(5000)) ) ) ) );4.2 高可用架构设计生产环境建议采用以下架构保证高可用集群部署至少3个master节点多个data节点读写分离为搜索和写入分别配置独立的客户端故障转移配置多个ES节点地址客户端自动重试监控告警通过Elasticsearch的监控API收集关键指标// 多节点客户端配置示例 Bean public ElasticsearchClient elasticsearchClient() { RestClientBuilder builder RestClient.builder( new HttpHost(es-node1, 9200, https), new HttpHost(es-node2, 9200, https), new HttpHost(es-node3, 9200, https) ); // 配置重试策略 builder.setFailureListener(new RestClient.FailureListener() { Override public void onFailure(Node node) { // 记录失败节点日志 } }); // ...其他配置 }4.3 数据同步策略保持数据库与Elasticsearch数据一致是实际项目中的难点推荐几种同步方案双写模式在业务代码中同时写入数据库和ESCDC模式通过Debezium等工具捕获数据库变更定时任务定期全量/增量同步数据消息队列通过MQ解耦数据同步过程// 使用Spring Data JPA事件监听实现双写 EntityListeners(ProductEntityListener.class) Entity public class Product { // JPA实体字段 } Component public class ProductEntityListener { private final ProductSearchRepository searchRepo; PostPersist PostUpdate public void onSave(Product product) { searchRepo.indexProduct(product); } PostRemove public void onDelete(Product product) { searchRepo.deleteProduct(product.getId()); } }5. 实战构建商品搜索API5.1 REST API设计基于Spring WebFlux设计响应式搜索APIRestController RequestMapping(/api/products) RequiredArgsConstructor public class ProductSearchController { private final ProductSearchService searchService; GetMapping(/search) public MonoPageResultProduct search( RequestParam(required false) String keyword, RequestParam(required false) String category, RequestParam(required false) Double minPrice, RequestParam(required false) Double maxPrice, RequestParam(defaultValue 0) int page, RequestParam(defaultValue 10) int size, RequestParam(required false) String sort ) { return Mono.fromCallable(() - searchService.searchProducts( new ProductSearchRequest(keyword, category, minPrice, maxPrice, page, size, sort) )).subscribeOn(Schedulers.boundedElastic()); } GetMapping(/suggest) public MonoListString suggest(RequestParam String prefix) { return Mono.fromCallable(() - searchService.suggestKeywords(prefix)) .subscribeOn(Schedulers.boundedElastic()); } }5.2 前端搜索交互优化结合Vue.js实现流畅的搜索体验// 搜索组件示例 export default { data() { return { keyword: , suggestions: [], products: [], loading: false } }, watch: { keyword(newVal) { if (newVal.length 2) { this.fetchSuggestions() } } }, methods: { async fetchSuggestions() { try { const res await axios.get(/api/products/suggest, { params: { prefix: this.keyword } }) this.suggestions res.data } catch (error) { console.error(获取搜索建议失败, error) } }, async searchProducts() { this.loading true try { const res await axios.get(/api/products/search, { params: { keyword: this.keyword } }) this.products res.data.items } finally { this.loading false } } } }5.3 压力测试与性能指标使用JMeter进行搜索API压测重点关注以下指标指标名称目标值测试工具平均响应时间200msJMeter99线响应时间500msGrafana吞吐量1000 QPSPrometheus错误率0.1%Elastic APM优化后的查询性能对比// 优化前查询 SearchResponseProduct response client.search(s - s .query(q - q.matchAll(m - m)), Product.class ); // 优化后查询 SearchResponseProduct response client.search(s - s .query(q - q .bool(b - b .filter(f - f .term(t - t.field(status).value(ON_SALE)) ) ) ) .source(sc - sc.filter(f - f .includes(id, name, price, image) )) .size(20), Product.class );在实际电商项目中经过优化的搜索接口可以将平均响应时间从350ms降低到120ms左右同时吞吐量提升3倍以上。关键在于合理使用filter缓存、控制返回字段数量以及避免深度分页等消耗性能的操作。
返回列表