使用 Lucene 搜索你的 Bean —— Highlighting

作者:来自 Elastic David Pilato

假设我们找到了一些与查询匹配的文档。但我们怎么知道,它具体匹配了文本中的什么位置?让我们尝试将存储的 title、artist 或 genre 中匹配的 token 加粗。

Suggest 和 highlighting 在 UI 中看起来很相似 ------ 两者都会用 <b> 包裹匹配的字母 ------ 但它们并不是同一个调用。

AnalyzingInfixSuggester.lookup(..., highlight=true) 从第二个 dictionary 中返回一个显示字符串 (highlightKey)。chip 的值保持干净(key);加粗 markup 从不会被存储。

UnifiedHighlighter 运行在 track 索引 上,使用刚刚为搜索结果评分的相同 Query 。它读取存储的字段文本并标记 token 的 offset ------ 包括尾随的 PrefixQuery(sincla → <b>Sinclar</b>)。

一个是"帮我编写 query"。另一个是"告诉我这一行为什么匹配"。

高亮我的搜索结果

Highlighting 位于自己的 artifact 中:

xml 复制代码
`

1.  <dependency>
2.    <groupId>org.apache.lucene</groupId>
3.    <artifactId>lucene-highlighter</artifactId>
4.    <version>10.5.1</version>
5.  </dependency>

`AI写代码

WholeBreakIterator 会将完整的存储值作为一个 fragment ------ 对于较短的 metadata 字段,不进行句子切分。使用与 playground 相同的 builder:

java 复制代码
`

1.  Query bob = new BooleanQuery.Builder()
2.    .add(new BoostQuery(new TermQuery(new Term("title", "bob")), 4.0f), BooleanClause.Occur.SHOULD)
3.    .add(new BoostQuery(new PrefixQuery(new Term("title", "bob")), 1.0f), BooleanClause.Occur.SHOULD)
4.    .add(new BoostQuery(new TermQuery(new Term("artist", "bob")), 3.0f), BooleanClause.Occur.SHOULD)
5.    .add(new BoostQuery(new PrefixQuery(new Term("artist", "bob")), 0.75f), BooleanClause.Occur.SHOULD)
6.    // ... genre^2 / album^1.5 / label^1 / comment^0.5 (+ prefixes) ...
7.    .setMinimumNumberShouldMatch(1)
8.    .build();

10.  // Search and retrieve the 10 first hits
11.  TopDocs hits = searcher.search(bob, 10);

13.  UnifiedHighlighter highlighter = UnifiedHighlighter.builder(searcher, analyzer)
14.      .withMaxLength(10_000)
15.      .withBreakIterator(WholeBreakIterator::new)
16.      .build();
17.  Map<String, String[]> hl = highlighter.highlightFields(
18.          new String[]{"title", "artist", "genre", "album", "label", "comment"},
19.          bob, hits);

`AI写代码![](https://csdnimg.cn/release/blogv2/dist/pc/img/runCode/icon-arrowwhite.png)

字段必须设置为 Field.Store.YES,否则 Lucene 无法提取源数据来进行 highlighting。

每个字段都对应一个并行数组 ------ 每个搜索结果对应一个 snippet。缺失或为空的 snippet 表示该字段没有为该文档贡献 markup。将这个 map 作为仅用于显示的 HTML 交给模板;chip 和 URL 仍然使用干净的 Bean。

q=Bob 会在 token 匹配的位置将其包裹起来:

css 复制代码
`

1.  Outro Lugar - <b>Bob</b> Sinclar Remix
2.  Crazy (<b>Bob</b> Sinclar vs. Dimitri Vegas & Like Mike remix)

`AI写代码

即使对于 PrefixQuery 也是如此,尽管高亮的是整个 term,而不仅仅是输入的前缀:

css 复制代码
`Outro Lugar - Bob <b>Sinclar</b> Remix`AI写代码

Lucene 中的工作原理

搜索回答的是"哪些文档包含这个 term?"Highlighting 回答的是"存储的字符串中的什么位置?"两者读取的是相同的倒排索引 ------ 只是向下深入了一层。

还记得我们在上一篇文章中生成的 posting list 吗?

markdown 复制代码
`

1.  artist:bob       →  1, 2, 3
2.  artist:claude    →  4, 5
3.  artist:francois  →  4, 5, 6
4.  artist:marley    →  2
5.  artist:sinclar   →  1, 3
6.  artist:valery    →  6

`AI写代码

实际上,它不仅为每个 term 生成了 posting list,还记录了这些 term 在每个文档中的位置。

Phrase query 的 positions

每个 posting 还会存储 positions:该 term 在该字段 token stream 中的位置序号(从 0 开始)。

例如,考虑 artist 名称"Claude François",它被分词为两个 term:claude 和 francois。这些 term 在文档中的位置如下:

位置 Term
0 claude
1 francois

对于 artist 名称 François "Valéry"(被分词为 francois 和 valery),位置如下:

位置 Term
0 francois
1 valery

对于 "Earth Wind and Fire ",位置如下:

位置 Term
0 earth
1 wind
2 and
3 fire

如果你搜索 francois valery,你会得到文档"François Valéry",因为其中一个查询 term 匹配了该文档。如果你搜索 valery francois,它会返回完全相同的两个文档,因为在简单的 conjunction query 中,term 的顺序并不重要。

如果顺序很重要呢?这种情况下,你需要使用 phrase query 或 span query 来强制要求 term 的顺序。这就是 term positions 发挥作用的地方。

Offsets 标记字符

但 highlighting 还需要再多一步:将 position 映射回存储的 原始字符串(Field.Store.YES)中的字符。

在分析阶段,每个 token 还会携带指向原始字符串的字符 offset ------ start 包含,end 不包含:

markdown 复制代码
`

1.  "Earth Wind and Fire"
2.   earth         → [0, 5)
3.   wind          → [6, 10)
4.   and           → [11, 14)
5.   fire          → [15, 19)

`AI写代码

UnifiedHighlighter 会在存储的值上重新运行相同的 analyzer,保留满足 query 的 token(TermQuery wind,或者匹配 wind 的 PrefixQuery win*),然后使用 <b></b> HTML 标签包裹这些 [start, end) 切片:

css 复制代码
`Earth <b>Wind</b> and Fire`AI写代码

现在,你只需要调整 CSS,使其呈现出你希望的样式即可。

q=wind ------ 在 Earth, Wind & Fire 以及 Ride Like the Wind 等标题中显示橙色的 <b>Wind</b>。

完整 Demo 位于 GitHub:lucene-search-tracks。

原文:Search your beans with Lucene --- Highlighting | David Pilato

相关推荐
HEJOO94 小时前
ElasticSearch 是什么?核心概念与应用场景详解
大数据·elasticsearch·搜索引擎
Elasticsearch5 小时前
猫咪大逃脱:OpenTelemetry、Elastic APM 与 Kibana 告警实战指南
elasticsearch
Elasticsearch5 小时前
Observability:当 YAML 遇上 IConfiguration:OpenTelemetry .NET SDK 的声明式配置
elasticsearch
Elasticsearch20 小时前
Elasticsearch:为自行管理的 Elastic Stack 自定义 TLS 证书
elasticsearch
Elasticsearch1 天前
斯坦福《2026 年 AI 指数报告》与金融服务业的安全现实
elasticsearch
Elasticsearch1 天前
Elasticsearch:安装自行管理的 Elastic Stack 集群
elasticsearch
程序员差不多先生1 天前
Anthropic 用 5 个月重写了 Git:当 AI Agent 把版本控制系统逼到墙角
人工智能·git·elasticsearch·deepseek v4 pro·kimi k3·glm 5.3·qwen3.7 max
Elastic 中国社区官方博客1 天前
Jina AI:Elasticsearch 集成
大数据·运维·人工智能·elasticsearch·搜索引擎·全文检索·jina
草莓熊Lotso1 天前
【C++ 基于微服务的即时通讯系统】Elasticsearch 从安装到 C++ 客户端二次封装全落地
linux·网络·数据库·c++·elasticsearch·微服务
.冰块.2 天前
ELK 日志分析系统实验:部署 Elasticsearch 双节点集群实现 Apache 日志采集与 Kibana 可视化
elk·elasticsearch·apache·日志