向量检索系统最难控制的成本,往往不是生成嵌入,而是让数百万乃至数十亿条 float32 向量长期驻留在索引、磁盘和内存中。针对 Jina v5 嵌入的一项动手测试比较了 Elasticsearch 的 BBQ 与标准 float32 向量索引:在五种语言的数据上测量内存、磁盘和 recall@10,结果显示向量体积最多可缩小约 29 倍,同时没有观察到召回率损失。
BBQ 改变的是索引表示,不是业务接口
标准 dense vector 通常用 float32 保存每个维度。假设嵌入有 D 个维度,单条原始向量仅数值部分就需要约 4 × D 字节,还没有计算 HNSW 图、文档字段和 Lucene 段的开销。
BBQ 可以把用于近似检索的向量压缩为紧凑的二进制表示。这样做的直接收益是减少向量索引的磁盘占用,并降低搜索过程中需要读取和比较的数据量。应用仍然提交浮点查询向量,量化过程由 Elasticsearch 索引层处理。
需要注意,29 倍是这次测试中的实测结果,不应直接当成所有数据集的固定压缩比。最终比例会受到以下因素影响:
- 嵌入维度和向量数量;
- 文档
_source是否保存原始向量; - HNSW 图参数以及 Lucene 段结构;
- Elasticsearch 版本和 BBQ 实现;
- 文本字段、元数据等非向量内容占整个索引的比例。
如果 _source 中仍保存一份完整浮点数组,索引总磁盘占用不会等比例缩小。因此,评估时要同时观察向量索引和整个索引,而不能只看理论上的位宽变化。
为什么要跨五种语言测 recall@10
压缩是否可用,不能只用磁盘数字判断。测试同时覆盖五种语言并检查 recall@10,重点是在不同语言的向量分布下,BBQ 返回的前十个结果是否仍与参考结果一致。
recall@10 可以按下面的方式计算:
recall@10 = |近似搜索 Top 10 ∩ 精确搜索 Top 10| / 10
跨语言测试很重要,因为多语言模型可能在不同语种、文字系统和文本长度上产生不同的向量分布。只在英语样本上调好的量化方案,不一定能代表整个多语言语料库。更可靠的评估集应包含:
- 每种语言的真实查询和相关文档;
- 跨语言查询,例如用中文检索英文文档;
- 短关键词、完整问题和长段落;
- 容易混淆的近邻,而不只是语义差异明显的样本。
“没有召回率损失”也应放在本次数据、查询集和 k=10 的边界内理解。换成更小的 num_candidates、更深的召回位置或不同领域语料后,结果仍需重新测量。
可以这样搭建 float32 与 BBQ 对照实验
下面是一个可直接改造的最小实验。假设 Elasticsearch 版本支持 bbq_hnsw,并且已经准备好 Jina v5 生成的浮点向量。运行前把 DIMS 改成实际嵌入维度,并确认当前版本对 BBQ 的映射语法和维度限制。
先设置地址并创建两个映射完全相同、只有向量索引类型不同的索引:
export ES_URL='http://localhost:9200'
export DIMS='1024' # 改成实际 Jina v5 向量维度
curl -sS -X PUT "$ES_URL/jina-float32" \
-H 'Content-Type: application/json' \
-d "{
\"mappings\": {
\"properties\": {
\"language\": { \"type\": \"keyword\" },
\"text\": { \"type\": \"text\" },
\"embedding\": {
\"type\": \"dense_vector\",
\"dims\": $DIMS,
\"similarity\": \"cosine\",
\"index_options\": { \"type\": \"hnsw\" }
}
}
}
}"
curl -sS -X PUT "$ES_URL/jina-bbq" \
-H 'Content-Type: application/json' \
-d "{
\"mappings\": {
\"properties\": {
\"language\": { \"type\": \"keyword\" },
\"text\": { \"type\": \"text\" },
\"embedding\": {
\"type\": \"dense_vector\",
\"dims\": $DIMS,
\"similarity\": \"cosine\",
\"index_options\": { \"type\": \"bbq_hnsw\" }
}
}
}
}"
准备 docs.ndjson,把同一批文档分别写入两个索引。每份向量必须具有 DIMS 个浮点数:
{"index":{"_index":"jina-float32","_id":"doc-1"}}
{"language":"zh","text":"如何降低向量索引内存","embedding":[0.012,-0.031,0.084]}
上面的三维数组只是格式示意,不能直接用于声明为 1024 维的索引。实际批量写入时,可以从已有 JSONL 数据生成两份 Bulk 请求,或者在客户端中对两个索引复用同一批向量。
完成写入和 refresh 后,对比索引存储量:
curl -sS -X POST "$ES_URL/jina-float32/_refresh"
curl -sS -X POST "$ES_URL/jina-bbq/_refresh"
curl -sS "$ES_URL/_cat/indices/jina-float32,jina-bbq?v&h=index,docs.count,store.size,pri.store.size"
curl -sS "$ES_URL/jina-float32,jina-bbq/_stats/store,segments?human=true"
查询时应固定查询向量、k 和过滤条件,只改变索引。下面的 QUERY_VECTOR 需要替换为 Jina v5 对查询文本生成的完整向量:
export QUERY_VECTOR='[0.012,-0.031,0.084]'
curl -sS -X POST "$ES_URL/jina-bbq/_search" \
-H 'Content-Type: application/json' \
-d "{
\"size\": 10,
\"knn\": {
\"field\": \"embedding\",
\"query_vector\": $QUERY_VECTOR,
\"k\": 10,
\"num_candidates\": 100
},
\"_source\": [\"language\", \"text\"]
}"
用脚本计算 recall@10
下面的 Python 脚本读取两个搜索结果文件,并计算每个查询的 Top 10 重合率。它适合快速验证 BBQ 相对于 float32 基线是否改变了结果集合。
先把两种索引的响应分别保存为 float-results.json 和 bbq-results.json。文件格式假设为一个对象数组,每个对象都是 Elasticsearch _search 的完整响应,而且两个数组中的查询顺序一致。
#!/usr/bin/env python3
import json
from pathlib import Path
K = 10
def load_top_ids(path: str) -> list[list[str]]:
responses = json.loads(Path(path).read_text(encoding="utf-8"))
return [
[hit["_id"] for hit in response["hits"]["hits"][:K]]
for response in responses
]
float_ids = load_top_ids("float-results.json")
bbq_ids = load_top_ids("bbq-results.json")
if len(float_ids) != len(bbq_ids):
raise SystemExit("The result files contain different query counts")
scores = []
for query_no, (expected, actual) in enumerate(zip(float_ids, bbq_ids), start=1):
recall = len(set(expected) & set(actual)) / K
scores.append(recall)
print(f"query={query_no:03d} recall@{K}={recall:.3f}")
mean_recall = sum(scores) / len(scores) if scores else 0.0
print(f"queries={len(scores)} mean_recall@{K}={mean_recall:.4f}")
运行方式:
python3 recall_at_10.py
严格来说,float32 HNSW 也是近似搜索,并不等于精确真值。正式实验应使用暴力相似度计算或受支持的精确检索方式生成 ground truth,再分别计算 float32 HNSW 和 BBQ 的 recall@10。上面的脚本更适合做回归对比,而不是证明绝对召回率。
上线时别只看压缩倍数
BBQ 的价值在大规模部署中才会充分显现,但上线决策至少应同时检查四类指标:索引磁盘、节点内存、查询延迟和召回质量。建议用生产数据抽样建立基线,按语言分别汇总 recall@10,并观察平均值之外的低分位查询。
迁移时可以保留 float32 索引,通过别名或影子流量并行查询 BBQ 索引。逐步提高流量前,应固定模型版本、向量归一化方法、相似度函数和检索参数。任何一个变量同时变化,都会让结果难以归因。
最终检查清单很直接:确认 Elasticsearch 版本支持 BBQ;使用真实维度和真实语料重建索引;清除缓存影响并重复测量;为五种语言分别报告指标;验证跨语言查询;检查 _source 对磁盘的贡献;在可接受的 recall@10 下再调低 num_candidates。29 倍压缩说明 BBQ 值得认真评估,但能否进入生产,仍取决于自己的数据和延迟目标。