# Neo4j Python SDK Ecosystem Analysis

**Date**: 2026-02-27
**Purpose**: Understand the gold-standard graph database Python SDK to inform Piggie SDK design for Apache AGE + pgvector.

---

## 1. Neo4j Python Driver (v6.1.0) -- The Base Layer

Package: `neo4j` on PyPI. Apache-2.0 licensed. Python 3.10--3.14.

### 1.1 Connection Management

The driver follows a **Driver -> Session -> Transaction** hierarchy:

```python
from neo4j import GraphDatabase

# Driver is the top-level entry point, holds connection pool
driver = GraphDatabase.driver(
    "neo4j://localhost:7687",
    auth=("neo4j", "password"),
    # Connection pool config (all optional with sane defaults)
    max_connection_pool_size=100,          # per host
    connection_acquisition_timeout=60.0,   # seconds
    max_connection_lifetime=3600,          # seconds
    connection_timeout=30.0,               # TCP connect timeout
    keep_alive=True,
)

# Verify connectivity eagerly
driver.verify_connectivity()

# Driver is a context manager
with GraphDatabase.driver(URI, auth=AUTH) as driver:
    ...
```

**Key design decisions**:
- Driver is long-lived, thread-safe, reusable across the application lifetime
- Sessions are lightweight, short-lived, NOT thread-safe
- Connection pool is managed internally (no raw connections exposed)
- Database selection happens at session or query level, not driver level

### 1.2 The Two-Tier Query API

Neo4j provides two distinct API tiers, which is a critical design choice:

**Tier 1: `driver.execute_query()` -- Simple/Convenience API**

```python
records, summary, keys = driver.execute_query(
    "MATCH (p:Person {name: $name}) RETURN p.name AS name, p.age AS age",
    name="Alice",                          # params as kwargs
    database_="neo4j",                     # trailing underscore = config param
    routing_=neo4j.RoutingControl.READ,    # read vs write routing
    result_transformer_=neo4j.Result.to_df # optional: transform to DataFrame
)
```

Returns a named tuple `EagerResult(records, summary, keys)`. This is the "80% use case" API -- one call, automatic transaction management, automatic retry on transient failures.

**Tier 2: Session-based API -- Full Control**

```python
with driver.session(database="neo4j") as session:
    # Managed transactions (with automatic retry)
    result = session.execute_read(lambda tx: list(
        tx.run("MATCH (p:Person) RETURN p.name").data()
    ))

    # Explicit transactions (no retry, full control)
    with session.begin_transaction() as tx:
        tx.run("CREATE (p:Person {name: $name})", name="Bob")
        tx.run("CREATE (p:Person {name: $name})", name="Carol")
        tx.commit()
```

**Design lesson**: The trailing-underscore convention (`database_`, `routing_`) distinguishes config parameters from Cypher parameters. This avoids namespace collisions without requiring a separate params dict.

### 1.3 Result Handling

Results are **streaming cursors** by default -- forward-only, consumed once:

```python
result = tx.run("MATCH (p:Person) RETURN p")

# Cursor methods
result.single(strict=False)   # One record or None
result.fetch(n)               # Up to n records
result.peek()                 # Preview without consuming
result.value(key=0)           # Extract single column
list(result)                  # Materialize all records
result.consume()              # Exhaust stream, return summary

# Record access
for record in result:
    record.data()              # -> dict
    record["name"]             # key access
    record.values()            # all values
```

**Critical constraint**: "A transformer must not return the Result object itself" -- results are invalidated after transaction closure. This forces eager consumption inside transaction boundaries.

### 1.4 Result Transformers (DataFrame Integration)

The `result_transformer_` parameter on `execute_query()` is the gateway to pandas/arrow:

```python
import neo4j

# Direct DataFrame conversion
df = driver.execute_query(
    "MATCH (p:Person) RETURN p.name AS name, p.age AS age",
    database_="neo4j",
    result_transformer_=neo4j.Result.to_df
)
# df is a pandas.DataFrame

# With expansion (flattens nested structures)
df = driver.execute_query(
    "MATCH (p:Person) RETURN p",
    database_="neo4j",
    result_transformer_=lambda res: res.to_df(expand=True, parse_dates=True)
)

# Graph object for visualization
graph = driver.execute_query(
    "MATCH path = (a)-[r]->(b) RETURN path",
    database_="neo4j",
    result_transformer_=neo4j.Result.graph
)
# graph.nodes -> set of Node objects
# graph.relationships -> set of Relationship objects
```

**Optional dependencies** (installed via extras):
- `neo4j[pandas]` -- enables `Result.to_df()`
- `neo4j[pyarrow]` -- enables Arrow-backed DataFrames
- `neo4j[numpy]` -- numpy array support

**Design lesson**: DataFrame support is opt-in via extras, not a core dependency. The transformer pattern is extensible -- users can write custom transformers that receive a `Result` and return anything.

### 1.5 Data Type Mapping

| Cypher Type    | Python Type      | Notes                                    |
|----------------|------------------|------------------------------------------|
| NULL           | `None`           |                                          |
| BOOLEAN        | `bool`           |                                          |
| INTEGER        | `int`            |                                          |
| FLOAT          | `float`          |                                          |
| STRING         | `str`            |                                          |
| LIST           | `list`           |                                          |
| MAP            | `dict`           |                                          |
| ByteArray      | `bytearray`      |                                          |
| Node           | `neo4j.graph.Node` | `.element_id`, `.labels`, properties   |
| Relationship   | `neo4j.graph.Relationship` | `.element_id`, `.type`, `.start_node`, `.end_node` |
| Path           | `neo4j.graph.Path` | Iterable of alternating nodes/rels     |
| Date/Time/DateTime | `neo4j.time.*` | Nanosecond precision; `.to_native()` for stdlib conversion |
| Point (Cartesian) | `neo4j.spatial.CartesianPoint` | `.x`, `.y`, `.z`, `.srid` |
| Point (WGS84)  | `neo4j.spatial.WGS84Point` | `.longitude`, `.latitude`, `.height`  |

**Design lesson**: Rich domain objects for graph primitives (Node, Relationship, Path) rather than raw dicts. This enables IDE autocomplete and type checking. But they also provide `.data()` for dict fallback.

### 1.6 Transaction Patterns

```python
# Pattern 1: Managed transactions with automatic retry
def create_person(tx, name):
    result = tx.run("CREATE (p:Person {name: $name}) RETURN p", name=name)
    return result.single()

session.execute_write(create_person, "Alice")

# Pattern 2: Decorated transaction functions
from neo4j import unit_of_work

@unit_of_work(timeout=5, metadata={"app": "piggie"})
def count_people(tx):
    result = tx.run("MATCH (a:Person) RETURN count(a) AS count")
    return result.single()["count"]

# Pattern 3: Explicit transactions (multi-statement)
with session.begin_transaction() as tx:
    tx.run("CREATE (:Person {name: 'A'})")
    tx.run("CREATE (:Person {name: 'B'})")
    tx.commit()

# Pattern 4: Causal consistency via bookmarks
with driver.session(bookmarks=prev_bookmarks) as session:
    ...
    new_bookmarks = session.last_bookmarks()
```

**Key constraint**: Managed transaction functions MUST be idempotent because the driver may retry them on transient failures.

### 1.7 Async API

Complete async mirror of the sync API:

```python
from neo4j import AsyncGraphDatabase

async_driver = AsyncGraphDatabase.driver(URI, auth=AUTH)

# Tier 1: Simple async query
records, summary, keys = await async_driver.execute_query(
    "MATCH (p:Person) RETURN p.name",
    database_="neo4j"
)

# Tier 2: Async session
async with async_driver.session(database="neo4j") as session:
    result = await session.execute_read(
        lambda tx: tx.run("MATCH (p) RETURN p").data()
    )

await async_driver.close()
```

**Design lesson**: The async API is a near-perfect mirror of sync. Same method names, same parameters, just `async`/`await` added. `AsyncDriver`, `AsyncSession`, `AsyncTransaction` parallel their sync counterparts.

### 1.8 Error Handling

```python
from neo4j.exceptions import (
    Neo4jError,            # Base class
    ServiceUnavailable,    # Connection failures
    AuthError,             # Authentication failures
    ClientError,           # Bad queries, constraint violations
    TransientError,        # Retryable failures (deadlocks, leader changes)
    DatabaseError,         # Server-side errors
)

try:
    driver.execute_query("INVALID CYPHER", database_="neo4j")
except Neo4jError as e:
    e.code        # "Neo.ClientError.Statement.SyntaxError"
    e.message     # Human-readable message
    e.is_retryable()  # Should this be retried?
```

---

## 2. Neo4j GraphRAG Library (neo4j-graphrag-python)

This layers on top of the base driver. Requires Python 3.10+.

### 2.1 Architecture

```
                    GraphRAG (orchestrator)
                   /          |            \
            Retriever        LLM         PromptTemplate
           /    |    \
   Vector  Hybrid  Text2Cypher  VectorCypher  HybridCypher  Tools
     |        |
  Embedder  Embedder + FulltextIndex
```

**Key insight**: The library accepts a raw `neo4j.Driver` instance -- it does not wrap or re-abstract the connection layer. This is the composability pattern we should follow.

### 2.2 Retriever Hierarchy

```python
# Base abstract class
class Retriever:
    def __init__(self, driver: neo4j.Driver, neo4j_database: str = None): ...
    def search(self, *args, **kwargs) -> RetrieverResult: ...
    def get_search_results(self, ...) -> RawSearchResult: ...  # abstract
    def convert_to_tool(self, name, description) -> Tool: ...

# Concrete implementations
class VectorRetriever(Retriever):
    def __init__(self, driver, index_name, embedder=None,
                 return_properties=None, result_formatter=None,
                 neo4j_database=None): ...
    def search(self, query_vector=None, query_text=None, top_k=5,
               effective_search_ratio=1, filters=None) -> RawSearchResult: ...

class HybridRetriever(Retriever):
    def __init__(self, driver, vector_index_name, fulltext_index_name,
                 embedder=None, return_properties=None,
                 result_formatter=None, neo4j_database=None): ...
    def search(self, query_text, query_vector=None, top_k=5,
               effective_search_ratio=1,
               ranker=HybridSearchRanker.NAIVE, alpha=None): ...

class VectorCypherRetriever(Retriever):
    def __init__(self, driver, index_name, retrieval_query,
                 embedder=None, result_formatter=None,
                 neo4j_database=None): ...
    def search(self, query_text=None, query_vector=None, top_k=5,
               effective_search_ratio=1, query_params=None,
               filters=None): ...

class Text2CypherRetriever(Retriever):
    """Uses LLM to convert natural language to Cypher"""
    ...

class ToolsRetriever(Retriever):
    """Wraps retrievers as LLM tools for agent-based retrieval"""
    ...
```

### 2.3 Embedder Interface

```python
class Embedder:
    def embed_query(self, text: str) -> list[float]: ...

# Implementations (installed via extras)
class OpenAIEmbeddings(Embedder): ...      # neo4j-graphrag[openai]
class SentenceTransformerEmbeddings(Embedder): ...  # neo4j-graphrag[sentence-transformers]
class OllamaEmbeddings(Embedder): ...      # neo4j-graphrag[ollama]
class VertexAIEmbeddings(Embedder): ...    # neo4j-graphrag[google]
class MistralAIEmbeddings(Embedder): ...   # neo4j-graphrag[mistralai]
class CohereEmbeddings(Embedder): ...      # neo4j-graphrag[cohere]
class AzureOpenAIEmbeddings(Embedder): ... # neo4j-graphrag[openai]
```

### 2.4 Index Management

```python
from neo4j_graphrag.indexes import (
    create_vector_index,
    create_fulltext_index,
    upsert_vector,
    upsert_vectors,
    drop_index_if_exists,
)

# Create vector index (wraps Neo4j's native vector index)
create_vector_index(
    driver,
    index_name="my_vector_index",
    label="Chunk",
    embedding_property="embedding",
    dimensions=1536,
    similarity_fn="cosine"   # or "euclidean"
)

# Upsert embeddings
upsert_vectors(
    driver,
    ids=["node-id-1", "node-id-2"],
    embedding_property="embedding",
    embeddings=[vector1, vector2],
    entity_type=EntityType.NODE
)
```

### 2.5 RAG Pipeline

```python
from neo4j_graphrag.generation import GraphRAG
from neo4j_graphrag.llm import OpenAILLM

llm = OpenAILLM(model_name="gpt-4o")
rag = GraphRAG(
    retriever=retriever,
    llm=llm,
    prompt_template=custom_template  # optional
)

response = rag.search(
    query_text="Who founded the company?",
    retriever_config={"top_k": 5},
    return_context=True
)
response.answer       # LLM-generated answer
response.retriever_result  # Raw retrieved items (if return_context=True)
```

### 2.6 Knowledge Graph Construction

```python
from neo4j_graphrag.experimental.pipeline.kg_builder import SimpleKGPipeline

kg_builder = SimpleKGPipeline(
    llm=llm,
    driver=driver,
    embedder=embedder,
    schema={
        "node_types": ["Person", "Organization"],
        "relationship_types": ["WORKS_AT", "KNOWS"],
        "patterns": [("Person", "WORKS_AT", "Organization")]
    },
    on_error="IGNORE",
    from_pdf=False
)

# Async execution
await kg_builder.run_async(text="Alice works at Acme Corp...")
```

### 2.7 LangChain/LlamaIndex Integration

Retrievers expose a standard `search()` interface that maps to LangChain's `BaseRetriever` pattern. The `convert_to_tool()` method enables agent-based usage.

---

## 3. LangChain AGEGraph -- The Existing AGE Integration

Source: `langchain_community.graphs.age_graph.AGEGraph`

### 3.1 What It Is

A minimal wrapper (~766 lines) that enables LangChain's `GraphCypherQAChain` to talk to Apache AGE. It does three things:

1. Connects to PostgreSQL via psycopg2
2. Wraps Cypher queries in `ag_catalog.cypher()` SQL calls
3. Extracts graph schema for LLM context

### 3.2 The API

```python
from langchain_community.graphs import AGEGraph

conf = {
    "dbname": "piggie",
    "user": "gregf",
    "host": "localhost",
    "port": 5433,
}

graph = AGEGraph(graph_name="my_graph", conf=conf, create=True)

# Query
results = graph.query("MATCH (n:Person) RETURN n.name")
# -> List[Dict[str, Any]]

# Schema (for LLM context)
graph.get_schema      # -> formatted string
graph.get_structured_schema  # -> dict with node_props, rel_props, relationships

# Document ingestion
graph.add_graph_documents(graph_documents, include_source=True)
```

### 3.3 The SQL Wrapping Pattern (Critical for Piggie)

AGE requires Cypher to be wrapped in a SQL function call:

```python
# AGEGraph._wrap_query() converts:
#   MATCH (p:Person) RETURN p.name AS name, p.age AS age
# into:
#   SELECT * FROM ag_catalog.cypher('my_graph', $$
#       MATCH (p:Person) RETURN p.name AS name, p.age AS age
#   $$) AS (name agtype, age agtype);
```

The wrapper must:
- Parse the RETURN clause to extract column names
- Map each column to `agtype` in the SQL projection
- Handle UNION/EXCEPT by splitting and re-joining
- Handle queries without RETURN (mutations)
- Reject `RETURN *` (AGE cannot resolve the column list)

### 3.4 Result Parsing

AGE returns results as `agtype` strings with type suffixes:

```
'{"id": 123, "label": "Person", "properties": {"name": "Alice"}}::vertex'
'{"id": 456, "label": "KNOWS", "start_id": 123, "end_id": 789, "properties": {}}::edge'
'"Alice"::text'
'42::int'
```

The `_record_to_dict()` method strips the `::type` suffix and deserializes with `json.loads()`.

### 3.5 Gaps and Limitations

| Gap | Impact |
|-----|--------|
| **No parameterized queries** | `params` arg is accepted but ignored; SQL injection risk |
| **psycopg2 only** (sync) | No async support, no psycopg3 (async-native) |
| **Single connection, no pooling** | One `psycopg2.connect()` call, stored on instance |
| **No transaction control** | Every `query()` auto-commits; no multi-statement transactions |
| **No vector support** | Zero awareness of pgvector; graph-only |
| **RETURN * unsupported** | Raises ValueError; AGE limitation but could be worked around |
| **Schema introspection is slow** | Issues separate Cypher queries per label, samples 100 rows per label |
| **No type-rich result objects** | Returns raw dicts, no Node/Relationship classes |
| **No DataFrame integration** | Results are always `List[Dict]` |
| **No connection lifecycle management** | No context manager, no close(), no health checks |
| **LangChain-coupled** | Inherits from `GraphStore`, not usable standalone |

---

## 4. Design Decisions for the Piggie SDK

### 4.1 What to Copy from Neo4j

1. **Two-tier API**: Simple `execute()` for 80% of cases, session-based for full control
2. **Trailing-underscore convention** for config params to avoid collision with Cypher params
3. **Result transformers**: Pluggable `result_transformer_` pattern for DataFrame/Arrow/custom
4. **Rich graph types**: `Node`, `Relationship`, `Path` classes with properties and `.data()` dict fallback
5. **Async mirror**: Same API surface, just async/await -- `AsyncDriver`, `AsyncSession`
6. **Optional extras**: `piggie[pandas]`, `piggie[pyarrow]`, `piggie[embeddings]`
7. **Streaming results**: Forward-only cursor as default, eager materialization opt-in
8. **Embedder interface**: Abstract base with pluggable implementations per provider
9. **Retriever hierarchy**: Base `Retriever` with `VectorRetriever`, `HybridRetriever`, `GraphRetriever`

### 4.2 Where to Deliberately Differ

1. **PostgreSQL-native connection**: Use psycopg3 (async-native) instead of Bolt protocol. This means we inherit PostgreSQL's connection pooling, TLS, pg_bouncer compatibility, etc. The driver IS a psycopg3 wrapper with AGE/pgvector awareness.

2. **Unified graph + vector API**: Neo4j's GraphRAG is a separate package because Neo4j added vector late. We should make vector operations first-class in the base SDK:
   ```python
   # Neo4j: two packages
   from neo4j import GraphDatabase
   from neo4j_graphrag.retrievers import VectorRetriever

   # Piggie: one package
   from piggie import connect
   db = connect("postgresql://localhost:5433/piggie")
   db.cypher("MATCH (n) RETURN n")           # graph via AGE
   db.vector_search("chunks", query_vec, k=5) # vector via pgvector
   db.hybrid_search(...)                       # combined
   ```

3. **SQL escape hatch**: Neo4j is Cypher-only. We can offer raw SQL alongside Cypher, and even mixed queries:
   ```python
   db.sql("SELECT * FROM users WHERE id = $1", user_id)
   db.cypher("MATCH (u:User {id: $id})-[:KNOWS]->(f) RETURN f", id=user_id)
   ```

4. **No `RETURN *` limitation**: We can pre-query `ag_catalog.ag_label` to resolve column names, or use a subquery wrapper to avoid the AGE limitation.

5. **Parameterized Cypher from day one**: The LangChain AGEGraph ignores params entirely. We must properly pass parameters through the `ag_catalog.cypher()` call, likely via `$1`, `$2` positional params in the outer SQL.

6. **Connection pooling via psycopg_pool or asyncpg**: Instead of building our own pool, use the PostgreSQL ecosystem's battle-tested connection pools.

7. **Transaction support**: Map to PostgreSQL transactions directly. `BEGIN`/`COMMIT`/`ROLLBACK` around multiple `ag_catalog.cypher()` calls. This is simpler than Neo4j's managed transaction retry pattern because PostgreSQL transactions are ACID with no leader-election retries needed.

8. **Schema introspection via catalog**: Instead of sampling data (AGEGraph's approach), query `ag_catalog.ag_label` and `information_schema` directly for schema. This is faster and complete.

9. **pgvector index management as first-class**: `create_vector_index()`, `create_hnsw_index()`, `create_ivfflat_index()` as SDK methods, not a separate package.

10. **Star schema awareness**: Unlike Neo4j which only knows graphs, we can offer helpers for the graph-vs-measures boundary:
    ```python
    db.cypher(...)        # graph queries (AGE)
    db.measures(...)      # star schema queries (SQL)
    db.vector_search(...) # embedding queries (pgvector)
    ```

### 4.3 Proposed Class Hierarchy

```
piggie/
  __init__.py              # connect(), async_connect()
  driver.py                # PiggieDriver (holds psycopg3 pool)
  session.py               # PiggieSession (wraps psycopg3 connection)
  transaction.py           # PiggieTransaction
  result.py                # Result, Record, EagerResult
  types.py                 # Node, Relationship, Path, Vector
  cypher/
    __init__.py
    wrapper.py             # Cypher -> ag_catalog.cypher() SQL translation
    parser.py              # RETURN clause parser
  vector/
    __init__.py
    index.py               # create_index, drop_index
    search.py              # vector_search, hybrid_search
  embeddings/
    __init__.py
    base.py                # Embedder (abstract)
    openai.py              # OpenAIEmbedder
    ollama.py              # OllamaEmbedder
    sentence_transformers.py
  retriever/
    __init__.py
    base.py                # Retriever (abstract)
    vector.py              # VectorRetriever
    graph.py               # GraphRetriever (Cypher-based)
    hybrid.py              # HybridRetriever (graph + vector)
  rag/
    __init__.py
    pipeline.py            # RAGPipeline
    templates.py           # PromptTemplate
  schema/
    __init__.py
    introspect.py          # get_graph_schema(), get_vector_indexes()
```

### 4.4 Priority API Sketch

```python
import piggie

# Connection (mirrors Neo4j's simplicity)
db = piggie.connect(
    "postgresql://gregf@localhost:5433/piggie",
    graph="my_graph",           # default AGE graph
    pool_size=10,               # psycopg3 pool
)

# Tier 1: Simple queries
records, summary, keys = db.cypher(
    "MATCH (p:Person {name: $name})-[:KNOWS]->(f) RETURN f.name AS friend",
    name="Alice",
    result_transformer_=piggie.Result.to_df   # optional
)

# Vector search (pgvector)
results = db.vector_search(
    table="document_chunks",
    column="embedding",
    query=embedding_vector,
    k=10,
    filter_={"category": "legal"}
)

# Hybrid: graph + vector in one query
results = db.hybrid_search(
    cypher="MATCH (d:Document)-[:ABOUT]->(t:Topic {name: $topic}) RETURN d",
    topic="contracts",
    vector_table="document_chunks",
    vector_column="embedding",
    query=embedding_vector,
    k=10
)

# Tier 2: Session-based
async with db.session() as session:
    async with session.transaction() as tx:
        await tx.cypher("CREATE (:Person {name: $name})", name="Bob")
        await tx.cypher("CREATE (:Person {name: $name})", name="Carol")
        await tx.sql("INSERT INTO measures.revenue ...")

# Schema
schema = db.get_schema()
schema.graph.node_labels     # ["Person", "Document", "Topic"]
schema.graph.rel_types       # ["KNOWS", "ABOUT"]
schema.graph.node_properties("Person")  # [("name", "STRING"), ("age", "INTEGER")]
schema.vector.indexes        # [VectorIndex("document_chunks", dims=1536, ...)]

# Async
async_db = await piggie.async_connect(...)
records = await async_db.cypher("MATCH (n) RETURN n LIMIT 10")
```

### 4.5 Key Technical Challenges

1. **Cypher parameter passing through SQL**: AGE's `ag_catalog.cypher()` uses dollar-quoting (`$$`). We need to pass Cypher parameters through the outer SQL layer without collision. Approach: use `ag_catalog.cypher($graph, $cypher_text)` with `agtype` parameter binding.

2. **Result type detection**: AGE returns everything as `agtype` strings with `::vertex`, `::edge`, `::path` suffixes. We need a robust parser to convert these into rich Python objects.

3. **RETURN clause parsing**: To build the SQL wrapper, we must parse the Cypher RETURN clause. The LangChain implementation is regex-based and fragile. We should build a proper tokenizer or use AGE's own parser if possible.

4. **Mixed graph + vector queries**: There's no native AGE+pgvector join syntax. We'll need to either (a) run two queries and merge in Python, (b) use CTEs that combine `ag_catalog.cypher()` with pgvector `<=>` operators, or (c) use the approach from our AGE pgvector integration work.

5. **Async from the start**: psycopg3 is async-native. Build async-first, provide sync wrappers, not the other way around.

---

## 5. Competitive Positioning Summary

| Capability | Neo4j Driver | Neo4j GraphRAG | LangChain AGEGraph | Piggie SDK (target) |
|---|---|---|---|---|
| Graph queries | Cypher (native) | via driver | Cypher via SQL wrap | Cypher via SQL wrap |
| Vector search | v5.11+ (native) | VectorRetriever | None | pgvector (native) |
| Hybrid search | v5.18+ | HybridRetriever | None | CTE-based graph+vector |
| SQL access | None | None | None | First-class |
| Async | Full mirror API | Partial | None | Async-first (psycopg3) |
| DataFrame | Result.to_df() | None | None | Result.to_df() |
| Connection pool | Built-in (100 default) | via driver | Single connection | psycopg3 pool |
| Transactions | Managed + explicit | via driver | Auto-commit only | PostgreSQL native |
| Rich types | Node/Rel/Path | RetrieverResult | Raw dicts | Node/Rel/Path + Vector |
| Schema introspection | Full | get_schema() | Data sampling | Catalog-based |
| Embedders | None | 7+ providers | None | Same providers |
| RAG pipeline | None | GraphRAG class | Via LangChain | Built-in |
| KG construction | None | SimpleKGPipeline | add_graph_documents | Planned |
| LangChain compat | BaseRetriever | convert_to_tool | Native | Adapter layer |
| License | Apache-2.0 | Apache-2.0 | MIT | Apache-2.0 |

---

## Sources

- [Neo4j Python Driver Manual](https://neo4j.com/docs/python-manual/current/)
- [Neo4j Python Driver on PyPI](https://pypi.org/project/neo4j/) (v6.1.0)
- [Neo4j Python Driver API Reference](https://neo4j.com/docs/api/python-driver/current/api.html)
- [Neo4j GraphRAG Python on GitHub](https://github.com/neo4j/neo4j-graphrag-python)
- [Neo4j GraphRAG API Documentation](https://neo4j.com/docs/neo4j-graphrag-python/current/api.html)
- [Neo4j GraphRAG RAG User Guide](https://neo4j.com/docs/neo4j-graphrag-python/current/user_guide_rag.html)
- [Neo4j Result Transformers Documentation](https://neo4j.com/docs/python-manual/current/transformers/)
- [LangChain AGEGraph Source](https://python.langchain.com/api_reference/_modules/langchain_community/graphs/age_graph.html)
- [LangChain Apache AGE Integration](https://docs.langchain.com/oss/python/integrations/graphs/apache_age)
- [Apache AGE + LangChain Blog Post](https://age.apache.org/blog/2024-04-21-how-to-integrate-langchain-with-apache-age-for-graph-database-applications/)
