Skip to content

Streams with metadata

Use StreamWithMetaStorage when tokens have variable-size annotations, such as concept lists attached to paragraphs. The token stream stays contiguous; a separate payload stores each span's annotation. A window can start or end inside a span, or cross document and shard boundaries, while retaining the mapping. Training passes this storage to StatefulDataLoader.from_storage with a sequence_length. The example opens a StreamDataset reader only to show one window; see the dataset API.

Tokens with chunk concepts and provenance

The custom format below stores 32-bit tokens as stream data. Its document payload is a binary document ID and chunk ID followed by a variable-length array of concept IDs. This is the kind of span annotation that a fixed-size stream field cannot represent directly.

import struct
from pathlib import Path
from tempfile import TemporaryDirectory

import numpy as np

from guideloom import StreamDataset, StreamWithMetaStorage
from guideloom.types import StreamWithMetaStorageFormat


class ConceptFormat(StreamWithMetaStorageFormat[dict, dict, np.int32]):
    prefix = struct.Struct("<II")
    concept_dtype = np.dtype("<i4")

    def __init__(self):
        super().__init__(np.dtype("<i4"))

    def size(self, data):
        return len(data["tokens"])

    def pack(self, data):
        tokens = np.asarray(data["tokens"], dtype=self.stream_dtype)
        assert len(data["chunk_ids"]) == len(tokens)
        for chunk_id in data["chunk_ids"]:
            payload = self.prefix.pack(data["document_id"], chunk_id)
            payload += np.asarray(data["concepts"][chunk_id], dtype=self.concept_dtype).tobytes()
            yield ((data["document_id"], chunk_id), payload)
        return tokens

    def unpack(self, stream_data, document_shard, document_index, document_data):
        annotations = []
        for shard, payload in zip(document_shard, document_data, strict=True):
            document_id, chunk_id = self.prefix.unpack_from(payload)
            annotations.append(
                {
                    "shard": int(shard),
                    "document_id": document_id,
                    "chunk_id": chunk_id,
                    "concepts": np.frombuffer(payload, dtype=self.concept_dtype, offset=self.prefix.size),
                }
            )
        return {"tokens": stream_data, "annotation_index": document_index, "annotations": annotations}


with TemporaryDirectory() as directory:
    storage = StreamWithMetaStorage(Path(directory), ConceptFormat())
    with storage.sync_writer() as writer:
        writer.append(
            0,
            {
                "document_id": 7,
                "tokens": [10, 11, 12],
                "chunk_ids": [0, 0, 1],
                "concepts": [[100, 101], [102]],
            },
        )
        writer.append(
            1,
            {
                "document_id": 8,
                "tokens": [13, 14, 15, 16, 17],
                "chunk_ids": [0, 0, 0, 0, 0],
                "concepts": [[200, 201, 202]],
            },
        )

    with StreamDataset(storage, sequence_length=4).sync_reader() as reader:
        sample = reader[0]
        assert sample["tokens"].tolist() == [10, 11, 12, 13]
        per_token = [sample["annotations"][i] for i in sample["annotation_index"]]
        assert [a["document_id"] for a in per_token] == [7, 7, 7, 8]
        assert per_token[-1]["shard"] == 1
        assert per_token[-1]["concepts"].tolist() == [200, 201, 202]

The packing contract

size(data) gives the number of stream elements. pack(data) must yield exactly one (key, payload) pair per element, then return the one-dimensional array with the declared dtype and length. Returning the array from the generator is different from yielding it.

Equal consecutive keys within a record share one stored payload. Keys must identify contiguous runs: [0, 0, 1, 1] is valid, but [0, 1, 0] is not. The first occurrence of each key needs a payload; later elements in that run may yield None instead of encoding the same payload again. The example repeats encoding for clarity; production formats can encode each run once.

Keys are local to an append call and are not stored as application identifiers. Include document and chunk IDs in the payload when you need them for tracing.

The unpacking contract

document_index has one entry per returned token. Each entry indexes document_data, the payloads needed by this read. document_shard gives the source shard for each payload. These indices are remapped for every read; they are not the original chunk IDs. Decode the payload's own IDs to recover provenance, as the example does.

A metadata format can also use a structured dtype for multiple stream fields. For a small fixed-size value such as one document ID per token, plain stream fields avoid the extra payload lookup.