⚠️ 此页面为自动翻译,翻译可能不完美。
blog-post

如何使用 bigram_index 加速短语搜索

TL;DR

bigram_index 可用于多种用途,本文重点聚焦于短语搜索性能:在下方的 1M 文档基准测试中,bigram_index='all' 将 QPS 提高了约 2.9x,并将平均短语查询延迟降低了约 3.2x

如果你的主要问题是将 xt850xt 850 匹配,而不是加速短语搜索,请参阅 如何让 xt850 匹配 xt 850

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

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

这正是 bigram_index 的用途。

bigram 索引实际上做了什么

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

  • noise cancelling
  • cancelling headphones

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

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

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

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

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

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

模式 1:默认行为

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

使用场景:

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

示例

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

bigram_index = all

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

使用场景:

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

示例

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

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 通过 --dumpdict ... --stats 导出字典统计信息,查看最频繁的标记,然后从这些结果中构建一个小型的 bigram_freq_words 列表。

示例

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

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

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

使用场景:

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

使用相同的列表:

  • of the 有资格
  • for iphone 没有资格
  • the dragon 没有资格

示例

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 核心上的功能变化。

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

基准测试设置

无 bigram 的数据加载:

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 的数据加载:

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 的搜索基准测试:

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

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

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

我的观察

在此次本地运行中:

QPS平均延迟
bench_bigram_none_rand7551.3 ms
bench_bigram_all_rand21750.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

安装Manticore Search

安装Manticore Search