【lucene核心】impacts的由来

在 Lucene 的 Impact 概念(出现在 `ImpactsEnum` / `Impact` 对象里)中:

字段 含义

freq 当前 term 在该文档中出现了多少次(即词频 term frequency)。

norm 当前 文档在该字段中的长度因子(即之前 norms 里保存的压缩长度值,用来归一化打分)。


举个例子

文档 7:

  • 字段 `title` 内容:`"lucene lucene search"`

  • 分词后:`lucene`(2 次)、`search`(1 次)

  • 对 term `"lucene"` 的 `Impact`

  • freq = 2(出现了 2 次)

  • norm = 压缩后的字段长度 3(3 个 token)


一句话

  • freq → term 在文档里的出现次数

  • norm → 文档在该字段的总 token 数(压缩后的长度因子)

/** Get the set of competitive freq and norm pairs, ordered by increasing freq and norm. */

public Collection<Impact> getCompetitiveFreqNormPairs() {

List<Impact> impacts = new ArrayList<>();

int maxFreqForLowerNorms = 0;

for (int i = 0; i < maxFreqs.length; ++i) {

int maxFreq = maxFreqs[i];

if (maxFreq > maxFreqForLowerNorms) {

impacts.add(new Impact(maxFreq, (byte) i));

maxFreqForLowerNorms = maxFreq;

}

}

if (otherFreqNormPairs.isEmpty()) {

// Common case: all norms are bytes

return impacts;

}

TreeSet<Impact> freqNormPairs = new TreeSet<>(this.otherFreqNormPairs);

for (Impact impact : impacts) {

add(impact, freqNormPairs);

}

return Collections.unmodifiableSet(freqNormPairs);

}

这段代码的作用是:

把当前 term 所有可能出现的 "最大词频 + 对应 norm" 组合,按 freq 升序、norm 升序整理成一条紧凑的 "上限表"(Impact 列表),

供 Block-Max WAND 等跳表算法 在查询时快速判断 "剩下这些文档再也不可能超过当前阈值",从而提前剪枝、加速 Top-k 检索。


  1. 背景:为什么要 "competitive pairs"

在 Block-Max WAND / MaxScore 算法里,必须知道 "一个块内最大可能的 score 是多少"。

  • 已知 score 只跟 freq(词频)和 norm(字段长度)有关。

  • 因此把 每个 norm level 对应的最大 freq 记录下来 → 形成 `(freq, norm)` 的上限数组。

  • 只要 当前阈值 大于块内最大的 `(freq, norm)` 上限,整个块即可跳过。


  1. 代码拆读

2.1 先把 "简单情况" 处理掉

```java

List<Impact> impacts = new ArrayList<>();

int maxFreqForLowerNorms = 0;

for (int i = 0; i < maxFreqs.length; ++i) {

int maxFreq = maxFreqs[i]; // 对 norm = i 的最大词频

if (maxFreq > maxFreqForLowerNorms) {

impacts.add(new Impact(maxFreq, (byte) i));

maxFreqForLowerNorms = maxFreq; // 保证单调递增

}

}

```

  • `maxFreqs[i]` 保存的是 "norm 为 i 的所有文档里,该 term 出现的 最大频次"。

  • 通过条件 `maxFreq > maxFreqForLowerNorms` 去掉冗余,确保列表严格按 `(freq ↑, norm ↑)` 排序。

2.2 再合并 "其他 norm"

```java

if (otherFreqNormPairs.isEmpty()) {

return impacts; // 全是 byte 值,直接返回

}

TreeSet<Impact> freqNormPairs = new TreeSet<>(otherFreqNormPairs);

for (Impact impact : impacts) {

add(impact, freqNormPairs);

}

return Collections.unmodifiableSet(freqNormPairs);

```

  • `otherFreqNormPairs` 里存的是非 byte 型 norm(罕见)或额外手工添加的组合。

  • 用 `TreeSet` 统一排序、去重,最终得到 全局有序且无冗余 的 competitive 列表。


  1. 一句话总结

`getCompetitiveFreqNormPairs()` 生成的就是 "score 上限表"(Impact 列表),

查询阶段用它来 快速判定某个文档块是否还有竞争力,从而实现 Block-Max WAND 的提前剪枝与加速。

不是"随着 norm 的增加取最大的 freq"那么简单,而是:

> 对每个 norm level,记录该 level 内出现的最大 freq,然后在所有 level 上再做一个"剪枝":只有当某 level 的 maxFreq 比之前所有更低 level 的 maxFreq 都大时,才保留这个 (maxFreq, norm) 对。

这样做的目的:

  • 保证最终列表 按 freq 严格递增,

  • 保证 norm 也单调递增,

  • 让 Block-Max WAND 在"从低打到高"的顺序里,能直接用 最大 freq 作为上限 来剪枝。


举个具体数字

假设:

norm (i) 该 norm 内最大 freq

0 3

1 5

2 4

3 6

代码流程:

  • i=0 → maxFreq=3 > 0 → 保留 (3, 0)

  • i=1 → maxFreq=5 > 3 → 保留 (5, 1)

  • i=2 → maxFreq=4 ≯ 5 → 丢弃

  • i=3 → maxFreq=6 > 5 → 保留 (6, 3)

最终 competitive 列表:

`(3,0), (5,1), (6,3)` ------ 既 freq 递增,norm 也递增。

level 就是 norm 本身的整数值(byte 或 int),

在代码里直接体现为循环变量 `i` 和 `Impact` 构造器里的第二个参数 `norm`:

```java

for (int i = 0; i < maxFreqs.length; ++i) { // i 就是 level

...

impacts.add(new Impact(maxFreq, (byte) i)); // (byte) i 就是 norm level

}

```

  • `maxFreqs` 数组的下标 `i` 就对应 norm level 0、1、2 ...。

  • 因此 level 没有额外存储,它就是 norm 的数值本身。

这是一种 "分层/分级上限(level-wise upper-bound)思想",

在 Lucene 里具体对应 Block-Max WAND / MaxScore 这类 Top-k 查询剪枝算法。


  1. 核心思想一句话

> 把 每级(level = norm 值)内的最大可能得分 提前算成一张 单调递增的上限表,

搜索时 按表从小到大扫描,一旦发现剩余块的上限已经低于当前第 k 个结果的真实得分,就整段跳过,不再计算。


  1. 算法家族

名称 出处 说明

WAND Broder et al. 2003 经典倒排链跳跃算法

Block-Max WAND Ding & Suel 2011 在倒排链的"块"内预存上限,块级跳过

MaxScore Turtle & Flood 1995 类似思路,用于 Lucene 早期

Lucene 8+ 实现 `ImpactsEnum` + `Impact` 把 norm-level 上限 预生成,驱动 Block-Max WAND


  1. 为什么要"分层"(level = norm)
  • norm 越小 → 字段越短 → 相同词频的得分越高

  • 因此把 norm 离散化成若干 level,每级算一次 max freq

  • 对 Block-Max WAND:

只要当前阈值 > level L 的 maxScore,所有 norm ≥ L 的文档都可以一次性跳过,节省大量 I/O 与计算。


  1. 一句话总结

这段代码体现的算法思想就是 "Block-Max WAND 的分层上限剪枝",

通过 norm level + max freq 生成单调递增的 Impact 表,实现 Top-k 查询中的早期终止与跳跃。

在 同一 term、同一词频(freq) 的情况下:

  • norm 越小 → 字段越短 → 该 term 的权重越大 → 得分越高。

因此,

> 在 `Impact` 表中,相同 freq 时,norm 越小,对应的潜在得分反而越高。

这也是 Block-Max WAND 把 `(freq, norm)` 按 freq 升序 + norm 升序 排序后,就能用单调上限剪枝的根本原因:

  • 先处理 低频或长字段(高 norm)的块,它们的得分上限低,容易跳过;

  • 后处理 高频或短字段(低 norm)的块,上限高,需要真正计算。

对于impacts 跳表的高层是低层impacts的和是吧?

不是简单的"求和"关系,而是 "高层 impact 是对自己管辖区间内所有低层 impact 的最大值的上界"。

  • 高层(higher level) 的 `Impact` 只是 该层所覆盖文档块内最大的 `(freq, norm)` 对。

  • 它保证:

任何落在这个块里的文档,其真实 `(freq, norm)` 都不会超过这条高层记录的 `(freq, norm)`。

  • 因此搜索时可以 用高层 impact 直接做块级剪枝,而不必逐条遍历低层。

类比

  • 高层 impact ≈ 块级"最大值"

  • 低层 impacts ≈ 块内真实值

  • 关系是 "max 覆盖"(max bound),而非 "累加"(sum)。

剪枝流程可以拆成 两步:

  1. 高层剪枝:一次跳过整块

  2. 低层剪枝:在块内再逐条跳过


  1. 高层剪枝(先发生)
  • 搜索线程维护一个 当前第 k 名的阈值 `minCompetitiveScore`。

  • 从 高层 Impact 表 顺序扫描 `(maxFreq, maxNorm)`。

  • 把 `(maxFreq, maxNorm)` 代入 BM25 公式得到一个 "这块的最高可能得分" `upperBoundScore`。

  • 如果 `upperBoundScore < minCompetitiveScore` → 整块 4 篇文档直接丢弃,指针跳到下一高层块。

```

高层表顺序:(3,0) (5,2) (6,0) (7,1) ...

只要这个上限 < 阈值,整块 0-3 全跳过

```


  1. 低层剪枝(块内发生)
  • 如果 `upperBoundScore ≥ minCompetitiveScore`,说明 块里可能还有候选,就进入该块。

  • 用低层的 `ImpactsEnum` 逐条读真正的 `(freq, norm)`;

  • 每读一条就 立即计算真实得分;

  • 一旦 真实得分 < minCompetitiveScore,就利用低层 跳表指针 跳到下一个可能大于阈值的文档,继续比较。


流程小结

```

高层 Impact 表

├─ block0 上限 < 阈值 → 整块跳过

├─ block1 上限 ≥ 阈值 → 进入块内

│ ├─ doc4 (7,1) 得分 ≥ 阈值 → 保留

│ ├─ doc5 (2,2) 得分 < 阈值 → 跳到下一条

│ └─ ...

└─ ...

```

高层 先快速丢大段,低层 再精准丢小段,这就是 Block-Max WAND 的剪枝节奏。

第 0 层(最底层)跳表也是以 block 为单位来剪枝的 ------ 只不过这里的 block 已经是最细粒度的 "文档块",而不是高层那种更大的逻辑块。

第 0 层的剪枝规则:

  • 第 0 层的每个 block 会提前计算一个 该块内最大的 `(freq, norm)` 对(即该块内最高的 Impact)。

  • 这个 最大值 被用来估算 该 block 的最高可能得分 `upperBoundScore`。

  • 只要这个 `upperBoundScore < minCompetitiveScore`,整个 block 就可以直接跳过;

  • 否则,就必须逐文档处理(因为块内可能有文档得分 ≥ 阈值)。


用之前的例子具体说明:

假设第 0 层有 4 个 block(每块 4 篇文档):

第 0 层 block 该块最大 Impact 估算最高得分

block-0 `(5,2)` `score_upper0`

block-1 `(7,1)` `score_upper1`

block-2 `(6,0)` `score_upper2`

block-3 `(4,3)` `score_upper3`

  • 如果 `minCompetitiveScore = 6.5`,而:

  • `score_upper0 = 5.1 < 6.5` → 整 block-0 直接跳过

  • `score_upper1 = 7.2 ≥ 6.5` → 必须逐文档处理 block-1

  • `score_upper2 = 6.0 < 6.5` → 整 block-2 直接跳过

  • `score_upper3 = 4.8 < 6.5` → 整 block-3 直接跳过


一句话总结

第 0 层同样以 block 为单位做剪枝;

只要某个 最底层 block 的最高可能得分 低于当前阈值,就整块跳过,否则逐文档处理。

private float computeMaxScore(List<Impact> impacts) {

float maxScore = 0;

for (Impact impact : impacts) {

maxScore = Math.max(scorer.score(impact.freq, impact.norm), maxScore);

}

return maxScore;

}

这段代码的用途一句话就能说清:

> 把一个 impacts 列表里所有 `(freq, norm)` 对的"最高分"算出来,作为该段(或该 block)的得分上限。


具体过程

  1. 遍历列表中的每一条 `Impact`

  2. 用 scorer 的公式当场算出这条 `(freq, norm)` 的得分

  3. 取最大值 `maxScore` 返回


在算法流程中的位置

  • 上层(高层)

先调 `computeMaxScore(impacts)` 得到 `maxScore`,

然后与当前 第 k 名的阈值 `minCompetitiveScore` 比较:

  • `maxScore < minCompetitiveScore` → 整块/整段直接跳过

  • 否则才继续逐文档精确打分或更细粒度剪枝。


注意点

  • 这里只是 "估算" 上限,不会真的去遍历文档。

  • 由于 `score(freq, norm)` 是单调函数,列表里最大的 `(freq, norm)` 必然给出最大分,所以只需要比较一次即可。

相关推荐
在未来等你2 天前
Elasticsearch面试精讲 Day 5:倒排索引原理与实现
elasticsearch·搜索引擎·面试·全文检索·lucene·分词·倒排索引
risc1234563 天前
【lucene】 中的impactsenum与impactsdisi有啥区别?
lucene
risc1234563 天前
【lucene】如何评测一款分析器Analyzer
lucene
playStudy6 天前
从0到1玩转 Google SEO
python·搜索引擎·github·全文检索·中文分词·solr·lucene
与火星的孩子对话16 天前
Unity高级开发:反射原理深入解析与实践指南 C#
java·unity·c#·游戏引擎·lucene·反射
risc12345623 天前
【lucene】文档id docid
lucene
risc12345623 天前
【lucene】lucene索引文件的读取器都是单向的只能向前不能后退读
lucene
chenglin0161 个月前
ClickHouse、Doris、OpenSearch、Splunk、Solr系统化分析
clickhouse·solr·lucene
Freed&1 个月前
倒排索引:Elasticsearch 搜索背后的底层原理
大数据·elasticsearch·搜索引擎·lucene