大家好,我是小悟。
一、ElasticSearch 是什么?
你有一个超级健忘的朋友(比如金鱼记忆的那种),但他却能在0.0001秒内从100万本书里找到你想要的句子。这就是 ElasticSearch(简称 ES)!
ES 的“人格特征”:
- 速度狂魔:搜索速度比咖啡因过量的程序员找Bug还快
- 文档收藏家:什么JSON、日志都能存
- 超级侦探:模糊搜索、精确搜索、拼音搜索样样精通
- 大象胃口:数据量再大也不怕(毕竟名字里就有“elastic”弹性)
- 马戏团团长:天生分布式,节点之间跳来跳去从不出错
二、整合大冒险:SpringBoot 与 ES 的“相亲大会”
第 1 步:先来个“相亲介绍人”(Maven 依赖)
<!-- pom.xml 里加入这些“红娘” --> <dependencies> <!-- SpringBoot 给 ES 的专属“情书” --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency> <!-- 防止程序说“我不会JSON” --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies>第 2 步:配置“约会地点”(application.yml)
# application.yml spring: elasticsearch: uris: http://localhost:9200 # ES 的“家庭地址” username: elastic # 用户名(默认是这个) password: your_password # 密码(安装时设置的) # 可选:让日志“多说点话”,方便调试 data: elasticsearch: repositories: enabled: true # 给 ES 客户端一点“咖啡因”,让它更精神 elasticsearch: connection-timeout: 5000 # 连接超时(毫秒) socket-timeout: 30000 # socket超时第 3 步:创建“相亲对象”(实体类)
import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; import java.util.Date; import java.util.List; @Data @Document(indexName = "book_index") // 告诉ES:“这是我家的书架名字” public class Book { @Id // 相当于书的“身份证号” private String id; @Field(type = FieldType.Text, analyzer = "ik_max_word") // 用中文分词器 private String title; // 书名 @Field(type = FieldType.Text, analyzer = "ik_smart") private String author; // 作者 @Field(type = FieldType.Double) private Double price; // 价格 @Field(type = FieldType.Date) private Date publishDate; // 出版日期 @Field(type = FieldType.Keyword) // 关键词,不分词 private String category; // 分类 @Field(type = FieldType.Nested) // 嵌套对象 private List<Tag> tags; @Data public static class Tag { @Field(type = FieldType.Keyword) private String name; @Field(type = FieldType.Integer) private Integer priority; } }第 4 步:找个“媒婆”(Repository 接口)
import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.elasticsearch.annotations.Query; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface BookRepository extends ElasticsearchRepository<Book, String> { // 方法名就是查询!Spring Data 的“魔法” // 1. 按作者精确查找(比找失散多年的兄弟还准) List<Book> findByAuthor(String author); // 2. 按标题模糊查找(支持分词) List<Book> findByTitleContaining(String keyword); // 3. 价格区间查找(找买得起的书) List<Book> findByPriceBetween(Double minPrice, Double maxPrice); // 4. 多条件查询(作者+分类) List<Book> findByAuthorAndCategory(String author, String category); // 5. 自定义查询(展示真正的技术) @Query("{\"bool\": {\"must\": [{\"match\": {\"title\": \"?0\"}}]}}") Page<Book> customSearch(String keyword, Pageable pageable); // 6. 统计某个作者有多少书 Long countByAuthor(String author); }第 5 步:写个“恋爱导师”(Service 层)
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; @Service public class BookService { @Autowired private BookRepository bookRepository; /** * 添加/更新一本书 * 如果书有id,就是更新;没有id,就是新增 */ public Book saveBook(Book book) { return bookRepository.save(book); } /** * 批量添加(ES最喜欢批量操作了,效率高) */ public void saveAllBooks(List<Book> books) { bookRepository.saveAll(books); } /** * 按ID查找(速度飞快) */ public Optional<Book> findById(String id) { return bookRepository.findById(id); } /** * 复杂搜索:按标题和作者搜索 */ public List<Book> searchBooks(String title, String author) { // 这里可以写更复杂的逻辑 if (title != null && author != null) { return bookRepository.findByTitleContainingAndAuthor(title, author); } else if (title != null) { return bookRepository.findByTitleContaining(title); } else { return bookRepository.findByAuthor(author); } } /** * 删除一本书(谨慎操作!) */ public void deleteBook(String id) { bookRepository.deleteById(id); } /** * 分页查询(大数据量的好朋友) */ public Page<Book> findAllBooks(Pageable pageable) { return bookRepository.findAll(pageable); } /** * 高级搜索:使用QueryBuilder */ public List<Book> advancedSearch(String keyword, Double minPrice, Double maxPrice) { // 使用NativeSearchQueryBuilder构建复杂查询 // 这里先省略,下面会有详细示例 return null; } }第 6 步:高级搜索的“秘密武器”
import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; import org.springframework.data.elasticsearch.core.SearchHits; import org.springframework.data.elasticsearch.core.query.NativeSearchQuery; import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder; import org.springframework.stereotype.Service; import java.util.List; import java.util.stream.Collectors; @Service public class AdvancedSearchService { @Autowired private ElasticsearchRestTemplate elasticsearchRestTemplate; /** * 多条件组合搜索(布尔查询) */ public List<Book> multiConditionSearch(String keyword, Double minPrice, Double maxPrice, String category) { // 1. 创建布尔查询构建器(相当于SQL的WHERE) BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); // 2. 添加必须条件(must = AND) if (keyword != null && !keyword.trim().isEmpty()) { boolQuery.must(QueryBuilders.multiMatchQuery(keyword, "title", "author") .analyzer("ik_max_word")); } // 3. 添加价格范围(range查询) if (minPrice != null || maxPrice != null) { var rangeQuery = QueryBuilders.rangeQuery("price"); if (minPrice != null) { rangeQuery.gte(minPrice); } if (maxPrice != null) { rangeQuery.lte(maxPrice); } boolQuery.must(rangeQuery); } // 4. 添加分类过滤(filter不计算分数,更快) if (category != null && !category.trim().isEmpty()) { boolQuery.filter(QueryBuilders.termQuery("category", category)); } // 5. 构建查询 NativeSearchQuery searchQuery = new NativeSearchQueryBuilder() .withQuery(boolQuery) .withSorts(org.springframework.data.elasticsearch.core.query.SortBuilders .fieldSort("price").order(org.springframework.data.domain.Sort.Direction.ASC)) .withPageable(org.springframework.data.domain.PageRequest.of(0, 10)) .build(); // 6. 执行查询 SearchHits<Book> searchHits = elasticsearchRestTemplate.search(searchQuery, Book.class); // 7. 转换结果 return searchHits.getSearchHits().stream() .map(hit -> hit.getContent()) .collect(Collectors.toList()); } /** * 聚合查询:统计每个分类有多少本书 */ public Map<String, Long> categoryStatistics() { NativeSearchQuery query = new NativeSearchQueryBuilder() .addAggregation(org.springframework.data.elasticsearch.core.query.aggregation.AggregationBuilders .terms("category_agg").field("category.keyword")) .build(); SearchHits<Book> searchHits = elasticsearchRestTemplate.search(query, Book.class); // 处理聚合结果(这里简化了,实际需要解析Aggregations) return new HashMap<>(); } }第 7 步:REST API 控制器(对外接口)
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Optional; @RestController @RequestMapping("/api/books") public class BookController { @Autowired private BookService bookService; @Autowired private AdvancedSearchService advancedSearchService; /** * 创建新书 */ @PostMapping public ResponseEntity<Book> createBook(@RequestBody Book book) { Book savedBook = bookService.saveBook(book); return ResponseEntity.ok(savedBook); } /** * 批量导入(适合初始化数据) */ @PostMapping("/batch") public ResponseEntity<String> batchImport(@RequestBody List<Book> books) { bookService.saveAllBooks(books); return ResponseEntity.ok("成功导入 " + books.size() + " 本书"); } /** * 搜索书籍(简单版) */ @GetMapping("/search") public ResponseEntity<List<Book>> searchBooks( @RequestParam(required = false) String title, @RequestParam(required = false) String author) { List<Book> books = bookService.searchBooks(title, author); return ResponseEntity.ok(books); } /** * 高级搜索(多条件) */ @GetMapping("/advanced-search") public ResponseEntity<List<Book>> advancedSearch( @RequestParam(required = false) String keyword, @RequestParam(required = false) Double minPrice, @RequestParam(required = false) Double maxPrice, @RequestParam(required = false) String category) { List<Book> books = advancedSearchService .multiConditionSearch(keyword, minPrice, maxPrice, category); return ResponseEntity.ok(books); } /** * 分页查询 */ @GetMapping("/page") public ResponseEntity<Page<Book>> getBooksByPage( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size) { Page<Book> books = bookService.findAllBooks(PageRequest.of(page, size)); return ResponseEntity.ok(books); } }第 8 步:配置类(锦上添花)
import org.elasticsearch.client.RestHighLevelClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.elasticsearch.client.ClientConfiguration; import org.springframework.data.elasticsearch.client.RestClients; import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate; import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories; @Configuration @EnableElasticsearchRepositories(basePackages = "com.yourpackage.repository") public class ElasticsearchConfig { @Bean public RestHighLevelClient elasticsearchClient() { ClientConfiguration clientConfiguration = ClientConfiguration.builder() .connectedTo("localhost:9200") .withBasicAuth("elastic", "your_password") .withConnectTimeout(5000) .withSocketTimeout(30000) .build(); return RestClients.create(clientConfiguration).rest(); } @Bean public ElasticsearchRestTemplate elasticsearchRestTemplate() { return new ElasticsearchRestTemplate(elasticsearchClient()); } }三、测试一下我们的“杰作”
测试数据(JSON格式)
POST /api/books/batch [ { "title": "SpringBoot从入门到放弃", "author": "程序猿老张", "price": 68.5, "category": "技术", "publishDate": "2023-01-01", "tags": [ {"name": "Java", "priority": 1}, {"name": "后端", "priority": 2} ] }, { "title": "ElasticSearch实战指南", "author": "搜索达人李", "price": 89.0, "category": "技术", "publishDate": "2023-02-15", "tags": [ {"name": "搜索", "priority": 1}, {"name": "大数据", "priority": 2} ] } ]搜索示例
GET /api/books/search?title=SpringBoot&author=程序猿老张 GET /api/books/advanced-search?keyword=实战&minPrice=50&maxPrice=100四、遇到的“坑”和解决方案
连接问题:确保ES服务已启动,检查端口9200
curl http://localhost:9200版本兼容:SpringBoot版本和ES版本要匹配
- SpringBoot 2.7.x → ES 7.17.x
- SpringBoot 3.x → ES 8.x
中文分词:安装IK分词器
# 进入ES容器/安装目录的plugins文件夹 ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.17.3/elasticsearch-analysis-ik-7.17.3.zip内存不足:调整JVM参数
# config/jvm.options -Xms1g -Xmx1g
五、总结:为什么选择这对“黄金搭档”?
SpringBoot + ES = 天作之合
优点大放送:
- 开发速度:SpringBoot的自动配置 + ES的简单API = 生产力翻倍
- 性能表现:ES的倒排索引 + 分布式架构 = 搜索如飞
- 扩展性:微服务架构中,ES可作为独立的搜索服务
- 生态完善:Spring Data Elasticsearch 封装了大多数常用操作
- 实时性:近实时搜索,数据一秒内可查
适用场景(ES大显身手的时候)
- 电商网站:商品搜索、筛选、排序
- 日志分析:ELK栈中的核心组件
- 内容平台:文章、新闻的全文检索
- 监控系统:实时数据分析
- 推荐系统:用户行为分析+相似度搜索
最后
- 不要滥用:简单的CRUD用MySQL,复杂搜索再用ES
- 数据同步:考虑使用Logstash或自定义同步机制
- 索引设计:合理的mapping设计是性能的关键
- 监控告警:用Kibana监控ES集群健康状态
SpringBoot整合ElasticSearch,就像给程序装上了“谷歌大脑”——存得多、找得快、查得准。虽然配置过程像在组装乐高,偶尔会找不到零件(版本兼容),但一旦搭建完成,你就能享受到“秒级搜索”的快感。
谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。
您的一键三连,是我更新的最大动力,谢谢
山水有相逢,来日皆可期,谢谢阅读,我们再会
我手中的金箍棒,上能通天,下能探海