Zarr Concepts#
An index is stored as Zarr, and this page explains
what that means, concept by concept, alongside the actual zarrs Rust
call ecp-core makes for each. It is useful both to someone who has
never used Zarr and wants to understand what is on disk, and to anyone
modifying ecp-core’s storage code and meeting zarrs’s own API for
the first time. Index Format covers ecpfs’s own array layout in detail;
this page is what to read first.
Store#
A Zarr store is where every array and group’s bytes actually live. It is
pluggable, backed by anything that can read, write, and list keyed byte
ranges. Every on-disk ecpfs index uses one concrete store,
FilesystemStore, opened from a path:
let store = Arc::new(FilesystemStore::new(&index_path)?);
zarrs also ships a MemoryStore, an in-memory backend with no disk
I/O at all. ecp-core’s own tests use it for fast, disk-free
fixtures, but no production code path does. Do not confuse it with
EmbeddingsSource::Memory, an unrelated in-memory source for a
build’s embeddings, holding a plain Array2<f32> with no Zarr store
involved.
Arrays#
A Zarr array is a chunked, typed, compressed N-dimensional array, stored
as a set of plain files rather than one binary blob. ecp-core creates
one with ArrayBuilder, given a shape, a chunk shape, a data type, and
a fill value:
let mut builder = ArrayBuilder::new(shape, chunk_shape, data_type, fill_value);
builder.bytes_to_bytes_codecs(compressor()); // compress each chunk with zstd
let array = builder.build(store.clone(), path)?; // Array, still only in memory
array.store_metadata()?; // write its zarr.json to the store
This writes the array’s shape, dtype, and chunking, not any values. Values are written in chunks, covered next.
Reading an existing array back is a plain path lookup:
let array = Array::open(store.clone(), path)?;
Chunking#
An array’s chunk shape is fixed when it is created, derived from
node_chunk_bytes or rep_chunk_bytes divided into whole rows. A
read or write never touches a whole array. It targets an
ArraySubset, and only the chunks that subset overlaps are decoded or
written:
array.store_array_subset(subset, embeddings)?; // write values
array.retrieve_array_subset::<Array2<f32>>(subset) // read that subset back
array.retrieve_array_subset::<Vec<u32>>(&array.subset_all()) // read the whole array
See Choosing Parameters for why the chunk size choice matters, and Index Format for what an underfilled chunk costs on disk, nothing, it compresses away, versus on a read, the whole chunk still decodes.
Compression#
Every array is compressed with zstd at level 3:
pub(super) fn compressor() -> Vec<Arc<dyn BytesToBytesCodecTraits>> {
vec![Arc::new(ZstdCodec::new(3, false))]
}
Data types#
A Zarr array’s own type, float32, float16, uint8, or
int8, maps directly to EmbeddingDtype through a small per-variant
match:
let data_type = match dtype {
EmbeddingDtype::F32 => float32(),
EmbeddingDtype::F16 => float16(),
EmbeddingDtype::UInt8 => uint8(),
EmbeddingDtype::Int8 => int8(),
};
Groups#
A Zarr group is a named collection of arrays and other groups, roughly
a directory. ecp-core registers one at every container path, the
store’s root, info, index_root, each level, and each node, all
through the same small helper:
pub(crate) fn build_group(store: &ReadableWritableListableStorage, path: &str) -> Result<()> {
GroupBuilder::default()
.build(store.clone(), path)? // create the group in memory
.store_metadata()?; // persist its zarr.json
Ok(())
}
This writes a zarr.json with "node_type": "group" at path.
Without it, path is not a group at all, just a directory prefix two
array paths happen to share.
Reading a group back is the same shape as opening an array:
let group = Group::open(store.clone(), path)?;
ecp-core never actually calls this. Its own reader opens every array
by its full path directly, never through a group. The read side is
shown here because it is genuinely useful on its own, for inspecting a
store by hand in a test or a script, even though ecp-core’s own code
has no need for it.
Groups exist for the store’s whitebox goal. A generic Zarr reader such as zarr-python can then browse and open the hierarchy on its own, without already knowing every path in advance.
On disk#
Every array and group in the Zarr v3 spec has its own zarr.json
metadata file. ecp-core’s own tests assert on this directly
(ecp-core/tests/format_version.rs), including a group’s, at the
store’s root and at every level and node path. An array whose data fits
in a single chunk along every axis stores that chunk in a file named
exactly c. An array chunked along more than one axis adds one path
segment per axis, so a two-dimensional array’s first chunk is c/0/0.
Because the store is just Zarr, any Zarr-compliant reader, such as zarr-python, can open, browse, and read it directly, independently of ecpfs’s own loader:
import zarr
root = zarr.open_group("my_index.zarr", mode="r") # open the store as a group
root.tree() # print the whole hierarchy
root["lvl_1"]["node_0"]["embeddings"][:] # read values, group by group
Index Format describes every array ecpfs actually writes, by name.