别再手动拼接高亮标签了!Spring Boot自动处理Elasticsearch高亮字段的终极方案

第一章:Spring Boot Elasticsearch 高亮字段处理概述

在构建现代搜索应用时,高亮显示匹配关键词是提升用户体验的重要功能。Spring Boot 集成 Elasticsearch 后,可以通过查询 DSL 灵活实现字段高亮,使搜索结果中关键字以醒目方式呈现。高亮功能不仅限于文本展示优化,还能帮助用户快速定位相关内容,尤其适用于日志检索、文档搜索和电商商品匹配等场景。

高亮的基本原理

Elasticsearch 在执行查询时,可通过 highlight 参数指定需要高亮的字段。系统会自动分析匹配片段,并将关键词包裹在预设的 HTML 标签中(如 <em>)。Spring Boot 通过 RestHighLevelClientElasticsearchRestTemplate 封装请求,简化高亮配置的编码过程。

启用高亮的典型配置

以下代码展示了如何在 Spring Boot 中构建包含高亮的搜索请求:

// 构建查询条件
QueryBuilder query = QueryBuilders.matchQuery("content", "spring boot");

// 构建高亮设置
HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("content"); // 指定高亮字段
highlightBuilder.preTags("<em>"); // 前缀标签
highlightBuilder.postTags("</em>"); // 后缀标签

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(query);
sourceBuilder.highlighter(highlightBuilder);

SearchRequest searchRequest = new SearchRequest("documents");
searchRequest.source(sourceBuilder);
  • 使用 HighlightBuilder 定义高亮字段与样式
  • 通过 preTagspostTags 控制关键词包装标签
  • 最终将高亮配置附加到搜索请求的源构造器中
配置项作用说明
field指定参与高亮的字段名称
fragmentSize控制高亮片段长度,默认为100字符
numberOfFragments返回的高亮片段数量
graph TD A[用户输入关键词] --> B{发起搜索请求} B --> C[执行全文匹配查询] C --> D[识别匹配字段内容] D --> E[生成高亮片段] E --> F[返回带标签的HTML结果]

第二章:Elasticsearch 高亮功能原理与配置详解

2.1 高亮查询的基本语法与工作原理

高亮查询用于在搜索结果中突出显示匹配关键词,提升用户对检索内容的可读性。其核心是在查询请求中附加高亮参数,由搜索引擎分析并返回带有标记的文本片段。
基本语法结构
{
  "query": {
    "match": { "content": "Elasticsearch" }
  },
  "highlight": {
    "fields": {
      "content": {}
    },
    "pre_tags": ["<em>"],
    "post_tags": ["</em>"]
  }
}
该查询在 content 字段中匹配“Elasticsearch”,并在结果中使用 <em> 标签包裹关键词。其中, pre_tagspost_tags 定义了高亮标签的起始与结束。
工作流程解析
  • 执行全文检索,定位匹配文档
  • 从原始字段提取包含关键词的文本片段
  • 对片段中的匹配词应用高亮标签
  • 返回带有 highlight 字段的结果集

2.2 在REST API中实现高亮搜索的实践

在构建支持全文搜索的REST API时,高亮显示匹配关键词是提升用户体验的关键功能。通常结合Elasticsearch或Solr等搜索引擎,在查询结果中返回带有标记的文本片段。
高亮配置示例
{
  "query": {
    "match": { "content": "高亮搜索" }
  },
  "highlight": {
    "fields": {
      "content": {
        "pre_tags": ["<em>"],
        "post_tags": ["</em>"]
      }
    }
  }
}
该请求会在匹配字段中使用 <em>标签包裹关键词。Elasticsearch自动分析查询词,并在原文中定位并标注出现位置,便于前端渲染为醒目的样式。
响应结构与处理
  • highlight 字段随结果返回,包含带标签的摘要片段;
  • 客户端可直接插入DOM,实现关键词高亮;
  • 支持多字段、多片段高亮,增强可读性。

2.3 高亮参数解析:fragment_size、number_of_fragments等

在Elasticsearch的高亮功能中, fragment_sizenumber_of_fragments 是控制高亮输出格式的核心参数。
关键参数说明
  • fragment_size:指定每个高亮片段的字符长度,默认为100。较小值可提升显示紧凑性,但可能截断上下文。
  • number_of_fragments:控制返回的高亮片段数量。设为0时返回完整字段,常用于短文本场景。
配置示例
{
  "highlight": {
    "fields": {
      "content": {
        "fragment_size": 150,
        "number_of_fragments": 3
      }
    }
  }
}
上述配置将从 content字段生成最多3个、每段最长150字符的高亮片段,适用于文章摘要类展示场景。

2.4 不同高亮模式(plain、fvh)的选择与应用场景

Elasticsearch 提供多种高亮模式以适应不同的查询和展示需求,其中 `plain` 与 `fvh`(Fast Vector Highlighter)是两种核心实现。
Plain Highlighter:基础文本高亮
适用于普通字段的全文检索高亮,基于原始文本重新分析生成片段。
{
  "highlight": {
    "fields": {
      "content": {}
    },
    "type": "plain"
  }
}
该模式对普通文本处理效果良好,但性能较低,尤其在大字段或复杂分析链下。
Fvh Highlighter:高性能向量高亮
基于字段的 term vectors,支持精准快速高亮,仅适用于启用 `term_vector: with_positions_offsets` 的字段。
  • 响应速度快,适合高并发场景
  • 支持 HTML 标签嵌入,保持格式完整性
  • 不支持通配符、模糊查询等复杂查询类型的高亮
选择建议
场景推荐模式
普通搜索、小文本plain
大字段、高并发fvh

2.5 高亮结果在实际业务中的结构分析

在实际业务场景中,高亮结果通常用于提升用户对搜索关键词的感知效率。以电商平台为例,商品标题、描述中的匹配字段需通过高亮标记突出显示。
高亮字段的典型结构
高亮数据一般嵌入在返回结果的特定字段中,如下所示:
{
  "product_id": "10023",
  "title": "全新升级版无线蓝牙耳机",
  "highlight": {
    "description": ["支持降噪功能的蓝牙耳机"]
  }
}
其中 <em> 标签包裹匹配关键词,便于前端渲染为斜体或彩色文本。
常见高亮策略对比
策略适用场景性能开销
全文高亮文档检索
字段级高亮结构化数据
前缀匹配高亮自动补全

第三章:Spring Data Elasticsearch 中的高亮支持

3.1 使用SearchHit获取原始高亮内容

在Elasticsearch查询结果中, SearchHit对象封装了每条命中记录的详细信息,包括文档源数据和高亮片段。通过该对象可精确提取原始高亮内容。
高亮内容提取方法
  • getHighlightFields():返回Map结构,包含字段名与对应高亮片段
  • getHighlightField("title"):获取指定字段的高亮结果
SearchHit hit = response.getHits().getAt(0);
Map<String, HighlightField> highlightMap = hit.getHighlightFields();
HighlightField titleField = highlightMap.get("title");
String highlightedTitle = titleField.getFragments()[0].string();
上述代码首先获取首个命中结果,提取其高亮字段映射表,再定位 title字段的高亮片段。注意, getFragments()返回的是文本片段数组,通常取第一个即可还原加亮后的文本内容。

3.2 集成HighlightQuery实现字段高亮检索

在Elasticsearch检索中,提升用户体验的关键之一是实现关键词高亮显示。通过集成`HighlightQuery`,可在搜索结果中突出匹配字段中的查询词。
配置高亮查询
使用Spring Data Elasticsearch时,可通过`HighlightQuery`指定需高亮的字段:

HighlightBuilder.Field highlightField = new HighlightBuilder.Field("title")
    .preTags("<em>").postTags("</em>");
query.addHighlightField(highlightField);
上述代码对`title`字段启用高亮,匹配词将被`<em>`标签包裹。`preTags`和`postTags`定义了高亮样式,支持自定义CSS渲染。
返回结果处理
检索响应中包含`highlight`字段,结构如下:
字段名说明
title包含高亮片段的数组
前端可直接渲染该片段,实现精准关键词定位。

3.3 自定义高亮构建器提升查询灵活性

在复杂搜索场景中,高亮显示匹配内容能显著提升用户体验。Elasticsearch 提供了默认高亮功能,但在多字段、多条件检索下,灵活性不足。
自定义高亮构建器设计
通过实现 `HighlightBuilder` 可精确控制高亮范围与样式:

HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.field("title").preTags("<em>").postTags("</em>");
highlightBuilder.field("content").fragmentSize(150);
上述代码为 `title` 和 `content` 字段分别设置高亮规则:`preTags` 与 `postTags` 定义包裹关键词的 HTML 标签,`fragmentSize` 控制摘要长度。
动态配置优势
  • 支持多字段差异化高亮策略
  • 可结合用户角色或终端类型动态调整样式
  • 提升前端渲染兼容性与可维护性

第四章:自动化高亮字段注入方案设计与实现

4.1 定义注解标识需高亮的实体字段

在数据展示层中,常需对特定实体字段进行高亮渲染。通过自定义注解,可声明哪些字段应被特殊处理。
注解定义与应用
使用 Java 注解标记目标字段,如下所示:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Highlight {
    String color() default "#FFFF00";
}
该注解作用于字段级别(FIELD),并在运行时保留(RUNTIME),便于反射读取。参数 color 指定高亮背景色,默认为黄色。
实体类中的使用示例
在实体类中应用注解,明确标识关键字段:

public class User {
    private Long id;

    @Highlight(color = "#FF5733")
    private String phone;
}
此处 phone 字段将被渲染为橙红色背景,提升视觉识别度。后续可通过反射机制解析注解信息,结合前端模板动态生成高亮内容。

4.2 利用反射机制动态绑定高亮结果

在处理搜索引擎返回的高亮片段时,对象字段与高亮内容的映射往往需要灵活的绑定策略。Go语言的反射机制为此类动态赋值提供了强大支持。
反射动态赋值流程
通过 reflect包获取结构体字段,并根据标签匹配高亮数据:

func BindHighlight(obj interface{}, highlights map[string]string) {
    v := reflect.ValueOf(obj).Elem()
    for i := 0; i < v.NumField(); i++ {
        field := v.Field(i)
        tag := v.Type().Field(i).Tag.Get("json")
        if text, ok := highlights[tag]; ok && field.CanSet() {
            field.SetString(text)
        }
    }
}
上述代码遍历结构体字段,读取 json标签作为键名,在高亮映射中查找对应文本并赋值。条件 field.CanSet()确保字段可被修改,避免运行时错误。
性能与安全性考量
  • 反射调用存在性能开销,建议缓存类型信息
  • 需校验输入类型为指针结构体,防止非法访问

4.3 实现通用高亮处理器工具类

在处理文本高亮需求时,需构建一个可复用的通用处理器工具类,以支持多种场景下的关键词匹配与HTML标签注入。
核心设计思路
通过正则表达式动态构建匹配模式,避免特殊字符引发的注入问题。同时保留原始文本结构,仅对目标关键词包裹高亮样式标签。
func Highlight(text, keyword string) string {
    re := regexp.MustCompile(`(?i)` + regexp.QuoteMeta(keyword))
    return re.ReplaceAllString(text, "<mark>$0</mark>")
}
上述代码中,`regexp.QuoteMeta` 确保关键字中的元字符被转义,`(?i)` 实现忽略大小写匹配。返回结果将所有匹配项包裹在 ` ` 标签内,便于CSS定制高亮样式。
扩展支持多关键词
可通过字符串切片传入多个关键词,循环处理实现批量高亮,提升组件复用性与调用灵活性。

4.4 整合Service层完成自动映射闭环

在完成DAO与Mapper的映射配置后,关键在于通过Service层串联业务逻辑,实现数据操作的自动闭环。Service作为中间协调者,负责调用Mapper接口并处理事务边界。
服务层接口定义
public interface UserService {
    User createUser(CreateUserRequest request);
    Optional<User> findUserById(Long id);
}
该接口抽象了用户管理的核心行为,由具体实现类注入Mapper完成持久化。
自动映射实现流程

Controller → Service → Mapper → DB

响应结果沿链路反向传递,形成完整闭环

依赖注入配置
  • @Service 注解标识业务服务
  • @Autowired 自动装配Mapper实例
  • 事务控制通过 @Transactional 声明

第五章:总结与未来优化方向

性能监控的自动化演进
现代系统架构日益复杂,手动监控已无法满足高可用性需求。通过集成 Prometheus 与 Grafana,可实现对微服务的实时指标采集与可视化告警。例如,在 Kubernetes 集群中部署 Prometheus Operator,自动发现并监控所有 Pod 的 CPU、内存及网络使用情况:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: app-monitor
  labels:
    release: prometheus-stack
spec:
  selector:
    matchLabels:
      app: backend-service
  endpoints:
  - port: http
    interval: 30s
数据库查询优化策略
慢查询是系统瓶颈的常见根源。通过对 MySQL 慢查询日志分析,结合 EXPLAIN 命令定位执行计划问题,可显著提升响应速度。某电商平台在订单查询接口中引入复合索引后,平均响应时间从 850ms 降至 90ms。
  • 识别高频查询语句,优先优化
  • 避免 SELECT *,只获取必要字段
  • 使用覆盖索引减少回表操作
  • 定期分析表统计信息(ANALYZE TABLE)
边缘计算场景下的缓存升级
在 CDN 边缘节点部署 Redis 集群,将静态资源缓存至离用户更近的位置。某新闻门户通过此方案使首页加载时间缩短 60%。以下为多级缓存失效策略的配置示例:
缓存层级TTL 设置失效机制
浏览器缓存5 分钟ETag 校验
CDN 缓存30 分钟主动 PURGE 触发
Redis 缓存2 小时写操作后发布失效消息
【无人机】基于改进粒子群算法的无人机路径规划研究[和遗传算法、粒子群算法进行比较](Matlab代码实现)内容概要:本文围绕基于改进粒子群算法的无人机路径规划展开研究,重点探讨了在复杂环境中利用改进粒子群算法(PSO)实现无人机三维路径规划的方法,并将其与遗传算法(GA)、标准粒子群算法等传统优化算法进行对比分析。研究内容涵盖路径规划的多目标优化、避障策略、航路点约束以及算法收敛性和寻优能力的评估,所有实验均通过Matlab代码实现,提供了完整的仿真验证流程。文章还提到了多种智能优化算法在无人机路径规划中的应用比较,突出了改进PSO在收敛速度和全局寻优方面的优势。; 适合人群:具备一定Matlab编程基础和优化算法知识的研究生、科研人员及从事无人机路径规划、智能优化算法研究的相关技术人员。; 使用场景及目标:①用于无人机在复杂地形或动态环境下的三维路径规划仿真研究;②比较不同智能优化算法(如PSO、GA、蚁群算法、RRT等)在路径规划中的性能差异;③为多目标优化问题提供算法选型和改进思路。; 阅读建议:建议读者结合文中提供的Matlab代码进行实践操作,重点关注算法的参数设置、适应度函数设计及路径约束处理方式,同时可参考文中提到的多种算法对比思路,拓展到其他智能优化算法的研究与改进中。
@Verify @Override public ResponseVo<List<GoodsSpuPageEsVo>> pageEs(EsPageParamPo pageParam, int page, int size) { if (page < 1) { throw new OSPException(OSPError.PARAM_ERROR, "page必须大于1"); } if (size < 1 || size > 500) { throw new OSPException(OSPError.PARAM_ERROR, "size范围1~100"); } //Integer cusOrgId1 = sysConfigService.getName("SET_DEFAULT_USER_ENTERPRISE_INFO", Integer.class); Pageable pageable = Pageable.newInstance(page, size, pageParam); CustomerEnterprise customerEnterprise = null; String cusEsPathPrefix = null; if (null != pageParam.getCusOrgId()) { customerEnterprise = customerEnterpriseMapper.selectById(pageParam.getCusOrgId()); if (customerEnterprise == null) { log.error("商品使用客群属性es搜索,客群id不存在 {}", pageParam.getCusOrgId()); return ResponseVo.empty(pageable); } cusEsPathPrefix = "cusInfo." + customerEnterprise.getId() + "."; } List<String> cusEsPathPrefixs = new ArrayList<>(); if (CollectionUtils.isNotEmpty(pageParam.getCusOrgIds())) { List<CustomerEnterprise> customerEnterprises = customerEnterpriseMapper.selectList(MCustomerEnterprise.in(MCustomerEnterprise.id, pageParam.getCusOrgIds())); if (CollectionUtils.isEmpty(customerEnterprises)) { log.error("商品使用客群属性es搜索,客群id不存在 {}", pageParam.getCusOrgId()); return ResponseVo.empty(pageable); } customerEnterprises.forEach(enterprise -> { cusEsPathPrefixs.add("cusInfo." + enterprise.getId() + "."); }); } String indexName = sysConfigService.getName(Constants.ES_GOODS_TO_ELASTICSEARCH_INDEX_KEY, String.class, Constants.ES_GOODS_TO_ELASTICSEARCH_INDEX_DEFUALT_VALUE); String minimumShouldMatch = sysConfigService.getName(Constants.ES_MINIMUM_SHOULD_MATCH_KEY, String.class, Constants.ES_MINIMUM_SHOULD_MATCH_DEFUALT_VALUE); HighlightBuilder highlightBuilder = new HighlightBuilder(); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); BoolQueryBuilder boolQuery = QueryBuilders.boolQuery(); if (StringUtils.isNotBlank(pageParam.getGoodsName())) { boolQuery.must(QueryBuilders.multiMatchQuery(pageParam.getGoodsName(), "author", "masterName", "publishYear", "searchTags", "name", "typeName", "publishingHouse") .field("name", 2.0f).operator(Operator.OR).minimumShouldMatch(minimumShouldMatch)); // 配置高亮显示 highlightBuilder.field("name"); highlightBuilder.field("author"); // 设置高亮标签 highlightBuilder.preTags("<span style=\"color:#FF0000;\">"); highlightBuilder.postTags("</span>"); /** 相似度搜索,提高全文搜索的_score评分 */ boolQuery.should(QueryBuilders.matchPhraseQuery("name", pageParam.getGoodsName()).slop(6)); boolQuery.should(QueryBuilders.matchPhraseQuery("isbn", pageParam.getGoodsName()).slop(4)); boolQuery.should(QueryBuilders.matchPhraseQuery("author", pageParam.getGoodsName()).slop(2)); boolQuery.should(QueryBuilders.matchPhraseQuery("searchTags", pageParam.getGoodsName()).slop(2)); boolQuery.should(QueryBuilders.matchPhraseQuery("typeName", pageParam.getGoodsName()).slop(2)); boolQuery.should(QueryBuilders.matchPhraseQuery("masterName", pageParam.getGoodsName()).slop(3)); boolQuery.should(QueryBuilders.matchPhraseQuery("publishYear", pageParam.getGoodsName()).slop(3)); boolQuery.should(QueryBuilders.matchPhraseQuery("publishingHouse", pageParam.getGoodsName()).slop(3)); // 设置minimum_should_match参数 boolQuery.minimumShouldMatch(1); } if (StringUtils.isNotBlank(pageParam.getSearchInfo())) { boolQuery.must(QueryBuilders.multiMatchQuery(pageParam.getSearchInfo(), "name", "isbn", "spuId") //TODO 这里重设了 字段的得分权重 .field("name", (float) 2.0).operator(Operator.OR).minimumShouldMatch(minimumShouldMatch)); highlightBuilder.field("name"); /**相似度搜索,提高全文搜索的_score的评分*/ boolQuery.should(QueryBuilders.matchPhraseQuery("name", pageParam.getSearchInfo()).slop(6)); boolQuery.should(QueryBuilders.matchPhraseQuery("isbn", pageParam.getSearchInfo()).slop(2)); boolQuery.should(QueryBuilders.matchPhraseQuery("spuId", pageParam.getSearchInfo()).slop(2)); } if (StringUtils.isNotBlank(pageParam.getLeftName())) { // 配置高亮显示 highlightBuilder.field("name"); // 设置高亮标签 highlightBuilder.preTags("<span style=\"color:#FF0000;\">"); highlightBuilder.postTags("</span>"); boolQuery.must(QueryBuilders.matchQuery("name", pageParam.getLeftName())); // 可选:添加模糊匹配提高召回率 //boolQuery.should(QueryBuilders.matchQuery("name", pageParam.getLeftName())); // 设置minimum_should_match //boolQuery.minimumShouldMatch(0); } String productClassCode = pageParam.getProductClassCode(); if (customerEnterprise != null) { boolQuery.must(QueryBuilders.termQuery(cusEsPathPrefix + "canGoods", "true")); boolQuery.must(QueryBuilders.termQuery(cusEsPathPrefix + "canSupplierCode", "true")); if (!productClassCode.equals(ProductClassCodeEnum.VIRTUAL.code)) { boolQuery.must(QueryBuilders.termQuery(cusEsPathPrefix + "canType", "true")); } } if (CollectionUtils.isNotEmpty(cusEsPathPrefixs)) { BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); cusEsPathPrefixs.forEach(prefix -> { // 为每个前缀创建一个必须满足所有条件的布尔查询 BoolQueryBuilder prefixQuery = QueryBuilders.boolQuery(); prefixQuery.must(QueryBuilders.termQuery(prefix + "canGoods", "true")); prefixQuery.must(QueryBuilders.termQuery(prefix + "canSupplierCode", "true")); if (!productClassCode.equals(ProductClassCodeEnum.VIRTUAL.code)) { prefixQuery.must(QueryBuilders.termQuery(prefix + "canType", "true")); } // 将每个前缀的查询添加到主查询中作为 should 条件 subBoolQuery.should(prefixQuery); }); // 设置 minimum_should_match 为 1,表示只需满足任意一个前缀的所有条件即可 subBoolQuery.minimumShouldMatch(1); boolQuery.must(subBoolQuery); } if (null != pageParam.getLowPrice() || null != pageParam.getHighPrice()) { RangeQueryBuilder rangeQueryBuilder = null; if (customerEnterprise != null) { rangeQueryBuilder = QueryBuilders.rangeQuery(cusEsPathPrefix + "price"); } else { rangeQueryBuilder = QueryBuilders.rangeQuery("sellPrice"); } if (null != pageParam.getLowPrice()) { rangeQueryBuilder.gte(pageParam.getLowPrice()); } if (null != pageParam.getHighPrice()) { rangeQueryBuilder.lte(pageParam.getHighPrice()); } boolQuery.must(rangeQueryBuilder); } if (null != pageParam.getCreateTimeStart() || null != pageParam.getCreateTimeEnd()) { RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("createTimeMillis"); if (null != pageParam.getCreateTimeStart()) { rangeQueryBuilder.gte(pageParam.getCreateTimeStart()); } if (null != pageParam.getCreateTimeEnd()) { rangeQueryBuilder.lte(pageParam.getCreateTimeEnd()); } boolQuery.must(rangeQueryBuilder); } if (null != pageParam.getUpdateTimeStart() || null != pageParam.getUpdateTimeEnd()) { RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("updateTimeMillis"); if (null != pageParam.getUpdateTimeStart()) { rangeQueryBuilder.gte(pageParam.getUpdateTimeStart()); } if (null != pageParam.getUpdateTimeEnd()) { rangeQueryBuilder.lte(pageParam.getUpdateTimeEnd()); } boolQuery.must(rangeQueryBuilder); } if (null != pageParam.getUpTimeStart() || null != pageParam.getUpTimeEnd()) { RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("upTimeMillis"); if (null != pageParam.getUpTimeStart()) { rangeQueryBuilder.gte(pageParam.getUpTimeStart()); } if (null != pageParam.getUpTimeEnd()) { rangeQueryBuilder.lte(pageParam.getUpTimeEnd()); } boolQuery.must(rangeQueryBuilder); } if (null != pageParam.getStockStart() || null != pageParam.getStockEnd()) { RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery("stock"); if (null != pageParam.getStockStart()) { rangeQueryBuilder.gte(pageParam.getStockStart()); } if (null != pageParam.getStockEnd()) { rangeQueryBuilder.lte(pageParam.getStockEnd()); } boolQuery.must(rangeQueryBuilder); } if (null != pageParam.getOnTop()) { boolQuery.must(QueryBuilders.termQuery("onTop", pageParam.getOnTop())); } if (null != pageParam.getSupplierCodeList() && !pageParam.getSupplierCodeList().isEmpty()) { BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); pageParam.getSupplierCodeList().forEach(one -> { subBoolQuery.should(QueryBuilders.termQuery("supplierCode", one)); }); boolQuery.must(subBoolQuery); } if (null != pageParam.getProductClassCodeList() && !pageParam.getProductClassCodeList().isEmpty()) { BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); pageParam.getProductClassCodeList().forEach(one -> { subBoolQuery.should(QueryBuilders.termQuery("productClassCode.keyword", one)); }); boolQuery.must(subBoolQuery); } if (StringUtils.isNotBlank(pageParam.getProductClassCode())) { boolQuery.must(QueryBuilders.termQuery("productClassCode.keyword", pageParam.getProductClassCode())); // boolQuery.must(QueryBuilders.termQuery("label.count", pageParam.getProductClassCode())); }/* else { boolQuery.must(QueryBuilders.termQuery("productClassCode.keyword", ProductClassCodeEnum.BOOK.code)); }*/ if (null != pageParam.getGoodsTypeIdList() && !pageParam.getGoodsTypeIdList().isEmpty()) { MGoodsType.in(MGoodsType.code, pageParam.getGoodsTypeIdList()).eq(MGoodsType.onDel, DeleteEnum.NO_DELETE.code).eq(MGoodsType.channelId, "3106").eq(MGoodsType.status, YESOrNOEnum.STATUS_1.code); if (StringUtils.isNotBlank(pageParam.getProductClassCode())) { MGoodsType.eq(MGoodsType.productClassCode, pageParam.getProductClassCode()); } else { MGoodsType.eq(MGoodsType.productClassCode, ProductClassCodeEnum.BOOK.code); } List<GoodsType> typeList = goodsTypeMapper.selectList(MGoodsType.build()); Stream<String> typePathList = typeList.stream().map(GoodsType::getPath); BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); typePathList.forEach(one -> { subBoolQuery.should(QueryBuilders.prefixQuery("typePath", one)); }); boolQuery.must(subBoolQuery); } if (null != pageParam.getSpuIds() && !pageParam.getSpuIds().isEmpty()) { BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); pageParam.getSpuIds().forEach(one -> { subBoolQuery.should(QueryBuilders.termQuery("spuId", one)); }); boolQuery.must(subBoolQuery); } if (CollectionUtils.isNotEmpty(pageParam.getBanSpuIds())) { BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery(); pageParam.getBanSpuIds().forEach(one -> { subBoolQuery.should(QueryBuilders.termQuery("spuId", one)); }); boolQuery.mustNot(subBoolQuery); } if (null != pageParam.getTags() && !pageParam.getTags().isEmpty()) { // boolQuery.must(QueryBuilders.matchQuery(channelKey + "tag", pageParam.getTags().stream().collect(Collectors.joining(" "))).minimumShouldMatch("60%")); Map<String, Object> params = Maps.newHashMap(); String scriptStr = ""; for (int i = 0; i < pageParam.getTags().size(); i++) { params.put("to_tag" + i, pageParam.getTags().get(i)); scriptStr += "doc['tags.keyword'].values.contains(params.to_tag" + i + ") "; if (i != (pageParam.getTags().size() - 1)) { scriptStr += "|| "; } } Script script = new Script(Script.DEFAULT_SCRIPT_TYPE, Script.DEFAULT_SCRIPT_LANG, scriptStr, params); boolQuery.must(QueryBuilders.scriptQuery(script)); } if (null != pageParam.getStatus()) { boolQuery.must(QueryBuilders.termQuery("status", pageParam.getStatus())); } if (null != pageParam.getMasterStatus()) { boolQuery.must(QueryBuilders.termQuery("masterStatus", pageParam.getMasterStatus())); } boolQuery.must(QueryBuilders.termQuery("onDel", DeleteEnum.NO_DELETE.code)); boolQuery.must(QueryBuilders.termQuery("masterOnDel", DeleteEnum.NO_DELETE.code)); searchSourceBuilder.query(boolQuery); if (!highlightBuilder.fields().isEmpty()) { searchSourceBuilder.highlighter(highlightBuilder); } searchSourceBuilder.from((page - 1) * size); searchSourceBuilder.size(size); //随机分页查询 放在排序前,否则无效 if (null != pageParam.getIsOpenRandomPage() && pageParam.getIsOpenRandomPage().equals(1)) { Script script = new Script("Math.random()"); ScriptSortBuilder sortBuilder = new ScriptSortBuilder(script, ScriptSortBuilder.ScriptSortType.NUMBER); searchSourceBuilder.sort(sortBuilder); } /**是否默认排序*/ boolean defaultSort = true; if (null != pageParam.getFixedPriceSort()) { defaultSort = false; searchSourceBuilder.sort("fixedPrice", SortTypeEnum.TYPE_1.code.equals(pageParam.getFixedPriceSort()) ? SortOrder.ASC : SortOrder.DESC); } if (null != pageParam.getUpTimeSort()) { defaultSort = false; searchSourceBuilder.sort("upTimeMillis", SortTypeEnum.TYPE_1.code.equals(pageParam.getUpTimeSort()) ? SortOrder.ASC : SortOrder.DESC); } if (null != pageParam.getCreateSort()) { defaultSort = false; searchSourceBuilder.sort("createTimeMillis", SortTypeEnum.TYPE_1.code.equals(pageParam.getCreateSort()) ? SortOrder.ASC : SortOrder.DESC); } if (null != pageParam.getUpdateSort()) { defaultSort = false; searchSourceBuilder.sort("updateTimeMillis", SortTypeEnum.TYPE_1.code.equals(pageParam.getUpdateSort()) ? SortOrder.ASC : SortOrder.DESC); } if (null != pageParam.getRecommendSort()) { defaultSort = false; Map<String, Object> params = Maps.newHashMap(); params.put("to_tag", "推荐"); // Script script=new Script(Script.DEFAULT_SCRIPT_TYPE,Script.DEFAULT_SCRIPT_LANG,"doc['"+channelKey+"tag.keyword'].value==params.to_tag?1:0",params); /**同时存在多个值时候,原来的== 判断就不起作用*/ Script script = new Script(Script.DEFAULT_SCRIPT_TYPE, Script.DEFAULT_SCRIPT_LANG, "doc['tags.keyword'].values.contains(params.to_tag)?1:0", params); ScriptSortBuilder scriptSortBuilder = SortBuilders.scriptSort(script, ScriptSortBuilder.ScriptSortType.NUMBER).order(SortTypeEnum.TYPE_1.code.equals(pageParam.getRecommendSort()) ? SortOrder.ASC : SortOrder.DESC); searchSourceBuilder.sort(scriptSortBuilder); searchSourceBuilder.sort(SortBuilders.fieldSort("onTop").order(SortOrder.DESC).unmappedType("integer")); searchSourceBuilder.sort(SortBuilders.fieldSort("sort").order(SortOrder.ASC).unmappedType("integer")); } if (null != pageParam.getPriceSort()) { defaultSort = false; if (customerEnterprise != null) { searchSourceBuilder.sort(cusEsPathPrefix + "price", SortTypeEnum.TYPE_1.code.equals(pageParam.getPriceSort()) ? SortOrder.ASC : SortOrder.DESC); } else { searchSourceBuilder.sort("sellPrice", SortTypeEnum.TYPE_1.code.equals(pageParam.getPriceSort()) ? SortOrder.ASC : SortOrder.DESC); } } if (null != pageParam.getSaleCntSort()) { defaultSort = false; searchSourceBuilder.sort("spuSaleCnt", SortTypeEnum.TYPE_1.code.equals(pageParam.getSaleCntSort()) ? SortOrder.ASC : SortOrder.DESC); } if (null != pageParam.getPublishTimeSort()) { // 根据出版时间排序 defaultSort = false; FieldSortBuilder fieldSortBuilder = SortBuilders.fieldSort("publishTimeMillis").order(SortTypeEnum.TYPE_1.code.equals(pageParam.getPublishTimeSort()) ? SortOrder.ASC : SortOrder.DESC).unmappedType("number"); searchSourceBuilder.sort(fieldSortBuilder); } if (StringUtils.isNotBlank(pageParam.getLeftName())) { searchSourceBuilder.sort("_score", SortOrder.DESC); } else { if (defaultSort) { if (StringUtils.isNotBlank(pageParam.getGoodsName())) { searchSourceBuilder.sort("_score", SortOrder.DESC); } /**当字段不存在的时候,排序需要指定 默认的字段数据类型 否则会报错*/ searchSourceBuilder.sort(SortBuilders.fieldSort("onTop").order(SortOrder.DESC).unmappedType("integer")); searchSourceBuilder.sort(SortBuilders.fieldSort("sort").order(SortOrder.ASC).unmappedType("integer")); } } log.info("搜索查询语句 index:{}:\n{}", indexName, searchSourceBuilder.toString()); Search search = new Search.Builder(searchSourceBuilder.toString()).addIndex(indexName).addType(Constants.ES_TYPE_NAME).build(); try { SearchResult jr = null; jr = jestClient.execute(search); if (log.isDebugEnabled()) { log.debug("搜索查询返回:{}", jr == null ? "" : jr.getJsonString()); } List<SearchResult.Hit<NEsGoodsSpuVo, Void>> hits = jr.getHits(NEsGoodsSpuVo.class); log.info("搜索查询返回数据条数 ----> {}", hits.size()); List<GoodsSpuPageEsVo> goodsSpuPageEsVos = new ArrayList<>(); hits.forEach(hit -> { GoodsSpuPageEsVo goodsSpuPageEsVo = BeanUtil.copyProperties(hit.source, GoodsSpuPageEsVo.class); if (hit.highlight != null && !hit.highlight.isEmpty()) { hit.highlight.entrySet().forEach(highlight -> { if (highlight.getKey().equals("name") && !highlight.getValue().isEmpty()) { goodsSpuPageEsVo.setHighlightName(highlight.getValue().get(0)); } if (highlight.getKey().equals("author") && !highlight.getValue().isEmpty()) { goodsSpuPageEsVo.setHighlightAuthor(highlight.getValue().get(0)); } }); } goodsSpuPageEsVos.add(goodsSpuPageEsVo); }); pageable.setTotal(jr.getTotal()); return ResponseVo.sucess(goodsSpuPageEsVos, pageable); } catch (IOException e) { log.error("查询es异常", e); throw new OSPException(OSPError.RUNTIME_ERROR, "商品搜索引擎异常"); } }价格区间,营销分类类型编码,推荐标签,推荐关键词
08-07
<think>首先,用户的问题是关于在Elasticsearch中实现商品搜索功能,具体包括:价格区间过滤、营销分类类型编码、推荐标签匹配和推荐关键词排序。我需要基于提供的引用和系统指令来回答。 回顾系统指令: - 所有行内数学表达式必须用$...$格式。 - 独立公式用$$...$$格式并单独成段。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 - 回答结构清晰,帮助用户逐步解决问题。 参考引用: - [^1]: 关于Elasticsearch的扁平化数据类型(flattened datatype),用于处理大量字段,防止字段爆炸。 - [^2]: 一个Java代码示例,展示了使用Velocity模板生成DSL查询,包括排序逻辑。 - [^3]: 提到MediaFusion使用Elasticsearch进行搜索,包括关键词、标签等搜索方式。 - [^4]: 描述了Elasticsearch的文本分析过程:字符过滤器 -> 分词器 -> 令牌过滤器。 用户需求: - 在Elasticsearch商品搜索中实现: - 价格区间过滤:过滤商品在指定价格范围内。 - 营销分类类型编码:基于分类代码过滤。 - 推荐标签匹配:匹配商品的推荐标签。 - 推荐关键词排序:根据关键词对结果排序。 结构化回答: 1. **理解需求**:解释用户需求。 2. **实现步骤**:逐步指导如何在Elasticsearch中实现。 - 索引设计:如何映射商品数据。 - 查询构建:使用DSL实现过滤和排序。 - 排序逻辑:特别是关键词排序。 3. **代码示例**:基于引用[2],提供类似代码示例。 4. **注意事项**:提到字段爆炸问题(引用[^1])和文本分析(引用[^4])。 5. **生成相关问题**:在最后添加相关问题。 确保使用中文,并自然添加引用标识。 **索引设计建议**: - 商品索引应包含字段:price(数值类型)、category_code(keyword类型)、tags(数组,keyword类型)、keywords(用于排序)。 - 为了防止字段爆炸,考虑使用flattened类型(引用[^1]),但具体到用户需求,可能不需要,除非有大量动态字段。 **查询构建**: - **价格区间过滤**:使用range查询。 - **营销分类类型编码**:使用term查询。 - **推荐标签匹配**:使用match或term查询,如果tags是数组。 - **推荐关键词排序**:使用function_score或script_score来提升包含关键词的商品排名。 对于排序,用户提到“推荐关键词排序”,可能意味着根据关键词的相关性排序。可以使用Elasticsearch的function_score查询来调整得分。 基于引用[^2],它使用了Velocity模板生成DSL,我可以提供一个类似的DSL示例。 **完整回答大纲**: 1. **介绍**:简述Elasticsearch在商品搜索中的应用。 2. **索引映射**:建议商品索引的映射。 3. **查询DSL**:详细解释和示例查询。 4. **排序实现**:专门解释关键词排序。 5. **代码示例**:参考引用[^2],提供伪代码或Java示例。 6. **注意事项**:性能考虑(引用[^1])。 7. **相关问题**:生成相关问题。 **生成相关问题**: - 如何优化Elasticsearch查询性能? - 如何处理多条件过滤和排序? - Elasticsearch中如何实现复杂排序逻辑? - 如何防止字段爆炸问题? 确保回答真实可靠,基于Elasticsearch知识。</think>### Elasticsearch 商品搜索实现:价格区间过滤、营销分类类型编码、推荐标签匹配和推荐关键词排序 在Elasticsearch中实现商品搜索功能,需要结合索引设计、查询DSL(Domain Specific Language)和排序逻辑。以下我将逐步解释如何满足您的需求:价格区间过滤(过滤指定价格范围内的商品)、营销分类类型编码(基于分类代码过滤)、推荐标签匹配(匹配商品的推荐标签)和推荐关键词排序(根据关键词对结果排序)。我会结合Elasticsearch的最佳实践和参考引用,提供具体实现方案。 #### 1. **索引设计:定义商品映射(Mapping)** 首先,确保商品索引的映射正确,以支持高效查询。基于您的需求,建议使用以下字段: - `price`:数值类型(如`float`或`integer`),用于价格区间过滤。 - `category_code`:keyword类型,用于精确匹配营销分类类型编码。 - `tags`:数组类型(如`keyword`),存储推荐标签,支持多标签匹配。 - `keywords`:text类型(结合keyword子字段),用于关键词排序和匹配。为防止字段爆炸问题(如引用[^1]所述,大量字段会导致性能下降),避免使用动态映射;如果标签或关键词数量未知,可考虑`flattened`类型[^1]。 示例映射JSON: ```json PUT /goods_index { "mappings": { "properties": { "price": { "type": "float" }, "category_code": { "type": "keyword" }, "tags": { "type": "keyword" }, "keywords": { "type": "text", "fields": { "keyword": { "type": "keyword" } // 用于精确排序 } }, // 其他字段如商品名称、描述等 } } } ``` #### 2. **查询构建:实现过滤和匹配** 使用Elasticsearch的布尔查询(`bool` query)组合多个条件。查询DSL包括: - **价格区间过滤**:使用`range`查询过滤价格。 - **营销分类类型编码过滤**:使用`term`查询精确匹配分类代码。 - **推荐标签匹配**:使用`terms`查询匹配标签数组(允许多标签OR逻辑)。 - **推荐关键词排序**:在查询中嵌入排序逻辑,优先使用`function_score`提升包含关键词的商品得分。 完整查询DSL示例: ```json GET /goods_index/_search { "query": { "bool": { "filter": [ // 过滤不影响得分,适合价格和分类 { "range": { "price": { "gte": 100, // 最低价格 "lte": 500 // 最高价格 } } }, { "term": { "category_code": "electronics" // 营销分类类型编码 } }, { "terms": { "tags": ["recommended", "hot"] // 推荐标签匹配,支持多个标签 } } ], "must": [ // 影响得分的查询部分 { "match": { "keywords": "discount" // 推荐关键词匹配,用于基础得分 } } ] } }, "sort": [ // 排序逻辑 { "_score": "desc" // 先按关键词匹配得分排序 }, { "price": "asc" // 次要排序,如价格升序 } ] } ``` - **解释**: - `filter` 用于价格、分类和标签的精确匹配,高效且不计算得分。 - `must` 用于关键词匹配,影响相关性得分。 - `sort` 优先按关键词匹配得分(`_score`)降序排序,确保推荐关键词靠前;您可添加其他字段如价格作为次要排序。 - 如果推荐关键词需要更智能的排序(如关键词权重),可使用`function_score`查询提升特定关键词的得分(参考引用[^2]中的排序实现)。 #### 3. **关键词排序优化** 对于“推荐关键词排序”,如果需基于多个关键词或自定义权重,使用`function_score`查询增强控制: - 示例:提升包含“recommended”关键词的商品。 ```json "query": { "function_score": { "query": { ... }, // 上述bool查询 "functions": [ { "filter": { "match": { "keywords": "recommended" } }, "weight": 2 // 权重提升 } ], "score_mode": "sum" // 得分累加方式 } } ``` 这能确保匹配推荐关键词的商品在结果中排名更高,类似于引用[^2]中使用的排序技巧[^2]。 #### 4. **完整代码示例(基于Java)** 参考引用[^2]的Velocity模板方法,您可以用Java生成动态DSL。以下是简化示例: ```java import co.elastic.clients.elasticsearch._types.query_dsl.*; import co.elastic.clients.elasticsearch.core.SearchRequest; public PageResult<GoodsEs> searchGoods(GoodsSearchDto dto, Page page) { // 构建bool查询 BoolQuery.Builder boolBuilder = new BoolQuery.Builder(); // 添加过滤条件 boolBuilder.filter(Query.of(q -> q .range(r -> r.field("price").gte(JsonData.of(dto.getMinPrice())).lte(JsonData.of(dto.getMaxPrice()))) )); boolBuilder.filter(Query.of(q -> q .term(t -> t.field("category_code").value(dto.getCategoryCode())) )); boolBuilder.filter(Query.of(q -> q .terms(t -> t.field("tags").terms(t2 -> t2.value(dto.getTags()))) // dto.getTags()为List<String> )); // 添加关键词匹配(影响得分) boolBuilder.must(Query.of(q -> q .match(m -> m.field("keywords").query(dto.getKeyword())) )); // 构建查询 Query query = Query.of(q -> q.bool(boolBuilder.build())); // 设置排序 SortOptions sort = SortOptions.of(s -> s .score(s1 -> s1.order(SortOrder.Desc)) // 按关键词得分降序 ); // 执行搜索 SearchRequest request = SearchRequest.of(s -> s .index("goods_index") .query(query) .sort(sort) .from(page.getOffset()) .size(page.getSize()) ); return goodsMapper.search(request); // 假设goodsMapper处理ES客户端 } ``` - **说明**:此代码基于Elasticsearch Java客户端,动态生成查询。上线后可参考引用[^2]的效果优化性能[^2]。 #### 5. **注意事项** - **性能优化**:为防止字段爆炸(如引用[^1]所述),避免动态字段;使用`flattened`类型处理未知数量标签[^1]。索引时确保文本分析正确(如引用[^4]的流程:字符过滤器 -> 分词器 -> 令牌过滤器)[^4]。 - **测试建议**:在开发环境验证查询响应时间;使用Kibana Dev Tools测试DSL。 - **扩展性**:如果推荐关键词需要语义匹配(如近义词),结合Elasticsearch的全文搜索特性调整分析器。 通过以上步骤,您能高效实现商品搜索的过滤和排序。如果索引数据量大,考虑分片和副本设置提升查询速度。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值