Skip to content

Triple Store Configuration

OntoCast stores ontologies and facts through a unified TripleStoreManager interface. Two backends are supported today:

  1. Apache Fuseki (production) — persistent RDF store with SPARQL
  2. In-Memory (pyoxigraph) (default) — zero-config backend for development and tests

When FUSEKI_URI and FUSEKI_AUTH are set, Fuseki is used. Otherwise OntoCast uses the in-memory backend automatically.


Configuration

Environment Variables

# Fuseki (optional — production; the bundled docker compose maps host port 3032)
FUSEKI_URI=http://localhost:3030
FUSEKI_AUTH=admin/admin
#FUSEKI_DATASET=ontocast--test--facts
#FUSEKI_ONTOLOGIES_DATASET=ontocast--test--ontologies
#FUSEKI_SHAPES_DATASET=ontocast--test--shapes

# Seed ontologies and SHACL shapes (optional — bootstrap only, not persistence)
ONTOCAST_ONTOLOGY_DIRECTORY=/path/to/seed/ttl
FACTS_SHAPES_DIR=/path/to/seed/shapes

Persistence is handled by the triple store only; batch TTL dumps go to the explicit --output-dir family of flags.

Ontology catalog reads (headers, by-IRI graphs, merged working graphs) go through OntologyManager, not ad-hoc fetch_ontologies() from callers — see Ontology Catalog.

Tenancy and Partitions

Both Fuseki and the in-memory backend isolate data by tenant/project:

  • {tenant}--{project}--facts — extracted facts graphs
  • {tenant}--{project}--ontologies — catalog / versioned ontologies
  • {tenant}--{project}--shapes — SHACL shapes documents

When dataset env vars are unset, OntoCast derives names from the default tenant ontocast and project test. Per-request ?tenant= / ?project= retarget the active partition at runtime. See Tenancy.

Detecting the Active Backend

from ontocast.config import Config

config = Config()
tool_config = config.get_tool_config()

if tool_config.fuseki.uri and tool_config.fuseki.auth:
    print("Using Fuseki triple store")
else:
    print("Using in-memory triple store")

Apache Fuseki Setup

Sample Docker configs: ontocast/docker.

cd docker/fuseki
cp .env.example .env
docker compose --env-file .env up fuseki -d

Configure OntoCast:

FUSEKI_URI=http://localhost:3032
FUSEKI_AUTH=admin/your-password

In-Memory Backend

No setup required. Data lives in process memory (pyoxigraph) and is lost on restart.

Use Fuseki for production deployments. The in-memory backend supports the same tenancy partition model as Fuseki.


Known Limitation: Integer Subtypes Collapse on Insert

pyoxigraph normalizes literals into its value space when a quad is added: "1"^^xsd:nonNegativeInteger is stored — and served back — as "1"^^xsd:integer, independent of serialization format. OWL 2 requires xsd:nonNegativeInteger on owl:qualifiedCardinality / owl:maxQualifiedCardinality, so an ontology round-tripped through the store is no longer OWL 2 DL conformant on those axioms, and an external reasoner may reject or ignore them.

Content hashing is insensitive to this (literals are canonicalized onto the value-space normal form before hashing), so it causes no identity drift inside OntoCast — but the served ontology is lossy. If strict OWL 2 DL conformance of exported ontologies matters, keep the authored Turtle as the source of truth (e.g. under ONTOCAST_ONTOLOGY_DIRECTORY) rather than re-exporting from the store.


Seed Ontologies and Shapes

Place .ttl files in ONTOCAST_ONTOLOGY_DIRECTORY. On startup, ToolBox scans that directory and materializes any ontologies not already present in the triple store. This is a one-way bootstrap path — ongoing persistence is through the triple store.

FACTS_SHAPES_DIR works the same way for SHACL shapes, into the shapes partition, except that the scan is recursive and each document is written on every startup rather than only when absent — so editing a seed shapes file takes effect on restart. Neither directory is ever written to: POST /ontologies and POST /shapes mutate the store alone, and the matching DELETE routes leave your files untouched. See Validation for why shapes get a partition of their own.

Why Shapes Are Not in the Ontologies Dataset

afetch_ontology_catalog() claims every named graph carrying an owl:Ontology subject. A SHACL shapes document declares one, so stored beside the ontologies it would register as a catalog ontology, be vector-indexed, and be offered to the renderer as schema. The third dataset removes that failure mode structurally rather than by a filter every read path must honour.


Targeted Catalog Reads

fetch_ontologies() materializes every stored ontology into rdflib. That is the right call at startup, but it is far too much for the per-content-unit retrieval path, which only needs to know which ontologies to pull. TripleStoreManager therefore exposes three narrower reads:

Method Returns Cost
aselect(query, *, store="ontologies") list[dict[str, str]] — one dict per SPARQL SELECT solution One query
aconstruct(query, *, store="ontologies") RDFGraph — real RDF terms, no prefix bindings One query
afetch_ontology_catalog() list[OntologyHeader]iri, version, hash, parent_hashes, created_at, graph_uri per stored version One query, no graphs
afetch_ontologies_by_iri(iris) list[Ontology] with graphs, restricted to iris (empty means no restriction) Only the named graphs requested

aselect rows carry each term's lexical value only — term kind and datatype are dropped, so constrain kinds in the query itself (FILTER(isIRI(?x))). Unbound variables are simply absent from the row. aconstruct has no such loss: blank nodes and datatypes survive. What it cannot carry is prefix bindings — those are serialization metadata rather than triples, so a caller that needs them must source them elsewhere.

Both raise rather than returning an empty result on failure, because empty is indistinguishable from "nothing matched".

OntologyHeader is deliberately not an Ontology: constructing an Ontology recomputes its hash from the graph, so a graph-less one would carry fabricated lineage. Run dedupe_terminal_ontologies() over headers to pick terminal versions without downloading anything — it accepts headers and ontologies alike, as does select_relevant_ontologies().

Custom Backends

Implementing a TripleStoreManager subclass still requires only fetch_ontologies(). Every method above has a working base-class default expressed in terms of it, so a custom backend keeps working unchanged — it just fetches more than it needs.

Two independent opt-ins into the fast paths, both dispatched on a predicate rather than on the concrete type:

  • supports_sparql_select()True plus aselect() — enables targeted catalog reads and reference expansion.
  • supports_sparql_construct()True plus aconstruct() — enables the optional candidate pushdown.

They are separate because a backend can answer row queries without returning triples: Fuseki's SELECT path speaks application/sparql-results+json only, and needs a different Accept header for CONSTRUCT.


Backend Comparison

Feature Fuseki In-Memory
Persistence Yes No (process lifetime)
SPARQL Full 1.1 Full 1.1 (pyoxigraph)
aselect fast path Yes Yes
aconstruct fast path Yes Yes
Tenancy partitions Yes Yes
Setup Docker + env Automatic

Flushing Data

# Clean active partition
curl -X POST http://localhost:8999/flush

# Clean a specific tenant/project partition
curl -X POST "http://localhost:8999/flush?tenant=acme&project=demo"

Warning: Flush is irreversible.