Chapter 04

多向量索引:Qdrant、Vespa、Weaviate

byaldi 把向量塞进本地 pickle 就能 demo,但到了百万页规模必须上向量数据库。问题是:ColPali 一页 1024 个向量——普通 HNSW 直接撑爆。这一章看业界三大解法。

问题规模

100 万页文档 × 1024 向量/页 = 10 亿向量 × 128 维 × 2 byte = 256 GB 存储 查询 = 20 token × 10 亿向量 MaxSim = 不可接受

需要两件事:

  1. 物理存储能放下 10 亿 128 维向量且支持随机访问
  2. 打分过程在高维空间做 MaxSim 近似(两阶段检索)

方案一:Qdrant Multivector

Qdrant 1.10+ 原生支持 multivector——每条 record 存一个矩阵,查询给一个矩阵,引擎自动做 MaxSim。

from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, MultiVectorConfig, MultiVectorComparator

client = QdrantClient("localhost", port=6333)

client.create_collection(
    "colpali-pages",
    vectors_config=VectorParams(
        size=128,
        distance=Distance.COSINE,
        multivector_config=MultiVectorConfig(
            comparator=MultiVectorComparator.MAX_SIM,
        ),
    ),
)
# 写入(每页 1024 个向量作为一个 record)
client.upsert(
    "colpali-pages",
    points=[
        PointStruct(
            id=i,
            vector=page_vecs[i].cpu().float().numpy(),  # (1024, 128)
            payload={"doc": doc_id, "page": page_num, "image": image_b64},
        ) for i in range(N)
    ],
)
# 查询
results = client.query_points(
    "colpali-pages",
    query=q_vecs.squeeze().cpu().float().numpy(),   # (n_tokens, 128)
    limit=5,
).points
优点
API 直观、Rust 实现、Binary Quantization/Scalar Quantization 开箱即用,配合可省 4-32 倍空间。
缺点
纯 MaxSim 全量扫描仍贵,高召回场景推荐配合下文"两段式"。

方案二:Vespa Tensor + ColBERT ranking

Vespa 是老牌搜索引擎(Yahoo 出品),ColBERT 论文作者原班人马给它做了 ColPali 集成。优点是内置两阶段检索:粗筛 + MaxSim 精排。

<!-- schemas/page.sd -->
schema page {
  document page {
    field embedding type tensor<bfloat16>(patch{}, v[128]) {
      indexing: attribute
    }
    field quant_emb type tensor<int8>(patch{}, v[16]) {
      indexing: attribute | index
    }
  }
  rank-profile colpali {
    inputs {
      query(qt) tensor<float>(querytoken{}, v[128])
    }
    first-phase {
      expression: // 粗筛:binary HNSW
        max(unpack_bits(attribute(quant_emb)) * query(qt))
    }
    second-phase {
      expression: // 精排:full MaxSim
        sum(reduce(sum(query(qt) * attribute(embedding), v), max, patch), querytoken)
      rerank-count: 100
    }
  }
}

这个 schema 存了两种 embedding:binary(16 字节/patch)和 bfloat16(256 字节/patch),第一阶段粗筛极快、第二阶段 top-100 全量算 MaxSim 精排。

方案三:Weaviate 1.28+

2025 年初 Weaviate 加入 multivector 支持,API 类似 Qdrant,对 Python 开发者友好:

import weaviate
from weaviate.classes.config import Configure, Property, DataType

client = weaviate.connect_to_local()

client.collections.create(
    "ColPaliPage",
    vectorizer_config=Configure.Vectorizer.none(),
    vector_index_config=Configure.VectorIndex.hnsw(
        multi_vector=Configure.VectorIndex.MultiVector.multi_vector(),
        quantizer=Configure.VectorIndex.Quantizer.rq(),   # Rotational Quantization
    ),
    properties=[
        Property(name="doc", data_type=DataType.TEXT),
        Property(name="page", data_type=DataType.INT),
    ],
)

三者横向对比

维度QdrantVespaWeaviate
Multivector 支持✅ 原生(1.10+)✅ 最早✅ 1.28+
二值量化RQ 量化
两阶段检索需要自己拼✅ 内置部分支持
学习曲线简单陡峭中等
部署复杂度Docker 一键需要 ZooKeeper 等Docker 一键
推荐场景< 1000 万页,快速上线> 1 亿页,极致性能TypeScript 生态

存储成本真相

精度每 patch 字节每页 (1024 patch)100 万页总量
float32512512 KB512 GB
bfloat16256256 KB256 GB
int8(Scalar Q)128128 KB128 GB
binary(Q 后)1616 KB16 GB
binary + token pool(下章)161-4 KB1-4 GB
二值化损多少?
实测 nDCG@5 从 0.82 降到 0.79(ViDoRe 平均)——对绝大多数业务可以接受,换来 16 倍存储节省。Qdrant 默认支持 binary,建议直接开。

真实性能数字

4 核 vCPU + 32GB 内存的 Qdrant,binary quantization,100 万页索引:

本章小结