mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
* 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)
248 lines
7.3 KiB
Markdown
248 lines
7.3 KiB
Markdown
---
|
|
title: "Apache AGE Graph Store"
|
|
description: "PostgreSQL + Apache AGE backend for openCypher queries alongside standard SQL."
|
|
icon: "database"
|
|
---
|
|
|
|
**Backend**: PostgreSQL + [Apache AGE](https://age.apache.org/)
|
|
**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+ |
|
|
|
|
```bash
|
|
pip install psycopg2-binary
|
|
```
|
|
|
|
<Note>Apache AGE must be compiled and installed into your PostgreSQL instance. See the [AGE installation guide](https://age.apache.org/age-manual/master/intro/setup.html).</Note>
|
|
|
|
|
|
## Quick Start
|
|
|
|
<Tabs>
|
|
<Tab title="Unified Facade">
|
|
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()
|
|
```
|
|
</Tab>
|
|
<Tab title="Direct (ApacheAgeStore)">
|
|
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()
|
|
```
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
|
|
## 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
|
|
|
|
```python
|
|
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:
|
|
|
|
<Steps>
|
|
<Step title="Load the extension">
|
|
`CREATE EXTENSION IF NOT EXISTS age;`
|
|
</Step>
|
|
<Step title="Activate AGE in the session">
|
|
`LOAD 'age';`
|
|
</Step>
|
|
<Step title="Set the search path">
|
|
`SET search_path = ag_catalog, "$user", public;`
|
|
</Step>
|
|
<Step title="Create the graph">
|
|
Creates the named graph if it does not already exist.
|
|
</Step>
|
|
</Steps>
|
|
|
|
|
|
## 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. |
|
|
|
|
```python
|
|
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"
|
|
```
|
|
|
|
<Warning>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.</Warning>
|
|
|
|
|
|
## 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.
|
|
|
|
```python
|
|
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:
|
|
|
|
```sql
|
|
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:
|
|
|
|
```python
|
|
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:
|
|
|
|
```python
|
|
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 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
|
|
|
|
```yaml
|
|
services:
|
|
age:
|
|
image: apache/age:latest
|
|
ports:
|
|
- "5432:5432"
|
|
environment:
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: secret
|
|
POSTGRES_DB: agedb
|
|
```
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
Then connect:
|
|
|
|
```python
|
|
store = GraphStore(
|
|
backend="age",
|
|
connection_string="host=localhost port=5432 dbname=agedb user=postgres password=secret",
|
|
)
|
|
```
|