Triple Store Configuration¶
OntoCast stores ontologies and facts through a unified TripleStoreManager interface. Two backends are supported today:
- Apache Fuseki (production) — persistent RDF store with SPARQL
- 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.
Configure OntoCast:
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()→Trueplusaselect()— enables targeted catalog reads and reference expansion.supports_sparql_construct()→Trueplusaconstruct()— 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.