接上篇《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条)
java
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代码示例:搜索"上海"的酒店,并按评分降序、价格升序排列
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 代码示例:搜索酒店名称或品牌中包含"希尔顿"的酒店,并对名称和品牌进行高亮
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();
}
}
效果:

三、功能整合与最佳实践
在实际项目中,我们通常需要将这些功能组合使用。下面是一个综合示例,模拟一个真实的酒店搜索场景:分页查询"上海"地区且商圈包含"陆家嘴"的酒店,按评分降序排列,并对酒店名称和商圈进行高亮。
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.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