rete · Guides CLI reference

CLI reference

The rete binary (crate rete-cli). Run rete <command> --help for the authoritative flags. Terms are written as canonical N-Triples tokens — <http://ex/Alice> for an IRI, "30" or "30"^^<…#integer> for a literal.

Rete-specific --json responses carry "schemaVersion": 1. This covers Dataset Cards, provenance, search, communities, cost, progressive metadata, and SHACL reports. Standard SPARQL Results JSON and JSON-LD keep their standard shapes and do not add that field.

Release archives also contain Bash, Zsh, Fish, and PowerShell completions plus the rete(1) man page. Maintainers generate these from the exact binary with the hidden rete generate --output <directory> packaging command; they are never maintained as hand-written copies of the CLI definition.

Building

rete build <inputs…> -o <out.rete> [--format nt|nq|ttl|trig|rdfxml] [--quoted-triple-syntax rdf12|rdf-star]

Build a file from one or more RDF inputs, merged under one shared dictionary. Format is detected by extension (.nt / .nq / .ttl / .trig, plus .rdf / .owl / .rdfxml for RDF/XML — how most OWL ontologies ship); - reads stdin and defaults to N-Triples; --format forces a format for all inputs (use --format rdfxml for an RDF/XML file with a non-standard extension). N-Quads inputs produce a dataset with named graphs.

rete build a.nt b.nt -o merged.rete
curl -s https://host/data.nt | rete build - -o data.rete

--materialize bakes RDFS/OWL-RL entailments into the file at build time (see Reasoning). --reason runs the same reasoner but instead of adding triples it stamps the coherence verdict into the Dataset Card (implies --card) — so a remote reader learns whether the graph is logically coherent from the index-free card with no compute; unlike --materialize it records coherent: false honestly rather than aborting (verify later with rete reason --verify-card, combine with --materialize to also bake the inferred triples). --card (and --card-file / --title / --license / --source / --description / --created) embeds a Dataset Card — data-catalog metadata plus an auto-derived profile; the card file's extra object carries bounded publisher-defined custom fields (no flag — see Custom fields); a card build also writes a build-info section (build timestamp, builder version, parameters, measured starter-query costs — kept outside the content hash so identical data still hashes identically). That same run is what proves each starter query answers: one measured at zero rows (or returning a row that binds nothing) is dropped from the card, loudly and with the reason recorded — see dataset-cards.md. --no-card-costs skips the run, and so skips that check as well. --text-index adds a full-text (word/CONTAINS) index over the literals for rete search --contains (see below). --type-predicate <IRI> overrides the predicate that types subjects with classes for the schema pyramid (default rdf:type, else auto-detected) — e.g. Wikidata's --type-predicate http://www.wikidata.org/prop/direct/P31. --no-pyramid skips the community pyramid entirely (no pyramid section): SPARQL / SHACL / triple-pattern / reachability queries don't use it, so the file stays fully queryable and is markedly smaller — only community / summary / progressive queries need the pyramid. --pyramid-algo louvain|types picks the community algorithm: louvain (default) detects topological communities; types partitions by rdf:type instead — deterministic, self-naming communities that stay feasible on graphs too large for the single-threaded Louvain build (it falls back to Louvain when the graph is untyped). All of these are opt-in; without them the output is byte-identical to a plain build.

--strict refuses input carrying an IRI that the N-Triples/N-Quads IRIREF grammar and RFC 3987 disallow. It is off by default and the build only counts them — see Invalid IRIs for what that means and why the default is a warning.

Quoted triples on input (--quoted-triple-syntax). The same flag name and the same two values rete export takes, pointed the other way: which surface a << … >> in a Turtle or TriG input means. rete validate takes it too.

value<< s p o >> reads as<<( s p o )>> reads as
rdf-star (default)a quoted triple — one term, subject or object positiona syntax error
rdf12a reifier: _:r rdf:reifies <<( s p o )>> plus a statement about _:ra triple term, object position only

The same file parses into two different graphs under the two values. That is not a rete quirk — RDF-star and RDF 1.2 genuinely give << s p o >> different meanings, and nothing in the bytes says which one the author meant, so it cannot be detected and has to be told.

rdf12 also reads the rest of RDF 1.2 Turtle: {| … |} annotations (which expand to a reifier carrying the annotation's properties) and "…"@lang--dir directional literals.

Why the input default is rdf-star while the export default is rdf12. They answer different questions. Export asks who can read this? — and current parsers want RDF 1.2. Input asks what did the author mean?, where the two possible mistakes are not equal:

  • An RDF-star file read as rdf12 parses, and gives you a different graph. Silently. That is the failure this flag exists to prevent, and it is the same one #262 found on the write side.
  • An RDF 1.2 file read as rdf-star is a hard parse error, because <<( belongs to RDF 1.2 alone. rete detects that case and names the flag in the error rather than complaining about a stray (.

Only one of those is recoverable, so the default is the one that makes the other mistake loud. The consequence to know: rete export --format trig writes rdf12 by default, so re-ingesting rete's own dump takes --quoted-triple-syntax rdf12 — or export with --quoted-triple-syntax rdf-star and it re-ingests with no flag at all.

N-Triples, N-Quads and RDF/XML ignore the flag. Their reader is rete's own, it has accepted both << s p o >> and <<( s p o )>> since 0.3.2, and RDF 1.2 N-Triples has no reifier syntax for << … >> to be — so there is no ambiguity to resolve and nothing to choose.

Nothing is stored differently. Either surface lands rete's one canonical token <<s p o>>, and an RDF 1.2 reifier is ordinary RDF — a blank node, an IRI and a term — which rete stored long before the flag existed. A file with no quoted triples builds byte-for-byte identically under either value. See Triple-store interop, which also covers using the two flags together to translate between the two worlds.

--permutations 3|6 (default 6) chooses how many index permutations to store. 6 writes SPO, POS, OSP, SOP, PSO and OPS; 3 writes only SPO, POS and OSP — the orders that decide routing. Those three tie the longest bound prefix on all eight triple-pattern shapes, so a 3-permutation file answers every query with the same rows, from the same tiles; what it gives up is the sort-merge join, on the three (bound-set, join-column) shapes SOP/PSO/OPS existed for — most visibly ?s <p1> ?o1 . ?s <p2> ?o2, two bound predicates sharing a subject. The three orders are 36.8%–50.5% of a built file (BENCHMARK.md), and the file records its own set in the header, so rete info / rete stats and the card's signals.permutations all report it.

A 3-permutation file is not readable by a Rete older than this one — it refuses loudly (malformed container: expected 6 permutation sections, exit 1) rather than answering wrongly, but it does refuse. Keep the default for anything published; see compatibility.md.

The flag exists on build only. The two commands that re-assemble an existing file do not re-decide how it was built: repyramid preserves its input's set (and says so on stderr when that set is not the default six), and merge writes the union of its inputs' sets — one full shard is enough to keep the merge-join orders its queries relied on. To change a file's set, rebuild it from its RDF source.

rete repyramid <file> -o <out.rete> [--type-predicate <IRI>] [--pyramid-algo louvain|types] [--text-index] [--card …]

Rebuild a file's pyramid in place, reading the triples straight from the existing .rete — no export | build N-Quads round-trip. Use it to add a schema pyramid (or a --text-index / a Dataset Card) to a file built before those existed, or to re-derive the schema pyramid under a different --type-predicate or --pyramid-algo (same semantics as rete build). The card flags match rete build (--card-file / --title / --license / …) — and take the curated half only from those flags, so a bare --card drops the publisher's title/license/source/description. --pyramid-algo likewise defaults to louvain rather than to whatever the file was built with.

It loads and materializes every quad, so its RAM tracks the statement count (~350–700 B each), not the file size — roughly 80 M statements on a 48 GB machine, past which the staged export --format nq | build route is the one that fits. See Maintaining a published card.

rete repyramid old.rete -o new.rete --type-predicate http://www.wikidata.org/prop/direct/P31
rete repyramid old.rete -o new.rete --text-index    # add full-text search to an existing file

# re-card a published file, carrying its curated fields across
rete card old.rete --json > old-card.json
python3 scripts/recard/card_tools.py curated old-card.json -o curated.json
rete repyramid old.rete -o new.rete --card --card-file curated.json --pyramid-algo types

repyramid preserves its input's permutation set: a 3-permutation file stays 3-permutation, a 6 stays 6, and it prints the preserved set on stderr when it is not the default six. It re-assembles an existing file rather than re-deciding how it was built, so it has no --permutations of its own — rebuild from source to change the set.

rete merge <inputs…> -o <out.rete> [--memory-budget-mb N] [--tmp-dir dir] [--card …]

Fold several .rete files into one without going back through text — the way to consolidate a sharded dataset when the original RDF is gone or expensive to re-emit. It reads the shards (dictionary-encoded and compressed, roughly a quarter of the RDF bytes), so it skips the conversion and the parse.

It does not skip the sorting. The dictionary is HDT-style (shared / subject-only / object-only) and IDs are assigned per section, so a term that is subject-only in one shard and object-only in another becomes shared in the merge and changes ID section: the shards' orderings do not survive the remap and every permutation is rebuilt. Memory is bounded at both ends: each input is opened lazily and streamed rather than loaded, and the quads feed the same memory-bounded external builder rete build --memory-budget-mb uses, which chunks and spills under this command's own --memory-budget-mb (default 4096) into --tmp-dir. Shards carrying named graphs fold like any other: the builder puts the graph in its sort key, so each graph comes back out of the merge as one contiguous run. Card flags match rete build.

The merged file carries the union of its inputs' permutation sets — an all-lean merge stays lean, and one six-permutation input is enough to keep the merge-join orders. Like repyramid, it has no --permutations flag: it consolidates shards, it does not re-decide how they were built.

rete merge shard-*.rete -o all.rete --memory-budget-mb 8192 --tmp-dir /spill \
  --card --card-file curated.json

Validating

rete validate <inputs…> [--format nt|nq|ttl] [--strict] [--quoted-triple-syntax rdf12|rdf-star]

Parse RDF input(s) without building, to check they are well-formed N-Triples/N-Quads/Turtle. Reports statement and named-graph counts, or exits non-zero with a precise parse error (file, line, column).

rete validate data.ttl
curl -s https://host/data.nt | rete validate - --format nt

It also reports invalid IRIs, because "it parses" and "it is valid RDF" are different claims and only the second one predicts whether a dump will load anywhere else. --strict turns the report into a non-zero exit.

--quoted-triple-syntax means exactly what it means on build, which makes validate the cheap way to ask which surface is this file actually in? — the counts differ, because the graphs do:

rete validate claims.ttl                               # 2 statement(s)  — << >> is a quoted triple
rete validate claims.ttl --quoted-triple-syntax rdf12  # 4 statement(s)  — << >> is a reifier

Invalid IRIs

rete's N-Triples/N-Quads reader is deliberately tolerant: it stores whatever sits between < and the next >, so the file is a faithful container of what it was given. That tolerance is why every published dataset built — and it is also why rete export --format nq could emit something no strict parser accepts. build and validate now count what they ingested:

warning: 5 statement(s) carry an invalid IRI (5 IRI occurrence(s)).
         They are stored verbatim, so `rete export --format nq` emits N-Quads that a
         strict parser (Oxigraph, Jena, rapper) rejects — and a bulk loader rejects the
         whole chunk, not the line.
               2  '[' or ']' outside an IP-literal host
                  e.g. <http://example.org/a[b]>
               1  more than one '#'
                  e.g. <http://example.org/c#d#e>
               1  '%' not followed by two hex digits
                  e.g. <http://example.org/%x-template-artifact>
               1  a character the IRIREF grammar excludes (space, control, or one of <>"{}|^`\)
                  e.g. <http://example.org/a b>

Validity is decided by a parser, not by a list

Two documents apply: the IRIREF production ('<' ([^#x00-#x20<>"{}|^] | UCHAR)* '>'), which is a character set, and RFC 3987, which the same grammar requires the content to satisfy as an **absolute** IRI. The first is settled by a scan. The second is settled by **oxiri`** — the crate Oxigraph's own N-Triples reader validates with — so rete's answer to "will a strict loader take this" is the loader's answer, not an imitation of it.

The classes below do not decide validity. They answer the other question: can this be repaired, and how.

classexamplerepairable by escaping
no scheme (relative IRI)<noscheme/path>no
an excluded character<http://ex/a b>yes
[ / ] outside an IP-literal host<http://ex/a[b]>yes
more than one #<http://ex/c#d#e>yes
% not followed by two hex digits<http://ex/%x>yes
rejected by RFC 3987, no class recognises it<https://::1>no

That last row is the important one. An IRI the parser rejects that matches none of the repair shapes is still counted and still blocks — it is never silently passed. <https://::1> is an IPv6 literal in the authority without the brackets RFC 3987 requires; it has a scheme, no excluded character, no bracket, one # and no %, so the five repair classes are all blind to it. A dump carrying one was published and Oxigraph refused to load it. Since the parser decides, a future shape nobody has thought of fails the same way this one now does: as an unrepairable defect that stops the export, not as a zero.

What is not judged: scheme semantics (<nonsense://x> is well-formed and accepted), whether the IRI resolves to anything, and normalisation — rete never case-folds a scheme or removes a dot segment, because that would change the dictionary key. Non-ASCII is judged now, by the parser: RFC 3987 ucschar is legal, so <http://ex/café> is valid and is never touched, while the narrow sub-ranges 3987 excludes are caught because the loader catches them too.

UCHAR escapes are resolved before the parser sees the IRI — <http://ex/café> denotes http://ex/café, and the backslash is N-Triples syntax, not IRI content. Skipping that step would flag every escaped dump, and flagging something valid means percent-encoding an IRI that was fine, which is the one failure mode a sanitizer must not have.

Three flags act on this, and they are separate on purpose:

  • rete build --strict / rete validate --strict — refuse the input, naming the IRI and the rule. Not the default: datasets that build today, including published ones, must keep building.
  • rete export --sanitize-iris — percent-encode the repairable classes on the way out. Also not the default, because it changes the data.

An IRI with no scheme is reported and never rewritten. Escaping cannot repair it: resolving it needs a base IRI the .rete never recorded. This is the class that cost the most in the field — one such line made a bulk loader drop an entire ~102,000-line chunk — and the only honest thing a tool can do with it is say where it is. The same holds for an unrecognised defect: rete will not invent a repair for something it cannot explain, because <https://::1> percent-encoded to <https://%3A%3A1> would name a different host entirely. Fix those at the source.

Automating on the report. --sanitize-iris prints one line meant to be parsed:

--sanitize-iris: totals invalid=11 repairable=11 unrepairable=0 unclassified=0

A publication gate should key on unrepairable > 0, never on one named class. unrepairable is the count of occurrences that are still invalid after everything the sanitizer could do, and it includes the unclassified bucket by construction — which is precisely why a gate written against it does not need updating when a new defect shape turns up.

Inspecting

rete info <file>

Print the decoded 1 KB header (the section directory, codecs, counts, content hash) — plus the Dataset Card catalog when the file carries one. Reads only the header and card byte ranges (the same two small reads card-url does over HTTP), so a 50 GB file answers in about a second.

rete stats <file>

Human-friendly overview: file size, default-graph triple count, distinct terms, named-graph count, pyramid levels, and top predicates. Notes the label-index and text-index sizes when the file carries them.

rete verify <file>

Recompute the blake3 content hash and compare to the header. Exits non-zero with FAILED — content hash mismatch on corruption/truncation.

rete search <file> [<prefix>] [--contains <word>…] [--contains-prefix P] [--limit N] [--json]

Two modes.

Label prefix (default) — the subjects whose label starts with prefix (case-insensitive), printed as label<TAB><iri> (or {"schemaVersion":1,"matches":[{"label":…,"subject":…}]} with --json). An empty prefix returns the first --limit labels (default 20). Answered from a bounded, label-sorted block in the pyramid-meta by binary search — no literal scan — so it is the fast path for autocomplete (~22× a FILTER(STRSTARTS(LCASE(?l), …)) scan at 6k labels; the gap widens with size). Labels come from rdfs:label, skos:prefLabel/altLabel, foaf:name, dc(terms):title, and schema:name; the block keeps the top 8,192 most-connected labeled subjects. Files built before this feature carry no label index (the block is additive — rebuild to add it).

Full-text (--contains <word>…) — the subjects whose literals contain every given word (whole-word, case-insensitive — AND), printed one IRI per line (or the same versioned matches envelope with {subject} entries under --json). --contains-prefix einst additionally requires a literal word starting with einst. Answered from the opt-in TEXT_INDEX section (rete build --text-index); on a remote file only the queried words' posting lists are fetched, not the whole index. A file built without --text-index reports that it has no text index. To search a file you have not downloaded, use rete search-url (below).

rete search data.rete gluc                       # label prefix (autocomplete)
rete search data.rete --contains glucose         # literals containing "glucose"
rete search data.rete --contains glucose phosphate  # both words (AND)
rete search data.rete --contains-prefix einst    # a word starting with "einst…"

rete card <file> [--json] [--format jsonld|croissant] [--sha256 <hex>]

Print the embedded Dataset Card — curated metadata (title/license/source/creators/publisher/…) plus the derived profile (counts, top predicates and classes, vocabularies), the content-hash checksum, and (when the file carries one) the build record: when it was built, by which rete, with which flags, and the starter queries' measured costs. Costs two small range reads (header + one coalesced card/build-info range), never the dictionary or index — instant on any size. --json emits the card object plus schemaVersion: 1 and a build block. --format jsonld projects the card to JSON-LD (VoID + schema.org + PROV-O — already RDF when lifted out); --format croissant emits the honestly-mappable Croissant subset (no recordSet: a graph is not a table). --sha256 supplies the whole-file sha256 the Croissant projection's FileObject requires — a file cannot carry its own, so compute it outside (sha256sum file.rete); with it the document is validator-clean. For a file built without one it prints (no dataset card — …), still naming what the header alone decides: whether the file carries a full-text index (signals.text_index, measured from the section directory rather than read out of the card — see Dataset Cards). rete info shows the same catalog beneath the header when a card is present.

rete graphs <file>

List the named-graph IRIs in a dataset (the default graph is unnamed).

rete export <file> [--format nq|ttl|trig|jsonld|hdt] [--graph G] [--subject S] [--predicate P] [--object O] [--sanitize-iris] [--no-prefixes] [--quoted-triple-syntax rdf12|rdf-star] [--compress none|zstd|gzip] [--compress-level N] [--in-memory]

Serialize the dataset, or a slice of it.

Two formats keep every graph and are lossless round-trips:

  • nq (the default) — N-Quads, one statement per line.
  • trig — TriG: Turtle syntax plus GRAPH <g> { … } blocks. The same data, in fewer bytes, because a subject and its namespaces are written once rather than once per statement.

Two carry no graph term and therefore serialize a single graph:

  • ttl — Turtle.
  • jsonld — expanded JSON-LD.

And one is binary:

  • hdt — HDT, a compact format that stays queryable without being decompressed. See below; it is triples-only and it has a size ceiling the text formats do not.

nq, ttl and trig stream: peak memory follows --memory-budget-mb and the output is byte-identical at every budget. jsonld builds the whole document in memory, so it is not the format for a file that does not fit in RAM.

rete export data.rete                   # N-Quads (default, lossless)
rete export data.rete --format trig     # TriG — lossless and smaller
rete export data.rete --format ttl      # Turtle, one graph
rete export data.rete --format jsonld   # expanded JSON-LD, one graph

Which graph a single-graph format writes. --graph if given, otherwise the default graph. The full ladder, which ttl and jsonld share and which always reports its choice on stderr:

situationwhat is exported
--graph <iri>that graph; an error naming the real graphs if it does not exist
--graph ''the default graph, explicitly
no --graph, default graph has contentthe default graph, noting that named graphs were left out
no --graph, default graph empty, one named graphthat graph
no --graph, default graph empty, several named graphsan error listing them, pointing at --graph and --format trig

Graphs are never silently merged, and a non-empty selection is never silently dropped.

Quoted triples: which surface (--quoted-triple-syntax). A quoted triple — a statement standing inside another statement — has two spellings in the wild, and nq, ttl and trig can write either. Both are supported; rete re-ingests either losslessly. They differ in who else can read the dump.

valuesurfaceread by
rdf12 (default)<<( s p o )>>, the ratified RDF 1.2 triple termoxttl 0.2 and everything on it, incl. the oxigraph CLI; Jena 5.x, GraphDB 11
rdf-star<<s p o>>, the RDF-star community-group surface — and the token rete storesoxrdf 0.2 / oxttl 0.1 (the versions rete itself links), and RDF-star mode in older Jena/GraphDB

The default is RDF 1.2 because the alternative is a dump current tooling will not take: oxigraph convert --from-format nq rejects <<s p o>> outright, and a load is atomic, so one such line costs the whole file. In Turtle and TriG the failure is quieter and worse — an RDF 1.2 parser reads << s p o >> as a reifier, so one statement silently becomes two with a blank node where the triple term was. Pick the surface your consumer speaks; if you do not know, the default is the one that fails loudly rather than quietly.

A file with no quoted triples in it is unaffected. The header records in one bit whether the file holds any, so the check is skipped entirely and the dump is byte-for-byte the same under either value.

Two edges worth knowing:

  • RDF 1.2 puts a triple term in object position only (ttSubject ::= iri | BlankNode). A graph with a quoted triple in subject position — legal RDF-star, and legal rete — has no RDF 1.2 spelling at all, so the export refuses it by name and points at --quoted-triple-syntax rdf-star, rather than writing a dump no parser will accept.
  • Reading a dump back takes the matching input surface. rete build and rete validate accept the same --quoted-triple-syntax flag with the same two values (see above), but their default is rdf-star, not rdf12 — so a default --format trig or --format ttl dump containing triple terms needs rete build --quoted-triple-syntax rdf12. The export says so on stderr when it writes one. --format nq round-trips with no flag in either surface: that reader is rete's own tokenizer and takes both spellings unconditionally.

--format jsonld and --format hdt have no term kind for a quoted triple at all and refuse a file that contains one, naming --format trig. They used to write it as though it were an IRI — a file that loads cleanly and means something else.

HDT (--format hdt). A binary serialization whose point is that a reader memory-maps it and answers triple patterns against the mapped bytes. Opening the 1.39 GB reference HDT costs 50.7 MB of RSS and 0.31 s, whatever the file's size — that is what a compressed text dump cannot do.

rete export data.rete --format hdt > data.hdt

Four limitations, all enforced rather than merely documented:

  • Triples only. HDT has no graph term, so it takes one graph by the same ladder Turtle uses. The quad extension (HDTQ) is niche and poorly supported and is not implemented.
  • It cannot stream, and it has a size ceiling. rete stores IRIs as <http://…> and HDT stores them bare, which changes the dictionary's sort order, so every term id has to be remapped and the triples re-sorted in the new order. That needs the graph in memory. rete export estimates the cost from the file header and refuses before doing any work if it exceeds --memory-budget-mb, naming both numbers. Measured, the estimate is about 512 bytes per distinct term plus 16 per triple; an 88M-triple, 39.7M-term graph needs roughly 14 GB.
  • No quoted triples. HDT predates RDF-star and RDF 1.2 and has no term kind for a triple term. It did not notice: a quoted triple was interned into HDT's dictionary as an IRI, brackets stripped by the same rule that strips a real one's, so a consumer read back an "IRI" with spaces and angle brackets in it. rete export now refuses, from the header flag, before any work.
  • A second, independent cap: 2^32 object ids. The reference implementation truncates object dictionary ids to 32 bits while building its query index, so a larger file would be written successfully and then answer queries incorrectly on whoever's machine read it. Rather than document that trap, rete export refuses to produce such a file at all.

Which limit binds depends on the graph's shape: a sparse graph with a huge object vocabulary hits the id cap, a dense one hits memory.

HDT is not the format to archive or ship in, and the numbers say so. Measured on a 1.5M-triple graph: HDT 14,217,647 bytes against trig.zst's 4,296,486 — HDT is 3.3x larger, and larger than the source .rete (11,810,101) too. On the 88M-triple DBLP graph the ratio is the same: HDT 1,378,298,174 against trig.zst's 462,563,780, 3.0x larger.

So pick by what happens to the file next:

you want to…use
store or ship it--format trig --compress zstd — smallest, lossless, no ceiling
feed a line-oriented pipeline--format nq
query it in place--format hdt

HDT earns its place on the last row only, and that advantage is real and measured: a reader memory-maps it and answers patterns in ~50 MB of RSS without decompressing anything, where trig.zst has to be decompressed and parsed in full before it can answer at all.

Above either limit, use --format trig --compress zstd — lossless, compact, and with no ceiling.

--compress is rejected with --format hdt: wrapping an in-place-queryable format in a codec removes the only property it has over a compressed text dump.

The first object-bound query a reader runs will build and cache a .index.v1-1 next to the file. That is normal, and safe for any file rete produces, because the object-id cap is enforced at export time.

Prefix compression (ttl and trig). IRIs are abbreviated to QNames — rdfs:label rather than <http://www.w3.org/2000/01/rdf-schema#label> — which is where most of the size advantage over N-Quads comes from. Well-known vocabularies keep their conventional prefix names; on top of that the exporter reads a bounded sample of the data (a hundred thousand statements, whatever the file's size) and names the namespaces that are frequent in this file, from their own last path segment. An IRI is abbreviated only when the part after the namespace is a legal Turtle PN_LOCAL needing no backslash escapes — otherwise it is written in full, because a smaller file that does not parse is not a smaller file. --no-prefixes writes every IRI in full.

Compression (--compress). zstd or gzip, applied as the dump is written. The codec sits in the writer chain, so --memory-budget-mb still bounds peak memory and a dump far larger than RAM still works — nothing is buffered up to be compressed afterwards.

rete export data.rete --format trig --compress zstd > dump.trig.zst
rete export data.rete --compress zstd --compress-level 19 > archive.nq.zst

Prefer zstd. Measured on a 609 MB N-Quads dump (see the table below), zstd at its default level produces a file 21% smaller than gzip's default while compressing 2.4x faster and decompressing 1.5x faster — which is the trade that matters for a dump written once and read repeatedly.

codecbytesof plaincompressdecompress
plain609,574,054100%——
zstd -131,411,7305.15%3.19s3.36s
zstd -330,417,7654.99%3.12s3.27s
zstd -6 (default)27,926,8904.58%3.27s3.26s
zstd -926,718,7104.38%3.24s3.41s
zstd -1923,760,3733.90%195.30s3.33s
gzip -142,476,4086.97%6.51s4.86s
gzip -635,249,5615.78%7.92s4.86s
gzip -932,656,7265.36%13.75s5.18s

--compress-level defaults to 6 for both codecs. For zstd that is deliberately not the library's own default of 3: the 3 -> 6 step is the largest remaining gain on the ratio curve (-8.2%), and everything after it returns less (-4.4% to level 9, -0.9% to level 12) until level 19, which costs 60x the time for 10% more. Negative zstd levels are accepted and trade ratio for speed.

stdout becomes binary when a codec is selected. Every note and report rete export prints already goes to stderr, so redirecting stdout to a file is safe — but do not pipe a compressed dump into something expecting text.

Compression applies to the text formats. Decompress with the standard tools; the output is ordinary zstd and gzip, verified against both CLIs.

Export streams from disk by default. The file is opened through the lazy ranged reader — header, section directories and index up front, then dictionary chunks and tiles faulted in as the scan reaches them — so the raw file image is never held resident. The old behaviour read the whole file into memory and decoded every dictionary chunk and all six index permutations first, so export RSS scaled ~3 GB per GB of file (6 GB on a 1.53 GB graph; a 52 GB dump would need ~150 GB and OOM). --in-memory forces that whole-file load back — faster for small files that comfortably fit in RAM, but it needs RAM ≈ the file size. The dump is byte-identical either way: the reader choice changes only how the bytes are fetched, never the order or content. A filtered export benefits most (only the touched tiles are read); a full dump still faults every dictionary chunk, so its floor is the resident dictionary, not the file.

--sanitize-iris percent-encodes IRIs that are outside the N-Triples/N-Quads grammar (see Invalid IRIs), so the dump loads into a strict store instead of being rejected — and a bulk loader rejects the whole chunk, not the line, so one bad IRI is worth a hundred thousand good statements.

It is off by default because it changes the data. <http://ex/a[b]> is written as <http://ex/a%5Bb%5D>, which is a different IRI: the dump no longer joins against the graph it came from, rete → store → rete stops being the identity, and if the graph happened to contain both spellings they collapse into one. Deciding to accept that is the exporter's call, which is why it is a flag. What it did goes to stderr (stdout is the dump), per class:

--sanitize-iris: totals invalid=5 repairable=5 unrepairable=0 unclassified=0
--sanitize-iris: percent-encoded 5 IRI occurrence(s). The dump's IRIs are NOT the
                 file's IRIs: it no longer joins against the source graph, and
                 rete → store → rete is no longer the identity.
                       2  '[' or ']' outside an IP-literal host
                       1  more than one '#'
                       …

An IRI escaping cannot repair — no scheme, or a defect no class recognises — is counted, named, and written verbatim. The dump is then still not valid N-Quads and the summary says exactly that rather than implying a fix it did not make; the totals line's unrepairable field is the machine-readable form of that statement. The triple-store interop page has the measured Oxigraph round-trip for all three cases.

Filters prune the file; they are not | grep. --graph /--subject / --predicate / --object become a triple pattern the engine routes: it picks the index permutation that sorts on the bound components, binary-searches its tile directory down to the tiles that can match, and drops the rest by their recorded synopsis without fetching them. On a lazily-opened file that is the difference between exporting a slice and reading the graph.

# one predicate of one named graph: 16 MB read instead of 376 MB, 0.4 s
# instead of 12.8 s, 183 MB peak RSS instead of 2.1 GB (cordis.rete, 801 MB)
rete export cordis.rete --format nq \
    --graph http://data.europa.eu/s66/graph/results \
    --predicate http://data.europa.eu/s66#doi

rete export data.rete --format nq --graph ''            # the default graph alone
rete export data.rete --format nq --subject http://ex/a # everything about one subject
rete export data.rete --format nq --object '"text"@en'  # an exact literal

Terms are bare IRIs or N-Triples tokens (<iri>, "lit"@en, "lit"^^<dt>, _:b) — quote literals for your shell. A term the file's dictionary does not contain matches nothing, which is an empty export, not an error. --graph '' selects the default graph alone; omitting --graph keeps the current behaviour (default graph, then every named graph).

Two caveats worth knowing before you measure it:

  • A filtered dump prunes the index, not the dictionary. Resolving the rows it keeps still faults the dictionary chunks their terms live in, so on a graph whose payload is long literals the saving is much smaller: on the same file, a predicate whose objects are abstracts went 261 MB → 213 MB, not 23×.
  • Rows arrive in the routed permutation's order. Unfiltered (and subject-bound) that is (s, p, o) as before; a bound predicate streams (p, o, s), a bound object (o, s, p). The set is identical; N-Quads does not care, but diff does — sort both sides.

Preview any of this before running it with rete cost --dump.

Querying

rete query <file> [--subject S] [--predicate P] [--object O]

Match a single triple pattern; omitted positions are wildcards.

rete query data.rete --predicate '<http://ex/knows>'

rete why <file> [--subject S] [--predicate P] [--object O] [--json]

Explain the provenance of each triple-pattern result. The command reports the matched terms, dictionary IDs, graph scope, the chosen permutation (one of the six), and the file byte ranges for the dictionary, full index container, selected permutation payload, and pyramid metadata. With --json, the same data is emitted as stable machine-readable JSON with schemaVersion: 1.

rete why data.rete --predicate '<http://ex/knows>'
rete why data.rete --subject '<http://ex/Alice>' --json

rete why-url <url> [--subject S] [--predicate P] [--object O] [--json]

The remote counterpart of rete why: explain a triple-pattern result over a .rete served on HTTP, range-fetching only the routed tiles — the same provenance (permutation, section, byte ranges, the physical tile) plus the bytes fetched and range-request count. The CLI version of the browser's why_url.

rete why-url https://host/data.rete --predicate '<http://ex/knows>'
# … provenance …
# (fetched 4096 bytes in 1 range request(s); file is 1048576 bytes)

Provenance is honest about the physical layout: it identifies the index container, the selected permutation payload, and — for tiled files — the physical tile holding each match (PERM/index) with its compressed byte range.

rete bgp <file> "<pattern> . <pattern> …"

Evaluate a Basic Graph Pattern. Patterns are separated by ., terms by spaces; ?name is a variable (terms may not contain spaces).

rete bgp data.rete "?x <http://ex/knows> ?y . ?y <http://ex/knows> ?z"

rete sparql <file> "<query>" [--json] [--entail]

Run SPARQL: SELECT / ASK / CONSTRUCT / DESCRIBE. With --json, emit standard SPARQL Results JSON (for SELECT/ASK). See SPARQL support.

--entail turns on OWL 2 QL reasoning: the query is rewritten so the answer includes ontology-entailed solutions (rdfs:subClassOf / subPropertyOf / domain / range / owl:inverseOf / someValuesFrom), computed over the raw data with no materialization — off by default, so a plain query is unchanged. Same flag on rete sparql-url reasons over a remote file, fetching only what the rewritten query touches. See Reasoning by query rewriting.

There is no union-default-graph flag here: the opt-in ⛁ All graphs mode — a pattern outside GRAPH matching the merge of the default graph and every named graph, for files whose data lives entirely in named graphs — exists today in the playground and the wasm query_opts API only, not on the CLI and not in rete serve. On the CLI, scope the query with GRAPH ?g { … } instead. See Union default graph.

rete sparql data.rete "PREFIX e: <http://ex/> SELECT ?p (COUNT(?f) AS ?n) WHERE { ?p e:knows ?f } GROUP BY ?p"
rete sparql data.rete "SELECT ?o WHERE { ?o a <…/Aves> }" --entail    # ontology-aware

Memory & I/O. Local files above 1 GiB open through the same lazy range reader the -url commands use (threshold: RETE_LOCAL_LAZY_ABOVE_MB, block size: RETE_BLOCK_KB, cache capped at 256 MiB), and aggregation streams through per-group accumulators — a COUNT over a 9.83 B-triple file runs in a 2 GiB container (benchmark). Preview a query's byte cost with rete cost, and see the exact ranges a query read with rete why.

rete serve <file> [--bind addr] [--token t] [--journal path]

Serve one .rete — or a manifest of segments (.json), whose visible fold becomes the served state — as a live SPARQL 1.1 Protocol endpoint — queries and SPARQL Update. The base file is never mutated: updates append to a plain-text journal next to it (<file>.changes, one +/--prefixed N-Quads line per change) and the merged state answers the very next query; a restart replays the journal. GET /snapshot.rete downloads the current state as a fresh .rete — the update cycle's publishable artifact (upload the snapshot, delete the journal). Any rete client — including the browser playground — can federate against the endpoint with SERVICE <http://host:port/sparql>.

  • GET/POST /sparql with query= → results (application/sparql-results+json; CONSTRUCT as N-Triples). Queries may themselves contain SERVICE blocks.
  • POST /sparql (or /update) with update= or an application/sparql-update body → INSERT DATA / DELETE DATA / DELETE/INSERT … WHERE / CLEAR / DROP (LOAD and USING are rejected).
  • Binds loopback by default. Expose deliberately (--bind 0.0.0.0:7878) and set --token: updates then require Authorization: Bearer <t> (reads stay open). CORS is enabled, so browsers can query it directly.
  • Scale: the state lives in memory and rebuilds after writes — built for living small/medium datasets (annotation, curation; up to a few million triples), not the multi-GB catalog files.
rete serve notes.rete
curl -s --data-urlencode 'query=SELECT * WHERE { ?s ?p ?o } LIMIT 5' http://127.0.0.1:7878/sparql
curl -s --data-urlencode 'update=INSERT DATA { <http://ex/n1> <http://ex/note> "hello" }' http://127.0.0.1:7878/sparql
curl -sO http://127.0.0.1:7878/snapshot.rete   # the updated companion .rete

rete cost <file-or-url> "<query>" [--json] [--explain]

Preview the byte/range-request cost of a SPARQL query without evaluating it. The report parses the query, lists the concrete predicates that can drive summary-based routing, and compares three access paths:

  • summary overview — header + dictionary + pyramid summary, skipping the triple index.
  • routed pattern open — for a single default-graph triple pattern, header + dictionary + the one selected permutation payload (the best of the six).
  • full query open — the current SPARQL engine path, which opens dictionary + index (+ pyramid/named-graph metadata when present) before evaluation.
rete cost data.rete "PREFIX e: <http://ex/> SELECT ?y WHERE { e:Alice e:knows ?y }"
rete cost https://host/data.rete "ASK { ?s <http://ex/knows> ?o }" --json

rete cost <file-or-url> --dump [--graph G] [--subject S] [--predicate P] [--object O] [--json]

The same preview for a dump — what rete export (or a client's streaming dump) will fetch for that filter, before it starts. Same report shape as above.

The index figure is computed, not sampled: the tile directories say which tiles the filter's routed scan can touch and how big each is, so no tile is fetched to produce it.

$ rete cost cordis.rete --dump \
      --graph http://data.europa.eu/s66/graph/results \
      --predicate http://data.europa.eu/s66#doi
  file bytes: 801016143
  lazy dump open: 43156179 bytes in 66 range request(s) · reads index
  graphs selected: 1
    <…/graph/results>: POS · 31 of 528 tile(s) admitted · 1217273 bytes (section 21227100)
  index tiles: 31 of 528 admitted · 1217273 bytes of 21227100 · computed from the
               tile directories, no tile fetched
  dictionary ceiling: 417246556 bytes
  estimated dump cost: 44373452 – 461620008 bytes

Read the range honestly: the floor (open + admitted tiles) is exact; the ceiling adds the whole dictionary, which only a dump touching every term pays. Term resolution faults chunks the tile directories cannot name, so the true cost sits between — near the floor when the slice is small and its terms are short, near the ceiling on a literal-heavy file however well the index prunes. The dump previewed above actually read 59,672,911 bytes.

For the exact summary-only shapes SELECT (COUNT(*) AS ?n) WHERE { ?s <p> ?o }, SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }, SELECT ?p (COUNT(*) AS ?n) WHERE { ?s ?p ?o } GROUP BY ?p, SELECT DISTINCT ?p WHERE { ?s ?p ?o }, SELECT (COUNT(DISTINCT ?p) AS ?n) WHERE { ?s ?p ?o }, ASK { ?s <p> ?o }, and ASK { ?s ?p ?o }, the JSON output includes summary_answer with the exact count/boolean value read from the pyramid summary. Predicate-specific shapes also include the predicate; predicate totals return all predicate/count pairs, predicate lists return all predicates present in the summary, and predicate distinct counts return the number of predicates. More complex shapes are marked requires_index.

Add --explain to include a planner explanation. In JSON, this adds an explain object with the classified query_shape, whether the answer is summary_exact, the planned access path (summary-only, routed-pattern, or full-index), and whether the current engine path still reads the index.

For HTTP(S), the host must honor Range requests, just like query-url and sparql-url. Treat this as a deployment/debugging preview: it reports the current SPARQL engine's range budget, the summary budget, and the exact routed single-pattern budget when the query shape allows it.

rete progressive <file-or-url> "<query>" [--json]

Run the first summary-only progressive query path. This command answers only the exact shapes that can be proven from the pyramid summary without opening the triple index:

  • SELECT (COUNT(*) AS ?n) WHERE { ?s <p> ?o }
  • SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }
  • SELECT ?p (COUNT(*) AS ?n) WHERE { ?s ?p ?o } GROUP BY ?p
  • SELECT DISTINCT ?p WHERE { ?s ?p ?o }
  • SELECT (COUNT(DISTINCT ?p) AS ?n) WHERE { ?s ?p ?o }
  • ASK { ?s ?p ?o }
  • ASK { ?s <p> ?o }
rete progressive data.rete "PREFIX e: <http://ex/> SELECT (COUNT(*) AS ?n) WHERE { ?s e:knows ?o }" --json

The JSON output is SPARQL Results JSON with schemaVersion: 1 and an added progressive object describing the summary stage, exactness, predicate when one is fixed, bytes fetched, request count, and reads_index: false. Other query shapes fail clearly; use rete sparql for full-index evaluation or rete cost --explain to inspect why a query is not summary-answerable yet.

rete cypher <file> "<query>" [--base <iri>] [--json]

Run a read-only Cypher subset (a prototype). The query is translated to an equivalent SPARQL SELECT and evaluated by the same engine — no second query engine. Supports MATCH … [WHERE …] RETURN … [LIMIT n]: node patterns ((a), (a:Label)), forward/reverse relationships ((a)-[:REL]->(b), (a)<-[:REL]-(b)), variable-length relationships (-[:REL*]-> → the SPARQL property path REL+, one-or-more), simple WHERE comparisons on a property (a.age > 30) or identity (a = <iri>) joined by AND/OR, and RETURN of variables and/or properties. Writes, OPTIONAL MATCH, WITH, aggregations, relationship variables/properties, and multiple labels are rejected with a clear error. See compatibility for the full subset and the name→IRI convention. With --json, emit standard SPARQL Results JSON.

A bare label/relationship/property name X maps to <BASE + X>, where BASE defaults to http://ex/ and is overridable with --base.

rete cypher deps.rete "MATCH (a:Application) RETURN a"
rete cypher deps.rete "MATCH (a)-[:dependsOn*]->(b) WHERE b = <http://ex/log4x> RETURN a"

Reasoning

rete reason [<file>] [--url <url>] [--materialize] [--check] [--verify-card] [--format nq|ttl]

Run the prototype OWL RL / RDFS reasoner: materialize RDFS/OWL entailments to a fixpoint and report any logical inconsistencies ("incoherent points", e.g. a disjoint-class violation). Prints the count of newly entailed triples and each inconsistency (kind + detail). Exits non-zero if any inconsistency is found (zero if coherent), so it works as a coherence gate in CI. With --materialize, also serialize the base + inferred graph (nq default, or ttl). This is a documented subset, not full OWL DL — see Reasoning & coherence.

  • --url <url> reads a remote .rete over HTTP range requests instead of a local file (omit the positional <file>).
  • --check is coherence-gate mode: print one verdict line and exit non-zero on any incoherent point (suppresses --materialize output) — the minimal CI gate.
  • --verify-card checks the file's baked coherence card (from rete build --reason) against a fresh reasoning run, guarding against drift or a stale ruleset; it exits non-zero if the stored verdict disagrees with recomputation.
rete reason data.rete
rete reason data.rete --materialize --format ttl
rete reason data.rete --check                  # one-line CI verdict
rete reason --url https://host/data.rete       # reason over a remote file
rete reason data.rete --verify-card            # baked card vs fresh run

Shape validation

rete shacl <file> --shapes <shapes.ttl> [--graph <iri>] [--format text|json|ttl]

Validate a .rete graph against SHACL Core shapes read from Turtle. The default graph is validated unless --graph names one dataset graph. The command exits zero when the report conforms and non-zero when it finds validation results, so it can be used as a CI data-quality gate. See SHACL validation for the supported components and current limits.

rete shacl data.rete --shapes shapes.ttl
rete shacl data.rete --shapes shapes.ttl --format json
rete shacl data.rete --shapes shapes.ttl --graph '<http://ex/snapshot>'

rete shacl-url <url> --shapes <shapes.ttl> [--format text|json|ttl]

Validate a remote .rete over HTTP, range-reading only what the shapes target. The file is opened lazily and each focus node's values are fetched as routed range reads, so a targeted shape (sh:targetClass / targetNode / targetSubjectsOf / targetObjectsOf) never downloads the whole graph — it faults only the tiles holding the target nodes and their property values. Reports the bytes fetched and the range-request count. Validates the default graph.

rete shacl-url https://host/data.rete --shapes shapes.ttl
# (fetched 38912 bytes in 7 range request(s); file is 1048576 bytes)

Coarse graphs (no index read)

rete summary <file> [--level k]

Print the structural coarse graph: the Louvain community quotient graph (community → community relations with counts), plus — for v2 files — the schema pyramid: a leveled rdf:type histogram where abstract classes describe coarse levels and leaf classes resolve as you zoom in (e.g. Agent → Person → Scientist → Astronomer). --level k prints just level k's type histogram (0 = coarsest / most abstract). Everything here reads index-free from the pyramid-meta — summary-url shows the same over HTTP without fetching the index.

rete schema <file>

Print the semantic coarse graph: classes (by rdf:type) with instance counts, and the class→predicate→class relations between them — the dataset's effective schema.

rete communities <file> [--json] [--profile] [--predicate IRI] [--round N] [--min-size N]

Recompute the Louvain communities and expose, per community, its member subjects and the literal text of its triples — the per-community text corpus for downstream topic modeling. --profile adds a no-ML "topic" profile per community (top literal words, rdf:type classes, and predicates). --predicate <iri> detects communities using only that relation's edges — a criterion-specific partition (see multi-criteria splitting). --json emits {schemaVersion:1, communities:[{community, size, members:[<iri>…], text:[lexical…]}]} (plus a profile object on each record when --profile is set); --round N cuts the dendrogram at a specific round (default: the round chosen for the tile budget); --min-size N drops communities with fewer than N members. See the topic modeling tutorial.

rete communities papers.rete --profile               # no-ML topic labels
rete communities researchers.rete --predicate '<http://ex/cites>'  # one criterion
rete communities papers.rete --json | python3 scripts/lda_topics.py --topics 3

rete predicates <file>

Exact per-predicate triple totals, summed from the pyramid summary alone — the triple index is never read.

Graph traversal

rete reach <file> --predicate <iri> --seed <iri>… [--seeds-file F] [--reverse] [--parallel] [--count]

Multi-source transitive reachability over one relation: for each seed, the set of nodes it transitively reaches. --reverse answers "who reaches the seed?" (impact analysis); --seeds-file reads one seed IRI per line; --count prints only sizes; --parallel runs one rayon task per seed (the batch-reachability workload that benchmarks ~14–15× on many cores).

# Who (transitively) depends on the vulnerable package?  (impact analysis)
rete reach deps.rete --predicate '<http://ex/dependsOn>' --seed '<http://ex/log4x>' --reverse

# Many seeds at once, in parallel:
rete reach g.rete --predicate '<http://ex/knows>' --seeds-file seeds.txt --parallel --count

Over HTTP

Both URL commands work against http:// and https:// hosts that honor Range requests (S3, GitHub, any CDN).

rete card-url <url> [--json] [--format jsonld|croissant] [--sha256 <hex>]

Fetch only the embedded Dataset Card — the header and one coalesced metadata + build-info range, in two small range requests. The dictionary, index, and pyramid are never fetched: this is the index-free CARD tier, the cold-start self-description (counts, vocabulary, class graph, signals, starter queries, build record) a client reads before it knows what to query. Reports bytes fetched + range count. The --format projections (and --sha256 for Croissant) match rete card.

rete card-url https://host/data.rete --json

rete card-audit <path|url> [--json] [--measure] [--only IDS] [--max-mb N] [--write-costs] [--allow-empty]

Do the starter queries a card already ships still answer on the file that carries them? A published .rete cannot be re-carded for free, so this decides each query's fate from the card's own profile — the CARD tier again, so a multi-GB file costs tens of KB to check.

Each query gets one verdict, and the two that matter are kept apart: empty is the card refuting a shipped query (a property path through a predicate the file does not have, a VALUES list disjoint from the dataset's link predicates, a class joined to a label predicate the class-link quotient accounts for and never pairs with it), while undecidable and suspect are the honest middle — run the query to settle those. answers is the card proving a row comes back, and revision says what a re-card would do to the body (current, superseded, dropped).

The judgement is the query generator's own (Cap::joint_with, NonEmpty, provably_empty), not a second copy of it. Input is a .rete (local path or URL) or a card JSON document from rete card --json / rete card-url --json.

rete card-audit https://host/data.rete
rete card-audit card.json --json | jq '.findings[] | select(.verdict=="empty")'

--measure: run them instead of reasoning about them

The static pass has a hard ceiling. Some templates are undecidable by construction — nothing in a card ties a subject to a predicate, so top-reach cannot be settled from one, and a card does not record which objects are also subjects, so top-dangling cannot either. No better card-only reasoning closes that; a run does, and records what the answer cost.

--measure runs each starter query cold — a fresh lazy open, logical range reads, no block cache — and reports rows, bytes, requests and a reference timing. It is the same measure_query a rete build uses to fill in its query_costs, so when the file already carries a build record the command checks itself against it and says whether the two agree (= build record / != build record). On switzerland-fedlex.rete — the one published file that has a record — all ten queries reproduce it exactly.

Each row shows the card's verdict and the run's outcome side by side; they are never merged, because one is what a card can prove and the other is what the file did.

card says    run says   query             rows               bytes      req           ms
answers      answers    ov-triples           1 row(s)       2800822 B     74 req      294 ms
suspect      empty      lb-labels            0 row(s)        228126 B     68 req       12 ms
undecidable  answers    top-dangling       100 row(s)       4468038 B    118 req      592 ms

Local or remote. Point it at a path or at an http(s):// URL. bytes and requests are the same quantity either way — no block cache is in the stack, so the range sequence is a function of layout and query, not of transport — but only the remote run actually pays for them. The output always names which it was, in the text header and in the JSON measurement.transport, because a cost figure without its transport is not a cost figure.

It is a download. --only ov-triples,lb-labels measures a subset; --max-mb 8 abandons a query once it has asked for more than that, and reports the abandonment with the bytes it spent — "costs more than 8 MB" is itself an answer. Both matter: eight of switzerland-fedlex's ten starter queries read ~1.02 GB each, so measuring that card remotely without a leash is an 8 GB download.

--write-costs: make the measurement durable

Records the run in the file's build-info section, so the next reader gets the figures from the CARD tier (two range requests) instead of re-measuring. Local files only.

The section sits outside the blake3 content hash — that is why two builds of identical data hash equal — so the file keeps its identity: same checksum, same rete verify, byte-identical N-Quads. It sits near the front, though, right after the card, so making room for it shifts everything behind it and the file is rewritten end to end. The rewrite streams through a 4 MiB buffer and commits by rename, but it is still one full pass of I/O and — for a published file — a full re-upload.

Where that is worth it: when the alternative is a re-card. A re-card rewrites the file too, and additionally costs 17–35× the file in RAM (repyramid) or 9–15× in staged N-Quads on disk (--mode stream, see scripts/recard), which puts anything past ~150 M statements out of reach. Attaching costs is one pass with no staging.

The RAM goes into the measurement, not the rewrite, and it is not free either: the engine evaluates eagerly, so a starter query with a big result materializes it. switzerland-fedlex (1.04 GB, 56.3 M quads) took 381 s and peaked at 14.2 GiB for --measure --write-costs — ng-list alone, which returns 497,905 rows, accounts for 3.2 GiB of that; the rewrite of the 1.04 GB file that followed is a bounded-buffer copy. Against repyramid's ≈36 GiB prediction and the staged path's ≤19.1 GiB for the same file, it is the cheaper route, but budget for the queries, not for the file.

So: if the audit says the queries are stale or broken, re-card — same rewrite, more value. If the queries answer and the only gap is the missing record, write the costs. The command enforces that split: it refuses when a query measured zero rows (--allow-empty overrides, for top-dangling on a fully-described graph), when a run did not finish, and when --only measured a subset that would be stored as if it were the whole card.

rete card-audit data.rete --measure --json | jq '.findings[] | select(.observed.outcome=="empty")'
rete card-audit https://host/data.rete --measure --only ov-triples --max-mb 8
rete card-audit data.rete --measure --write-costs

rete search-url <url> [<prefix>] [--contains <word>…] [--contains-prefix P] [--limit N] [--json]

rete search over HTTP — the same two modes and the same output, without downloading the file.

The open is deliberately narrower than any other remote command's. It reads the header and the subject halves of the dictionary (the shared and subject-only sections), and stops: no permutation tile directories, no pyramid, no index. --contains then faults the TEXT_INDEX token table once and one range request per posting list; the bare prefix mode faults the pyramid instead, where the label index lives.

That narrowness is the point, because on a literal-heavy graph the dictionary is most of the file, and on a file built before 2026-08 its object-only chunk directory — which carried every chunk's first term verbatim — is most of what a normal open costs. (Newer builds key that directory by the shortest separator instead, a few bytes per chunk: on epfl-infoscience that is 234 MB of routing keys against ~600 KB. It is a write-side change, so a published file keeps its large directory until it is rebuilt — the figures below are the published one.) Searching epfl-infoscience.rete (1.64 GB; a 1.35 GB dictionary, a 186 MB text index over titles and 132k abstracts) for photosynthesis:

bytes fetchedrequeststime
sparql-url with FILTER(CONTAINS(…))334 MB8328.6 s
search-url --contains29.5 MB54.8 s

Same five hits. The 29.5 MB is the token table itself; label-prefix mode over the same file costs 1.4 MB in 4 requests. A file built without --text-index says so instead of silently scanning.

rete search-url https://host/data.rete --contains glucose phosphate
rete search-url https://host/data.rete --contains graphs --contains-prefix existen --json
rete search-url https://host/data.rete "Photosynth" --limit 3   # label prefix

rete summary-url <url>

Fetch only the header + dictionary + pyramid summary and print the coarse graph. The (large) index is never fetched — the "overview first" path.

rete query-url <url> [--subject S] [--predicate P] [--object O]

Match a triple pattern over HTTP, fetching only the byte ranges the query needs and reporting how many ranges/bytes were pulled. Bound terms are resolved from the dictionary first; if they exist, the reader fetches only the selected permutation payload rather than the whole index container. If a bound term is unknown, the index is skipped entirely and the result is empty.

rete query-url https://host/data.rete --object '<http://ex/Dave>'

rete sparql-url <url> "<query>" [--json]

Run a full SPARQL query over HTTP with lazy tile faulting (tiled files): the open fetches the header, dictionary, pyramid, and the index's small tile directories; index tiles are then range-fetched only when the query's scans and probes touch them, so a selective query reads O(touched tiles) rather than the whole index. A range failure mid-query is reported as an error, never as silently fewer rows.

rete sparql-url https://host/data.rete \
  "PREFIX e: <http://ex/> SELECT ?d WHERE { ?d e:dependsOn+ e:log4x }"

Federation

rete federate <sources…> --query "<SPARQL>" [--json] [--no-route]

Run one SPARQL query across several .rete sources — local file paths and/or http(s):// URLs, mixed — and merge the results at the term (string) level: SELECT rows are unioned + deduped, ASK is OR'd across sources, CONSTRUCT triples are unioned + deduped. Routing (default on) reads each source's predicate set from its summary (index never touched) and prunes sources whose predicates are disjoint from the query's; --no-route queries every source. Per-source diagnostics and the queried/pruned tally go to stderr.

This is union federation — correct for sharded data where each file independently yields complete rows. It does not do cross-file joins, and aggregates (COUNT/GROUP BY) and LIMIT are evaluated per source then unioned (a federated COUNT(*) returns per-source counts, not a global sum). See Federated queries for the full model, limitations, and a real OpenCitations multi-shard example.

rete federate data/opencitations/cites-2021.rete data/opencitations/cites-2024.rete \
  --query 'SELECT ?citing WHERE { ?citing <http://purl.org/spar/cito/cites>
                                          <https://doi.org/10.1038/s41586-021-03819-2> } LIMIT 5'

rete manifest <init|add|status|query|seal|compact>

Manage a writable logical graph as an ordered log of immutable .rete segments plus tombstone (deletion) segments, described by a small JSON manifest — grow a dataset across sessions, delete/update quads, query it all as one graph (joins across segments resolve, unlike federate's UNION), and fold it back into a single .rete. rete serve accepts a manifest, and manifest seal checkpoints the server's journal into fresh segments. See Writable graphs — manifest & WAL.

rete manifest init   g.rete-manifest.json base.rete
rete manifest add    g.rete-manifest.json --adds delta.rete [--dels tomb.rete]
rete manifest query  g.rete-manifest.json "SELECT … WHERE { … }"
rete manifest status g.rete-manifest.json --count
rete manifest seal   g.rete-manifest.json     # journal → segments (stop serve first)
rete manifest compact g.rete-manifest.json    # the whole log → ONE fresh .rete

Exit codes

  • 0 — success.
  • 1 — runtime, data, or network failure: malformed RDF, a missing/corrupt or unsupported .rete, HTTP failure, ignored Range, or unsupported query input.
  • 2 — command-line usage error reported by Clap.
  • 3 — the command completed its requested check, but the graph failed it: SHACL non-conformance or reasoning incoherence.