Skip to main content

vortex-rdflib

CI CodSpeed PyPI Python versions License: MIT

An rdflib Store implementation for Vortex-RDF, a columnar zero-copy RDF serialization format — so .vortex files can be queried with SPARQL.

The native layer is the vortex-rdf package (PyO3 bindings over the vortex-rdf-core Rust crate), pulled in as a dependency: stores are opened lazily from .vortex files by default, and queried in place without loading the dataset into memory. However, a .vortex file can also be fully loaded in memory, with exactly the same data structure and queried.

Install

pip install vortex-rdflib

Python 3.11+. The vortex-rdf dependency ships prebuilt wheels for Linux (x86_64, aarch64), macOS (x86_64, arm64) and Windows (x64); on other platforms it builds from source.

Usage

from rdflib import Graph
from vortex_rdflib import VortexRdflibStore

graph = Graph(store=VortexRdflibStore("data.vortex"))
for row in graph.query("""
    SELECT ?s ?o WHERE {
        ?s <http://xmlns.com/foaf/0.1/name> ?o
    }
    LIMIT 10
"""):
    print(row.s, row.o)

SPARQL evaluation is rdflib's engine; the store serves quad patterns from the Vortex file. The store is read-only so far; mutation support is on the roadmap.

A .vortex file holds quads, so the store is context-aware. A Dataset gives the named graphs, and GRAPH works in SPARQL:

from rdflib import Dataset
from vortex_rdflib import VortexRdflibStore

# default_union=True makes the SPARQL default graph the union of every graph
dataset = Dataset(store=VortexRdflibStore("data.vortex"), default_union=True)

for graph in dataset.graphs():
    print(graph.identifier, len(graph))

for row in dataset.query("""
    SELECT ?g (COUNT(*) AS ?n) WHERE {
        GRAPH ?g { ?s ?p ?o }
    }
    GROUP BY ?g
"""):
    print(row.g, row.n)

A plain Graph(store=VortexRdflibStore(path)) — as in the example above — is the view over the whole file, every graph included. It is a multiset view — a triple in two graphs is yielded twice: the store streams the quads it holds rather than building the RDF merge.

To produce a .vortex file from an RDF file, use the binding layer directly (or the vortex-rdf CLI):

from vortex_rdf import serialize_rdf

serialize_rdf("data.nq", "data.vortex", format="nquads", layout="dictionary")

layout accepts "default", "typed-object" and "dictionary" (described here); opening auto-detects the layout. The "dictionary" layout is the fastest to query from Python — it enables the SPARQL pushdowns described below.

How it works

Term codes instead of strings. For Dictionary-layout stores, matched rows cross the native boundary as zero-copy u32 term-code columns (vortex_rdf.VortexRdfStore.match_codes), and each distinct code is decoded to an rdflib term once — in one GIL-released TermDict.decode_many call per batch — and cached for the store's lifetime. Other layouts fall back to N-Triples string columns, parsing each distinct term once.

SPARQL pushdown. Constructing a VortexRdflibStore registers an rdflib CUSTOM_EVALS hook that answers the algebra operators it understands over vortex term codes instead of leaving them to rdflib's per-row evaluation: basic graph patterns, FILTER, OPTIONAL, MINUS, FILTER (NOT) EXISTS, nested groups and VALUES, projection, DISTINCT, ORDER BY, LIMIT/OFFSET, ASK and COUNT aggregates above them. Anything else is evaluated by rdflib. Each pushdown is described, with an example and numbers, in docs/pushdown.md; the switches to disable or narrow it are in the table below.

File-backed vs in-memory. The default open is lazy and file-backed. VortexRdflibStore(path, in_memory=True) (or env VORTEX_RDF_IN_MEMORY=1) loads the store into memory once, so queries skip the per-call file-read pipeline. That helps mainly point lookups and joins; the scan-dominated queries are bound by rdflib's own result handling either way.

Secondary indexes. serialize_rdf(..., indexes=["secondary-by-copy"]) (or "secondary-by-reference") writes index components into the .vortex file, for a modest increase in build time and file size. They pay off on file-backed stores answering single-pattern lookups, where they largely erase the file-backed penalty for object and predicate-object lookups. On an in-memory store they change nothing measurable, since the rows are resident already, and on multi-pattern joins the run-to-run spread is wider than any effect they have. Enable them for lookup-heavy file-backed workloads.

For Dictionary-layout files, the term dictionary is held in memory when it fits the residency budget; pass VortexRdflibStore(path, max_resident_bytes=...) (the dictionary's compressed size in bytes) to raise the budget (recommended for large stores).

Environment variables

Variable Effect
VORTEX_RDF_IN_MEMORY=1 Load stores into memory instead of file-backed lazy open
VORTEX_RDF_DISABLE_CODE_PATH=1 Force the N-Triples string path instead of u32 codes
VORTEX_RDF_DISABLE_PUSHDOWN=1 Keep rdflib's default evaluator for every operator (see docs/pushdown.md)
VORTEX_RDF_PUSHDOWN_OPS=<list> Only push down the listed algebra nodes (bgp = basic graph patterns only)
VORTEX_RDF_FILTER_FAST=0 Evaluate every FILTER value through rdflib's expression evaluator (still once per distinct value)
VORTEX_RDF_TRACE_TRIPLES=1 Print every triples() pattern (debugging)
VORTEX_RDF_TRACE_QUERY=1 Print the pushdown's query plan as JSON lines on stderr — counts, matches, probes, restrictions, joins, with row counts and timings (debugging; VORTEX_RDF_TRACE_QUERY_ID labels the lines)

Benchmarks

A comparative benchmark — VortexRdflibStore against rdflib's in-memory Memory store, oxrdflib (Oxigraph), pycottas (COTTAS) and rdflib-hdt (HDT) — runs on every push to main and publishes the current numbers to GitHub Pages: https://vortex-rdf.github.io/vortex-rdflib/. That dashboard is the reference for how these variants actually compare; timings vary with machine and dataset.

It executes a synthetic representative SPARQL set (lookups/scans, star and chain joins, FILTER/DISTINCT/ORDER BY/GROUP BY, and the shapes that name a graph) and records per-store peak RSS; each store's full lifecycle runs in its own process. SPARQL evaluation is rdflib's engine for every store, so the store serving quad patterns is the only variable — a store's own SPARQL engine is out of scope, since it skips rdflib's parse and algebra and is not measuring the same work.

The dataset is a set of quads — every statement about a subject goes into one graph, so the union of the graphs is exactly the triple set — and each store loads it as an rdflib Dataset whose default graph is that union. HDT and COTTAS cannot serve named graphs through rdflib — HDT's format has none, and pycottas' COTTASStore does not expose the ones COTTAS files can hold — so those two rows load the flattened N-Triples, the same statements, and are not asked the graphs group, whose cells stay empty for them.

The Vortex rows are all Dictionary layout — the layout that enables the term codes path — crossed over the two axes that change how a store answers: residency (file-backed vs in-memory) and secondary index (none, by-copy, by-reference).

pycottas and rdflib-hdt pin dependencies that cannot share the project environment — pycottas an exact pyoxigraph, rdflib-hdt an exact rdflib — so run_bench builds each a throwaway virtualenv and runs that worker with its interpreter. rdflib-hdt reads HDT but cannot write it, so the HDT file is built by the Rust crate's CLI, which the refresh script installs on demand.

Run it locally with scripts/refresh.sh — it syncs the contenders, ensures the HDT builder, measures, and re-renders the dashboard:

scripts/refresh.sh                      # every stage, at the 250k CI scale
BENCH_TRIPLES=20000 scripts/refresh.sh  # scale down
scripts/refresh.sh --only render        # template-only edits: no re-measurement

Regression tracking (CodSpeed)

The dashboard answers "how does this compare?"; it cannot answer "did this commit make things slower?", because wall-clock numbers from a shared CI runner move on their own. bench/test_codspeed.py covers that: the same dataset generator and the same query set, measured per commit under CodSpeed's CPU simulation so every task gets a deterministic instruction count. Every pull request gets a report at https://app.codspeed.io/vortex-rdf/vortex-rdflib, so a change that costs instructions is visible before it lands.

Only the vortex variants are measured: another library's instruction count moves when it releases, which is not a signal this repo can act on. And since instruction counts are deterministic, the suite does not run the full configurations × queries cross product; the whole query set runs on the primary configuration (Dictionary layout, in-memory, pushdown on) and each other axis is isolated on the queries where it can move the number — the pushdown A/B on the join queries, file-backed opens and secondary indexes on the lookups they target, the Store.triples() service per pattern selectivity, the u32 term codes path against the N-Triples string fallback, and each residency's open cost.

The suite is not part of uv run pytest (which runs tests/ only); run it explicitly. 32,768 triples by default — small enough for Valgrind, and the size the vortex-rdf Rust and JS suites share, so a shared-core regression lands in every tab at comparable magnitude — override with CODSPEED_BENCH_TRIPLES:

uv run pytest bench/test_codspeed.py --codspeed   # wall-clock, no instrumentation
CODSPEED_BENCH_TRIPLES=5000 uv run pytest bench/test_codspeed.py --codspeed

Development

The repo is managed with uv; uv.lock pins the development environment.

uv sync              # create .venv and install deps + dev group
uv run pytest
uv run ruff format   # format (check mode in CI)
uv run ruff check    # lint
uv run ty check      # type check
uv build             # sdist + wheel into dist/

One-time setup after cloning — install the git hooks so git push runs the same checks as CI first (and commit messages follow Conventional Commits):

./scripts/install-git-hooks.sh

Run the checks manually with ./scripts/ci-check.sh; skip a hook once with git commit --no-verify / git push --no-verify.

Releasing

.github/workflows/release.yml builds the sdist and wheel and uploads them to PyPI via Trusted Publishing — no API token is stored in the repository. To cut a release:

  1. Bump version in pyproject.toml (and run uv lock to sync the lockfile).
  2. Update the changelog: scripts/update-changelog.sh v<version> stamps the [Unreleased] section (regenerated from Conventional Commits via git-cliff; refresh anytime with scripts/update-changelog.sh).
  3. Commit, then push a matching vX.Y.Z tag.

The full CI matrix runs on the tagged commit and must pass before anything is built; the workflow refuses to publish if the tag, pyproject.toml and uv.lock disagree on the version; and the wheel is smoke-tested against the test suite before upload. Running the workflow by hand (Actions → Release → Run workflow) is a dry run — build, validate, smoke-test, publish nothing — unless the "Publish to PyPI" toggle is on, which is how to retry a release whose publish step failed.

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

vortex_rdflib-0.1.2.tar.gz (78.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vortex_rdflib-0.1.2-py3-none-any.whl (54.6 kB view details)

Uploaded Python 3

File details

Details for the file vortex_rdflib-0.1.2.tar.gz.

File metadata

  • Download URL: vortex_rdflib-0.1.2.tar.gz
  • Upload date:
  • Size: 78.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for vortex_rdflib-0.1.2.tar.gz
Algorithm Hash digest
SHA256 ee6ece4c0cf08b8fe2ed12b716aaccadd060e15166ee0f49970738c815c6268b
MD5 1bda826d8c7cd08d278eba23b7968907
BLAKE2b-256 f8db4d1d5155c76c549bcbd7ed17a5ac8d9ee63ef03be55cd2826ac24e6c43b4

See more details on using hashes here.

Provenance

The following attestation bundles were made for vortex_rdflib-0.1.2.tar.gz:

Publisher: release.yml on vortex-rdf/vortex-rdflib

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file vortex_rdflib-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: vortex_rdflib-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 54.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for vortex_rdflib-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 7266a47ffdc74e626604712388ccb33788dde7b4275f1e5a0508bb0a8e325fd3
MD5 bef42bce2a952d6187880138a86dcc1d
BLAKE2b-256 fec13b10ab97ff76a706ff19cdb0351b76684a7c1708c4b78c9c98d4e12b8e4f

See more details on using hashes here.

Provenance

The following attestation bundles were made for vortex_rdflib-0.1.2-py3-none-any.whl:

Publisher: release.yml on vortex-rdf/vortex-rdflib

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page