数据湖向量检索: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
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 仍是合适的选择。
| 导入原生 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_search | SQL 优先的团队,在 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 在同一批表上提供低延迟检索。三者读写的是同一份开放格式数据,不存在谁同步谁的问题。
| 层 | 常用工具 | 职责 |
|---|---|---|
| 写入与向量化 | 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、检索。
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。
延伸阅读
- When should you use a Milvus external collection?
- How can Milvus search data stored in Lance?
- How can Milvus search an Apache Iceberg table?
- How do Lance, Iceberg, and Parquet play different roles in a vector lake?
- How should you refresh a Milvus external collection safely?
- How does the Spark-Milvus connector fit into an AI data pipeline?
- How should deletions propagate from a data lake to vector search?
- What is a vector data lake?
常见问题
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 一致。
资料来源与技术文档 (21)
- https://milvus.io/docs/create-an-external-collection.md
- https://milvus.io/docs/storage-v3.md
- https://milvus.io/docs/alter-external-collection-schema.md
- https://milvus.io/docs/tiered-storage-overview.md
- https://milvus.io/docs/snapshots.md
- https://milvus.io/docs/integrate_with_spark.md
- https://milvus.io/api-reference/pymilvus/v3.0.x/MilvusClient/Collections/refresh_external_collection.md
- https://milvus.io/docs/release_notes.md
- https://milvus.io/docs/import-data.md
- https://milvus.io/blog/announcing-milvus-3-lake-native-vector-search-and-a-more-powerful-retrieval-engine.md
- https://github.com/milvus-io/milvus/blob/v3.0.3/pkg/util/externalspec/specutil/spec.go
- https://github.com/milvus-io/milvus-docs/tree/v3.0.x/site/en
- https://docs.lancedb.com/lance
- https://docs.zilliz.com/docs/external-collection-limits
- https://github.com/milvus-io/milvus
- https://github.com/lance-format/lance
- https://docs.lancedb.com/indexing/vector-index
- https://hudi.apache.org/blog/2026/07/06/bringing-vector-search-to-the-lakehouse-with-apache-hudi/
- https://juejin.cn/post/7684064249389105204
- https://forum.mirrorship.cn/t/topic/21772
- https://github.com/lancedb/lancedb/blob/main/LICENSE