Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01CifR2BE7PBvRy5hT8TS9oz
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.