Backend: PostgreSQL + Apache AGE
Driver: psycopg2
Apache AGE is a PostgreSQL extension that adds graph database functionality, enabling you to run openCypher queries alongside traditional SQL. This backend lets Semantica use AGE as a property graph store with the same interface as Neo4j and FalkorDB.

Prerequisites

ComponentVersion
PostgreSQL12+
Apache AGE1.4+ (compiled and installed)
psycopg22.9+
pip install psycopg2-binary
Apache AGE must be compiled and installed into your PostgreSQL instance. See the AGE installation guide.

Quick Start

Use GraphStore(backend="age", …): the same interface as Neo4j and FalkorDB:
from semantica.graph_store import GraphStore

store = GraphStore(
    backend="age",
    connection_string="host=localhost dbname=agedb user=postgres password=secret",
    graph_name="semantica",
)
store.connect()

alice = store.create_node(labels=["Person"], properties={"name": "Alice", "age": 30})
bob   = store.create_node(labels=["Person"], properties={"name": "Bob",   "age": 25})
store.create_relationship(alice["id"], bob["id"], "KNOWS", {"since": 2023})

result = store.execute_query("MATCH (p:Person) RETURN p", cols="p agtype")
print(result["records"])
store.close()

Configuration

Environment Variables

VariableDescriptionDefault
GRAPH_STORE_AGE_CONNECTION_STRINGPostgreSQL connection stringhost=localhost dbname=agedb user=postgres password=postgres
GRAPH_STORE_AGE_GRAPH_NAMEAGE graph namesemantica

Programmatic Configuration

from semantica.graph_store.config import graph_store_config

graph_store_config.set("age_connection_string", "host=db.example.com dbname=prod_age user=app")
graph_store_config.set("age_graph_name", "production")

Connection & Initialization

On connect(), the store performs idempotent setup: safe to call repeatedly:
1

Load the extension

CREATE EXTENSION IF NOT EXISTS age;
2

Activate AGE in the session

LOAD 'age';
3

Set the search path

SET search_path = ag_catalog, "$user", public;
4

Create the graph

Creates the named graph if it does not already exist.

ID Handling

Apache AGE auto-generates internal vertex/edge IDs (large integers). These are not the same as any semantic or application-level ID you may want to assign.
ConceptDescription
AGE internal IDAuto-generated by AGE. Exposed as "id" in all returned dicts. Used in delete_node(), get_node(), etc.
Semantic IDApplication-level identifier. Store it in the semantica_id property.
node = store.create_node(
    labels=["Document"],
    properties={"semantica_id": "doc-abc-123", "title": "My Doc"},
)
# node["id"] → AGE internal ID (e.g., 844424930131969)
# node["properties"]["semantica_id"] → "doc-abc-123"
Never mix AGE internal IDs with semantic IDs. Use node["id"] for graph operations (delete, update, traverse) and node["properties"]["semantica_id"] for application-level lookups.

Label Handling

AGE supports exactly one label per vertex. Semantica handles this transparently:
  • labels[0] → used as the primary AGE vertex label.
  • labels[1:] → stored in a labels property array on the vertex.
When reading nodes, the store reconstructs the full label list automatically.
node = store.create_node(
    labels=["Person", "Employee", "Admin"],
    properties={"name": "Alice"},
)
# In AGE: vertex with label "Person" and property labels=["Employee", "Admin"]
# Returned: {"id": ..., "labels": ["Person", "Employee", "Admin"], "properties": {"name": "Alice"}}

Cypher Query Execution

All Cypher queries are executed via AGE’s SQL wrapper:
SELECT * FROM cypher('graph_name', $$ <cypher_query> $$) AS (col1 agtype, ...);

Parameter Substitution

AGE does not support $param style binding inside cypher() calls. The store safely converts parameters to Cypher literals with proper escaping:
result = store.execute_query(
    "MATCH (p:Person) WHERE p.age > $min_age RETURN p",
    parameters={"min_age": 25},
    cols="p agtype",
)

Column Specification

For custom queries, pass the cols option to specify the AS clause:
result = store.execute_query(
    "MATCH (a)-[r]->(b) RETURN a, r, b",
    cols="a agtype, r agtype, b agtype",
)
If omitted, the store attempts to infer columns from the RETURN clause.

Transactions

The store uses explicit PostgreSQL transactions:
  • SuccessCOMMIT
  • ExceptionROLLBACK, then re-raise as ProcessingError
  • No silent failures

API Reference

All methods match the standard Semantica graph store backend interface:
MethodDescription
connect(**options)Connect and initialize AGE
close()Close the connection
create_node(labels, properties)Create a vertex
create_nodes(nodes)Batch create vertices
get_node(node_id)Get vertex by AGE ID
get_nodes(labels, properties, limit)Query vertices
update_node(node_id, properties, merge)Update vertex properties
delete_node(node_id, detach)Delete a vertex
create_relationship(start_id, end_id, type, properties)Create an edge
get_relationships(node_id, rel_type, direction, limit)Query edges
delete_relationship(rel_id)Delete an edge
execute_query(query, parameters)Run arbitrary Cypher
get_neighbors(node_id, rel_type, direction, depth)Graph traversal
shortest_path(start_id, end_id, rel_type, max_depth)Path finding
create_index(label, property_name, index_type)Create a PostgreSQL index
get_stats()Graph statistics

Docker Setup

services:
  age:
    image: apache/age:latest
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: agedb
docker compose up -d
Then connect:
store = GraphStore(
    backend="age",
    connection_string="host=localhost port=5432 dbname=agedb user=postgres password=secret",
)