Files
semantica/docs/graph_stores/apache_age.md
T

6.7 KiB

Apache AGE Graph Store

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

Component Version
PostgreSQL 12+
Apache AGE 1.4+ (compiled and installed)
psycopg2 2.9+
pip install psycopg2-binary

Note

: Apache AGE must be compiled and installed into your PostgreSQL instance. See the AGE installation guide.


Quick Start

from semantica.graph_store import GraphStore

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

# Create nodes
alice = store.create_node(labels=["Person"], properties={"name": "Alice", "age": 30})
bob = store.create_node(labels=["Person"], properties={"name": "Bob", "age": 25})

# Create relationship
rel = store.create_relationship(alice["id"], bob["id"], "KNOWS", {"since": 2023})

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

store.close()

Direct Usage (without facade)

from semantica.graph_store.age_store import ApacheAgeStore

store = ApacheAgeStore(
    connection_string="host=localhost dbname=agedb user=postgres password=secret",
    graph_name="my_graph",
)
store.connect()

node = store.create_node(["Entity"], {"semantica_id": "ent-001", "value": "test"})
print(node)
# {"id": 844424930131969, "labels": ["Entity"], "properties": {"semantica_id": "ent-001", "value": "test"}}

store.close()

Configuration

Environment Variables

Variable Description Default
GRAPH_STORE_AGE_CONNECTION_STRING PostgreSQL connection string host=localhost dbname=agedb user=postgres password=postgres
GRAPH_STORE_AGE_GRAPH_NAME AGE graph name semantica

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:

  1. CREATE EXTENSION IF NOT EXISTS age;
  2. LOAD 'age';
  3. SET search_path = ag_catalog, "$user", public;
  4. Creates the named graph if it does not already exist.

This is safe to call repeatedly.


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.

Concept Description
AGE internal ID Auto-generated by AGE. Exposed as "id" in all returned dicts. Used in delete_node(), get_node(), etc.
Semantic ID Application-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"

Important

: 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:

Method Description
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",
)