Spring Boot集成Elasticsearch实战:从模糊查询到高性能搜索服务

发布时间:2026/8/14 11:04:48
Spring Boot集成Elasticsearch实战:从模糊查询到高性能搜索服务 1. 从“搜不到”到“搜得准”为什么你的项目需要一个真正的搜索引擎我见过太多项目初期用数据库的LIKE语句对付搜索需求开发快上线也快。但随着数据量从几千条涨到几十万、几百万条问题就全暴露出来了。用户抱怨“搜不到东西”明明标题里有这个词就是不出来或者搜“苹果手机”出来一堆“苹果”、“手机”的无关结果相关性一塌糊涂。后台的数据库CPU更是常年飙高一个模糊查询就能拖慢整个应用。这时候团队才会开始认真考虑引入一个专业的全文搜索引擎而ElasticsearchES几乎是这个领域绕不开的名字。ES 不是一个简单的数据库替代品它是一个分布式的、基于 Lucene 的搜索和分析引擎。它的核心价值在于两点极致的搜索体验和强大的数据分析能力。对于搜索它解决了LIKE语句的几大痛点首先是分词ES 能理解“苹果手机”是一个整体也能拆开搜索其次是相关性评分它会根据词频、权重等因素给结果排序把最相关的结果排在最前面最后是性能其倒排索引结构让它在海量数据中查找关键词的速度是毫秒级的。至于分析你可以把它当成一个实时的大数据分析平台做聚合统计、可视化报表这是很多项目后期挖掘数据价值的利器。在 Java 生态里尤其是 Spring Boot 成为事实上的开发标准后集成 ES 变得异常顺畅。你不用再手动处理复杂的 HTTP 客户端、连接池和序列化Spring Data Elasticsearch 模块提供了一套近乎声明式的 Repository 接口让操作 ES 像操作 JPA 一样简单。但这恰恰也是新手容易踩坑的地方——因为太“简单”了很多人忽略了 ES 本身复杂的数据模型、映射机制和查询语法导致项目后期性能不佳、数据不一致。所以这篇内容不是一份简单的“整合教程”。我会带你从零开始彻底搞懂 ES 的核心概念和基本语法然后无缝衔接到 Spring Boot 中的实战。你会明白每一步配置背后的原因知道常见的坑在哪里最终搭建一个既跑得起来也跑得稳的搜索服务。无论你是正在被模糊查询性能折磨的开发者还是计划在新项目中引入搜索功能这篇内容都能给你一套可直接落地的方案。2. 理解 Elasticsearch核心概念与数据模型拆解在动手写代码之前我们必须先统一“语言”。ES 有自己的一套术语如果直接用关系型数据库的概念去套后面会处处碰壁。2.1 核心概念类比ES vs. 关系型数据库虽然不提倡生硬类比但初期建立一个粗略的映射关系有助于理解索引 (Index)相当于数据库中的库 (Database)。它是一个逻辑命名空间用于存储一组具有相似特征的文档。例如你可以有一个product_index用于存储商品一个article_index用于存储文章。类型 (Type)在 7.x 版本之后这个概念已经被废弃。在早期版本中它类似于数据库中的表 (Table)。现在一个索引通常只包含一种类型的文档。这简化了数据模型避免了以前因类型引起的映射冲突问题。文档 (Document)相当于表中的一行记录 (Row)。它是 ES 中可被索引的基本信息单元以 JSON 格式表示。一个文档代表一个具体的实体比如一件具体的商品。字段 (Field)相当于表中的列 (Column)。文档由多个字段组成比如商品的title,price,description。映射 (Mapping)相当于表的结构定义 (Schema)。它定义了索引中的文档包含哪些字段每个字段的数据类型如text,keyword,integer,date以及如何被索引如是否分词、用什么分词器。这是 ES 中最关键也最容易出问题的部分之一。注意千万不要认为 ES 是另一个 MySQL。它的“索引”和“映射”机制是为了快速检索而设计的与关系型数据库为事务一致性和关联查询的设计初衷有本质区别。试图在 ES 里做复杂的事务或多表关联会非常痛苦。2.2 倒排索引ES 为何这么快这是 ES 性能的基石。我们通过一个简单的例子来理解。假设我们有三个文档{“id”: 1, “content”: “Elasticsearch is powerful”}{“id”: 2, “content”: “SpringBoot is cool”}{“id”: 3, “content”: “Elasticsearch with SpringBoot is great”}关系型数据库是“正排”的按文档 ID 存储内容。而 ES 会构建一个“倒排索引”关键词 (Term)文档 ID 列表 (Posting List)elasticsearch[1, 3]springboot[2, 3]powerful[1]cool[2]great[3]is[1, 2, 3]with[3]当用户搜索 “Elasticsearch” 时ES 直接去这个“词典”里找到 “elasticsearch”立刻拿到文档 ID 列表[1, 3]然后返回这两个文档。这个过程是 O(1) 或 O(log n) 的复杂度与文档总数无关只与包含该词的文档数量有关因此速度极快。2.3 分词与分析器让搜索更智能“倒排索引”的前提是需要把文本拆分成一个个独立的“词条”(Term)。这个过程就叫分词 (Analysis)。负责分词的组件就是分析器 (Analyzer)。一个标准分析器 (standard analyzer) 对文本 “Elasticsearch is powerful!” 的处理流程如下字符过滤器 (Character Filters)处理原始文本如去除 HTML 标签。这里没有变化。分词器 (Tokenizer)按规则切分文本。标准分词器按空格和标点切分得到[“Elasticsearch”, “is”, “powerful”]。词元过滤器 (Token Filters)对切分后的词条再加工。如转小写得到[“elasticsearch”, “is”, “powerful”]或移除停用词如 “is”, “the”, “a” 等无实际意义的词得到[“elasticsearch”, “powerful”]。为什么这很重要如果你用默认分析器索引了“苹果手机”它会被分成[“苹”, “果”, “手”, “机”]四个单字。用户搜索“苹果”时能匹配到但搜索“手机”时所有包含“手”和“机”的文档都可能被搜出来噪音极大。这就需要我们为中文选择专门的分词器如 IK 分词器它能将“苹果手机”正确地分成[“苹果”, “手机”]两个词条。理解这些概念后我们才能正确地创建索引、定义映射并为后续的查询打下坚实基础。很多搜索不准的问题根子都出在分词阶段。3. Elasticsearch 基本语法实战从索引管理到复杂查询现在我们暂时离开 Spring Boot通过 ES 原生的 RESTful API使用 Kibana Dev Tools 或 curl 命令来直观感受其语法。这是理解其工作原理的最佳方式。3.1 索引与映射管理打好数据地基创建索引并定义映射在插入数据前最好先明确定义映射。这就像建表之前先定义好字段类型。PUT /product_index { “mappings”: { “properties”: { “id”: { “type”: “long” }, “title”: { “type”: “text”, // text类型会被分词用于全文搜索 “analyzer”: “ik_max_word”, // 指定中文分词器 “search_analyzer”: “ik_smart” // 指定搜索时用的分词器通常更粗粒度 }, “price”: { “type”: “double” }, “brand”: { “type”: “keyword” // keyword类型不会被分词用于精确匹配、聚合、排序 }, “tags”: { “type”: “keyword” }, “createdTime”: { “type”: “date” }, “specs”: { // 对象类型 “type”: “object”, “properties”: { “color”: { “type”: “keyword” }, “memory”: { “type”: “keyword” } } } } } }关键点解析textvskeyword这是最常见的混淆点。title需要被分词搜索所以用textbrand通常作为整体过滤或聚合如“列出所有小米品牌的商品”所以用keyword。如果一个字段既要全文搜索又要精确匹配可以定义成多字段类型“brand”: { “type”: “text”, “fields”: { “keyword”: { “type”: “keyword” } } }这样brand用于搜索brand.keyword用于精确匹配。analyzer与search_analyzer索引和搜索时可以使用不同的分词器。ik_max_word分得最细粒度最大ik_smart分得较粗。索引时用细粒度保证召回率搜索时用粗粒度提升准确率。查询索引映射GET /product_index/_mapping插入文档POST /product_index/_doc/1 // 指定文档ID为1 { “id”: 1, “title”: “小米13 5G智能手机 徕卡影像”, “price”: 3999.00, “brand”: “小米”, “tags”: [“新品”, “旗舰”, “拍照”], “createdTime”: “2023-10-01T10:00:00”, “specs”: { “color”: “黑色”, “memory”: “12GB256GB” } } POST /product_index/_doc // 不指定IDES会自动生成 { “id”: 2, “title”: “Apple iPhone 15 Pro 原色钛金属”, “price”: 8999.00, “brand”: “Apple”, “tags”: [“高端”, “iOS”], “createdTime”: “2023-09-22T14:30:00”, “specs”: { “color”: “原色钛金属”, “memory”: “256GB” } }3.2 查询 DSL 深度解析匹配、过滤与复合查询ES 的查询使用基于 JSON 的 DSL (Domain Specific Language)。核心查询分为两大类查询上下文 (Query Context)和过滤上下文 (Filter Context)。查询上下文关心文档与查询条件的匹配程度并计算相关性得分 (_score)。用于全文搜索。过滤上下文只关心文档是否匹配条件结果是“是”或“否”不计算得分且结果可以被缓存。用于精确过滤如状态、范围、术语。1. 匹配查询 (Match Query) - 查询上下文最常用的全文搜索。GET /product_index/_search { “query”: { “match”: { “title”: “小米手机” } } }ES 会用分词器把“小米手机”分词例如分成“小米”、“手机”然后去倒排索引里查找包含这两个词中任意一个的文档。这是一个or的逻辑。如果想要求同时包含使用match_phrase短语匹配或设置operator“match”: { “title”: { “query”: “小米手机”, “operator”: “and” // 要求同时包含“小米”和“手机” } }2. 术语查询 (Term Query) - 过滤上下文用于精确匹配keyword类型字段或未经分词的字段。GET /product_index/_search { “query”: { “term”: { “brand.keyword”: “小米” // 精确匹配品牌为“小米”的文档 } } }重要陷阱如果你对titletext类型使用term查询“title”: “小米手机”会搜不到因为title字段被分词后存储的是[“小”, “米”, “手”, “机”]或[“小米”, “手机”]不存在一个完整的“小米手机”词条。3. 范围查询 (Range Query) - 过滤上下文GET /product_index/_search { “query”: { “range”: { “price”: { “gte”: 3000, “lte”: 5000 } } } }4. 布尔查询 (Bool Query) - 组合之王这是功能最强大的查询可以组合多个子查询。GET /product_index/_search { “query”: { “bool”: { “must”: [ // 必须满足贡献得分 { “match”: { “title”: “手机” } } ], “filter”: [ // 必须满足但不贡献得分效率高 { “term”: { “brand.keyword”: “小米” } }, { “range”: { “price”: { “lte”: 5000 } } } ], “should”: [ // 应该满足满足的会增加得分 { “match”: { “tags”: “新品” } } ], “must_not”: [ // 必须不满足 { “term”: { “specs.color”: “白色” } } ] } } }这个查询的意思是查找标题包含“手机”、品牌是小米、价格低于5000且颜色不是白色的商品如果标签是“新品”会获得更高的排名。3.3 聚合分析从搜索到洞察聚合 (Aggregation) 提供了分组统计和数据分析的能力。GET /product_index/_search { “size”: 0, // 不返回具体文档只关注聚合结果 “aggs”: { “brand_stats”: { “terms”: { // 按品牌分组 “field”: “brand.keyword”, “size”: 10 }, “aggs”: { // 在品牌分组内再做子聚合 “avg_price”: { // 计算每个品牌的平均价格 “avg”: { “field”: “price” } }, “price_range”: { // 统计每个品牌的价格区间分布 “range”: { “field”: “price”, “ranges”: [ { “to”: 3000 }, { “from”: 3000, “to”: 6000 }, { “from”: 6000 } ] } } } } } }这个聚合请求会返回每个品牌下的商品数量、平均价格以及在不同价格区间的分布情况对于后台商品分析报表非常有用。掌握了这些核心语法你就已经具备了直接操作 ES 的能力。接下来我们把这些能力集成到 Spring Boot 应用中。4. Spring Boot 集成 Elasticsearch两种主流方式与选型Spring Boot 为 ES 提供了两种主要的集成方式Spring Data Elasticsearch和Elasticsearch Java Client。选择哪种取决于你的项目需求和团队习惯。4.1 方式一Spring Data Elasticsearch (Repository 模式)这种方式抽象程度最高使用起来最像 Spring Data JPA能极大提升开发效率。1. 引入依赖在pom.xml中注意版本对应。Spring Boot 3.x 通常对应 Spring Data Elasticsearch 5.x 和 Elasticsearch Client 8.x。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-elasticsearch/artifactId /dependency2. 配置连接在application.yml中配置。这里使用 RestClient 连接方式推荐。spring: elasticsearch: uris: http://localhost:9200 # ES 服务器地址 # 如果启用了安全认证如用户名密码 # username: your-username # password: your-password connection-timeout: 5s # 连接超时 socket-timeout: 30s # 套接字超时3. 定义实体类与映射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.time.LocalDateTime; import java.util.List; Document(indexName “product_index”, createIndex false) // createIndexfalse 表示不自动创建索引 public class Product { Id private Long id; Field(type FieldType.Text, analyzer “ik_max_word”, searchAnalyzer “ik_smart”) private String title; Field(type FieldType.Double) private Double price; Field(type FieldType.Keyword) private String brand; Field(type FieldType.Keyword) private ListString tags; Field(type FieldType.Date, format DateFormat.date_hour_minute_second) private LocalDateTime createdTime; Field(type FieldType.Object) private Specs specs; // 嵌套的对象类 public static class Specs { Field(type FieldType.Keyword) private String color; Field(type FieldType.Keyword) private String memory; // getters and setters } // getters and setters }4. 创建 Repository 接口import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import java.util.List; public interface ProductRepository extends ElasticsearchRepositoryProduct, Long { // 方法名派生查询根据品牌查找 ListProduct findByBrand(String brand); // 方法名派生查询根据价格范围查找 ListProduct findByPriceBetween(Double minPrice, Double maxPrice); // 使用 Query 注解进行自定义 DSL 查询 Query(“{“bool”: {“must”: [{“match”: {“title”: “?0”}}]}}”) ListProduct searchByTitle(String title); }5. 在 Service 中调用Service RequiredArgsConstructor // 使用 Lombok 简化构造器注入 public class ProductService { private final ProductRepository productRepository; public void saveProduct(Product product) { productRepository.save(product); } public ListProduct searchProducts(String keyword, String brand, Double maxPrice) { // 构建一个简单的 BoolQueryBuilder BoolQueryBuilder boolQuery QueryBuilders.boolQuery(); if (StringUtils.hasText(keyword)) { boolQuery.must(QueryBuilders.matchQuery(“title”, keyword)); } if (StringUtils.hasText(brand)) { boolQuery.filter(QueryBuilders.termQuery(“brand”, brand)); } if (maxPrice ! null) { boolQuery.filter(QueryBuilders.rangeQuery(“price”).lte(maxPrice)); } NativeSearchQuery searchQuery new NativeSearchQueryBuilder() .withQuery(boolQuery) .build(); return elasticsearchRestTemplate.search(searchQuery, Product.class) .stream() .map(SearchHit::getContent) .collect(Collectors.toList()); } }优点开发快代码简洁适合 CRUD 和简单查询。缺点复杂查询的 DSL 用Query注解写起来麻烦且容易出错对 ES 新特性的支持可能有延迟。4.2 方式二Elasticsearch Java Client (Low-Level Client)这是 Elastic 官方推荐的客户端提供最全面、最及时的功能支持控制粒度最细。1. 引入依赖dependency groupIdco.elastic.clients/groupId artifactIdelasticsearch-java/artifactId version8.13.0/version !-- 版本需与ES服务端匹配 -- /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId /dependency2. 配置 Client BeanConfiguration public class ElasticsearchConfig { Bean public ElasticsearchClient elasticsearchClient() { RestClient restClient RestClient.builder( new HttpHost(“localhost”, 9200) ).build(); ElasticsearchTransport transport new RestClientTransport( restClient, new JacksonJsonpMapper() ); return new ElasticsearchClient(transport); } }3. 使用 Client 进行查询Service RequiredArgsConstructor public class ProductServiceV2 { private final ElasticsearchClient client; public ListProduct complexSearch(String keyword, String brand, Double maxPrice) throws IOException { // 1. 构建查询条件 Query titleMatch StringUtils.hasText(keyword) ? MatchQuery.of(m - m.field(“title”).query(keyword))._toQuery() : null; Query brandTerm StringUtils.hasText(brand) ? TermQuery.of(t - t.field(“brand.keyword”).value(brand))._toQuery() : null; Query priceRange (maxPrice ! null) ? RangeQuery.of(r - r.field(“price”).lte(JsonData.of(maxPrice)))._toQuery() : null; // 2. 组合 Bool 查询 BoolQuery.Builder boolBuilder new BoolQuery.Builder(); if (titleMatch ! null) boolBuilder.must(titleMatch); if (brandTerm ! null) boolBuilder.filter(brandTerm); if (priceRange ! null) boolBuilder.filter(priceRange); Query finalQuery boolBuilder.build()._toQuery(); // 3. 执行搜索 SearchResponseProduct response client.search(s - s .index(“product_index”) .query(finalQuery) .size(100), // 设置返回条数 Product.class // 指定返回类型需要有无参构造器和getter/setter ); // 4. 处理结果 return response.hits().hits().stream() .map(hit - hit.source()) .collect(Collectors.toList()); } }优点功能强大且与 ES 版本同步类型安全使用 Builder 模式执行效率高适合复杂、高性能场景。缺点代码量稍多需要直接处理序列化/反序列化。选型建议对于大多数业务系统以管理后台、简单搜索为主Spring Data Elasticsearch 的 Repository 模式是首选能快速上手。对于搜索为核心功能、查询极其复杂、对性能要求苛刻、或需要用到 ES 最新特性的项目建议使用Elasticsearch Java Client。一个折中的方案是在同一个项目中简单操作用Repository复杂查询用ElasticsearchRestTemplateSpring Data 提供或直接注入ElasticsearchClient。5. 实战中的核心细节与避坑指南集成只是第一步要让搜索服务稳定高效以下几个细节必须处理好。5.1 索引生命周期与数据同步策略ES 的索引不是建好就一劳永逸的。随着数据增长你需要考虑索引模板 (Index Template)用于自动为新索引如按日期滚动生成的索引product_index_2024_05应用预定义的映射和设置如分片数。索引别名 (Alias)给你的索引起一个“别名”如product_current指向实际的索引product_index_v2。这样在重建索引或切换索引时业务代码无需修改只需将别名指向新的索引。数据同步如何将 MySQL 等主数据库的数据同步到 ES常用方案有应用层双写在业务代码中插入/更新数据库后同步写 ES。简单但无法保证强一致性且业务侵入性强。基于 Binlog 的异步同步使用 Canal、Debezium 等工具监听数据库的 Binlog解析后发送到 MQ如 Kafka再由消费程序写入 ES。这是推荐方案解耦业务能保证最终一致性并能同步历史数据。定时任务扫描作为补充用于修复数据不一致或同步初始化数据。5.2 映射设计与性能调优禁用_all字段在旧版本中ES 默认会将所有字段的值复制到一个_all字段中进行搜索这会增加索引体积和降低性能。在新版本中它已被废弃但请确保你的映射中没有启用它。合理设置分片与副本主分片数索引创建后不可更改。设置依据主要是数据总量和节点数。单个分片建议在 20GB-50GB 之间。对于中小型项目5-10个主分片通常足够。副本分片数可以动态调整。提供高可用和提升读取吞吐量。生产环境至少设置为1。PUT /my_index { “settings”: { “number_of_shards”: 5, “number_of_replicas”: 1 } }慎用nested和join类型ES 对嵌套对象和父子文档的支持性能开销较大。如果关系不复杂尽量用object类型拍平如果必须用要清楚其查询限制。5.3 常见异常排查与解决unable to retrieve version information from elasticsearch nodes 这通常是客户端与服务器版本不兼容或网络/连接配置错误导致的。首先检查spring-boot-starter-data-elasticsearch或elasticsearch-java客户端的版本是否与你的 ES 服务端大版本兼容如 8.x 客户端连 8.x 服务端。其次检查spring.elasticsearch.uris配置是否正确以及 ES 服务是否真的在运行curl http://localhost:9200。index_not_found_exception 尝试操作一个不存在的索引。在代码中执行操作前可以先使用RestHighLevelClient或ElasticsearchClient的indices().exists()方法检查索引是否存在或确保应用启动时有创建索引的逻辑。查询结果不符合预期 这是最常见的问题。排查步骤使用GET /your_index/_analyzeAPI 分析你的查询词和字段的实际分词结果。确认分词器是否按你预期工作。检查字段映射类型。你是否在用term查询一个text字段或者期望keyword字段被分词查看 ES 返回的详细搜索结果打开explain参数看相关性得分是如何计算的。GET /product_index/_search { “explain”: true, “query”: { ... } }5.4 中文分词器 IK 的安装与使用对于中文项目IK 分词器是必需品。下载从 GitHub 发布页面下载与 ES 版本匹配的 IK 插件 zip 文件。安装在 ES 安装目录的plugins下创建ik文件夹解压 zip 文件内容至此。重启 ES。验证GET /_cat/plugins查看插件列表。使用如前文映射示例在字段映射中指定“analyzer”: “ik_max_word”和“search_analyzer”: “ik_smart”。自定义词典在ik/config目录下可以编辑IKAnalyzer.cfg.xml来扩展自定义词典将项目特有的词汇如品牌名、型号加入避免被错误拆分。6. 构建一个完整的商品搜索服务从 API 到前端联调让我们把上面的知识点串起来实现一个简单的商品搜索后端 API。1. 定义搜索请求与响应 DTOData public class ProductSearchRequest { private String keyword; private String brand; private Double minPrice; private Double maxPrice; private ListString tags; private Integer page 1; private Integer size 10; private String sortBy “_score”; // 默认按相关性排序 private String sortOrder “desc”; } Data public class PageResultT { private Long total; private Integer page; private Integer size; private ListT list; }2. 实现复杂的搜索 Service 方法这里使用ElasticsearchRestTemplate演示它比 Repository 更灵活又比 Low-Level Client 简单。Service RequiredArgsConstructor public class ProductSearchService { private final ElasticsearchRestTemplate elasticsearchRestTemplate; public PageResultProduct searchProducts(ProductSearchRequest request) { // 1. 构建 BoolQueryBuilder BoolQueryBuilder boolQuery QueryBuilders.boolQuery(); if (StringUtils.hasText(request.getKeyword())) { boolQuery.must(QueryBuilders.matchQuery(“title”, request.getKeyword()).operator(Operator.AND)); } if (StringUtils.hasText(request.getBrand())) { boolQuery.filter(QueryBuilders.termQuery(“brand.keyword”, request.getBrand())); } if (request.getMinPrice() ! null || request.getMaxPrice() ! null) { RangeQueryBuilder rangeQuery QueryBuilders.rangeQuery(“price”); if (request.getMinPrice() ! null) rangeQuery.gte(request.getMinPrice()); if (request.getMaxPrice() ! null) rangeQuery.lte(request.getMaxPrice()); boolQuery.filter(rangeQuery); } if (request.getTags() ! null !request.getTags().isEmpty()) { boolQuery.filter(QueryBuilders.termsQuery(“tags”, request.getTags())); } // 2. 构建排序 SortBuilder? sortBuilder null; if (“price”.equals(request.getSortBy())) { sortBuilder SortBuilders.fieldSort(“price”).order(SortOrder.fromString(request.getSortOrder())); } else if (“createdTime”.equals(request.getSortBy())) { sortBuilder SortBuilders.fieldSort(“createdTime”).order(SortOrder.fromString(request.getSortOrder())); } // 默认按 _score 排序 // 3. 构建分页 int from (request.getPage() - 1) * request.getSize(); // 4. 构建完整查询 NativeSearchQueryBuilder queryBuilder new NativeSearchQueryBuilder() .withQuery(boolQuery) .withPageable(PageRequest.of(request.getPage() - 1, request.getSize())); if (sortBuilder ! null) { queryBuilder.withSort(sortBuilder); } NativeSearchQuery searchQuery queryBuilder.build(); // 5. 执行查询 SearchHitsProduct searchHits elasticsearchRestTemplate.search(searchQuery, Product.class); // 6. 封装结果 PageResultProduct result new PageResult(); result.setTotal(searchHits.getTotalHits()); result.setPage(request.getPage()); result.setSize(request.getSize()); result.setList(searchHits.stream().map(SearchHit::getContent).collect(Collectors.toList())); return result; } // 聚合示例获取所有品牌及其商品数量 public MapString, Long getBrandAggregation() { NativeSearchQuery query new NativeSearchQueryBuilder() .withQuery(QueryBuilders.matchAllQuery()) .addAggregation(AggregationBuilders.terms(“brand_agg”).field(“brand.keyword”)) .build(); SearchHitsProduct searchHits elasticsearchRestTemplate.search(query, Product.class); Terms terms searchHits.getAggregations().get(“brand_agg”); return terms.getBuckets().stream() .collect(Collectors.toMap(Terms.Bucket::getKeyAsString, Terms.Bucket::getDocCount)); } }3. 提供 RESTful APIRestController RequestMapping(“/api/products”) RequiredArgsConstructor public class ProductController { private final ProductSearchService productSearchService; PostMapping(“/search”) public ResponseEntityPageResultProduct search(RequestBody ProductSearchRequest request) { return ResponseEntity.ok(productSearchService.searchProducts(request)); } GetMapping(“/brands”) public ResponseEntityMapString, Long listBrands() { return ResponseEntity.ok(productSearchService.getBrandAggregation()); } }4. 前端联调要点API 设计搜索接口通常设计为POST因为查询条件可能很复杂放在 Request Body 中更合适。参数处理前端传递的空字符串、null值需要在后端妥善处理避免构建无意义的查询条件。高亮显示在搜索关键词时可以使用 ES 的高亮功能将匹配到的词在结果中标记出来如加粗。在NativeSearchQueryBuilder中调用.withHighlightFields(new HighlightBuilder.Field(“title”))即可实现。错误处理做好全局异常处理将 ES 客户端的异常转换为对前端友好的错误信息。走到这一步一个具备基本搜索、过滤、排序、分页和聚合功能的商品搜索后端就完成了。这只是一个起点你可以在此基础上继续深化比如引入搜索建议Completion Suggester、实现更复杂的相关性排序Function Score Query、或者与 Redis 缓存结合来应对热点查询。记住ES 的学习曲线在于理解其“搜索优先”的思维模式一旦掌握它将成为你项目中处理海量数据检索和实时分析的无价工具。