【Elasticsearch入门到落地】18、Elasticsearch实战:Java API详解高亮、排序与分页
接上篇《17、手把手教你玩转Term、Range、Bool查询与优雅封装》
一、引言
在上一篇博客中,我们介绍了如何使用RestClient进行term、range、bool查询,以及封装公用方法简化查询代码。
一个简单的搜索框是搜索功能的起点,但一个真正强大、用户友好的搜索功能,远不止于此。试想一下,当用户在搜索酒店时,他们期望:
1.快速定位:在长长的搜索结果中,能一眼看到搜索关键词(如“希尔顿”)在酒店名称或地址中的哪里被匹配了。这就是高亮功能的用武之地。
2.结果有序:搜索结果不是随意排列的,而是应该按照一定的规则,比如评分高的靠前、价格低的靠前,或者离市中心最近的优先。这依赖于排序功能。
3.浏览顺畅:当搜索结果有成百上千条时,一次性全部加载是不现实的。我们需要将结果分成一页一页地展示,这就是分页功能。
本文将深入探讨如何在Java应用程序中,利用Elasticsearch的高级功能,实现上述需求,从而打造一个专业的酒店搜索系统。
二、功能实战详解
我们将以hotel索引库为例,该索引库的文档结构主要包含以下字段:id, name, address, price, score, brand, city, starName, business, pic等。
前提:我们已经有了一个获取 RestHighLevelClient的工具类 ElasticsearchClient,并了解了基础的 SearchRequest和 SearchSourceBuilder的使用。
1. 分页查询:掌控数据量的艺术
分页是处理大量数据的基本手段。在Elasticsearch中,分页主要通过from和size两个参数实现。
from:指定从第几条结果开始返回(偏移量)。例如,from=10表示跳过前10条结果。
size:指定一次查询返回的最大结果数(每页大小)。例如,size=10表示每页显示10条记录。
Java代码示例:搜索第2页的酒店(每页5条)
package com.example;import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
public class HotelSearchWithPagination {public static void main(String[] args) throws Exception {// 通过提前写好的工具类,获取 Elasticsearch 客户端RestHighLevelClient client = ElasticsearchClient.getClient();// 构建查询请求,指定索引名,创建 SearchRequest 对象,并指定要查询的索引名为hotel。SearchRequest request = new SearchRequest("hotel");// 构建查询条件,SearchSourceBuilder:用于定义查询的详细参数(如查询内容、分页、排序等)。SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();// 1. 构建查询条件:查询所有酒店sourceBuilder.query(QueryBuilders.matchAllQuery());// 2.设置分页参数int pageSize = 5; //每页大小int pageNum = 2; //查询第2页(页码从第1开始计算)int from = (pageNum - 1) * pageSize; //计算偏移量sourceBuilder.from(from);sourceBuilder.size(pageSize);// 3.将查询条件绑定到请求request.source(sourceBuilder);// 4.执行查询SearchResponse response = client.search(request, RequestOptions.DEFAULT);SearchHits searchHits = response.getHits();System.out.println("总命中数:"+searchHits.getTotalHits().value);System.out.println("当前页结果:");for(SearchHit hit: searchHits.getHits()){String sourceAsString = hit.getSourceAsString();System.out.println(sourceAsString);}ElasticsearchClient.close();}
}
效果:

代码解释与注意事项:
from( (pageNum - 1) * pageSize )是分页的核心计算逻辑。
深度分页问题:当from值非常大时(例如翻到第1000页),Elasticsearch需要花费很高的成本来汇集和排序大量数据,性能会急剧下降,甚至可能耗尽内存。对于深度分页,推荐使用search_after参数,这将在后续博客中介绍。
2. 结果排序:让最重要的结果排在最前
默认情况下,Elasticsearch按score(相关性评分)降序排列。但在很多业务场景下,我们需要按特定字段排序。
按字段值排序:如按价格price升序、按评分score降序。
多级排序:先按一个字段排序,再按另一个字段排序。
Java代码示例:搜索“上海”的酒店,并按评分降序、价格升序排列
package com.example;import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;public class HotelSearchWithSorting {public static void main(String[] args) throws Exception {RestHighLevelClient client = ElasticsearchClient.getClient();SearchRequest request = new SearchRequest("hotel");SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();// 1. 构建查询条件:查询城市为“上海”的酒店sourceBuilder.query(QueryBuilders.termQuery("city", "上海"));// 2. 设置排序// 第一优先级:按评分(score)降序排列(评分高的在前)sourceBuilder.sort("score", SortOrder.DESC);// 第二优先级:按价格(price)升序排列(价格低的在前)sourceBuilder.sort("price", SortOrder.ASC);request.source(sourceBuilder);SearchResponse response = client.search(request, RequestOptions.DEFAULT);for (SearchHit hit:response.getHits().getHits()) {System.out.println(hit.getSourceAsString());}ElasticsearchClient.close();}
}
效果:

可以看到,结果先按照分值排序,在相同分值的情况下,价格从低到高。
3. 搜索结果高亮:一眼锁定关键词
高亮功能可以将匹配到的搜索词在返回的文本中标记出来,通常用HTML标签包裹(如em),方便前端渲染。
高亮器:使用HighlightBuilder来配置高亮。
高亮字段:指定需要对哪些字段进行高亮(如 name, address, business)。
高亮参数:可以自定义高亮标签、返回的片段数量等。
Java 代码示例:搜索酒店名称或品牌中包含“希尔顿”的酒店,并对名称和品牌进行高亮
package com.example;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;public class HotelSearchWithHighlight {public static void main(String[] args) throws Exception {RestHighLevelClient client = ElasticsearchClient.getClient();SearchRequest request = new SearchRequest("hotel");SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();// 1. 构建查询条件:多字段匹配查询sourceBuilder.query(QueryBuilders.multiMatchQuery("希尔顿", "name", "brand"));// 2. 创建并配置高亮器HighlightBuilder highlightBuilder = new HighlightBuilder();// 指定要高亮的字段HighlightBuilder.Field nameField = new HighlightBuilder.Field("name");HighlightBuilder.Field brandField = new HighlightBuilder.Field("brand");highlightBuilder.field(nameField);highlightBuilder.field(brandField);// 设置高亮格式:用红色加粗标签包裹匹配到的词highlightBuilder.preTags("<b style=\"color:red\">");highlightBuilder.postTags("</b>");// (可选)设置高亮参数highlightBuilder.numOfFragments(1); //从每个字段中返回的片段数,0表示返回整个字段highlightBuilder.fragmentSize(100); //每个片段的大小// 3. 将高亮器添加到查询源中sourceBuilder.highlighter(highlightBuilder);request.source(sourceBuilder);// 4. 执行查询SearchResponse response = client.search(request, RequestOptions.DEFAULT);// 5. 处理结果,特别注意高亮部分for (SearchHit hit: response.getHits().getHits()) {// 获取原始文档源String sourceAsString = hit.getSourceAsString();System.out.println("原始文档: " + sourceAsString);// 获取高亮结果if (hit.getHighlightFields() != null && !hit.getHighlightFields().isEmpty()) {System.out.println("高亮内容");// 遍历所有有高亮内容的字段hit.getHighlightFields().forEach((fieldName, highlightField) -> {// 一个字段可能有多个高亮片段,这里取第一个Text[] fragments = highlightField.getFragments();if (fragments != null && fragments.length > 0) {System.out.println(" "+ fieldName + ": "+ fragments[0].string());}});}System.out.println("---");}ElasticsearchClient.close();}
}
效果:

三、功能整合与最佳实践
在实际项目中,我们通常需要将这些功能组合使用。下面是一个综合示例,模拟一个真实的酒店搜索场景:分页查询“上海”地区且商圈包含“陆家嘴”的酒店,按评分降序排列,并对酒店名称和商圈进行高亮。
package com.example;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortOrder;import java.util.Map;public class HotelComprehensiveSearch {public static void main(String[] args) throws Exception {RestHighLevelClient client = ElasticsearchClient.getClient();SearchRequest request = new SearchRequest("hotel");SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();// 1. 构建复合查询条件// 使用 Bool Query 组合多个条件sourceBuilder.query(QueryBuilders.boolQuery().must(QueryBuilders.termQuery("city", "上海")) // 城市必须是上海.must(QueryBuilders.matchQuery("name", "如家")) // 名称包含如家);// 2. 设置排序:按评分降序sourceBuilder.sort("score", SortOrder.DESC);// 3. 设置分页:查询第1页,每页10条sourceBuilder.from(0);sourceBuilder.size(10);// 4. 设置高亮HighlightBuilder highlightBuilder = new HighlightBuilder();highlightBuilder.field("city");highlightBuilder.field("name");highlightBuilder.preTags("<em>");highlightBuilder.postTags("</em>");sourceBuilder.highlighter(highlightBuilder);request.source(sourceBuilder);// 5. 执行查询SearchResponse response = client.search(request, RequestOptions.DEFAULT);SearchHits hits = response.getHits();System.out.printf("找到 %d 家符合条件的酒店:\n", hits.getTotalHits().value);for (SearchHit hit:hits) {// 解析原始文档(这里可以用 Jackson 等库映射为 Java 对象)Map<String, Object> sourceMap = hit.getSourceAsMap();String hotelName = (String) sourceMap.get("name");String brand = (String) sourceMap.get("brand");Integer score = (Integer) sourceMap.get("score");Integer price = (Integer) sourceMap.get("price");System.out.printf("酒店 %s (%s), 评分 %d, 价格 %d%n", hotelName, brand, score, price);// 处理高亮Map<String,org.elasticsearch.search.fetch.subphase.highlight.HighlightField> highlightFields = hit.getHighlightFields();if (highlightFields.containsKey("city")) {String highlightedCity = highlightFields.get("city").getFragments()[0].string();System.out.println(" 高亮城市: " + highlightedCity);}if (highlightFields.containsKey("name")) {String highlightedName = highlightFields.get("name").getFragments()[0].string();System.out.println(" 高亮名称: " + highlightedName);}System.out.println("---");}ElasticsearchClient.close();}
}
效果:

最佳实践与注意事项:
1.性能考量:
高亮:高亮计算会增加查询的 CPU 开销,尽量只对必要的字段进行高亮。
深度分页:坚决避免使用 from + size进行深度分页(如 from 10000),应使用 search_after参数。
2.字段映射:
排序:确保用于排序的字段被正确映射。例如,对文本字段排序,应使用其keyword类型(如brand.keyword),否则可能得到非预期的结果。
高亮:高亮通常作用于被分析的文本字段(text类型)。
结果解析:在实际项目中,建议使用 JSON 反序列化库(如 Jackson)将 hit.getSourceAsString()转换为对应的 Java POJO 对象(如 Hotel),这样更利于后续处理。
四、总结
通过本文的学习,我们掌握了如何使用Elasticsearch Java API实现三个至关重要的搜索增强功能:
分页:使用from和size参数,优雅地处理大量数据展示。
排序:使用sort方法,让结果按照业务规则(如价格、评分)有序排列。
高亮:使用HighlightBuilder,让用户能快速定位到匹配的关键词。
将这些功能与基础查询相结合,我们就能构建出一个体验良好、功能完善的搜索系统,无论是酒店搜索、商品搜索还是内容搜索,其核心原理都是相通的。
在下一篇博客中,我们将探讨如何解决深度分页问题的search_after技术,敬请期待!
转载请注明出处:https://blog.csdn.net/acmman/article/details/154315452
