【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条)

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

相关推荐
码上零乱7 小时前
跟着小码学算法Day19:路径总和
java·数据结构·算法
ai旅人7 小时前
深入理解OkHttp超时机制:连接、读写、调用超时全面解析
java·网络·okhttp
NON-JUDGMENTAL7 小时前
Tomcat 配置问题速查表
java·tomcat
一 乐7 小时前
农产品销售系统|农产品电商|基于SprinBoot+vue的农产品销售系统(源码+数据库+文档)
java·javascript·数据库·vue.js·spring boot·后端·农产品销售系统
蒲公英源码8 小时前
java企业OA自动化办公源码
java·spring boot·后端
鬼火儿8 小时前
集成RabbitMQ+MQ常用操作
java·后端
ZHE|张恒8 小时前
Java 通配符
java
Merrick8 小时前
Java 方法参数默认值新方案:使用DefArgs!
java·后端
程序员小假8 小时前
finally 释放的是什么资源?
java·后端