How to Perform SPARQL Reasoning Across RDF Triple Stores with Semantica

Semantica enables SPARQL reasoning across heterogeneous RDF triple stores by combining a unified TripletStore façade with a SPARQLReasoner that expands queries using inference rules and post-processes results to derive implicit knowledge.

Semantica is a modular Python framework designed for semantic reasoning over distributed knowledge graphs. It abstracts backend-specific implementations—such as Blazegraph, Apache Jena, RDF4J, Anzo, and Oxigraph—behind a common interface, allowing you to run inference-augmented SPARQL queries without modifying application logic when switching stores.

Understanding the Core Architecture

The framework centers on three primary components defined in the semantica-agi/semantica repository:

This separation allows reasoning logic to remain agnostic of whether the underlying store is a remote Jena Fuseki endpoint or an embedded Oxigraph database.

Initializing the TripletStore

Begin by selecting your RDF backend. The TripletStore constructor validates the selection and creates a backend-specific store object (e.g., JenaStore) along with a QueryEngine instance stored in self.query_engine.

from semantica.triplet_store import TripletStore

# Connect to a remote Jena endpoint

store = TripletStore(
    backend="jena",
    endpoint="http://localhost:3030/ds",
)

# Or use an in-memory Oxigraph store (no external endpoint required)

local_store = TripletStore(backend="oxigraph")

The backend identifier must exist in SUPPORTED_BACKENDS. Upon initialization, TripletStore also configures namespaces and prepares bulk-loading utilities.

Loading Data into the Store

Before reasoning, populate the store with RDF data. The store() method converts input dictionaries or objects into internal Triplet representations and uses the backend’s bulk loader.

store.store(knowledge_graph=my_kg, ontology=my_ontology)

This extracts standard RDF namespaces and inserts triples via the backend-specific implementation found in modules like semantica/triplet_store/jena_store.py.

Configuring the SPARQLReasoner

Instantiate the SPARQLReasoner with your initialized store and enable inference processing. The reasoner creates an internal Reasoner instance (a generic rule engine) for managing rule sets.

from semantica.reasoning import SPARQLReasoner

reasoner = SPARQLReasoner(
    triplet_store=store, 
    enable_inference=True
)

When enable_inference is active, the reasoner will expand queries and post-process results using the rule engine defined in semantica/reasoning/reasoner.py.

Expanding Queries with Inference Rules

The expand_query() method parses your SPARQL, retrieves active inference rules from the internal Reasoner, converts each rule to a SPARQL pattern via _rule_to_sparql, and appends these patterns as comment-marked blocks to the original query.

base_query = """
SELECT ?person WHERE {
    ?person a <http://example.org/Person> .
}
"""

expanded_query = reasoner.expand_query(base_query)

# Output includes original query plus "# Inference: ..." blocks

This expansion ensures that the triple store evaluates not only explicit patterns but also rule-derived patterns during execution.

Executing Queries Through the QueryEngine

Currently, SPARQLReasoner.execute_query is a stub raising NotImplementedError until the triplet-store execution path is finalized. Execute the expanded query directly through the store’s QueryEngine, which handles validation, caching, and optimization.

raw_result = store.query_engine.execute_query(
    expanded_query,
    store_backend=store._store_backend,  # The concrete backend instance

    graph=None,                            # Optional named graph

)

The QueryEngine performs the following steps:

  1. Validates SPARQL syntax
  2. Checks self.query_cache for existing results (if enabled)
  3. Applies optimize_query() transformations
  4. Delegates to store_backend.execute_sparql()

It returns a QueryResult dataclass containing bindings, variables, execution metadata, and for CONSTRUCT queries, the raw triples.

Post-Processing Results for Inference

Wrap the raw QueryResult in a SPARQLQueryResult and pass it to infer_results() to apply rule-based deductions after retrieval. This method iterates enabled rules, applies them via _apply_rule_to_results, generates new bindings through _generate_binding_from_conclusion, and deduplicates results using _deduplicate_bindings.

from semantica.reasoning.sparql_reasoner import SPARQLQueryResult

# Wrap the raw engine output

sparql_result = SPARQLQueryResult(
    bindings=raw_result.bindings,
    variables=raw_result.variables,
    metadata=raw_result.metadata,
)

# Apply inference rules to derive additional results

final_result = reasoner.infer_results(sparql_result)

print(f"Original count: {final_result.metadata['original_count']}")
print(f"Inferred count: {final_result.metadata['inferred_count']}")

The returned object includes metadata tracking the number of inferred versus original bindings.

Adding Custom Inference Rules

Define domain-specific logic using the rule syntax ?x predicate object => ?x derived_predicate derived_object. The add_inference_rule() method delegates to the underlying Reasoner which parses and stores the rule for future expansion and inference cycles.


# Rule: Every Person is also a Mortal

rule_text = "?x is_a Person => ?x is_a Mortal"
new_rule = reasoner.add_inference_rule(rule_text)

print(f"Registered rule: {new_rule.name}")

Once added, this rule automatically participates in query expansion (fetching Persons when querying for Mortals) and result post-processing.

Complete Workflow Example

The following example demonstrates an end-to-end pipeline using an in-memory Oxigraph backend:

from semantica.triplet_store import TripletStore
from semantica.reasoning import SPARQLReasoner
from semantica.reasoning.sparql_reasoner import SPARQLQueryResult

# 1️⃣ Initialize store

store = TripletStore(backend="oxigraph")

# 2️⃣ Load data

store.store(knowledge_graph=my_kg, ontology=my_ontology)

# 3️⃣ Create reasoner with inference enabled

reasoner = SPARQLReasoner(triplet_store=store, enable_inference=True)

# 4️⃣ Define query

query = """
SELECT ?x WHERE {
    ?x a <http://example.org/Mortal> .
}
"""

# 5️⃣ Expand with rules (includes Persons if the rule above was added)

expanded = reasoner.expand_query(query)

# 6️⃣ Execute via QueryEngine

raw = store.query_engine.execute_query(
    expanded,
    store_backend=store._store_backend,
)

# 7️⃣ Wrap and infer

wrapped = SPARQLQueryResult(
    bindings=raw.bindings,
    variables=raw.variables,
    metadata=raw.metadata,
)
final = reasoner.infer_results(wrapped)

print("Final bindings:", final.bindings)

Summary

  • Semantica unifies access to Blazegraph, Jena, RDF4J, Anzo, and Oxigraph through the TripletStore façade in semantica/triplet_store/triplet_store.py.
  • Query execution flows through QueryEngine.execute_query(), which validates, caches, and optimizes SPARQL before delegating to backend-specific endpoints.
  • Reasoning occurs in two phases: SPARQLReasoner.expand_query() modifies queries to include rule patterns, while infer_results() derives new bindings from existing data post-execution.
  • Custom rules are registered via add_inference_rule() and parsed by the Reasoner class in semantica/reasoning/reasoner.py, enabling domain-specific inference without modifying core code.

Frequently Asked Questions

Which RDF triple stores are supported by Semantica?

Semantica supports Blazegraph, Apache Jena, RDF4J (Eclipse rdf4j), Anzo, and Oxigraph. The TripletStore constructor validates your selection against an internal SUPPORTED_BACKENDS registry and instantiates the appropriate concrete store class (e.g., JenaStore or OxigraphStore) located in semantica/triplet_store/.

How does query expansion work in the SPARQLReasoner?

When you call expand_query(), the reasoner parses your SPARQL string, retrieves all active rules from its internal Reasoner instance, converts each rule to a SPARQL graph pattern using _rule_to_sparql, and appends these patterns as inference blocks to your query. This ensures the triple store evaluates both explicit and implicit patterns during execution.

Is query caching available for reasoning workflows?

Yes, caching is implemented in the QueryEngine class (semantica/triplet_store/query_engine.py) via self.query_cache. While SPARQLReasoner.execute_query() is currently a stub awaiting full implementation, executing queries directly through store.query_engine.execute_query() leverages this cache automatically, checking for stored results before running expensive SPARQL operations.

What syntax is used for custom inference rules?

Rules follow the pattern ?variable predicate object => ?variable derived_predicate derived_object. For example, ?x is_a Person => ?x is_a Mortal creates a subclass inference. The add_inference_rule() method passes this string to the underlying Reasoner, which parses and stores it for use in both query expansion and result post-processing phases.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →