数据湖向量检索:Lance 和 Iceberg 上直接建索引,数据不搬

数据湖向量检索,就是直接对已经躺在 Lance 数据集或 Apache Iceberg 表里的向量建索引、提供检索,而不是再导出一份到向量数据库。Milvus 3.0+ 用外部集合(external collection,也就是外部表)做到零拷贝的向量检索:把湖里的列映射成 schema,Milvus 自己建索引,用标准 API 提供向量检索、标量过滤和混合检索,行数据始终留在对象存储里。

  • Milvus 3.0+
  • 支持 Lance、Iceberg、Parquet、Vortex
  • 源数据只读,不拷贝
  • Apache-2.0,GitHub 40K+ star
数据由 Spark、Ray 等写湖任务负责;Milvus 只管映射、manifest 和索引。
数据由 Spark、Ray 等写湖任务负责;Milvus 只管映射、manifest 和索引。

Milvus 外部集合(外部表)是什么?

外部集合是行数据存放在数据湖、而不在 Milvus 里的 collection。external_source 指向 Lance 数据集、Iceberg 表或 Parquet / Vortex 前缀,每个字段的 external_field 写明它读哪一列。数据源对 Milvus 只读,文件不会被改写,也不会被复制。

湖仍然是唯一的数据源(single source of truth),数据工程师照常追加数据、演进 schema、跑分析;Milvus 在上面加一层低延迟的在线检索:ANN 索引、标量过滤、BM25 全文检索和混合检索。一份数据同时服务离线分析和在线召回,这就是向量数据湖的形态。

  • 湖里:权威数据,由 Spark、Ray、DuckDB 或训练任务写入。
  • Milvus 里:字段映射、manifest,向量 / 标量 / JSON / BM25 索引和缓存。
  • API 与普通 collection 一致:search、query、get、混合检索。
  • 不用搭 ETL 同步链路,也没有第二份向量要对齐。

数据湖里的向量能直接检索,还是必须导入向量数据库?

向量已经在湖仓表里,就不必再拷。传统做法是导出到独立的向量数据库:存储翻倍,每次插入、删除、改 schema 都要靠同步链路跟上。外部集合直接在原表上建索引。如果应用本身要通过 Milvus 高频写入向量,导入原生 collection 仍是合适的选择。

湖仓一体向量数据库的两种做法:导入 vs 外部集合
导入原生 collection外部集合(外部表)
数据存放拷进 Milvus 存储留在 Lance / Iceberg / Parquet / Vortex
数据份数两份(湖 + Milvus)一份(湖)
谁来写应用通过 insert、upsert、delete湖上任务(Spark、Ray、Flink、dbt)
数据新鲜度写入即可见refresh_external_collection 之后可见
索引向量、标量、JSON、全文映射字段上的向量、标量、JSON、BM25
数据治理分散在两套系统跟着湖表走

湖上向量检索有哪几种做法?

湖仓一体的向量检索有三种形态,差别在索引放在哪、由谁服务查询。Milvus 走第三种:独立服务层,直接在湖表上建索引、回答在线查询。

湖上向量检索的三种做法:向量索引放在哪、适合什么场景
做法索引在哪例子适合
表格式自带向量列和 SQL 函数表格式自己的服务和查询引擎里Apache Hudi 的 VECTOR 类型 + hudi_vector_searchSQL 优先的团队,在 join、聚合旁边跑批量 top-k
索引文件由表格式 / 文件格式持有与数据文件放在一起,由格式自身的工具构建Paimon 2.0 向量索引,Doris、StarRocks 直查;Lance 数据集自带 IVF_PQ、IVF_HNSW 索引,LanceDB、DuckDB 读取已经在读这张表的分析引擎,批量查询
独立服务层在原表上建索引服务系统里,湖表保持只读Milvus 外部集合(外部表),支持 Lance、Iceberg、Parquet、Vortex面向大量并发客户端的在线检索,带过滤、BM25 和混合检索

Lance 向量检索:怎样用 Milvus 服务 Lance 数据集?

Lance 是面向 AI 数据的开放列式格式,支持随机行访问、数据集版本和对象存储上的列操作。Milvus 可直接映射已有 Lance 列,在检索后按需读取输出字段。将 external_source 指向数据集目录,format 设为 lance-table。

Lance vs Parquet 的差别主要在随机读:Parquet 为顺序扫描优化,Lance 在列式布局之上兼顾按行号取数,更适合检索后回表、训练采样这类随机访问。两种格式 Milvus 都能直接读,不需要为检索专门转换格式。

  • 照常用 Lance 的 Python、Spark 或 Ray 集成写入和追加。
  • Milvus 把数据集的 fragment 映射进 manifest,并在自己的存储里建 HNSW、IVF 或 DiskANN 索引;不往数据集里写任何东西,Lance 自带的索引和版本原样保留。
  • 新 fragment 写入后 refresh:没变的数据沿用原有 segment 和索引。
  • 多模态湖仓(多模态数据湖)里的图片、文本、向量放同一张表,Milvus 只映射需要的列。

Iceberg 向量检索:怎样给 Apache Iceberg 里的向量建索引?

Iceberg 没有向量类型,向量通常存成 float 的 list 或定长 list;只要长度等于 dim,Milvus 都能按 FLOAT_VECTOR 读取。external_source 指向表的 metadata JSON 文件,并固定一个 snapshot_id,检索就始终对应一个确定的表状态,直到你主动推进。

  • 继续用 Spark、Flink 或任意 Iceberg 引擎写表。
  • Milvus 规划该 snapshot 的数据文件,并处理 position delete。
  • refresh 前把 equality delete 改写成 position delete,例如通过 compaction。
  • 用更新的 metadata 文件和 snapshot_id 调 refresh,即可前进到新快照。

湖上建索引和刷新,怎样保证湖是唯一数据源?

湖上数据的索引和原生 collection 一样,区别只在新鲜度。刷新是显式、异步的:扫描源端元数据、发布新的 manifest,元数据阶段通常 150–250 ms(不含建索引)。刷新完成前,查询继续读旧状态,不会看到改了一半的数据。

需要暴露源表新增的列时,用 add_collection_field 映射过去再 refresh,不必重建集合。外部字段还可以作为 Function 的输入,在 Milvus 内生成 BM25 稀疏向量或文本 embedding,源表不用多写一列。

外部表向量检索的生命周期
步骤Milvus 做什么对湖的影响
创建保存 schema 和 external_field 映射无
刷新扫描元数据、写 manifest、复用未变的 segment只读
建索引HNSW/AUTOINDEX、标量、JSON、BM25 索引无,索引存在 Milvus 存储里
服务search、query、get、混合检索,按需回源取输出字段只读
演进add_collection_field 映射新列,再 refresh无

数据湖向量检索的成本:存算分离怎么省?

存储成本就是你已经在付的对象存储,加上 Milvus 的 manifest 和索引;原始行和向量都没有第二份,也省掉了同步链路的计算和运维。计算与存储分离(存算分离),QueryNode 按查询负载配,而不是按数据量配。

  • 一份数据,开放格式,放在 S3、GCS、Azure 或 S3 兼容存储上。
  • Tiered Storage 懒加载 segment、预热热点字段,缓存可以比数据小。
  • 对湖的读 IOPS 有限速且可配置,保护共享 bucket。
  • 同一份湖上数据可以同时由多个 Milvus 实例提供服务。

湖仓一体和向量数据库是替代还是互补?

互补。湖仓一体的向量检索按长处分工:批处理引擎(Spark、Ray)负责写入和加工(包括批量生成 embedding),分析引擎(DuckDB、Trino)负责扫描和评估,Milvus 在同一批表上提供低延迟检索。三者读写的是同一份开放格式数据,不存在谁同步谁的问题。

基于 Lance 和 Iceberg 的湖仓向量检索架构
层常用工具职责
写入与向量化Spark、Ray、Flink、PyArrow生成行和向量,写入 Lance 或 Iceberg
存储对象存储上的 Lance 数据集、Iceberg 表唯一数据源,带版本
分析DuckDB、Spark SQL、Trino在同一批文件上扫描、join、评估
在线检索Milvus 外部集合ANN、过滤、全文和混合检索
回流Milvus Snapshot、Spark-Milvus connector批处理任务读取 Milvus collection

怎样用 Milvus 检索 Lance 数据集或 Iceberg 表?

先开启 Storage V3(common.storage.useLoonFFI: true),再创建指向湖上数据源的 schema,用 external_field 映射字段,然后 refresh、建索引、load、检索。

python
import json, os, time
from pymilvus import DataType, MilvusClient

client = MilvusClient(uri="http://localhost:19530", token="root:Milvus")
extfs = {
    "cloud_provider": "aws",
    "region": "us-east-1",
    "access_key_id": os.environ["AWS_ACCESS_KEY_ID"],
    "access_key_value": os.environ["AWS_SECRET_ACCESS_KEY"],
}

def lake_collection(name, source, spec):
    schema = client.create_schema(external_source=source, external_spec=json.dumps(spec))
    # Map Milvus fields to existing source columns.
    schema.add_field(field_name="id", datatype=DataType.INT64, external_field="doc_id")
    schema.add_field(field_name="emb", datatype=DataType.FLOAT_VECTOR, dim=1024, external_field="embedding")
    schema.add_field(field_name="title", datatype=DataType.VARCHAR, max_length=1024, external_field="title")
    client.create_collection(collection_name=name, schema=schema)

# Lance: point at the dataset directory.
lake_collection("docs_lance", "s3://lake/docs.lance",
                {"format": "lance-table", "extfs": extfs})

# Iceberg: point at the table metadata file and pin a snapshot.
lake_collection("docs_iceberg", "s3://lake/docs/metadata/v1.metadata.json",
                {"format": "iceberg-table", "snapshot_id": 123456789, "extfs": extfs})

name = "docs_iceberg"
job_id = client.refresh_external_collection(collection_name=name)
deadline = time.monotonic() + 600
while (p := client.get_refresh_external_collection_progress(job_id=job_id)).state != "RefreshCompleted":
    if time.monotonic() > deadline:
        raise TimeoutError("External collection refresh timed out; inspect the job before retrying")
    if p.state == "RefreshFailed":
        raise RuntimeError(p.reason)
    time.sleep(1)

index_params = client.prepare_index_params()
index_params.add_index(field_name="emb", index_type="HNSW", metric_type="COSINE")
client.create_index(collection_name=name, index_params=index_params)
client.load_collection(collection_name=name)

hits = client.search(collection_name=name, data=[embed("rotate credentials")],  # your 1024-d embedding
                     anns_field="emb", limit=5, output_fields=["title"])
print(hits)

format 取值:lance-table、iceberg-table、parquet、vortex。snapshot_id 只对 iceberg-table 生效。extfs 也支持 AWS IAM、Milvus 全局凭证和 IAM Role ARN;S3 兼容存储用 endpoint_url。

常见问题

Iceberg 表能直接做向量检索吗?

可以。Milvus 3.0+ 创建外部集合,设置 "format": "iceberg-table",指向表的 metadata JSON 文件和 snapshot_id,然后 refresh、建索引、检索。存成 float list 的向量映射为 FLOAT_VECTOR。

湖上向量检索有哪几种做法?

三种:表格式自带向量列和 SQL 函数(如 Hudi);索引文件由表格式持有、分析引擎直查(Paimon 向量索引 + Doris/StarRocks,Lance 自带索引 + LanceDB/DuckDB);独立服务层在原表上建索引(Milvus 外部集合)。在线高并发、要过滤和混合检索,选第三种。

Lance 自带向量索引,为什么还要用 Milvus 建索引?

Lance 数据集可以自带 IVF_PQ、IVF_RQ、IVF_HNSW_SQ、IVF_HNSW_FLAT 索引,由 LanceDB 在进程内查询。Milvus 在自己的存储里另建 HNSW、IVF 或 DiskANN 索引,一个集群可以把同一份数据集服务给大量客户端,并叠加过滤、BM25 和混合检索。

LanceDB 和 Milvus 对比,怎么选?

两者都可以把数据放在 Lance 里。LanceDB OSS 是嵌入式库,在进程内查询数据集;Milvus 是分布式服务,能给 Lance、Iceberg、Parquet、Vortex 数据源建索引,并对多个客户端提供检索。

外部表做向量检索,湖表更新后索引怎么刷新?

湖上数据变化后调用 refresh_external_collection。它是异步、增量的:没变的数据沿用原有 segment 和索引,刷新完成前查询继续读旧状态。

Lance、Iceberg、Parquet,向量该放哪种格式?

Milvus 三种都能读。需要快速随机读、版本管理、频繁加列的 AI 数据集适合 Lance;Iceberg 在 Parquet 数据文件之上提供表级快照、schema 演进和 SQL 引擎的治理;只用 Parquet 则工具生态最广。

想用全托管的湖上检索?

Zilliz Cloud 是全托管的 Milvus 服务,以 Public Preview 形式提供 Lance、Iceberg、Parquet、Vortex 外部集合,API 与 MilvusClient 一致。