在构建搜索功能时,"高亮显示"绝对是提升用户体验的"神来之笔"。当用户在海量数据中搜索关键词时,如果结果列表里密密麻麻全是黑字,用户需要逐行阅读去寻找匹配项,这简直是一场灾难。
在 Elasticsearch 8.13.4 中,高亮(Highlighting)机制已经非常成熟,但也隐藏着许多容易被忽视的细节和性能陷阱。本文将带你从入门到精通,玩转 ES 8.13.4 的高亮功能。
一、 核心原理:不仅仅是"标红"
首先要明白,ES 的高亮不是在返回结果后由应用层做的字符串替换,而是在倒排索引检索阶段就已经介入的分析过程。
当你的查询命中文档后,ES 会从原始存储的 _source 或专门的高亮存储中提取文本片段,根据你的查询词进行标记。
基础配置(Hello World 版):
json
GET /my-index-8.13/_search
{
"query": {
"match": {
"content": "elasticsearch 高亮"
}
},
"highlight": {
"fields": {
"content": {}
}
}
}
默认返回结果 :
ES 会默认使用 <em> 和 </em> 包裹关键词,并在结果中增加一个 highlight 字段:
json
"hits": {
"hits": [
{
"_source": { "content": "学习 Elasticsearch 的高亮显示功能..." },
"highlight": {
"content": [
"学习 <em>Elasticsearch</em> 的<em>高亮</em>显示功能..."
]
}
}
]
}
二、 进阶玩法:定制你的"荧光笔"
默认的 <em> 标签可能不符合你的前端 UI 框架(比如 Vue 或 React 可能需要 <span class="red">)。在 8.13.4 中,我们可以深度定制。
1. 自定义标签与样式
如果你不喜欢斜体,想要加粗并带背景色:
json
"highlight": {
"fields": {
"content": {
"pre_tags": ["<mark style='background:#ffeb3b; font-weight:bold;'>"],
"post_tags": ["</mark>"],
"fragment_size": 100
}
}
}
pre_tags/post_tags:支持数组,可定义多组标签实现复杂逻辑(如不同关键词不同颜色,需配合 Encoder,较复杂)。fragment_size:片段长度。默认 100 个字符。太短看不全上下文,太长影响性能。
2. 碎片数量控制 (number_of_fragments)
默认情况下,ES 会返回 5 个高亮片段。如果你只需要最精彩的那一段,或者需要全部展示:
json
"content": {
"number_of_fragments": 1, // 只返回最相关的 1 个片段
"fragment_size": 200
}
3. 边界扫描器 (boundary_scanner) ------ 8.x 推荐
这是 ES 8.x 非常实用的功能。默认按字符截断(chars)可能会切断 HTML 标签或把单词切成两半。使用 sentence 可以按句子截断,体验更好:
json
"content": {
"boundary_scanner": "sentence", // 按句子边界截断
"boundary_max_scan": 10, // 最多向前扫描 10 个字符找边界
"fragment_offset": 0 // 从匹配词开头处开始截取
}
三、 避坑指南:性能与安全的博弈
高亮虽好,但它是内存和 CPU 杀手。因为高亮需要访问原始文本并进行分析,在大文本字段上开启高亮极易引发 OOM。
1. 必须开启 require_field_match
默认情况下,只要 Query 命中了文档,highlight 会对你指定的所有字段(如 content, title, summary)都做高亮处理,哪怕 Query 只查了 content。
这是巨大的浪费!
json
"highlight": {
"require_field_match": true, // 强烈建议开启
"fields": {
"content": {}, // 只有 query 涉及 content 时才高亮它
"title": {}
}
}
2. 警惕 max_analyzed_offset
在 8.13.4 中,为了防止深度分页时的内存爆炸,默认限制了分析的字符数(通常是 1MB)。如果你的文本非常长(如长文章、日志),且关键词在很后面,可能会因为超过限制而导致高亮失败(返回空)。
对策 :如果确实需要对超长文本高亮,需在索引 Mapping 中调整 index.highlight.max_analyzed_offset(但这会增加内存风险)。
3. 统一高亮器 (Unified Highlighter)
从 7.0 开始,ES 废弃了旧的 fast-vector-highlighter,统一使用 Unified Highlighter 。它在索引时不需要额外存储 term_vectors(除非你用 plain 模式),直接利用 _source 或 doc values。
- 优势:索引体积更小。
- 注意 :如果你没存
_source且没开store: true,高亮将无法工作,因为它没原始文本可分析!
4. XSS 安全漏洞
如果你允许用户自定义搜索词,且直接将高亮结果渲染到页面,务必小心 XSS 攻击。
虽然 ES 默认会转义 HTML,但如果你为了显示富文本而设置了 "escape_html": false,攻击者输入 <script>alert(1)</script> 作为搜索词,高亮结果就会执行脚本。
原则:除非万不得已,不要关闭 HTML 转义。
四、 实战案例:电商商品搜索高亮优化
假设我们有一个商品索引,需要对"标题"和"描述"进行高亮。
需求:
- 标题高亮加粗。
- 描述只取一段,且按句子截断,防止截断到标签中间。
- 只有查询命中的字段才高亮。
请求 DSL:
json
GET /products/_search
{
"query": {
"multi_match": {
"query": "智能降噪耳机",
"fields": ["title^3", "description"]
}
},
"highlight": {
"require_field_match": true,
"fields": {
"title": {
"pre_tags": ["<b>"],
"post_tags": ["</b>"],
"number_of_fragments": 1
},
"description": {
"type": "unified",
"boundary_scanner": "sentence",
"fragment_size": 150,
"number_of_fragments": 1
}
}
}
}
前端处理逻辑 :
拿到 highlight.title[0] 或 highlight.description[0] 后,直接使用 v-html (Vue) 或 dangerouslySetInnerHTML (React) 渲染即可。如果高亮字段不存在,则回退显示 _source 中的原始内容(不带高亮)。
总结
Elasticsearch 8.13.4 的高亮功能强大而精细。要用好它,请记住这三句口诀:
- 按需高亮 :开启
require_field_match,别做无用功。 - 保护体验 :长文本用
boundary_scanner: sentence,别把单词切两半。 - 守住底线 :除非你是内部系统且完全信任输入,否则别关
escape_html。
掌握了这些,你的搜索结果就能像黑夜中的萤火虫一样,精准地把用户想要的信息"亮"出来!