探秘新一代向量存储格式Lance-format (一)Lance 项目概览与设计理念

402 阅读7分钟

第一章:Lance 项目概览与设计理念

核心概览

Lance 是一个面向多模态 AI 工作流的列式数据格式。它解决的根本问题是:如何在统一的存储格式中,高效地支持向量搜索、随机访问、SQL 查询和多模态数据存储

想象您在构建一个视频推荐系统:

  • 需要存储视频元数据(标题、描述、标签)
  • 需要存储视频的向量嵌入用于相似度搜索
  • 需要存储原始视频数据(BLOB)
  • 需要在这些数据上执行复杂的 SQL 查询和向量搜索

Lance 就是为这样的场景设计的。

Lance 主架构设计

graph TB
    subgraph "应用层 Application Layer"
        A1["Python API<br/>(PyLance)"]
        A2["Java API<br/>(JNI)"]
        A3["Rust API"]
    end
    
    subgraph "查询引擎层 Query Engine"
        B1["DataFusion 集成<br/>lance-datafusion"]
        B2["SQL 查询<br/>解析与规划"]
        B3["Scanner<br/>查询执行"]
    end
    
    subgraph "数据集层 Dataset Layer"
        C1["Dataset<br/>版本管理"]
        C2["Transaction<br/>事务处理"]
        C3["Fragment<br/>数据分片"]
        C4["Schema<br/>演化管理"]
    end
    
    subgraph "索引层 Index Layer"
        D1["向量索引<br/>IVF/HNSW/FLAT"]
        D2["标量索引<br/>BTree/Bitmap"]
        D3["全文索引<br/>Tantivy"]
    end
    
    subgraph "表管理层 Table Layer"
        E1["Manifest<br/>lance-table"]
        E2["Commit 协议<br/>版本控制"]
        E3["RowID 系统<br/>行地址管理"]
    end
    
    subgraph "存储层 Storage Layer"
        F1["文件格式<br/>lance-file"]
        F2["编码器<br/>lance-encoding"]
        F3["IO 调度<br/>lance-io"]
    end
    
    subgraph "核心层 Core Layer"
        G1["类型系统<br/>lance-core"]
        G2["缓存管理"]
        G3["Arrow 集成<br/>lance-arrow"]
    end
    
    subgraph "对象存储 Object Storage"
        H1["S3"]
        H2["Azure Blob"]
        H3["GCS"]
        H4["本地文件系统"]
    end
    
    A1 --> B1
    A2 --> B1
    A3 --> C1
    B1 --> B3
    B2 --> B1
    B3 --> C1
    C1 --> C2
    C1 --> C3
    C1 --> C4
    C1 --> D1
    C1 --> D2
    C3 --> E1
    C2 --> E2
    C1 --> E3
    D1 --> F1
    D2 --> F1
    D3 --> F1
    E1 --> F1
    E2 --> F1
    E3 --> F1
    F1 --> F2
    F1 --> F3
    F2 --> G1
    F3 --> G1
    G1 --> G3
    F3 --> H1
    F3 --> H2
    F3 --> H3
    F3 --> H4

核心功能时序图

1. 数据写入流程

sequenceDiagram
    participant User
    participant Dataset
    participant Transaction
    participant Writer
    participant Encoder
    participant ObjectStore
    participant Manifest
    
    User->>Dataset: write_dataset(data, uri)
    Dataset->>Transaction: begin_transaction()
    Transaction->>Writer: create_writer(schema)
    
    loop 每个 RecordBatch
        Writer->>Encoder: encode_batch(batch)
        Encoder->>Encoder: 选择编码方式
        Encoder->>ObjectStore: write_data_file()
        ObjectStore-->>Encoder: 确认写入
    end
    
    Writer->>Dataset: 返回 Fragments
    Dataset->>Manifest: create_new_version()
    Manifest->>ObjectStore: write_manifest()
    Dataset->>Transaction: commit()
    Transaction->>ObjectStore: 原子性提交
    Transaction-->>User: 返回新版本

2. 数据扫描流程

sequenceDiagram
    participant User
    participant Scanner
    participant Dataset
    participant Fragment
    participant FileReader
    participant Decoder
    participant Cache
    
    User->>Scanner: scan().project([columns])
    Scanner->>Scanner: build_execution_plan()
    Scanner->>Dataset: get_fragments()
    Dataset-->>Scanner: Fragment列表
    
    loop 每个 Fragment
        Scanner->>Fragment: open_reader()
        Fragment->>FileReader: create_reader()
        FileReader->>Cache: check_cache()
        
        alt 缓存命中
            Cache-->>FileReader: 返回缓存数据
        else 缓存未命中
            FileReader->>Decoder: decode_page()
            Decoder->>Decoder: 解压与解码
            Decoder-->>FileReader: RecordBatch
            FileReader->>Cache: store_cache()
        end
        
        FileReader-->>Scanner: RecordBatch
    end
    
    Scanner-->>User: Stream<RecordBatch>

3. 向量索引创建流程

sequenceDiagram
    participant User
    participant Dataset
    participant IndexBuilder
    participant KMeans
    participant Quantizer
    participant IVFStorage
    participant ObjectStore
    
    User->>Dataset: create_index(column, IVF_PQ)
    Dataset->>IndexBuilder: build_index()
    IndexBuilder->>Dataset: scan_vectors()
    Dataset-->>IndexBuilder: 向量数据流
    
    IndexBuilder->>KMeans: train_centroids(vectors, k)
    KMeans->>KMeans: 迭代聚类
    KMeans-->>IndexBuilder: 质心列表
    
    IndexBuilder->>Quantizer: train_pq(vectors)
    Quantizer->>Quantizer: 学习 codebooks
    Quantizer-->>IndexBuilder: PQ 模型
    
    IndexBuilder->>IndexBuilder: assign_to_partitions()
    
    loop 每个分区
        IndexBuilder->>Quantizer: quantize_vectors()
        Quantizer-->>IndexBuilder: PQ codes
        IndexBuilder->>IVFStorage: write_partition()
        IVFStorage->>ObjectStore: 持久化
    end
    
    IndexBuilder->>Dataset: update_index_metadata()
    Dataset->>ObjectStore: 更新 Manifest
    Dataset-->>User: 索引创建完成

4. 向量搜索流程

sequenceDiagram
    participant User
    participant Scanner
    participant VectorIndex
    participant IVF
    participant Quantizer
    participant PostFilter
    participant Ranker
    
    User->>Scanner: nearest(query, k=10)
    Scanner->>VectorIndex: search(query_vector)
    
    VectorIndex->>IVF: compute_distances_to_centroids()
    IVF-->>VectorIndex: 排序的分区列表
    
    VectorIndex->>VectorIndex: select_nprobes(nprobes)
    
    loop 对于每个选中的分区
        VectorIndex->>IVF: load_partition()
        IVF->>Quantizer: approximate_distances()
        Quantizer-->>IVF: 近似距离列表
        IVF->>Ranker: add_candidates()
    end
    
    Ranker->>Ranker: top_k_selection()
    Ranker->>PostFilter: refine_distances()
    PostFilter->>PostFilter: 计算精确距离
    PostFilter-->>Scanner: 最终 Top-K 结果
    Scanner-->>User: RecordBatch with scores

5. 事务提交流程

sequenceDiagram
    participant Client
    participant Transaction
    participant CommitHandler
    participant DynamoDB
    participant ObjectStore
    participant Manifest
    
    Client->>Transaction: commit_changes()
    Transaction->>Transaction: validate_operations()
    Transaction->>Manifest: build_new_manifest()
    Manifest-->>Transaction: manifest_data
    
    Transaction->>CommitHandler: begin_commit()
    
    alt 使用 DynamoDB 锁
        CommitHandler->>DynamoDB: acquire_lock(dataset_id)
        DynamoDB-->>CommitHandler: lock_acquired
    else 使用文件锁
        CommitHandler->>ObjectStore: create_lock_file()
        ObjectStore-->>CommitHandler: lock_acquired
    end
    
    CommitHandler->>ObjectStore: write_manifest(version_n)
    ObjectStore-->>CommitHandler: write_success
    
    CommitHandler->>ObjectStore: update_latest_pointer()
    ObjectStore-->>CommitHandler: update_success
    
    alt 使用 DynamoDB 锁
        CommitHandler->>DynamoDB: release_lock()
    else 使用文件锁
        CommitHandler->>ObjectStore: delete_lock_file()
    end
    
    CommitHandler-->>Transaction: commit_success
    Transaction-->>Client: 返回新版本号

第一部分:Lance 的定位与应用场景

What - Lance 是什么?

定义:Lance 是一个 Apache 许可的、开源的列式数据格式和表格式(Lakehouse Format)。

核心特点:

  • 列式存储:按列而非行存储数据,适合分析型查询
  • 向量原生支持:内置向量索引和搜索能力
  • 多模态数据:支持图像、视频、音频、文本、向量等混合存储
  • ACID 事务:支持原子性更新和版本控制

Why - 为什么需要 Lance?

问题背景

传统数据格式存在的痛点:

功能需求ParquetORCIcebergLance
随机访问速度⭐⭐⭐⭐⭐⭐⭐
向量搜索支持
多模态支持
Schema 演化困难困难
版本控制需要外部系统需要外部系统

为什么选择 Lance?

  1. 100 倍随机访问速度提升:Parquet 为 100ns/随机访问,Lance 为 1ns,对于 ML 训练至关重要
  2. 向量搜索加速:内置 IVF-PQ、HNSW 等索引,毫秒级查询
  3. 零拷贝版本控制:无需数据复制即可实现版本管理和时间旅行
  4. 多模态一体化:在同一格式中存储结构化数据和非结构化数据

How - Lance 如何解决这些问题?

核心设计原则
┌─────────────────────────────────────────────────────────┐
│         Lance 的四大设计支柱                               │
├─────────────────────────────────────────────────────────┤
│ 1️⃣ 高效的列式存储                                        │
│    → 通过多种编码(Bitpacking、Dict、RLE)最小化存储    │
│    → 支持快速列扫描和随机访问                            │
│                                                          │
│ 2️⃣ 向量原生能力                                         │
│    → 内置向量索引和量化技术                             │
│    → 支持混合搜索(向量+SQL)                            │
│                                                          │
│ 3️⃣ 灵活的版本管理                                       │
│    → Manifest + Fragment 设计                           │
│    → 无锁并发更新                                       │
│                                                          │
│ 4️⃣ 云原生架构                                          │
│    → 基于对象存储(S3/GCS/Azure)                       │
│    → 分布式计算友好                                     │
└─────────────────────────────────────────────────────────┘

第二部分:与其他格式的对比优势

Parquet 与 Lance 的差异

场景 1:机器学习训练

# Parquet 的问题:需要顺序读取大部分数据
# 训练一个 1000 万行的数据集,但每次迭代只需要 100 行
# - 读取 Parquet:必须扫描整个文件,性能低

# Lance 的优势:支持随机行访问
dataset = lance.open("features.lance")
# 直接获取第 5000、8000、12000 行,无需全扫描
rows = dataset.take([5000, 8000, 12000])

场景 2:多模态存储

# 需要存储:文本 + 图像向量 + 原始图像
import pandas as pd
import lance

df = pd.DataFrame({
    'text': ['A cat sleeping', 'A dog running', ...],
    'image_vector': [[0.1, 0.2, 0.3, ...], ...],  # 向量嵌入
    'image_blob': [image_bytes_1, image_bytes_2, ...],  # 原始图像
})

# Lance 原生支持这种混合存储
dataset = lance.write_dataset(df, "multimodal.lance")

场景 3:实时特征更新

# 需要不断更新特征值,同时保持版本历史
# Iceberg 需要重写大量文件
# Lance 只需追加新 Fragment + 更新 Manifest

dataset = lance.open("features.lance")
dataset.add_column("new_feature", values)  # 原子性添加
# 自动版本控制,可回溯到任何历史版本

与 Iceberg 的差异

方面IcebergLance
设计目标数据湖、表管理向量搜索、ML 工作流
向量支持无原生支持内置索引和量化
随机访问需要额外优化内置优化
学习曲线陡峭平缓
生态支持更完善(Spark、Flink)正在快速发展

第三部分:整体架构设计思想

分层架构模型

graph TB
    subgraph "应用层 Application"
        A1["Python API"]
        A2["Java API"]
        A3["SQL Interface"]
    end
    
    subgraph "查询引擎层 Query Engine"
        B1["DataFusion"]
        B2["Scanner"]
        B3["Index Planner"]
    end
    
    subgraph "数据管理层 Data Management"
        C1["Dataset"]
        C2["Transaction"]
        C3["Version Control"]
    end
    
    subgraph "索引加速层 Index Acceleration"
        D1["向量索引<br/>IVF/HNSW"]
        D2["标量索引<br/>BTree/Bitmap"]
    end
    
    subgraph "存储编码层 Storage & Encoding"
        E1["File Format"]
        E2["Compression"]
        E3["Encoding"]
    end
    
    subgraph "对象存储 Object Storage"
        F["S3/GCS/Local"]
    end
    
    A1 --> B1
    A2 --> B1
    A3 --> B1
    B1 --> B2
    B1 --> B3
    B2 --> C1
    B3 --> C1
    C1 --> C2
    C1 --> C3
    C1 --> D1
    C1 --> D2
    D1 --> E1
    D2 --> E1
    E1 --> E2
    E2 --> E3
    E3 --> F

核心设计思想解析

1. 模块化与关注点分离

Lance 遵循严格的分层设计:

┌──────────────────────────────────────────────────────────┐
│  应用层:提供用户友好的 API(Python、Java、SQL)         │
├──────────────────────────────────────────────────────────┤
│  查询层:负责查询规划、优化、执行                        │
├──────────────────────────────────────────────────────────┤
│  数据层:Dataset、Transaction、版本管理                  │
├──────────────────────────────────────────────────────────┤
│  索引层:向量索引、标量索引的构建和查询                  │
├──────────────────────────────────────────────────────────┤
│  存储层:文件格式、编码、压缩、IO 调度                   │
├──────────────────────────────────────────────────────────┤
│  基础设施:对象存储、缓存、线程管理                      │
└──────────────────────────────────────────────────────────┘

优势

  • 每层可独立优化和扩展
  • 清晰的接口边界便于理解和维护
  • 便于新功能的添加(如新的索引类型)
2. 异步优先设计

Lance 大量使用 Rust 的异步编程(tokio 运行时):

// 不阻塞 IO 的写入
await dataset.write(data);

// 并发读取多个 Fragment
let futures = fragments
    .iter()
    .map(|f| f.read_async())
    .collect::<Vec<_>>();
futures::future::join_all(futures).await;

为什么?

  • I/O 不再阻塞计算线程
  • 单个线程可处理数千个并发请求
  • 在云环境中表现优异
3. 零拷贝与 Arrow 原生

Lance 与 Apache Arrow 深度集成:

// 数据从文件解码直接变成 Arrow RecordBatch
// 无需中间转换或拷贝
let batch: RecordBatch = decoder.decode().await?;

// RecordBatch 可直接传递给 NumPy、Pandas、Polars
// 共享内存地址,零拷贝
4. 索引透明化

索引对用户完全透明:

# 用户编写简单的查询
results = dataset.search(query_vector, k=10)

# 内部自动选择最优索引
# - 如果有向量索引 → 使用 IVF/HNSW
# - 如果有标量索引 → 使用谓词下推
# - 否则 → 全表扫描

实战场景示例

场景 1:电商推荐系统

需求

  • 存储 1 亿个商品(ID、名称、描述、价格、分类)
  • 每个商品有 768 维向量嵌入
  • 需要基于向量的相似商品推荐
  • 需要基于价格、分类的标量过滤

Lance 解决方案

import lance
import pandas as pd
from lance.vector_search import IVF_PQ

# 1. 初始数据加载
products = pd.read_csv("products.csv")
dataset = lance.write_dataset(products, "products.lance")

# 2. 创建向量索引加速搜索
dataset.create_index("vector", index_type=IVF_PQ(num_partitions=256))

# 3. 推荐查询(向量 + SQL)
query_vector = embed_text("高端运动鞋")
results = dataset.search(
    query_vector,
    k=100,
    where="price < 500 AND category == '鞋类'",  # 标量过滤
    metric="cosine"
)
# 毫秒级返回最相关的 100 个商品

为什么 Lance 优于 Elasticsearch?

  • 更紧凑的存储(编码优化)
  • 更快的向量搜索(PQ 量化)
  • 原生 SQL 支持(无需写 JSON 查询)
  • 更低的学习成本

场景 2:生物信息学分析

需求

  • 存储基因序列数据(超大 BLOB)
  • 存储 DNA 序列的向量化表示
  • 支持相似性搜索和统计分析

Lance 优势

# 存储原始 DNA 序列 + 向量化表示
dna_data = pd.DataFrame({
    'gene_id': ['BRCA1', 'TP53', ...],
    'sequence': [dna_seq_1, dna_seq_2, ...],  # 超大字符串
    'embedding': [vec1, vec2, ...],  # 768 维向量
    'properties': [prop1, prop2, ...]  # 元数据
})

dataset = lance.write_dataset(dna_data, "dna.lance")

# 直接支持混合查询
results = dataset.search(
    query_embedding,
    k=10,
    where="properties.organism == 'human'"
)

核心工作流概览

一个完整的 Lance 使用周期

sequenceDiagram
    participant User
    participant Python API
    participant Dataset
    participant Index
    participant Storage
    
    User->>Python API: 1. 创建数据集
    Python API->>Dataset: write_dataset(data)
    Dataset->>Storage: 写入文件
    
    User->>Python API: 2. 创建索引
    Python API->>Dataset: create_index()
    Dataset->>Index: 构建向量索引
    Index->>Storage: 保存索引
    
    User->>Python API: 3. 执行查询
    Python API->>Dataset: search(query)
    Dataset->>Index: 使用索引加速
    Index->>Storage: 读取数据
    Python API-->>User: 返回结果
    
    User->>Python API: 4. 更新数据
    Python API->>Dataset: add_column()/update()
    Dataset->>Storage: 原子性提交
    Dataset-->>User: 新版本号

关键设计决策总结

决策原因权衡
Rust 实现性能、内存安全开发效率稍低
Async First高并发、云原生编程复杂度增加
列式存储扫描性能、压缩率不适合行级更新
多种编码适应不同数据类型编码/解码开销
向量索引ML 工作流必需索引构建成本
零拷贝设计减少内存占用依赖 Arrow 兼容性

总结

Lance 通过以下创新解决了 AI 时代的数据存储难题:

  1. 性能优先:100 倍随机访问、毫秒级向量搜索
  2. 功能全面:向量、SQL、多模态在一个系统中
  3. 用户友好:自动索引选择、透明的优化
  4. 生产就绪:ACID 事务、版本控制、云支持

在接下来的章节中,我们将深入探讨 Lance 的内部实现细节,从项目结构、数据模型、文件格式、到索引和查询引擎,全面理解这个强大的系统是如何运作的。


关键概念速记

  • 列式存储:按列而非行组织数据
  • 向量索引:加速向量相似性搜索的数据结构
  • IVF-PQ:乘积量化的倒排文件,Lance 中常用的索引方式
  • Manifest:记录数据集版本信息的元数据文件
  • Fragment:数据集中的数据分片单位
  • 零拷贝:数据在内存中传递而无需复制