Load a tiny RDF dataset, turn on OWL 2 RL reasoning, and query an inferred triple.
This tutorial takes one path from an empty store to a query answer that only appears because HornDB reasoned about it. Follow the steps in order; each one shows what you should see before you move on.
Note
This page runs against a real HornDB server at render time. Quarto starts the server, sends it the query below over HTTP, and captures the actual response — the output you see is tested, not hand-typed.
Before you start
You need:
HornDB’s source checked out, with the serve binary built: cargo build -p horndb-sparql --bin serve.
Python 3, to drive the walkthrough. No package install is needed — every cell below uses only the Python standard library.
HornDB does not yet publish a Python package on PyPI, so this tutorial talks to the server directly over HTTP instead of through a client library.
Step 1 — Start a reasoning server over two facts
Write a small class hierarchy to a Turtle file — a Cat is a Mammal, and Felix is a Cat — then start serve with --materialize, which runs OWL 2 RL forward-chaining over the data before it answers any query.
import atexitimport http.clientimport jsonimport subprocessimport tempfileimport timefrom pathlib import Pathfrom urllib.parse import urlencodeturtle ="""@prefix ex: <http://example.org/> .@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .ex:Cat rdfs:subClassOf ex:Mammal .ex:Felix a ex:Cat ."""data_file = tempfile.NamedTemporaryFile(mode="w", suffix=".ttl", delete=False)data_file.write(turtle)data_file.close()log_path = Path(tempfile.mktemp(suffix=".log"))def find_serve_binary(start: Path) -> Path:for parent in [start, *start.parents]: candidate = parent /"target"/"debug"/"serve"if candidate.exists():return candidateraiseFileNotFoundError("target/debug/serve not found; build it first with: ""cargo build -p horndb-sparql --bin serve" )serve_bin = find_serve_binary(Path.cwd())host, port ="127.0.0.1", 18173withopen(log_path, "w") as log_file: server = subprocess.Popen( [str(serve_bin),"--data", data_file.name,"--materialize","--bind", f"{host}:{port}", ], stdout=log_file, stderr=subprocess.STDOUT, )def stop_server():if server.poll() isNone: server.terminate()try: server.wait(timeout=5)except subprocess.TimeoutExpired: server.kill() server.wait(timeout=5)atexit.register(stop_server)deadline = time.monotonic() +15whileTrue:if server.poll() isnotNone:raiseRuntimeError(f"serve exited early (code {server.returncode}); log:\n"f"{log_path.read_text()}" )try: conn = http.client.HTTPConnection(host, port, timeout=0.5) conn.connect() conn.close()breakexceptOSError:if time.monotonic() > deadline:raiseTimeoutError("serve did not start accepting connections in time") time.sleep(0.1)print(log_path.read_text().strip().splitlines()[-1])
serve: 155 triples loaded; SPARQL query endpoint at http://127.0.0.1:18173/query
The last line confirms the store is loaded and the query endpoint is up. The triple count is higher than the two facts you wrote — OWL 2 RL’s own vocabulary axioms join the closure alongside them.
Step 2 — Ask a question the data does not state
No triple says Felix is a Mammal — you never asserted it. Ask anyway, over the server’s /query route:
params = urlencode( {"query": "PREFIX ex: <http://example.org/> SELECT ?x WHERE { ?x a ex:Mammal }"})conn = http.client.HTTPConnection(host, port, timeout=5)conn.request("GET",f"/query?{params}", headers={"Accept": "application/sparql-results+json"},)response = conn.getresponse()result = json.loads(response.read())conn.close()for binding in result["results"]["bindings"]:print(binding["x"]["value"])
http://example.org/Felix
HornDB applied the OWL 2 RL rule cax-sco — anything in a subclass is also in the superclass — to infer Felix a ex:Mammal and answer the query.
Proof tracking is not exposed yet
HornDB’s reasoner keeps a proof for every triple it derives internally, but that proof is not yet reachable through SPARQL or the HTTP API. This tutorial stops at the answer; it does not show which rule and premises produced it.