Files
Mohd Kaif a326c7d3bd Fix/mintlify theme (#645)
* fix: replace invalid Mintlify theme 'venus' with 'mint'

* docs: replace em dashes with colons across all docs files

* fix: strip UTF-8 BOM from all docs files (broke frontmatter detection)
2026-06-17 13:26:19 +05:30

7.3 KiB

title, description, icon
title description icon
Apache AGE Graph Store PostgreSQL + Apache AGE backend for openCypher queries alongside standard SQL. database

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

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:
```python
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()
```
Import `ApacheAgeStore` directly when you need AGE-specific behaviour:
```python
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: safe to call repeatedly:

`CREATE EXTENSION IF NOT EXISTS age;` `LOAD 'age';` `SET search_path = ag_catalog, "$user", public;` 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.

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"

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