* 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)
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()
```
```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:
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 alabelsproperty 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:
- Success →
COMMIT - Exception →
ROLLBACK, then re-raise asProcessingError - 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",
)