Lance v2 Format
В предыдущем уроке мы разобрали архитектуру Lance на уровне dataset’ов: фрагменты, manifest’ы, version log. Теперь спустимся на уровень отдельного data file — формат Lance v2, опубликованный в апреле 2025 (arxiv 2504.15247).
Lance v2 — это радикально другой подход к колоночному хранению по сравнению с Parquet. Его ключевая идея: контейнерный формат без встроенной системы типов и фиксированного набора кодировок. Вместо этого — protobuf “any” messages, адаптивные structural encodings и extensible encoding pipeline.
Parquet определяет фиксированный набор из ~10 кодировок (PLAIN, RLE, DICTIONARY, DELTA_BINARY_PACKED и др.) и жёсткую иерархию типов. Добавление новой кодировки в Parquet требует изменения спецификации формата — процесс, занимающий месяцы через Apache governance. Lance v2 позволяет добавлять произвольные кодировки без изменения спецификации.
Философия контейнерного формата
Lance v2 называет себя “columnar container format” — формат-контейнер, который хранит данные, но не навязывает, как они закодированы:
Parquet (фиксированный)
Parquet: формат определяет систему типов (INT32, INT64, BYTE_ARRAY, FIXED_LEN_BYTE_ARRAY, FLOAT, DOUBLE, BOOLEAN) и фиксированный набор кодировок. Reader обязан поддерживать все кодировки — иначе файл нечитаем.Lance v2 (контейнер)
Lance v2: контейнер не знает о типах данных и кодировках. Кодировки хранятся как protobuf 'any' messages — самоописывающие бинарные блобы. Reader интерпретирует encoding на лету.Это даёт три конкретных преимущества:
Encoding Pipeline
Lance v2 использует двухступенчатый encoding pipeline: сначала logical encoding (компрессия значений), затем structural encoding (физический layout на диске):
Arrow Array (входные данные)
Входные данные: Arrow array (например, Int64Array из 1M значений). Encoding pipeline обрабатывает каждую колонку независимо. Pipeline запускается при записи фрагмента.Encoded Data + Protobuf Descriptor
Результат: закодированные данные + protobuf descriptor кодировки. Descriptor содержит: тип logical encoding, тип structural encoding, параметры (block size, dictionary size). Этот descriptor хранится в column metadata файла.Adaptive Structural Encoding
Ключевое нововведение v2 — адаптивный выбор structural encoding на уровне chunk’ов:
Chunk (~4096 строк)
Encoder анализирует каждый chunk (по умолчанию ~4096 строк) и выбирает оптимальный structural encoding. Решение принимается на основе простых эвристик: null ratio, data width, value distribution.Адаптивность per-chunk означает, что одна колонка может использовать разные structural encodings в разных chunk’ах. Например, колонка description в первых 4096 строках — 100% null (ALL_NULL), в следующих 4096 — заполнена (MINI_BLOCK + offsets). Parquet выбирает encoding per-page, но из фиксированного набора.
Page Layout
Lance v2 data file организован как последовательность pages — но page в Lance v2 отличается от page в Parquet:
Lance v2 Data File
Lance v2 data file: один файл для одной или нескольких колонок в рамках фрагмента. Формат: data pages → column metadata → global metadata → footer. Чтение начинается с footer (как в Parquet), но layout принципиально другой.Protobuf-based Column Metadata
Каждая колонка описывается protobuf message, содержащим информацию о всех chunk’ах:
Column Metadata (protobuf)
Column metadata — центральный элемент self-describing формата. Содержит полное описание всех кодировок всех chunk'ов. Reader, получив column metadata, знает как декодировать каждый байт data section без дополнительных запросов.Protobuf “any” message для encoding descriptor:
message ColumnEncoding {
// Logical encoding type
google.protobuf.Any logical_encoding = 1;
// Structural encoding type
google.protobuf.Any structural_encoding = 2;
// Page offset in data file
uint64 page_offset = 3;
// Compressed size in bytes
uint64 compressed_size = 4;
// Number of rows in chunk
uint32 num_rows = 5;
// Column statistics
ColumnStats stats = 6;
}
message DictionaryEncoding {
uint32 dictionary_size = 1;
uint32 index_bits = 2;
bytes dictionary_data = 3;
}
message MiniBlockEncoding {
uint32 block_size = 1;
uint32 value_width = 2;
CompressionCodec compression = 3;
}
Protobuf any — механизм расширяемости. Если reader v1.0 встречает encoding type, добавленный в v2.0, он видит google.protobuf.Any с неизвестным type URL. Поведение: пропустить chunk, вернуть null array (или fallback к PLAIN, если доступен). Парсер не падает — формат forward-compatible.
Mini-Block Encoding
Mini-block — основной structural encoding, обеспечивающий O(1) random access:
Parquet: чтение строки #42
Parquet page: ~64KB, содержит множество значений. Для доступа к строке N нужно: загрузить page → декодировать ВСЕ значения в page → найти значение #N. Decode — обязателен (RLE, DICTIONARY требуют последовательной обработки).Lance: чтение строки #42
Lance mini-block: данные разбиты на блоки фиксированного размера. Каждый блок — самостоятельный. Для доступа к строке N: вычислить block_index + offset_in_block, загрузить один блок, прочитать значение.Именно это обеспечивает заявленный 100x выигрыш в random access: вместо загрузки + декодирования 64KB page ради одной строки — прямой доступ к байтам значения.
v1 → v2: эволюция формата
Lance v1 (2022-2024) использовал другой подход к колоночному хранению. v2 — переработка на основе опыта продакшена:
Lance v1 (2022-2024)
Lance v1: каждая колонка = отдельный файл внутри фрагмента. Encoding зафиксирован при создании dataset. Нет adaptive encoding per-chunk. Footer в каждом column file.Lance v2 (2025+)
Lance v2: все колонки в одном файле (или группах). Adaptive encoding per-chunk. Protobuf 'any' для расширяемости. Оптимизирован для object storage: один GET per read (no LIST).Сравнение Encoding-подходов
Как Lance v2 encoding pipeline соотносится с подходами Parquet и Apache Arrow:
Write Path: выбор кодировки
При записи данных Lance v2 writer проходит через pipeline выбора кодировки для каждого chunk каждой колонки:
Chunk: 4096 строк
Входной chunk: ~4096 строк одной колонки. Writer анализирует данные перед кодированием: вычисляет null ratio, cardinality, value range, width uniformity.Read Path: lazy decode
При чтении Lance v2 применяет lazy decoding — данные декодируются только когда запрошены:
Arrow RecordBatch (zero-copy)
Результат: Arrow RecordBatch с одной строкой, двумя колонками. Zero-copy transfer в Pandas/DuckDB/PyTorch. Суммарный I/O: ~3 mini-block reads вместо full page decodes.Lazy decode — ключ к производительности. В Parquet reader загружает и декодирует целый page даже если нужна одна строка. Lance v2 mini-block encoding позволяет загрузить и декодировать только нужный блок — часто это десятки байт вместо десятков килобайт.
Итоги
Lance v2 — формат, построенный на принципе “контейнер не навязывает содержимое”:
В следующем уроке мы разберём ML-специфичные возможности Lance: 100x random access через sliceable encodings, встроенный векторный поиск (IVF-PQ, HNSW), мультимодальные данные и Python API.