SpringBoot与ElasticSearch实战:从零构建高效CRUD应用

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;
}

这里有几个实用技巧:

  1. @Document的indexName要小写,ES默认不支持大写索引名
  2. 中文搜索记得用ik分词器,需要提前在ES中安装
  3. 日期字段的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"));
}

查询优化也很重要。我常用的技巧包括:

  1. 合理使用filter代替query,filter结果会被缓存
  2. 限制返回字段,减少网络传输
  3. 使用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 BootElasticsearch
2.4.x7.9.x
2.5.x7.12.x
2.6.x7.15.x
2.7.x7.17.x

连接超时问题也很让人头疼。我现在的解决方案是:

spring:
  elasticsearch:
    rest:
      connection-timeout: 10s
      socket-timeout: 30s
      max-conn-total: 100
      max-conn-per-route: 10

映射异常经常发生在字段类型变更时。比如把String改成Integer,ES会直接报错。我的处理流程是:

  1. 创建新索引
  2. 使用reindex API迁移数据
  3. 别名切换
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());
}
源码直接下载地址: https://pan.quark.cn/s/d280357b18e5 在网页构建领域中,HTML5被视为当代网页工程的基础规范,其问世显著增强了页面的视觉表现力用户互动性。本工程致力于运用HTML5技术开发一个电视剧信息展示页面,目的是呈现诸如剧名、演员构成、故事梗概等电视剧关键资料。接下来将深入阐释如何借助HTML5的结构化组件和样式管理功能达成此项目目标。 我们必须掌握HTML5的核心框架。一个规范的HTML5文档一般包含`<!DOCTYPE html>`声明、`<html>`根标记、`<head>`头部标记和`<body>`主体标记。在头部区域,可以配置网页的基本元数据,例如字符集设定、页面标题等。在主体部分,将具体构建电视剧信息列表的内容。 电视剧展示页面通常包含多个条目,每个条目对应一部电视剧。HTML5中的`<section>`标记用于内容模块化,适合表示单个电视剧的详细信息区域。每个`<section>`内部,可使用`<h2>`标题标记显示剧名,`<img>`图像标记插入宣传剧照,`<p>`段落标记呈现剧情介绍,而`<ul>`无序列表`<li>`列表项标记则用于罗列演员阵容。 为了优化页面布局,需要借助CSS(层叠样式表)进行样式管理。HTML5引入了创新的CSS选择器布局模型,例如Flexbox和Grid,使页面布局更加灵活多变。在此场景下,可以利用Flexbox为电视剧信息列表实现自适应布局,保障在不同设备尺寸下均能呈现理想视觉效果。具体操作时,可将`<section>`标记设定为Flex容器,通过`display: flex;`属性,并运用`justify-content`和`align-items`属性调整子元素的对...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值