Open a corpus you downloaded
Check a .urna file you did not build: validate it, read which embedding model it needs, and set up the query side for potion or registry models.
A .urna file is self-contained: text, vectors, indices and the search contract travel together, so a corpus someone else built can be queried without their build setup. What you do need is the query embedder for the model the corpus was built with. This guide checks a downloaded file, finds out which model it needs, and sets up the query side. Ready-made corpora are available at shop.urna.dev.
Validate first
urna validate corpus.urnaOn the quickstart corpus:
OK: examples/quickstart/out/quickstart.urna is a valid .urna v1 file
Header checksum: valid
Section checksums: 9 sections OK
Footer hash: valid
Manifest: valid (contract enforced)
Required sections: all present
Embedding values: no NaN/Inf
File hash: sha256:e4d5f8907faad38c192dc6929e36dbf16db558410b2dae5abb4d65f10f508832
Content hash: sha256:1147b2560863331b21bd9d60fe6bdd99507dc34e17108444dc38194f8e6f09dfExit 0 means every checksum and hash matches, the manifest passes its contract, the embeddings hold no NaN or Inf, and every media blob stored inside the file matches its recorded sha256. Any failure exits 1 and names what broke; a truncated or corrupted download fails here.
validate does not decode the HNSW, BM25 and graph payloads. ask, retrieve and the Python urna.open do, when they open the file, so a file can pass validate and still be refused at open.
If the publisher lists a checksum, compare it with the File hash line. file_hash is the sha256 of the whole file, the same digest shasum -a 256 corpus.urna prints.
Find the model
urna stats corpus.urnaThe lines that decide the query side, from the quickstart corpus:
model: minishlab/potion-base-8M/v1
model_hash: sha256:8f2eb91a754b4da59cdd8223d0ba196185fed1bb6f092fd2be0ff02b893b1c98
dim: 256
dtype: float32
index_type: hnswmodel is the manifest embedding_model. It decides which query embedder urna ask and urna retrieve run, and model_hash is what that embedder must reproduce to pass the model gate. For a script, urna inspect --json corpus.urna | jq -r .manifest.embedding_model gives the same value.
model | What answers it |
|---|---|
starts with minishlab/potion | The installed binary after urna setup. Nothing else to install. |
| a registry model (table below) | A checkout of the repository, the model's Python dependencies, and its weights on disk. |
| anything else | Not ask or retrieve. A sentence-transformers corpus built in a checkout answers through urna search-text; otherwise embed the query yourself and use the raw-vector verbs or the Python API. |
A potion corpus
Run urna doctor. If it exits 0, ask:
urna ask corpus.urna "your question" -k 3The payload's potion table has model_hash sha256:8f2eb91a754b4da59cdd8223d0ba196185fed1bb6f092fd2be0ff02b893b1c98. A corpus whose model_hash differs was built with another potion table, and the gate refuses it with model_hash mismatch. Ask and retrieve from the terminal covers the rest of the query workflow.
A registry-model corpus
These are the manifest names the registry knows:
model | Preset | Python dependencies | Also needs |
|---|---|---|---|
open_clip/ViT-B-32/openai | clip-vit-b32 | torch, open_clip_torch, pillow | |
open_clip/ViT-B-16-SigLIP2/webli | siglip2 | torch, open_clip_torch, pillow | |
jinaai/jina-embeddings-v5-omni-nano | jina-v5-omni-nano | torch, sentence-transformers>=5.7, transformers==5.2.0 | URNA_ALLOW_REMOTE_CODE |
jinaai/jina-embeddings-v5-omni-small | jina-v5-omni-small | same as above | URNA_ALLOW_REMOTE_CODE |
tencent/WeMM-Embedding-2B | wemm-2b | the jina set plus qwen-vl-utils==0.0.14 | URNA_ALLOW_REMOTE_CODE |
tencent/WeMM-Embedding-4B, tencent/WeMM-Embedding-9B | wemm-4b, wemm-9b | same as wemm-2b | URNA_ALLOW_REMOTE_CODE and URNA_ALLOW_HEAVY=1 |
numpy is needed in every case. The registry checks only that each package imports, not its version.
The installed binary cannot answer these
The release payload carries only the potion query embedder. With an installed binary alone, ask and retrieve on a registry-model corpus stop with embedder script not found: python/forge/embed_query_model.py (override with --embedder), and urna setup cannot fix it. The steps below use a checkout. See Known limits.
Clone the repository and make an environment with the model's dependencies (wemm-2b shown):
git clone https://github.com/hoffresearch/urna
cd urna
python3 -m venv .venv-query
.venv-query/bin/pip install numpy tokenizers torch "sentence-transformers>=5.7" "transformers==5.2.0" "qwen-vl-utils==0.0.14"Put the weights on disk. The query embedders set the Hugging Face offline variables, so plan on local weights. For the jina and wemm presets, point URNA_MODEL_DIR_<PRESET> (the preset name upper-cased, - as _) or --model-path at a local snapshot, or have it in the Hugging Face cache; URNA_ALLOW_DOWNLOAD=1 lets them fetch it when no local directory resolves. See Offline by construction.
Ask from the root of the checkout, so the CLI finds python/forge/embed_query_model.py, with the interpreter pinned:
URNA_PYTHON=$PWD/.venv-query/bin/python \
URNA_MODEL_DIR_WEMM_2B=/models/WeMM-Embedding-2B \
URNA_ALLOW_REMOTE_CODE="wemm-2b" \
urna ask /path/to/corpus.urna "your question" -k 3Pin the interpreter with URNA_PYTHON: without it the CLI picks the urna setup venv (numpy and tokenizers only), a nearby .venv, or python3. From another directory, add --embedder /path/to/urna/python/forge/embed_query_model.py.
The query side builds the embedder from the preset's defaults. For the sentence-transformers presets the model_hash includes the dtype policy, which follows the device: bfloat16 on CUDA, float16 on MPS, float32 on CPU. A corpus built on one device class fails the gate on another with model_hash mismatch unless URNA_ST_DTYPE pins the dtype the publisher used. Ask the publisher which one that was.
Treat the file as untrusted input
The checksums inside a .urna are unkeyed sha256: they detect corruption, and anyone who edits a file can recompute them. A file that passes validate is consistent, not vouched for. Handle a downloaded corpus the way you handle any file from outside:
- Opening it runs urna's parser on its bytes. The parser is bounds-checked and fuzzed, and a file that crashes it is a security bug, but opening still means executing that code on input you did not produce.
- The manifest decides which query embedder runs. A potion name runs the potion script; any other name runs the registry script, which refuses unknown models and refuses presets that execute model-repo code unless you set
URNA_ALLOW_REMOTE_CODEfor that preset. Set it because you trust the model, never because a file asks for it. - The interpreter ladder can pick up the nearest
.venv/bin/python. PinURNA_PYTHONwhen you runurnainside a directory you do not control. - The text is the publisher's text. Content returned by a search is data, including when an agent reads it.
A corpus also carries the license of the content it embeds. See Security and Data governance.
Use urna from Python
Install the urna wheel, build a small .urna file from Python, embed a query offline with potion, search, retrieve cited chunks and resolve a citation.
Give a corpus to an agent
Wire a .urna corpus into an LLM agent as a tool: urna retrieve JSONL as the tool result, urna cite as verification, and rules for quoting stored text.