// Docs

Lance

Read immutable Lance dataset versions as fragment-aligned Refiner shards

Lance support is optional:

pip install macrodata-refiner[lance]

Use load_lance(...) to read a pinned Lance dataset version:

import refiner as mdr

pipeline = mdr.load_lance(
    "s3://my-bucket/hands.lance",
    version=42,
    columns=["image", "frame_id"],
    num_shards=32,
    max_rows=2_000,
)

When version is omitted, Refiner resolves and pins the latest version when that execution stage starts. This lets a later stage read a Lance dataset created or updated by an earlier stage in the same job. Pass version=... when you want to pin a specific version while constructing the pipeline.

Column projection is pushed into Lance.

Use the pipeline-wide execution block limit when rows contain large media or other variable-sized values:

pipeline = mdr.load_lance("s3://my-bucket/hands.lance").with_max_block_rows(128)

Refiner propagates this limit into Lance's scanner, so the source does not materialize a larger Arrow batch and then immediately split it. The same limit continues to apply to blocks produced by downstream transforms. Omit it to use Lance's internal read-batch default.

When source rows are lightweight references but downstream transforms expand them into large media values, tune the two boundaries independently:

pipeline = mdr.load_lance(
    "s3://my-bucket/hands.lance",
    read_batch_rows=256,
).with_max_block_rows(8)

Here Lance scans 256 rows at a time while transforms and sinks receive blocks of at most eight rows. An explicit read_batch_rows value takes precedence over scanner inheritance from with_max_block_rows.

Limit the number of rows

Set max_rows to a non-negative integer to read only that many leading rows from the pinned dataset version. Refiner uses the same global bounded-source wrapper as other built-in readers while avoiding projected-value reads beyond the final requested prefix. This keeps quick tests bounded even for wide or binary-heavy datasets. Datasets with fewer rows simply yield all available rows.

The limit is applied before pipeline transforms. Omit max_rows to read the entire pinned version, or use max_rows=0 to produce no source rows. For example, this processes only the first 10,000 stored rows:

pipeline = mdr.load_lance(
    "s3://my-bucket/hands.lance",
    max_rows=10_000,
)

Classic Lance blob columns are returned as lazy Refiner blob references:

row = pipeline.take(1)[0]
encoded = mdr.read_blob(row["image"])

Each reference contains path, offset, and size. read_blob(...) performs an exact byte-range read, so scanning rows does not materialize the blob bytes. For large assets such as video, stream the byte range into a consumer instead:

import av

with mdr.open_blob_stream(row["video"]) as stream:
    with av.open(stream, mode="r") as container:
        frames = list(container.decode(video=0))

open_blob_stream(...) is non-seekable and context-managed. It reads 8 MiB chunks by default and buffers at most four queued chunks, applying backpressure when the consumer is slower than storage. Set chunk_bytes and prefetch_chunks when a workload needs different bounds. Closing the stream early signals cancellation and waits for the producer for a bounded time. If a storage backend remains blocked in open() or read(), close still returns; producer and source cleanup completes if and when that backend call returns. The reference remains valid only while the pinned dataset version's data files are retained. Lance Blob V2 columns are currently rejected because packed and dedicated V2 storage does not expose a stable physical object path that Refiner can preserve in this three-field representation.

load_lance uses Lance's native storage layer. It rejects configured fsspec filesystem objects and storage_options; provide a URI whose endpoint and credentials are available through provider-standard environment configuration to both Lance and fsspec. Credential-bearing URIs and URI query parameters are rejected so pipeline plans do not serialize secrets.

By default, Refiner creates one shard per Lance fragment up to the 1,000-shard automatic limit. For datasets with more fragments, it groups adjacent fragments without dropping any data. Set num_shards to request up to 10,000 scheduling units explicitly. Fragments remain atomic, so requesting more shards than fragments still produces one shard per fragment. A worker may claim and process multiple shards over its lifetime.