npx skills add ...
npx skills add neo4j-contrib/neo4j-skills --skill neo4j-graphrag-skill
Build GraphRAG retrieval pipelines on Neo4j using the neo4j-graphrag Python
npx skills add neo4j-contrib/neo4j-skills --skill neo4j-graphrag-skill
neo4j-graphrag Python packageretrieval_query Cypher fragments for graph-augmented contextGraphRAG pipelineToolsRetrieverneo4j-document-import-skillneo4j-vector-index-skillneo4j-vector-index-skillneo4j-gds-skillneo4j-agent-memory-skillneo4j-cypher-skill| Retriever | Vector | Fulltext | Graph | Best For |
|---|---|---|---|---|
VectorRetriever | ✓ | — | — | Baseline semantic search |
HybridRetriever | ✓ | ✓ | — | Better recall, no graph expansion |
VectorCypherRetriever | ✓ | — | ✓ | GraphRAG without fulltext |
HybridCypherRetriever | ✓ | ✓ | ✓ | Production GraphRAG — default |
Text2CypherRetriever | — | — | ✓ | NL→Cypher, no embedder |
ToolsRetriever | varies | varies | varies | LLM-routed multi-retriever |
WeaviateNeo4jRetriever | ✓ | — | ✓ | Vectors in Weaviate |
PineconeNeo4jRetriever | ✓ | — | ✓ | Vectors in Pinecone |
QdrantNeo4jRetriever | ✓ | — | ✓ | Vectors in Qdrant |
Requires: Python >= 3.10, neo4j >= 5.17.0 (driver 6.x supported).
Pick from Retriever Selection table above.
For custom Cypher hybrid search outside the neo4j-graphrag retriever APIs, use neo4j-vector-index-skill.
Vector backend selection [v1.16+, auto]: on Neo4j 2026.01+ all four vector/hybrid retrievers auto-route through the Cypher 25 SEARCH ... WHERE clause when filters are SEARCH-compatible (simple AND comparisons) and all filter props are declared in the index WITH [n.prop] list. $or, $in, $like, or undeclared props → automatic fallback to db.index.vector.queryNodes() procedure path (with warning log). Declare filterable properties via filterable_properties=[...] on create_vector_index().
If index not ONLINE: wait, poll every 5s. Do NOT start ingestion until ONLINE.
Translates natural language to Cypher using an LLM. No embedder required.
Security (v1.16.0+): Every LLM-generated Cypher is run through
EXPLAINfirst. Any statement classified as write/destructive raisesText2CypherRetrievalErrorinstead of executing — prevents prompt-injection attacks.
Since v1.19, the vector and vector-Cypher retrievers automatically prefix SEARCH queries
with CYPHER 25 and fall back to the procedure-based vector search when SEARCH is
unsupported or fails.
All components moved out of the experimental namespace. Old imports still work but emit a
DeprecationWarning and will be removed in 2.0:
neo4j_graphrag.pipeline (v1.21+) = new lazy dataflow DSL (Pipeline, LocalInterpreter, Sink), unrelated to the experimental task-graph Pipeline behind SimpleKGPipeline. neo4j_graphrag.pipeline.kg_builder does not exist → ModuleNotFoundError.
Also since v1.19: Component / RunContext / TaskProgressNotifierProtocol live in
neo4j_graphrag.components.base, and malformed components raise ComponentDefinitionError
(no longer PipelineDefinitionError).
Lazy neo4j_graphrag.pipeline.Pipeline + LocalInterpreter(observers=[...]); TextSplitter.iter_chunks(). Full API → references/pipeline-dsl.md.
If neo4j_schema=None: retriever fetches schema automatically. For large schemas, pass a trimmed string to reduce LLM prompt size.
Destructive-query guard [v1.16+]: Text2CypherRetriever runs EXPLAIN on the generated Cypher before execution and rejects queries that produce writes (CREATE, MERGE, DELETE, SET, REMOVE, etc.). LLM-generated writes are never executed against the graph.
OpenAILLM, AnthropicLLM, GeminiLLM, VertexAILLM, BedrockLLM, …), base_url, token usage, close(); embedder classes + dims → references/providers.md| Error | Cause | Fix |
|---|---|---|
ModuleNotFoundError: neo4j_genai | Old package name | pip uninstall neo4j-genai && pip install neo4j-graphrag |
retrieval_query returns 0 rows | Missing MATCH or wrong rel direction | EXPLAIN the fragment; check CALL db.schema.visualization() |
KeyError: 'score' in results | retrieval_query RETURN missing score | Add score to every retrieval_query RETURN clause |
score variable not found | score re-declared in retrieval_query | Do not re-declare score — it is auto-injected |
Text2CypherRetrievalError | LLM generated a write statement | Expected security behavior (v1.16.0+); refine prompt or schema |
TypeError: coroutine | Missing await / asyncio.run() | Wrap async calls: asyncio.run(pipeline.run_async(...)) |
| Empty results from HybridRetriever | Fulltext index not ONLINE | SHOW INDEXES YIELD name, state WHERE state <> 'ONLINE' |
VectorCypherRetriever + filters raises "requires: node_label, embedding_node_property, …" (v1.19 name-mismatch bug) | _fetch_index_infos sets self._embedding_node_property, constructor sets _node_embedding_property | Shim after construction: r._node_embedding_property = r._embedding_node_property or "embedding"; append filter props to r._filterable_properties |
| Embedding dimension mismatch | Index dims ≠ model dims | Recreate index with correct dimensions= value |
neo4j-graphrag (not neo4j-genai) installed; neo4j >= 5.17.0 drivercreate_vector_index match the embedder outputretrieval_query returns node and score in RETURN (not re-declared)query_params passed via retriever_config on rag.search() (not on retriever constructor)llm.close() called when done to release resourcesLoad on demand:
result_formatter, pre-filter operatorsPipeline DSL, Source/Sink, stage observers [v1.21]SimpleKGPipeline constructor, schema, splitterspip install neo4j-graphrag[openai] # OpenAI LLM + embeddings
pip install neo4j-graphrag[anthropic] # Anthropic Claude
pip install neo4j-graphrag[google] # Vertex AI / Gemini
pip install neo4j-graphrag[bedrock] # Amazon Bedrock (boto3)
pip install neo4j-graphrag[cohere] # Cohere
pip install neo4j-graphrag[mistralai] # MistralAI
pip install neo4j-graphrag[ollama] # Ollama (local)
pip install neo4j-graphrag[weaviate] # Weaviate external retriever
pip install neo4j-graphrag[pinecone] # Pinecone external retriever
pip install neo4j-graphrag[qdrant] # Qdrant external retriever// Vector index (all retrievers need this)
CREATE VECTOR INDEX chunk_embedding IF NOT EXISTS
FOR (c:Chunk) ON (c.embedding)
OPTIONS { indexConfig: {
`vector.dimensions`: 1536,
`vector.similarity_function`: 'cosine'
} };
// Fulltext index (Hybrid retrievers only)
CREATE FULLTEXT INDEX chunk_fulltext IF NOT EXISTS
FOR (c:Chunk) ON EACH [c.text];
// Confirm ONLINE before ingesting:
SHOW INDEXES YIELD name, state
WHERE name IN ['chunk_embedding', 'chunk_fulltext']
RETURN name, state;
// Both must show state = 'ONLINE'from neo4j import GraphDatabase
from neo4j_graphrag.embeddings import OpenAIEmbeddings
from neo4j_graphrag.generation import GraphRAG
from neo4j_graphrag.llm import OpenAILLM
from neo4j_graphrag.retrievers import HybridCypherRetriever
driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USERNAME, NEO4J_PASSWORD))
embedder = OpenAIEmbeddings(model="text-embedding-3-large") # OPENAI_API_KEY from env
# retrieval_query: Cypher fragment executed after the vector/fulltext lookup.
# Auto-injected variables: node (matched node) score (similarity float)
# MUST include a RETURN clause. score must appear in RETURN.
retrieval_query = """
MATCH (node)<-[:HAS_CHUNK]-(article:Article)
OPTIONAL MATCH (article)-[:MENTIONS]->(org:Organization)
RETURN node.text AS chunk_text,
article.title AS article_title,
collect(DISTINCT org.name) AS mentioned_organizations,
score
"""
retriever = HybridCypherRetriever(
driver=driver,
vector_index_name="chunk_embedding",
fulltext_index_name="chunk_fulltext",
retrieval_query=retrieval_query,
embedder=embedder,
)
llm = OpenAILLM(model_name="gpt-4.1", model_params={"temperature": 0})
rag = GraphRAG(
retriever=retriever,
llm=llm,
)
response = rag.search(
query_text="Who does Alice work for?",
retriever_config={"top_k": 5},
)
print(response.answer)
driver.close()from neo4j_graphrag.retrievers import VectorCypherRetriever
retriever = VectorCypherRetriever(
driver=driver,
index_name="chunk_embedding",
retrieval_query=retrieval_query,
embedder=embedder,
)
response = rag.search(
query_text="What happened at Apple?",
retriever_config={"top_k": 10},
)from neo4j_graphrag.retrievers import Text2CypherRetriever
retriever = Text2CypherRetriever(
driver=driver,
llm=OpenAILLM(model_name="gpt-4.1"),
neo4j_schema=None, # None = auto-fetch schema from DB; pass string to trim
examples=[
"Q: Who works at Neo4j? A: MATCH (p:Person)-[:WORKS_AT]->(c:Company {name:'Neo4j'}) RETURN p.name"
],
)
results = retriever.search(query_text="Which people work at Neo4j?")from neo4j_graphrag.retrievers import ToolsRetriever
tools_retriever = ToolsRetriever(
llm=llm,
retrievers=[vector_retriever, text2cypher_retriever],
)
# LLM decides which retriever(s) to invoke per query
# Convert any retriever to a standalone Tool:
tool = vector_retriever.convert_to_tool()results = retriever.search(
query_text="quarterly earnings",
top_k=5,
filters={
"date": {"$gte": "2024-01-01"},
"source": {"$eq": "10-K"},
},
)
# Operators: $eq $ne $lt $lte $gt $gte $between $in $like $ilikeretrieval_query = """
MATCH (node)<-[:HAS_CHUNK]-(a:Article)-[:MENTIONS]->(org:Organization {name: $entity_name})
RETURN node.text, a.title, score
"""
# Pass via retriever.search directly:
results = retriever.search(
query_text="What happened at Apple?",
top_k=10,
query_params={"entity_name": "Apple"},
)
# Or via GraphRAG.search:
response = rag.search(
query_text="What happened at Apple?",
retriever_config={"top_k": 10, "query_params": {"entity_name": "Apple"}},
)# Enable SEARCH clause syntax in vector/hybrid retrievers (requires Neo4j 2026+)
retriever = VectorRetriever(
driver=driver,
index_name="chunk_embedding",
embedder=embedder,
use_search_clause=True,
)# v1.19+ — preferred
from neo4j_graphrag.components.text_splitters.fixed_size_splitter import FixedSizeSplitter
# deprecated (removed in 2.0)
from neo4j_graphrag.experimental.components.text_splitters.fixed_size_splitter import FixedSizeSplitter
# SimpleKGPipeline did NOT move — only valid path:
from neo4j_graphrag.experimental.pipeline.kg_builder import SimpleKGPipelineresults = retriever.search(
query_text="...",
top_k=10,
order_by="score DESC",
)from neo4j_graphrag.generation.prompts import RagTemplate
template = RagTemplate(
template="""Answer using ONLY the context below.
Context: {context}
Question: {query_text}
Answer:""",
expected_inputs=["context", "query_text"],
)
rag = GraphRAG(retriever=retriever, llm=llm, prompt_template=template)response = rag.search(
query_text="...",
retriever_config={"top_k": 5},
return_context=True, # include raw retrieved chunks
response_fallback="No relevant context.", # skip LLM call if retriever returns nothing
)
print(response.answer)
print(response.retriever_result) # RawSearchResult when return_context=Truefrom neo4j_graphrag.message_history import InMemoryMessageHistory
history = InMemoryMessageHistory()
r1 = rag.search(query_text="Who is Alice?", message_history=history)
r2 = rag.search(query_text="Where does she work?", message_history=history)from neo4j_graphrag.indexes import create_vector_index
# Vector index — adjust dimensions to match your embedding model
create_vector_index(
driver,
name="chunk_embedding",
label="Chunk",
embedding_property="embedding",
dimensions=1536,
similarity_fn="cosine", # or "euclidean"
)
# Fulltext index (run as Cypher)
# CREATE FULLTEXT INDEX chunk_fulltext IF NOT EXISTS
# FOR (c:Chunk) ON EACH [c.text]from neo4j_graphrag.schema import get_schema, get_structured_schema
schema_str = get_schema(driver, sample=1000) # human-readable string
schema_dict = get_structured_schema(driver, sample=1000) # dict with labels/rels/props