Ray + LanceDB + Daft 向量数据分析
在大模型时代,向量数据的规模正在爆炸式增长。一个典型的图文检索系统可能需要管理数十亿条 Embedding 向量,同时还要支持高效的过滤、聚合和相似度检索。传统方案要么用 Milvus/Qdrant 等专用向量数据库但缺乏灵活的分析能力,要么用 Spark 做分析但向量检索性能差。本文介绍一套新兴的技术栈:Ray + LanceDB + Daft,兼顾大规模分布式计算与高性能向量存储分析。
一、技术栈概览
| 组件 | 角色 | 核心优势 |
|---|---|---|
| Ray | 分布式计算引擎 | 弹性扩缩容,统一调度 Task/Actor/Data |
| LanceDB | 向量数据库 | 基于 Lance 列式格式,零拷贝访问,内嵌 ANN 索引 |
| Daft | 分布式 DataFrame | 原生运行在 Ray 上,支持多模态列(图片、Embedding) |
三者的协作关系:
┌─────────────────────────────────────────────┐
│ 应用层 │
│ 数据摄入 / ETL / 向量检索 / 分析报表 │
├─────────────────────────────────────────────┤
│ Daft (分布式 DataFrame) │
│ 读取 → 转换 → Embedding → 写入 → 分析 │
├─────────────────────────────────────────────┤
│ Ray (计算调度层) │
│ Task 调度 / Actor 管理 / 内存共享 │
├──────────────────────┬──────────────────────┤
│ LanceDB (向量存储) │ 对象存储 (S3/OSS) │
│ ANN 索引 + 元数据 │ 原始文件 │
└──────────────────────┴──────────────────────┘
二、LanceDB 核心特性
LanceDB 基于 Lance 列式存储格式,与 Parquet 相比有几个关键优势:
-
随机访问 O(1):Lance 格式支持按行号直接定位,不需要扫描整个 Row Group
-
向量索引内嵌:IVF-PQ、HNSW 等 ANN 索引直接存储在数据文件中
-
零拷贝与版本管理:类似 Delta Lake 的 MVCC 机制,支持时间旅行
-
嵌入式部署:无需独立服务进程,直接在 Python 中 import 使用
import lancedb
import numpy as np
import pyarrow as pa连接 LanceDB(本地目录或 S3 路径)
db = lancedb.connect("./my_vector_db")
准备数据:模拟 10000 条 768 维 Embedding
num_rows = 10000
dim = 768data = pa.table({
"id": range(num_rows),
"text": [f"document_{i}" for i in range(num_rows)],
"category": np.random.choice(["tech", "finance", "medical"], num_rows).tolist(),
"vector": [np.random.randn(dim).astype(np.float32).tolist() for _ in range(num_rows)],
})创建表并写入数据
table = db.create_table("embeddings", data, mode="overwrite")
print(f"表创建完成,共 {table.count_rows()} 行")创建 IVF-PQ 向量索引(大数据量时显著加速检索)
table.create_index(
metric="cosine",
num_partitions=16,
num_sub_vectors=48,
vector_column_name="vector"
)
print("向量索引创建完成")
向量相似度检索
# 构造查询向量
query_vector = np.random.randn(dim).astype(np.float32).tolist()
# Top-K 相似度检索,支持元数据过滤
results = (
table.search(query_vector)
.where("category = 'tech'") # 先过滤再检索,减少计算量
.limit(10)
.to_pandas()
)
print(results[["id", "text", "category", "_distance"]].to_string())
三、Daft 分布式 DataFrame
Daft 是专为多模态数据设计的分布式 DataFrame 库,原生支持在 Ray 上运行。与 Pandas 不同,Daft 的列类型支持图片、Embedding、张量等复杂数据。
基础用法
import daft
from daft import col
# 设置 Ray 作为执行后端
daft.context.set_runner_ray()
# 从 LanceDB 的 Lance 文件直接读取(绕过 LanceDB API,直接读底层格式)
df = daft.read_lance("./my_vector_db/embeddings.lance")
# 查看 Schema
print(df.schema())
# 分布式过滤 + 聚合
category_stats = (
df.where(col("category") == "tech")
.agg(
col("id").count().alias("total_count"),
col("id").min().alias("min_id"),
col("id").max().alias("max_id"),
)
.collect()
)
print(category_stats)
分布式 Embedding 生成管道
下面是一个实际场景:从 OSS 读取原始文本,用模型生成 Embedding,写入 LanceDB。
import daft
from daft import col, DataType
import ray
import lancedb
import numpy as np
ray.init(address="auto")
daft.context.set_runner_ray()
# 第一步:读取原始数据
raw_df = daft.read_parquet("s3://my-bucket/raw_texts/*.parquet")
# 第二步:定义 Embedding UDF,在 Ray Worker 上分布式执行
@daft.udf(return_dtype=DataType.list(DataType.float32()))
def generate_embedding(texts: daft.Series):
"""在每个 Ray Worker 上加载模型并批量推理"""
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-base-zh-v1.5")
text_list = texts.to_pylist()
embeddings = model.encode(text_list, batch_size=64, show_progress_bar=False)
return [emb.tolist() for emb in embeddings]
# 第三步:应用 UDF 生成 Embedding
embedded_df = raw_df.with_column(
"vector",
generate_embedding(col("text"))
)
# 第四步:收集结果并写入 LanceDB
result_table = embedded_df.to_arrow()
db = lancedb.connect("./my_vector_db")
db.create_table("production_embeddings", result_table, mode="overwrite")
print("Embedding 管道执行完成")
四、完整管道:端到端示例
将上述组件串联,构建一个完整的"数据摄入 → Embedding → 索引 → 检索"管道:
import ray
import daft
from daft import col, DataType
import lancedb
import numpy as np
import pyarrow as pa
import time
# ========== 配置 ==========
LANCE_DB_PATH = "./production_vector_db"
TABLE_NAME = "product_embeddings"
EMBEDDING_DIM = 768
# ========== 初始化 ==========
ray.init(ignore_reinit_error=True)
daft.context.set_runner_ray()
# ========== 阶段 1:数据摄入与清洗 ==========
print("[阶段1] 数据摄入...")
raw_df = daft.read_parquet("./raw_data/products/*.parquet")
cleaned_df = (
raw_df
.where(col("title").is_null().if_else(daft.lit(False), daft.lit(True)))
.where(col("title").str.length() > 5)
.select(col("product_id"), col("title"), col("description"), col("category"))
)
print(f"清洗后数据量: {cleaned_df.count_rows()}")
# ========== 阶段 2:分布式 Embedding ==========
print("[阶段2] 生成 Embedding...")
@daft.udf(return_dtype=DataType.list(DataType.float32()))
def batch_embed(texts: daft.Series):
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-base-zh-v1.5")
results = model.encode(texts.to_pylist(), batch_size=128, normalize_embeddings=True)
return [r.tolist() for r in results]
embedded_df = cleaned_df.with_column("vector", batch_embed(col("title")))
# ========== 阶段 3:写入 LanceDB ==========
print("[阶段3] 写入 LanceDB...")
arrow_table = embedded_df.to_arrow()
db = lancedb.connect(LANCE_DB_PATH)
table = db.create_table(TABLE_NAME, arrow_table, mode="overwrite")
# 创建向量索引
table.create_index(metric="cosine", num_partitions=32, num_sub_vectors=48)
print(f"写入完成,共 {table.count_rows()} 行,索引已创建")
# ========== 阶段 4:向量检索 ==========
print("[阶段4] 执行检索...")
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-base-zh-v1.5")
query_vec = model.encode("无线蓝牙降噪耳机").tolist()
start = time.time()
results = (
table.search(query_vec)
.where("category = '电子产品'")
.limit(20)
.to_pandas()
)
elapsed = time.time() - start
print(f"检索耗时: {elapsed*1000:.1f}ms")
print(results[["product_id", "title", "category", "_distance"]].head(10))
五、性能优化建议
5.1 LanceDB 写入优化
# 批量写入时使用 add() 追加,避免反复 create_table
for batch in data_batches:
table.add(batch)
# 写入完成后统一创建索引,比边写边建索引快 10 倍以上
table.create_index(metric="cosine", num_partitions=64, num_sub_vectors=96)
5.2 Daft 分区调优
# 根据集群规模调整分区数,一般为 CPU 核数的 2-4 倍
df = daft.read_parquet("s3://bucket/data/", io_config=io_config)
df = df.repartition(128) # 128 个分区,充分利用 Ray 并行度
5.3 Ray 资源配置
# Embedding UDF 需要 GPU 时,声明资源需求
@daft.udf(return_dtype=DataType.list(DataType.float32()))
class GpuEmbedder:
def __init__(self):
import torch
self.device = "cuda" if torch.cuda.is_available() else "cpu"
from sentence_transformers import SentenceTransformer
self.model = SentenceTransformer("BAAI/bge-base-zh-v1.5").to(self.device)
def __call__(self, texts: daft.Series):
results = self.model.encode(texts.to_pylist(), device=self.device)
return [r.tolist() for r in results]
六、何时选择这套技术栈
适合的场景:
- 向量数据量在千万到十亿级别
- 需要同时做向量检索和结构化分析(过滤、聚合、JOIN)
- 团队已有 Ray 集群基础设施
- 数据存储在对象存储(S3/OSS)上,希望避免额外的数据库运维
不适合的场景:
- 需要毫秒级在线检索(考虑 Milvus/Qdrant + 专用部署)
- 数据量小于百万级(直接用 FAISS + Pandas 更简单)
- 需要复杂的向量数据库特性如多向量字段、动态 Schema(考虑 Milvus)
总结
Ray + LanceDB + Daft 这套组合的核心价值在于"统一":用 Ray 统一调度,用 Lance 格式统一存储向量和结构化数据,用 Daft 统一 ETL 和分析。对于需要在大规模向量数据上同时做 ETL、分析和检索的场景,这套方案比传统的"Spark + 向量数据库"组合更轻量、更高效。
推荐标签 :LanceDB Ray Daft 向量数据库 分布式计算 Embedding 大规模数据分析 ANN检索