Skip to main content

Vortex-RDF for Python

PyPI

Python bindings for Vortex-RDF, a columnar RDF store format built on Vortex. Stores are opened memory-mapped from .vortex files and queried in place, without loading the dataset into memory. The bindings are read-only, with no in-memory build: build .vortex files with serialize_rdf (file → file), then open and query them.

A separate vortex-rdflib package builds an rdflib integration on these bindings; see its own documentation.

Install

pip install vortex-rdf

Quick start

from vortex_rdf import VortexRdfStore, serialize_rdf

serialize_rdf("data.nt", "data.vortex", layout="dictionary")   # RDF file -> .vortex file
store = VortexRdfStore("data.vortex")                            # lazy open; layout auto-detected
store.count_quads(p="<http://xmlns.com/foaf/0.1/name>")          # match count, no terms materialized
store.get_quads(p="<http://xmlns.com/foaf/0.1/name>")            # [(s, p, o, g), ...]

Reading quads

Every read takes a pattern as the keyword arguments s, p, o, g; an omitted position is a wildcard. Terms cross the boundary as N-Triples strings (<iri>, _:b0, "lit"@en, "3"^^<http://www.w3.org/2001/XMLSchema#integer>); the graph of a quad in the default graph is the empty string, which is also how a pattern selects it. A malformed term raises ValueError; a failing store operation raises VortexRdfError. That includes opening a store written by vortex-rdf 0.11 or earlier: its root layout is refused, and the error says to rebuild it from its RDF source with serialize_rdf.

len(store)                                                   # number of quads
store.layout()                                               # "dictionary" | "default" | "typed-object"
store.indexes()                                              # e.g. ["secondary-by-reference"]
store.count_quads(p="<http://xmlns.com/foaf/0.1/name>")      # int
store.get_quads(p="<http://xmlns.com/foaf/0.1/name>")        # [(s, p, o, g), ...]
store.match_columns(p="<http://xmlns.com/foaf/0.1/name>")    # (subjects, predicates, objects, graphs)

get_quads returns whole quads; match_columns returns the same rows transposed into four parallel columns, for callers that work a position at a time. Both are served from the term-code columns whenever the store can (Dictionary layout) and from the matched quads otherwise; results are identical. On the code path a term that repeats down a column is usually one shared Python string: a small cache of recently decoded codes serves the repeats, and a code it has dropped is decoded again as an equal, separate string.

Term codes (low-level)

For Dictionary-layout stores, match_codes returns the matched rows as four zero-copy u64 term-code columns (U64Column) — memoryview(col).cast("Q") views the Rust memory directly — decodable through a term_dict() handle. A dictionary holds at most 231 - 1 terms; a code is a Python int from 0 to 264 - 1, and anything else is refused (OverflowError or ValueError), never narrowed:

cols = store.match_codes(p="<http://xmlns.com/foaf/0.1/name>")  # (s, p, o, g) or None
dictionary = store.term_dict()                                    # TermDict or None
subjects = memoryview(cols[0]).cast("Q")
dictionary.decode(subjects[0])                       # N-Triples string for that code
dictionary.decode_many(cols[0])                      # bulk-decode a whole column
dictionary.encode("<http://xmlns.com/foaf/0.1/name>")  # code for a term, or None

decode_many decodes a batch in one GIL-released call. Buffer-protocol inputs — a column straight from match_codes, memoryview(col).cast("Q"), an array("Q", ...), a uint64 or non-negative int64 NumPy array — are read in a single bulk copy with no per-element int conversion; a raw-byte buffer whose length is a multiple of 8 (memoryview(col) itself, before the cast) is read as native-endian u64s; a buffer of any other items (a cast("I") view, a uint32 array) raises ValueError rather than being read element by element. Any sequence of ints works too. encode is the inverse of decode and tolerant of spelling: an IRI with or without angle brackets, a literal with an explicit xsd:string type or an upper-case language tag, and the default graph as "", default or [] all resolve to the stored form's code (a malformed term raises ValueError); encode_many does a batch. Both term_dict() and match_codes return None when the code path does not apply (a non-Dictionary layout). A file store's dictionary is read from the mapped file on demand — TermDict.file_backed says so — with the same calls.

Consumers can join, count, and de-duplicate entirely in code space and decode each distinct term once, never materializing a term string for a row they discard. The handle and the columns carry the pieces a query layer pushes below a pattern:

NAME = "<http://xmlns.com/foaf/0.1/name>"
d = store.term_dict()
lo, hi = d.prefix_range("<http://xmlns.com/foaf/0.1/")   # codes of one namespace: one range
literals = d.prefix_range('"')                            # codes of every literal

# A FILTER over the candidates a query produced: sorted, unique codes.
cols = store.match_codes(p=NAME)
codes = sorted(memoryview(cols[2].distinct()).cast("Q"))
passed, undecided = d.filter_codes("num_lt", "42", codes)
passed, undecided = d.filter_codes("regex", "^ali", codes, flags="i")
passed, undecided = d.filter_codes("contains", '"bob"', codes, case="lower", as_str=True)

# Narrow inside the store, before a row is gathered: a code set or range per position.
cols = store.match_codes(p=NAME, keep={"o": range(lo, hi)})
cols = store.match_codes(p=NAME, keep={"o": passed, "g": [0]}, limit=100, offset=20)
n = store.count_quads(p=NAME, keep={"o": passed}, limit=1)   # an existence test

# Many probes in one GIL-released call, answered in input order.
views = store.match_codes_many([(None, NAME, None, None), {"s": "<http://ex.org/bob>", "limit": 5}])
counts = store.count_quads_many([{"p": NAME, "keep": {"o": literals}}])

# Column kernels, order-preserving: first-seen distinct values, nested-loop joins.
s, p, o, g = cols
s.distinct(); o.value_counts()
left_idx, right_idx = o.join_indices(s)     # rows where o == s, as index pairs
o.take(left_idx)                            # gather a joined column

filter_codes(kind, arg, codes, *, flags="", case=None, as_str=False) evaluates a FILTER predicate over the candidate codes (sorted and unique, else ValueError) and answers (passed, undecided), both subsets of codes; a candidate in neither fails, and the undecided ones are left to the caller's own evaluator. No verdict is memoized. Kinds: is_literal, is_iri, is_blank; datatype <iri>, lang <tag>, lang_matches <range>; num_lt/num_le/num_gt/num_ge/num_eq/num_ne <number> (value comparison for well-formed numeric literals; xsd:long/xsd:unsignedLong beyond 64 bits undecided; different XSD datatypes order by their IRIs; a non-literal fails the orderings and =, passes !=); and the string kinds str_prefix <p>, contains/strstarts/strends <constant's N-Triples spelling>, regex <pattern> with flags. String kinds follow rdflib 7.6: the text is a string literal's lexical form (with as_str=True, SPARQL STR(): an IRI's string or a literal's lexical form; a blank node, or a literal whose datatype rdflib normalizes, is undecided), case="lower"|"upper" wraps that text (Python's str.lower()/str.upper(), decided on ASCII text only), a language-tagged constant needs the same tag on the text, and regex decides only patterns from an allow-listed subset that agrees with Python's re; the stub's docstring has the exact rules.

keep takes a dict from position ("s", "p", "o", "g" or 0–3) to a code set or a code range. A set is a U64Column, a u64 buffer or a list of ints. A range is a range with step 1 (a stepped range raises ValueError) or a 2-tuple (lo, hi), the half-open codes lo <= code < hi. A 2-tuple is always a range, so a set of codes is passed as a list or a U64Column, never as tuple(codes). limit/offset window the rows in base order, and a filtered file scan stops at the first block that fills the window. A probe of the *_many calls is an (s, p, o, g) tuple or a dict with keys s, p, o, g, keep, limit, offset; every probe is parsed before any is evaluated.

Build options

serialize_rdf(input_path, output_path, *, format=None, layout="dictionary", indexes=[])

serialize_rdf writes beside output_path and renames the finished file into place, so a build that fails (a parse error halfway through the input, say) leaves no partial file and an existing store untouched. A store file the process cannot write (a read-only file) is never replaced: serialize_rdf raises PermissionError. On Linux and macOS rebuilding a path that an open VortexRdfStore has mapped is safe: the open store keeps reading the file it mapped. On Windows the rename is refused while a store has the file mapped, so the rebuild fails with an OSError until that store, and every TermDict and U64Column taken from it, is dropped. Every option after the two paths is keyword-only. format is an RDF format name ("ntriples", "nquads", "turtle", "trig", "n3", "rdfxml", "jsonld", or the short aliases nt, nq, ttl, rdf, xml), detected from the input file extension when omitted. Opening auto-detects the layout and indexes — VortexRdfStore takes no layout argument; store.layout() and store.indexes() report the same names.

layout — how terms are encoded into columns. "dictionary" is the default in every vortex-rdf frontend (Python, JS and the CLI):

Value Notes
"dictionary" (default) Terms replaced by codes into a sorted term dictionary. Most compact and fastest to query; backs match_codes/term_dict
"default" All four terms as N-Triples strings
"typed-object" Object split into kind/value/datatype/language columns

indexes — secondary access paths, each costing extra space:

Value Notes
"secondary-by-reference" Sorted predicate/object columns plus row-id back-references, so predicate-only and object-only patterns use a binary search instead of a full scan
"secondary-by-copy" Two complete extra copies of the quad columns — one sorted by (p, o, s, g), one by (o, s, p, g) — giving predicate- and object-bound patterns (including predicate+object prefix lookups) the same sorted access path subjects have

Bytes & files

A file store is memory-mapped: opening reads only the footer (and, under the Dictionary layout, the dictionary's window bounds), and every query reads the pages it touches straight from the file. What stays in RAM is the operating system's page cache, which shows up as file-backed RSS (RssFile) and is reclaimable, plus at most 4 MiB of rebuilt dictionary windows per memory-mapped store, which every VortexRdfStore(path) is — the process's own (anonymous) memory stays small whatever the store's size. The mapping lives as long as the store, or any TermDict or U64Column taken from it, does. Never truncate or rewrite a store file in place while it is open (serialize_rdf replaces it by rename instead); network filesystems are not supported. VortexRdfStore(path, in_memory=True) loads the whole store into memory instead, so each subsequent match skips the file.

Stores also round-trip through bytes: store.to_bytes() serializes to the native container (the same exchange format as the .vortex file, the CLI and the JS bindings), and VortexRdfStore.from_bytes(data) opens such a buffer — bytes or bytearray — as a fully in-memory store.

The bindings start their worker threads on first use. A process that forks after calling into them (opening a store, serialize_rdf) leaves the child without them, and batch calls in the child hang. Call into the bindings only after forking, or use the spawn or forkserver start method.

Development

Managed with uv; maturin runs under the hood as the build backend:

cd python
uv sync                      # creates .venv, builds + installs the extension
uv run pytest tests          # run the test suite
uv run maturin develop --uv  # fast rebuild while iterating on Rust code

Rust source changes are picked up by uv sync automatically (see [tool.uv] cache-keys in pyproject.toml). Without uv: python -m venv .venv && pip install maturin pytest "rdflib>=7.6,<8" && maturin develop && pytest tests (rdflib is the oracle of tests/test_filter_codes_differential.py).

Building from source (the sdist or a development build) additionally requires libclang: a transitive build dependency of the Vortex file engine (custom-labels, via vortex-io) generates C bindings with bindgen at compile time. It is preinstalled on most dev setups (Xcode, LLVM on Windows); on Linux install e.g. clang-devel (dnf) or libclang-dev (apt). Installing a published wheel needs none of this.

Benchmarks

bench/run.py measures these bindings against pyoxigraph, pycottas, rdflib and lightrdf on a file → store → query workload and writes bench/results.json for the dashboard's Python tab; bench/test_codspeed.py is the instrumented suite CodSpeed runs.

python3 python/bench/run.py                 # full run
BENCH_DIM=32 python3 python/bench/run.py    # quick pilot
uv run pytest bench/test_codspeed.py --codspeed

Harness design (per-library virtualenvs, dataset parity with js/bench/datasets.ts, unsupported cells where a library lacks the operation, matched-row counts cross-checked and any disagreement recorded in config.countWarnings) is documented in bench/run.py, bench/worker.py and bench/adapters.py. Configuration variables:

Var Default Meaning
BENCH_SIZE 1,048,576 rows (value shared with the Rust and JS suites)
BENCH_DIM unset optional cube shorthand, D³ rows; ignored if BENCH_SIZE is set
BENCH_GRAPHS_QUADS 8 named graphs the comparative bench asks for
MUT_BATCH 10000 quads per add/delete batch
BENCH_PYTHON 3.13 Python version the per-library virtualenvs are provisioned with
BENCH_SUBJ_RATIO / BENCH_OBJ_RATIO 0.1 / 0.5 distinct subjects / objects per row
BENCH_PREDICATES 32 distinct predicates
BENCH_GRAPHS 1 distinct named graphs in the generator; 1 means default graph only
BENCH_LITERAL_FRAC 0.4 fraction of objects that are literals
BENCH_SLOW_PHASE_MS 30000 a phase slower than this runs once, without warmup
PY_BENCH_QUERY_ITERS / PY_BENCH_QUERY_WARMUP 10 / 5 measured / warmup iterations per query
PY_BENCH_HEAVY_ITERS / PY_BENCH_FULL_SCAN_ITERS 3 / 3 iterations for the heavy and full-scan phases
CODSPEED_BENCH_DIM / CODSPEED_BENCH_DIM_QUADS 32 / 13 CodSpeed suite: D³ triples / D⁴ quads

License

MIT

Metadata

Release files for vortex-rdf 0.12.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vortex-rdf 0.12.1
File Size Uploaded
vortex_rdf-0.12.1.tar.gz 679.0 kB Details

Built distributions (wheels)

Table of built distributions (wheels) for vortex-rdf 0.12.1
File
vortex_rdf-0.12.1-cp311-abi3-win_amd64.whl CPython 3.11 abi3 Windows x86-64 Details
vortex_rdf-0.12.1-cp311-abi3-manylinux_2_28_x86_64.whl CPython 3.11 abi3 Linux glibc 2.28+ x86-64 Details
vortex_rdf-0.12.1-cp311-abi3-manylinux_2_28_aarch64.whl CPython 3.11 abi3 Linux glibc 2.28+ ARM64 Details
vortex_rdf-0.12.1-cp311-abi3-macosx_11_0_arm64.whl CPython 3.11 abi3 macOS 11.0+ ARM64 Details
vortex_rdf-0.12.1-cp311-abi3-macosx_10_12_x86_64.whl CPython 3.11 abi3 macOS 10.12+ x86-64 Details

Total release size: 64.9 MB

Release files / vortex_rdf-0.12.1.tar.gz

Download URL vortex_rdf-0.12.1.tar.gz
Size 679.0 kB
Tags Source
SHA-256 checksum
How to use checksums
5ea1338a7ca9c9e1b85f8643a2ef5850be3f56fe4782ddb17be52fb3df66fce4
BLAKE2b-256 checksum
How to use checksums
7634a6e5d090fe24e4996b8b20437e95af7bb3bedaf63c6e4de392e285185295
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release files / vortex_rdf-0.12.1-cp311-abi3-win_amd64.whl

Download URL vortex_rdf-0.12.1-cp311-abi3-win_amd64.whl
Size 13.7 MB
Tags CPython 3.11 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
ea7e69655554beef4e6e71703e4a7bf1a4f7ae3329cb426496f0ee81c5ce587c
BLAKE2b-256 checksum
How to use checksums
b6b43c07d3c29c61663fd2992d4d09f573223d8180b2b894cf3402d28630dd19
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release files / vortex_rdf-0.12.1-cp311-abi3-manylinux_2_28_x86_64.whl

Download URL vortex_rdf-0.12.1-cp311-abi3-manylinux_2_28_x86_64.whl
Size 13.5 MB
Tags CPython 3.11 Linux glibc 2.28+ x86-64 abi3
SHA-256 checksum
How to use checksums
42630c638efe55d5800be3039803876b231a7b6893c0071eb91e6879a56c87f2
BLAKE2b-256 checksum
How to use checksums
537cd91988227811f7bb5a37c2e1e676b8a105da0f4ccf57eb6743df810dc847
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release files / vortex_rdf-0.12.1-cp311-abi3-manylinux_2_28_aarch64.whl

Download URL vortex_rdf-0.12.1-cp311-abi3-manylinux_2_28_aarch64.whl
Size 12.6 MB
Tags CPython 3.11 Linux glibc 2.28+ ARM64 abi3
SHA-256 checksum
How to use checksums
ad1f72a83f110a7f882f2fa64d271f7a3622812e3481d309f9bb56ce46926b5b
BLAKE2b-256 checksum
How to use checksums
7d6f2f477069d3f4fcbaa9ab0402412cfced849471050cb14bf0e103b3e4ce2f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release files / vortex_rdf-0.12.1-cp311-abi3-macosx_11_0_arm64.whl

Download URL vortex_rdf-0.12.1-cp311-abi3-macosx_11_0_arm64.whl
Size 11.6 MB
Tags CPython 3.11 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
743e734b7bb5a902f86dd09f059a92b0c48b7f5944aaa9cecdd52e3b316c5fb5
BLAKE2b-256 checksum
How to use checksums
c0cc92fe188a7580b513ee253a57c0a6ccfe13db41bc907d76fced838500daba
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release files / vortex_rdf-0.12.1-cp311-abi3-macosx_10_12_x86_64.whl

Download URL vortex_rdf-0.12.1-cp311-abi3-macosx_10_12_x86_64.whl
Size 12.7 MB
Tags CPython 3.11 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
25166b7d48d12161ddebc21478577ee521192bfddab01a6a83c2a0d32e80e49e
BLAKE2b-256 checksum
How to use checksums
8f3dbd2cd33dbeec3e29492e60ab48cb38f74ed4643cdea7029165a512a9c44a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.12.1 This release

6 release files

0.12.0

6 release files

0.10.0

6 release files

0.5.0

6 release 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