# 如何使用 bigram_index 加速短语搜索

使用 bigram_index 加速 Manticore Search 中短语查询的实用指南，清晰解释 all、first_freq、both_freq 的用法，并附带可复现的 manticore-load 基准测试。

## TL;DR

[bigram_index](https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Low-level_tokenization#bigram_index) 可用于多种用途，本文重点聚焦于短语搜索性能：在下方的 1M 文档基准测试中，`bigram_index='all'` 将 QPS 提高了约 `2.9x`，并将平均短语查询延迟降低了约 `3.2x`。

如果你的主要问题是将 `xt850` 与 `xt 850` 匹配，而不是加速短语搜索，请参阅 [如何让 xt850 匹配 xt 850](/blog/how-to-make-searches-like-xt850-match-xt-850/)。

短语搜索可能非常耗时。即使查询很短，引擎仍需验证顺序和邻近性，而当以下情况发生时，这种工作量会更加明显：

- 单个词是常见词
- 数据集很大
- 短语查询在你的工作负载中很频繁

这正是 [bigram_index](https://manual.manticoresearch.com/Creating_a_table/NLP_and_tokenization/Low-level_tokenization#bigram_index) 的用途。

## bigram 索引实际上做了什么

通常，像 `"noise cancelling headphones"` 这样的短语会被处理为需要按正确顺序和相邻出现的独立标记。bigram 索引允许 Manticore 预先存储相邻标记对，例如：

- `noise cancelling`
- `cancelling headphones`

这为引擎在短语匹配期间更快地缩小候选文档提供了途径。

本文专门聚焦于短语加速。

## 重要注意事项：bigram 在分词级别工作

这是在你只关注加速路径时容易忽略的部分。

`bigram_index` 仅在分词级别工作。它不考虑后续的转换，如词形变化、词形或停用词，而这些可能会显著改变短语匹配的预期。

实际结论很简单：bigram 对短语速度非常有效，但如果你的索引严重依赖词形变化、词形或停用词，在广泛启用此设置之前，请测试你实际关心的短语行为。

## 模式 1：默认行为

这是基准线。未启用显式的 bigram 索引，因此不会存储 bigram 倒排列表。

使用场景：

- 短语搜索很少
- 文档很短
- 你希望使用最轻量的索引路径

### 示例

```sql
DROP TABLE IF EXISTS bi_none_demo;

CREATE TABLE bi_none_demo(title text);

INSERT INTO bi_none_demo VALUES
  (1,'wireless noise cancelling headphones'),
  (2,'noise cancelling microphone'),
  (3,'wireless gaming headset');

SELECT id, title FROM bi_none_demo WHERE MATCH('"noise cancelling"');
```

这是基准行为。查询匹配预期的行，但 Manticore 没有预计算的 bigram 倒排列表来更高效地解决短语。

## 模式 2：`all`

```ini
bigram_index = all
```

这是最激进的短语加速模式。每个相邻的标记对都会被索引为 bigram。

使用场景：

- 精确短语搜索是核心功能
- 短语查询经常包含常见词并产生大量候选
- 你希望获得最强的短语加速
- 你不想调整频繁词列表

### 示例

```sql
DROP TABLE IF EXISTS bi_all_demo;

CREATE TABLE bi_all_demo(title text)
  bigram_index='all';

INSERT INTO bi_all_demo VALUES
  (1,'lord of the rings trilogy'),
  (2,'house of the dragon season 2'),
  (3,'made for iphone charger');

SELECT id, title FROM bi_all_demo WHERE MATCH('"house of the dragon"');
SELECT id, title FROM bi_all_demo WHERE MATCH('"made for iphone"');
```

这里的重要点不是不同的匹配，而是不同的索引策略：`all` 存储每个相邻对，因此短语查询在搜索时可以获得最大数量的 bigram 帮助。

选择 `all` 的原因是在短语搜索变得昂贵时，因为许多文档匹配单个词，Manticore 然后需要进行更多位置验证以确认精确短语。`all` 通过更早地缩小候选来帮助。

## 模式 3：`first_freq`

```ini
bigram_index = first_freq
bigram_freq_words = for, of, the, with
```

此模式仅在第一个标记在你的频繁词列表中时存储对。

使用场景：

- 短语搜索很重要
- 你想要比 `all` 更轻量的替代方案
- 你的数据中的许多短语包含在你自己的语料库中真正频繁的词

使用上述列表：

- `for iphone` 有资格
- `of the` 有资格
- `the dragon` 有资格
- `made for` 没有资格
- `lord of` 没有资格

在生产环境中，不要从记忆中选择 `bigram_freq_words`。从你自己的数据中推导它。一种实用方法是使用 [indextool](https://manual.manticoresearch.com/Miscellaneous_tools#indextool) 通过 `--dumpdict ... --stats` 导出字典统计信息，查看最频繁的标记，然后从这些结果中构建一个小型的 `bigram_freq_words` 列表。

### 示例

```sql
DROP TABLE IF EXISTS bi_first_freq_demo;

CREATE TABLE bi_first_freq_demo(title text)
  bigram_index='first_freq'
  bigram_freq_words='for,of,the,with';

INSERT INTO bi_first_freq_demo VALUES
  (1,'made for iphone charger'),
  (2,'lord of the rings trilogy'),
  (3,'house of the dragon season 2');

SELECT id, title FROM bi_first_freq_demo WHERE MATCH('"made for iphone"');
SELECT id, title FROM bi_first_freq_demo WHERE MATCH('"lord of the"');
```

查询仍然返回预期的行。变化的是哪些对被索引：

- `"made for iphone"` 从 `for iphone` 获益
- `"lord of the"` 从 `of the` 获益

这使得 `first_freq` 在许多有用短语包含常见连接词时成为 `all` 的更轻量替代方案。

## 模式 4：`both_freq`

```ini
bigram_index = both_freq
bigram_freq_words = for, of, the, with
```

这是最狭窄的频率模式。仅当两个标记都在频繁词列表中时，才存储对。

使用场景：

- 你希望最保守的 bigram 足迹
- 你主要关心由在你语料库中高度频繁的词构建的对
- 你在调整大型语料库，不想索引每个相邻对

使用相同的列表：

- `of the` 有资格
- `for iphone` 没有资格
- `the dragon` 没有资格

### 示例

```sql
DROP TABLE IF EXISTS bi_both_freq_demo;

CREATE TABLE bi_both_freq_demo(title text)
  bigram_index='both_freq'
  bigram_freq_words='for,of,the,with';

INSERT INTO bi_both_freq_demo VALUES
  (1,'lord of the rings trilogy'),
  (2,'house of the dragon season 2'),
  (3,'made for iphone charger');

SELECT id, title FROM bi_both_freq_demo WHERE MATCH('"lord of the"');
SELECT id, title FROM bi_both_freq_demo WHERE MATCH('"made for iphone"');
```

查询仍然匹配，但内部选择性不同：

- `"lord of the"` 包含 `of the`，这是 `both_freq` 愿意存储的
- `"made for iphone"` 包含 `for iphone`，这是 `first_freq` 会覆盖但 `both_freq` 不会覆盖的

## 你应该选择哪种性能模式？

本文中的基准测试显示 `all` 可以提供显著的速度提升，但它仍然是单一工作负载上的一个基准测试。

Manticore 自己的文档指出，对于大多数用例，`both_freq` 是最佳模式。这是一个合理的默认值，因为它旨在在短语加速和索引成本之间实现更平衡的权衡。

使用这些模式时：

- 选择 `both_freq` 作为一般短语搜索工作负载的默认起点  
- 选择 `all` 当短语搜索特别重要且您希望获得最强加速，接受更高的索引成本  
- 选择 `first_freq` 当您的数据中许多有用短语涉及常见连接词，且您希望比 `both_freq` 更广泛的结果  
- 选择默认行为当短语加速不重要时  

## 基准测试：bigram索引真的能加速短语搜索吗？  

是的。在一个简单的本地基准测试中，差异很容易测量。  

我使用 `manticore-load` 在同一 Manticore 实例上构建了两个包含 100 万文档的表：  

- 一个没有显式的 `bigram_index` 设置  
- 一个设置为 `bigram_index='all'`  

文档是随机的 60-80 字文本，基准测试重复运行随机的 2 字短语查询。  

为清晰起见，索引和搜索均使用 `--threads=1` 运行。多线程的数值当然会更高，但单线程运行更容易观察单个 CPU 核心上的功能变化。  

```sql
SELECT COUNT(*) FROM bench_bigram_* WHERE MATCH('"<text/2/2>"')
```

### 基准测试设置  

无 bigram 的数据加载：  

```bash
manticore-load \
  --drop \
  --wait \
  --threads=1 \
  --batch-size=1000 \
  --total=1000000 \
  --init="CREATE TABLE bench_bigram_none_rand(title text)" \
  --load="INSERT INTO bench_bigram_none_rand(id,title) VALUES(<increment>,'<text/60/80>')"
```

包含所有 bigram 的数据加载：  

```bash
manticore-load \
  --drop \
  --wait \
  --threads=1 \
  --batch-size=1000 \
  --total=1000000 \
  --init="CREATE TABLE bench_bigram_all_rand(title text) bigram_index='all'" \
  --load="INSERT INTO bench_bigram_all_rand(id,title) VALUES(<increment>,'<text/60/80>')"
```

无 bigram 的搜索基准测试：  

```bash
manticore-load \
  --threads=1 \
  --total=5000 \
  --load="SELECT COUNT(*) FROM bench_bigram_none_rand WHERE MATCH('\\\"<text/2/2>\\\"')"
```

包含所有 bigram 的搜索基准测试：  

```bash
manticore-load \
  --threads=1 \
  --total=5000 \
  --load="SELECT COUNT(*) FROM bench_bigram_all_rand WHERE MATCH('\\\"<text/2/2>\\\"')"
```

### 我的观察  

在此次本地运行中：  

| 表 | QPS | 平均延迟 |  
| --- | ---: | ---: |  
| `bench_bigram_none_rand` | `755` | `1.3 ms` |  
| `bench_bigram_all_rand` | `2175` | `0.4 ms` |  

这大约是 QPS 的 `2.9x` 提升，且在相同 100 万文档工作负载下平均延迟提升了约 `3.2x`。  

使用 `bigram_index='all'` 时索引速度更慢，这是预期的：  

- 无 bigram：约 `45k docs/sec`  
- 使用 `all`：约 `17k docs/sec`  

这种权衡正是为何存在多种模式的原因。  

## 最终结论  

如果您的主要问题是短语搜索性能，请首先将 `bigram_index` 视为加速功能。  

对于大多数实际工作负载，从 `both_freq` 开始并进行测量。如果需要更强的效果且能承担额外的索引成本，请切换到 `all`。当您的短语工作负载高度依赖常见连接词时，请考虑 `first_freq`。
