Quickstart
Build the twelve-paragraph example corpus from a checkout, ask it a question, read the route it took, then cite, validate and open it in the terminal.
This page builds the example corpus in examples/quickstart/ (twelve short CC0 paragraphs about urna), asks it questions, resolves a citation and validates the file. Every output below is the real output of urna 0.5.1 on that corpus.
urna build runs the build tooling (the forge) that lives in the python/ tree of the repository, and no release artifact ships it. The installed binary answers queries anywhere, but building needs a checkout, on macOS or Linux.
urna setup suggests a build it cannot run
The last screen of urna setup suggests urna build --spec corpus.toml, and outside a checkout urna build fails with urna_forge.py not found (...); run from the repo or install the forge payload. There is no forge payload to install: clone the repository as shown below. See Known limits.
Install urna and run setup
Follow Installation for your platform, then run urna setup once. It lays down the offline embedder and a Python env with numpy and tokenizers, then runs the health checks. If you already did this, urna doctor confirms the install and exits 0.
urna setup
urna doctorClone the repository and prepare it
git clone https://github.com/hoffresearch/urna.git
cd urna
sh scripts/fetch_potion.sh
cargo build --release -p urna-python --features pyo3/extension-module
cp target/release/lib_urna.dylib python/_urna.so # macOS; on Linux copy lib_urna.soEach command has a reason:
- The default embedding model, potion, is a 30 MB table stored with Git LFS. Without LFS the clone holds a small pointer file instead.
scripts/fetch_potion.shdownloads the real table from Hugging Face at a pinned revision and accepts it only when its SHA-256 matches the pointer; if the table is already there it does nothing. - The build writes the file through the Python extension, which a checkout loads from
python/_urna.so. Build it withpyo3/extension-module, as shown, or it can crash under standalone Python interpreters.
urna build runs the forge with the first interpreter it finds: URNA_PYTHON, then the env urna setup created, then the nearest .venv, then python3. It prints its choice on stderr as [urna] embedder interpreter: <path>. The interpreter needs Python 3.12 or later, numpy and tokenizers.
Build the corpus
Run the build from the repository root, because the paths in the spec resolve against the current directory:
urna build --spec examples/quickstart/corpus.tomlThe spec reads one JSONL row per paragraph, orders the rows by id, and embeds each row's text with the potion model:
[corpus]
name = "quickstart"
chunker_version = "quickstart/1" # changes => every chunk_id (citation) changes
[source]
kind = "jsonl"
path = "examples/quickstart/docs.jsonl"
order_by = ["id"] # must be a total order (verified at build)
[source.text]
template = "{text}" # one row = one chunk; source_uri comes from the row
[[models]]
preset = "potion" # offline static table, no torch, no download
text = "default"
[output]
mode = "single"
dir = "examples/quickstart/out"The build prints a JSON summary on stdout and writes three files:
quickstart.urna is the corpus. The manifest records what went in (rows, models, spaces) and the lock records the build environment. The embeddings are also cached under ~/.cache/urna, so a second build of the same rows skips the embedding step. Build artifacts describes each file.
The build is reproducible, so your file should carry the same hashes shown on this page.
Ask a question
urna ask examples/quickstart/out/quickstart.urna "can I use this offline" -k 1to keep that promise for a brand-new user, the default embedder is a static, offline embedder that ships with the tool. it needs no model download and no network round-trip on first use, and it is deterministic, so a build is byte-identical and reproducible. a power user can bring a stronger embedding model instead, and the model's fingerprint is recorded so the corpus and the query embedder must agree or the search fails loudly.
-- urna://sha256:1147b2560863331b21bd9d60fe6bdd99507dc34e17108444dc38194f8e6f09df/sha256:eed9a60b68133464e91c831f8af5960491f6c444cf1645fc5e4864435ab4bd44 (demo/04-offline-sovereignty.md)ask embeds the question offline with the same model the file was built with, checks the embedder's model_hash against the file, searches, and prints the stored text of each hit with its citation and source. -k 1 keeps one hit; the default is 10.
See how the answer was found
--disclose explain adds the search route, the candidate counts and where the score came from:
urna ask examples/quickstart/out/quickstart.urna "can I use this offline" -k 1 --disclose explainroute: hnsw
candidates: exact=0 ann=12 bm25=0 graph=0 fusion=none
rerank_source: real cosine
recall: (not computed; rerank guarantees real cosine)
to keep that promise for a brand-new user, the default embedder is a static, offline embedder that ships with the tool. it needs no model download and no network round-trip on first use, and it is deterministic, so a build is byte-identical and reproducible. a power user can bring a stronger embedding model instead, and the model's fingerprint is recorded so the corpus and the query embedder must agree or the search fails loudly.
-- urna://sha256:1147b2560863331b21bd9d60fe6bdd99507dc34e17108444dc38194f8e6f09df/sha256:eed9a60b68133464e91c831f8af5960491f6c444cf1645fc5e4864435ab4bd44 (demo/04-offline-sovereignty.md)The HNSW index proposed 12 candidates, and each one was rescored by exact cosine against the stored float32 vectors, so the score is the real cosine and not an approximation. Search paths and the exact rerank explains the routes.
The BM25 index in this file is not used
urna build defaults to the hybrid preset, which writes an HNSW index and a BM25 index but declares index_type = "hnsw". ask, retrieve and search-text route by that field, so they take the HNSW path and never read the BM25 index, as the bm25=0 count shows. See Known limits.
Get results as JSON
retrieve returns the hits in a form another program can read, one JSON object per line:
urna retrieve examples/quickstart/out/quickstart.urna "how do citations work" -k 2 --format jsonl{"chunk_id":"sha256:b5dfeb09a643f6f0afde3f361316dde3ea503ff86c525d297e6ba79b5781a4be","score":0.5004974007606506,"score_type":"cosine","source_uri":"demo/03-citations.md","offset_start":7,"offset_end":8,"citation_id":"urna://sha256:1147b2560863331b21bd9d60fe6bdd99507dc34e17108444dc38194f8e6f09df/sha256:b5dfeb09a643f6f0afde3f361316dde3ea503ff86c525d297e6ba79b5781a4be","text":"because the citation points at content, two people who build the same logical corpus on two machines get the same citation, and a stored corpus and a compressed one cite identically. resolving a citation returns the exact canonical text and the original byte span it came from, which is what lets an agent quote a source it can prove.","file_hash":"sha256:e4d5f8907faad38c192dc6929e36dbf16db558410b2dae5abb4d65f10f508832","content_hash":"sha256:1147b2560863331b21bd9d60fe6bdd99507dc34e17108444dc38194f8e6f09df","rerank_source":"full_precision"}
{"chunk_id":"sha256:2be5a0f62d1bb7a69556b1a59d15e3a7d7cf9991baa579e83c1ae67cdd657748","score":0.27243560552597046,"score_type":"cosine","source_uri":"demo/03-citations.md","offset_start":8,"offset_end":9,"citation_id":"urna://sha256:1147b2560863331b21bd9d60fe6bdd99507dc34e17108444dc38194f8e6f09df/sha256:2be5a0f62d1bb7a69556b1a59d15e3a7d7cf9991baa579e83c1ae67cdd657748","text":"the returned similarity score is a real cosine value, recomputed by an exact rerank, never an approximate proxy. a result you can cite is a result you can trust.","file_hash":"sha256:e4d5f8907faad38c192dc6929e36dbf16db558410b2dae5abb4d65f10f508832","content_hash":"sha256:1147b2560863331b21bd9d60fe6bdd99507dc34e17108444dc38194f8e6f09df","rerank_source":"full_precision"}score is the exact cosine. In a corpus built by urna build, offset_start and offset_end hold the row's position after sorting (row 7 is the span 7 to 8), not byte offsets into a document. --format json prints one JSON array instead.
Resolve a citation
Pass any citation_id to cite to get the stored text back, with the hashes that identify the file and the chunk:
urna cite examples/quickstart/out/quickstart.urna urna://sha256:1147b2560863331b21bd9d60fe6bdd99507dc34e17108444dc38194f8e6f09df/sha256:b5dfeb09a643f6f0afde3f361316dde3ea503ff86c525d297e6ba79b5781a4becitation_id: urna://sha256:1147b2560863331b21bd9d60fe6bdd99507dc34e17108444dc38194f8e6f09df/sha256:b5dfeb09a643f6f0afde3f361316dde3ea503ff86c525d297e6ba79b5781a4be
file: examples/quickstart/out/quickstart.urna
file_hash: sha256:e4d5f8907faad38c192dc6929e36dbf16db558410b2dae5abb4d65f10f508832
content_hash: sha256:1147b2560863331b21bd9d60fe6bdd99507dc34e17108444dc38194f8e6f09df
chunk_id: sha256:b5dfeb09a643f6f0afde3f361316dde3ea503ff86c525d297e6ba79b5781a4be
source_uri: demo/03-citations.md
byte_start: 7
byte_end: 8
text:
because the citation points at content, two people who build the same logical corpus on two machines get the same citation, and a stored corpus and a compressed one cite identically. resolving a citation returns the exact canonical text and the original byte span it came from, which is what lets an agent quote a source it can prove.The first half of the citation is the file's content_hash, so a citation from a different corpus, or from an older build of this one, fails with content_hash mismatch instead of returning the wrong text. Citations and hashes covers what moves each hash.
Validate the file
urna validate examples/quickstart/out/quickstart.urnaOK: 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:1147b2560863331b21bd9d60fe6bdd99507dc34e17108444dc38194f8e6f09dfvalidate exits 0 when every check passes and 1 otherwise. The checks prove the bytes are intact, not who built the file.
Open it in the terminal
urna tui examples/quickstart/out/quickstart.urnaThe explorer opens on the corpus. The corpus tab shows the manifest, the hashes and the section table; the ask tab takes a question and lists the hits with their scores, stored text and citation. tab moves between tabs and ctrl+q quits. The terminal needs at least 50 columns by 16 rows. See The terminal explorer for every key.
The Python version
examples/quickstart/quickstart.py builds the same twelve paragraphs through urna.build and queries them with UrnaFile.retrieve. It runs with the wheel (pip install "urna[embed]") or in a prepared checkout:
python examples/quickstart/quickstart.pyIt writes to the same path, examples/quickstart/out/quickstart.urna, but it is a different file. The script builds with the exact preset and gives each chunk the span 0 to the length of its text, where urna build uses the row position. Spans are part of every chunk_id, so the chunk ids, the content_hash and every citation differ, and the citations on this page stop resolving against its output. Run urna build --spec examples/quickstart/corpus.toml again to get this page's file back. Use urna from Python covers the Python surface.
Next, build a corpus from your own rows in Your first corpus.