Files
spring-ai/graphrag-neo4j
..

graphrag-neo4j

GraphRAG on Spring AI 2.0 and Neo4j, next to plain vector RAG over the same documents. Companion code for GraphRAG in Java with Neo4j and Spring AI: Turning Documents into a Knowledge Graph.

Read this first: no language model is called anywhere in this module. The extraction step runs through the real ChatClient.entity(...) against RuleBasedExtractorModel, a stand-in that finds eight fixed sentence patterns with regular expressions and answers with the JSON a good model would. Everything downstream of it (Neo4j, the vector indexes, MiniLM embeddings, the Cypher, the retrieval numbers) is real. Nothing here says how well a real model extracts from real prose. Answer generation is not exercised at all; the tests print the prompt that would be sent.

Versions

Spring Boot 4.1.2
Spring AI 2.0.2 (spring-ai-client-chat, spring-ai-neo4j-store, spring-ai-transformers)
Neo4j server neo4j:2026.10.1-community (Docker); also run on neo4j:5-community, see output/03b-graph-build-neo4j-5.txt
Neo4j Java driver 6.1.0 (managed by Boot)
Testcontainers 2.0.5 (testcontainers-neo4j)
Embeddings all-MiniLM-L6-v2 (384 dims) in-process through ONNX Runtime, as in rag-ingestion
Java 25 (Temurin 25.0.5+8)

Run it

./scripts/run-all.sh                       # regenerates output/ (needs Docker; first run downloads MiniLM and the Neo4j image)
NEO4J_IMAGE=neo4j:5-community ./scripts/run-all.sh

A full run takes a few minutes: the Neo4j container needs about a minute to start, and the sweep test runs a few hundred retrievals.

Layout

File What it does
Extraction, EntityType, RelationType The record the model must fill; the enums are the closed vocabulary and the allow-list for relationship types put into Cypher
Extractor ChatClient.entity(Extraction.class) per chunk, then drops relations whose endpoints were never listed
stub/RuleBasedExtractorModel The stand-in for the model, with failure modes (fenced JSON, truncated JSON, unknown and hostile relation types, dangling relation)
EntityResolver Merges names: exact key, alias table, optional embedding-similarity threshold
GraphStore Every Cypher statement: schema, vector index, writes, variable-length paths
GraphRetriever Span linking through the entity vector index, path walk, path ranking
Pipeline Chunks into Neo4jVectorStore, extraction, resolution, graph write; the plain vector search used as the baseline
Answerer Builds the final prompt from selected paths and source passages
Question The 9 evaluation questions and, for each, the documents that together hold the facts
src/main/resources/corpus/ 35 short invented documents: 21 with facts, 14 distractors that share the vocabulary

Captured output

File Shows
output/01-extraction.txt The exact user message (with the JSON schema Spring AI appends), the parsed record, call and size totals
output/02-extraction-failures.txt What each bad reply does
output/03-graph-build.txt Server version, the two vector indexes, the duplicate-entity problem and what each merge threshold does
output/03b-graph-build-neo4j-5.txt The same test against neo4j:5-community
output/04-vector-vs-graph.txt Plain vector RAG against GraphRAG, per question
output/05-graph-sweeps.txt Hops, paths kept and link threshold against the result; path counts by hop
output/06-prompt.txt The prompt GraphRAG builds for one question
output/07-fixed-shape-cypher.txt Hand-written Cypher for questions whose shape is known

What the numbers are and are not

The corpus is small and invented, the nine questions were written by hand after the corpus, and the default retriever settings (3 hops, 6 paths, link threshold 0.80) were fixed before the first result. The sweep in 05 was run afterwards, on the same questions, so a best cell in that table is tuned on the test set. "Complete" means every supporting document is in the context; it does not mean a model answered correctly.