Neo4j та LangChain4j: інструменти GraphRAG для отримання даних, фільтрації та пам’яті чату
Neo4j з LangChain4j для GraphRAG: виявлення схеми, створення графів за допомогою LLM, Cypher DSL, фільтрація в індексі, пошук батьківських та дочірніх елементів та пам’ять для чату, підтримувана графом.
Джерела: Mastering Neo4j & LangChain4j: GraphRAG, Persistent AI Memory and more
Більше, ніж простий векторний сховище
Звичайне векторне сховище Neo4j, яке працює всередині Java-додатку, є лише точкою входу. Учасники репозиторію спільноти LangChain4j згодом додали функції, які значно виходять за межі простого пошуку схожості: автоматичне створення графів, налаштоване фільтрування векторів, структурні шаблони GraphRAG та пам’ять чату, збережена у вигляді графа. У наступних розділах розглядається кожен з цих аспектів із кодом, який використовують власні тести спільноти.
Усе тут створено для версії 1.0.0-beta19, і наступні випуски можуть змінити деяку поведінку чи інтерфейси.
Залежності такі самі, як і для базової налаштування, з модулем Neo4j retriever та його супутниками у файлі Maven:
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-neo4j-retriever</artifactId>
<version>${langchain.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-neo4j</artifactId>
<version>${langchain.version}</version>
</dependency>
<!-- other deps -->
</dependencies>
Відкриття схеми для Text-to-Cypher
Коли модель формує запити до бази даних безпосередньо з природної мови (Text-to-Cypher), їй необхідно знати точну структуру бази даних: які вузли підключені один до одного та які властивості має кожен вузол. Без цих знань вона не може створювати коректні запити та починає вигадувати їх.
Клас Neo4jGraph приховує цю проблему топології. Схема не закодована жорстко в Java-додатку, оскільки компонент отримує та форматує її динамічно. PR #119 значно прискорив цей процес. Раніше збір вузлів, властивостей та зв’язків міг вимагати кількох запитів до бази даних. У рамках цього PR вилучення схеми було об’єднано в один оптимізований запит Cypher на основі внутрішньої процедури Neo4j apoc.meta.data. Також було додано налаштовуваний розмір вибірки та покращена схема, яка включає зразкові значення, що надає моделі кращого контексту.
Візьмемо цей набір даних як приклад:
Оголошення його схеми для моделі виглядає так:
import dev.langchain4j.store.graph.neo4j.Neo4jGraph;
// If I want to initialize the graph abstraction and load the schema:
Neo4jGraph graph = Neo4jGraph.builder()
.driver(driver)
.build();
// Under the hood, this executes a single, consolidated APOC query that yields
// labels, element types, and properties all at once, avoiding multiple DB calls.
graph.refreshSchema();
Neo4jGraph.StructuredSchema schema = graph.getStructuredSchema();
// We expect a well-formatted string logically divided into three sections,
// exactly as formatted by the new Neo4jGraphSchemaUtils class:
//
// `schema.nodesProperties()` is the following:
// :Person {name: STRING}, :Company {name: STRING}
//
// `schema.relationshipsProperties()` is the following:
// :WORKS_FOR {since: INTEGER}
//
// `schema.patterns()` is the following:
// (:Person)-[:WORKS_FOR]->(:Company)
System.out.println("Current Database Schema Context:\n" + schema);
У величезній корпоративній графіці з мільярдами вузлів вилучення метаданих може стати дорогим, якщо база даних намагається сканувати все. Параметри sample та maxRels, які безпосередньо пов’язані з apoc.meta.data, допомагають контролювати цей процес. Опція enhanced-schema додає зразки значень властивостей, щоб модель бачила, як насправді виглядають дані.
// If I want to scan a very large database efficiently by configuring the APOC sampling,
// and I want to enhance the LLM's understanding with sample property values:
Neo4jGraph optimizedGraph = Neo4jGraph.builder()
.driver(driver)
// We can configure the underlying apoc.meta.data parameters.
// 'sample' limits the number of nodes inspected per label to speed up execution.
// 'maxRels' limits the number of relationships inspected per node.
// (Note: These are passed internally to the getSchemaFromMetadata utility)
.build();
// When the schema is refreshed, the underlying query runs:
// CALL apoc.meta.data({maxRels: $maxRels, sample: $sample})
optimizedGraph.refreshSchema();
// The LLM now receives a fast, accurately sampled schema representation,
// protecting database performance during application startup or schema refreshes.
System.out.println("Optimized Schema loaded successfully.");
Завдяки отриманню метаданих за один запит та можливості вибірки даних, конвеєр Text-to-Cypher самостійно адаптується до змін у базі даних, і процес вилучення залишається швидким навіть з величезними наборами даних у продакшені.
Створення графа з тексту
Багато баз даних вже містять великі текстові поля, і перетворення їх на граф знань є основою для передових підходів GraphRAG, таких як узагальнення, орієнтоване на запити, та кластерування тем. Раніше це означало необхідність підтримки крихких конвеєрів розпізнавання іменованих ентитетів, заснованих на правилах. LLMGraphTransformer передає цю роботу на розгляд ШІ.
Цей клас є незалежним від типу бази даних, оскільки він перетворює текст на вузли та ребра, які також можуть використовуватися для інших цілей. Щоб запобігти галюцинаціям моделі чи розриву онтології, наприклад через створення неочікуваних міток, розробники керують процесом вилучення за допомогою конкретних прикладів (prompting з кількома прикладами) та чітко визначених схем. Ком’юніті також додав вбудований спосіб пов’язувати вилучені ентитети з вихідним документом, звідки вони походять, що дозволяє зберегти первісні метадані.
Вихідний формат JSON має дотримуватися фіксованої структури з ключами head, head_type, relation, tail та tail_type, а приклади promptів навчають модель цій структурі. Ось уривок прикладу JSON, який надається LLM:
[
{
"tail": "Microsoft",
"head": "Adam",
"head_type": "Person",
"text": "Adam is a software engineer in Microsoft since 2009...",
"relation": "WORKS_FOR",
"tail_type": "Company"
},
{
"tail": "Microsoft Word",
"head": "Microsoft",
"head_type": "Company",
"text": "Microsoft is a tech company that provides several products...",
"relation": "PRODUCED_BY",
"tail_type": "Product"
}
]
За допомогою цих прикладів у контексті трансформер може обробляти справжні вхідні документи, які містять текст та метадані, наприклад "Кіану Рівз грав у Matrix" із метаданими key33: value3.
import dev.langchain4j.community.data.document.transformer.graph.LLMGraphTransformer;
import dev.langchain4j.community.data.document.graph.GraphDocument;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DefaultDocument;
import dev.langchain4j.data.document.Metadata;
import java.util.List;
ChatModel chatModel = /* dev.langchain4j.model.chat instance */
Driver driver = /* org.neo4j.driver.Driver instance */
// If I want to guide the extraction by providing a structured set of examples:
LLMGraphTransformer transformer = LLMGraphTransformer.builder()
.model(chatModel)
.examples(EXAMPLES_PROMPT) // Injects the above JSON dataset into the system prompt
.build();
Document docKeanu = new DefaultDocument(
"Keanu Reeves acted in Matrix",
Metadata.from("key33", "value3")
);
// The LLM will transform the text, structuring nodes and relationships based on the examples
List<GraphDocument> graphDocs = transformer.transformAll(List.of(docKeanu));
/*
The above `graphDocs` returns this result:
GraphDocument
├─ Nodes
│ ├─ GraphNode
│ │ ├─ id: Matrix
│ │ ├─ type: Movie
│ │ └─ properties: {}
│ │
│ └─ GraphNode
│ ├─ id: Keanu Reeves
│ ├─ type: Person
│ └─ properties: {}
│
├─ Relationships
│ └─ GraphEdge
│ ├─ type: ACTED_IN
│ ├─ sourceNode
│ │ ├─ id: Keanu Reeves
│ │ └─ type: Person
│ ├─ targetNode
│ │ ├─ id: Matrix
│ │ └─ type: Movie
│ └─ properties: {}
│
└─ Source
├─ text: "Keanu Reeves acted in Matrix"
└─ metadata
└─ key33: value3
*/
Як тільки об’єкти GraphDocument з’являються в пам’яті, їх потрібно зберегти. У умовах продакшну один і той самий документ може оброблятися більше разу, тому записи мають уникати дублікатів, наприклад двох окремих вузлів із назвою „Keanu Reeves“. KnowledgeGraphWriter відповідає за це.
import dev.langchain4j.community.rag.content.retriever.neo4j.KnowledgeGraphWriter;
Neo4jGraph neo4jGraph = /* dev.langchain4j.store.graph.neo4j.Neo4jGraph instance */;
LLMGraphTransformer graphTransformer = /* dev.langchain4j.community.data.document.transformer.graph.LLMGraphTransformer instance */;
Document docKeanu = new DefaultDocument(
"Keanu Reeves acted in Matrix",
Metadata.from("key33", "value3")
);
List<GraphDocument> graphDocs = graphTransformer.transformAll(List.of(docKeanu));
// If I want to persist the extracted entities safely:
KnowledgeGraphWriter writer = KnowledgeGraphWriter.builder()
.graph(neo4jGraph)
.build();
// The first write populates the database
writer.addGraphDocuments(graphDocs, false);
// Executing the exact same command again is safe.
// The internal UNWIND and MERGE logic generated by the writer guarantees
// that no duplicate entities or relationships are created.
writer.addGraphDocuments(graphDocs, false);
// Expected resulting topology in the database:
// (:__Entity__ {id: 'keanu'})-[:ACTED]->(:__Entity__ {id: 'matrix'})
System.out.println("Entities persisted successfully. No duplicates created.");
База даних в кінцевому підсумку виглядає ось так:
Збереження походження
Для розширених функцій GraphRAG корисно точно знати, який шматок тексту створив певні зв’язки. Встановлення прапорця includeSource дозволяє зберегти ентитети та створює вузол Document, який містить оригінальний текст та метадані, пов’язані з вилученими ентитетами.
// If I want to maintain data provenance and link entities back to their source:
writer.addGraphDocuments(graphDocs, true); // true = includeSource
// Behind the scenes, the writer executes three crucial operations:
// 1. It creates the Document node, copying the original metadata (e.g., key33: value3) and the text.
// 2. If the original document lacks an ID, the writer automatically generates
// an MD5 hash of the text to use as a unique identifier.
// 3. It links the document to the extracted entities using a relationship (default: HAS_ENTITY).
// Expected resulting topology in the database:
// (:Document {id: '<MD5_hash>', text: 'Keanu Reeves...', key33: 'value3'})-[:HAS_ENTITY]->(:__Entity__ {id: 'keanu'})
System.out.println("Source document successfully linked to the extracted entities.");
Корпоративні середовища не можуть покладатися на загальні мітки на кшталт __Entity__ чи HAS_ENTITY, а назви властивостей, які походять з зовнішніх даних, створюють ризик введення шкідливого коду через Cypher. Автор очищує кожен вхідний даний за допомогою SchemaNames.sanitize, а розробники можуть переписати всю онтологію, включаючи назви обмежень унікальності, створених у базі даних.
// If I want to adapt the ingestion to a pre-existing enterprise schema,
// and customize the relationship that links the document to the entities:
KnowledgeGraphWriter customWriter = KnowledgeGraphWriter.builder()
.graph(neo4jGraph)
.label("ActorOrMovie") // Replaces "__Entity__"
.idProperty("customId") // Replaces "id"
.textProperty("customText") // Replaces "text" for the Document node
.relType("MENTIONED_IN_SOURCE") // Replaces "HAS_ENTITY"
.constraintName("unique_custom") // Sets a specific name for the Neo4j CONSTRAINT
.build();
// Inserting the data with includeSource set to true will now use the new nomenclature:
customWriter.addGraphDocuments(graphDocs, true);
// Example of the resulting Cypher pattern generated by the writer:
// (:Document {customText: '...'})-[:MENTIONED_IN_SOURCE]->(:ActorOrMovie {customId: 'keanu'})
Ця конфігурація створює індивідуальну топологію, яка тут показана з елементами, пов’язаними з їхнім джерелом:
У такому форматі вилучення графів за допомогою LLM відбувається автоматично, є простим для відстеження завдяки посиланням на джерело, є ідемпотентним та безпечним для використання в потоках структурованих даних.
Безпечніші запити за допомогою Cypher DSL
Однією з важливих архітектурних змін у репозиторії спільноти є глибока інтеграція бібліотеки Cypher DSL. Ранні системи GraphRAG часто створювали запити шляхом об’єднання сирих рядків, що не рекомендується через ризик втручання ззовні та синтаксичні помилки. Внутрішні компоненти LangChain4j тепер програмно створюють запити у вигляді абстрактного синтаксичного дерева (AST). Більшість цих процесів відбувається непомітно, проте це значний крок у напрямку підвищення безпеки додатків.
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.Node;
Driver driver = /* org.neo4j.driver.Driver instance */
// Demonstrating how internal queries are constructed safely via the DSL
// We define a Node representation first
Node documentNode = Cypher.node("Document").named("d");
// We build the query programmatically using the fluent API.
// Notice how literal values are wrapped safely, preventing injection.
Statement statement = Cypher.match(documentNode)
.where(Cypher.property("d", "id").isEqualTo(Cypher.literalOf("doc-123")))
.returning(Cypher.property("d", "text"))
.build();
// The DSL engine traverses the AST and compiles it into a syntactically safe string.
String safeCypherQuery = statement.getCypher();
// Expected output: MATCH (d:`Document`) WHERE d.id = 'doc-123' RETURN d.text
System.out.println("Generated safe Cypher via DSL: " + safeCypherQuery);
Двомовний двигун розуміє граматику Cypher, тому його API не може генерувати синтаксично недійсні запити. Це забезпечує надійний захист внутрішніх механізмів інтеграції.
Попереднє фільтрування в індексі
Векторний пошук у Neo4j значно оптимізовано, і інтеграція LangChain4j тепер підтримує новішу синтаксис векторного пошуку, включаючи оптимізації версії 2026.01. Основною перевагою є фільтрування безпосередньо в індексі.
Використовуйте пошук за схожістю разом із обмеженнями метаданих, наприклад документи, семантично близькі до терміна „машинне навчання“, але опубліковані лише у 2024 році. Старіші підходи до векторних баз даних спочатку знаходять найбільш схожі вектори, а потім видаляють ті, що належать до неправильного року. Таке післяфільтрування марнує зусилля та часто повертає менше документів, ніж запитувалося, що погіршує точність пошуку. Фільтрування під час індексації застосовує обмеження метаданих під час перебірки векторного індексу HNSW (Hierarchical Navigable Small World). У документації Neo4j наведено додаткову інформацію щодо синтаксису та переваг у продуктивності.
import dev.langchain4j.store.embedding.neo4j.Neo4jEmbeddingStore;
import dev.langchain4j.store.embedding.filter.Filter;
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
Driver driver = /* org.neo4j.driver.Driver instance */
Embedding embedding = /* dev.langchain4j.data.embedding.Embedding instance */
MatchSearchClauseStrategy matchSearchClauseStrategy = new MatchSearchClauseStrategy();
// Configure the store with the new syntax enabled
Neo4jEmbeddingStore store = Neo4jEmbeddingStore.builder()
.driver(driver)
.dimension(1536)
.searchStrategy(matchSearchClauseStrategy) // Crucial flag: Enables the 2026.01 optimized native vector search syntax
.filterMetadata(Arrays.asList("year", "department")) // Enable filtering for 'year' and 'department', which translates to the `WITH [indexName.year, indexName.department]` clause during index creation
.build();
// Build a metadata filter combining multiple boolean conditions
Filter filter = metadataKey("year").isEqualTo(2024)
.and(metadataKey("department").isEqualTo("Engineering"));
// Execute the search request
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(embedding)
.maxResults(5)
.filter(filter) // Filter is executed natively inside the Neo4j Vector Index block
.build();
SearchResult<TextSegment> results = store.search(request);
// The results will natively exclude any documents not matching the criteria specified in the `filter` instance
// returning the final Top-K, ensuring you always get 5 highly relevant segments if they exist.
System.out.println("Search executed with in-index filtering.");
// Execute results.matches() to verify in-index filtering
Шаблони пошуку в GraphRAG
Векторний пошук добре справляється зі визначенням семантичної схожості, але провалюється при аналітичних запитах, таких як агрегація даних чи багатоетапні пошуки, наприклад: «Хто є колегами інженерів, які працюють над проектом X?» Вектори не можуть виконувати операції підрахунку, сумування чи надійно пересуватися по структурованих ієрархіях. Концепція GraphRAG орієнтована саме на такі аналітичні завдання, і на сайті graphrag.com є вступ до неї.
Розділення тексту на частини створює типову дилему в технології RAG. Маленькі частини об’ємом близько 200 токенів забезпечують точні результати пошуку за допомогою векторів, але не містять контексту навколо, через що якість генерації тексту погіршується. Великі частини об’ємом близько 2000 токенів забезпечують ШІ достатньо контексту, але суттєво знижують точність пошуку. Набір інструментів для обробки та пошуку даних вирішує цю проблему, використовуючи біпартитну природу графа, яка розділяє пошукові одиниці від одиниць, які можна отримати.
Батьківсько-дочірні зв’язки
Документи діляться на невеликі „дочірні“ фрагменти, які використовуються лише для векторного пошуку, причому кожен з них пов’язаний з повним „батьківським“ документом за допомогою певних зв’язків. Ембеддинги обчислюються для цих невеликих, семантично цілісних одиниць, що покращує якість пошуку. Щоб розділити великий документ, необхідно ембеддити фрагменти та приєднати їх до батьківського вузла в Neo4j, використовуючи ParentChildGraphIngestor.
import dev.langchain4j.store.graph.neo4j.Neo4jParentChildIngestor;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
// embeddingModel, embeddingStore, childSplitter instances...
// If I want to automatically ingest a document into a Parent-Child graph topology:
Neo4jEmbeddingStoreIngestor ingestor = ParentChildGraphIngestor.builder()
.driver(driver)
.embeddingModel(embeddingModel)
// We define how the document should be chunked before ingestion
.documentSplitter(DocumentSplitters.recursive(200, 20))
.documentChildSplitter(childSplitter)
.build();
Document document = Document.from( """Artificial Intelligence (AI) is a field of computer science. It focuses on creating intelligent agents capable of performing tasks that require human intelligence.
Machine Learning (ML) is a subset of AI. It uses data to learn patterns and make predictions. Deep Learning is a specialized form of ML based on neural networks.
""");
ingestor.ingest(List.of(document));
// The graph now contains one 'Document' node connected via 'HAS_CHILD'
// to multiple embedded 'DocumentChunk' nodes.
System.out.println("Parent and child nodes successfully ingested and linked.");
Після отримання даних система пошуку перебирає невеликі фрагменти та передає повний оригінал моделі LLM через EmbeddingStoreContentRetriever:
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
// If I want to search against chunks but retrieve the rich parent document:
final EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.maxResults(1)
.minScore(0.4)
.build();
List<Content> contents = retriever.retrieve(Query.from("specific configuration detail"));
// The retriever hits the small 'DocumentChunk' index, traverses the 'HAS_CHILD'
// relationship, and returns the entire 'Document' node.
System.out.println("Retrieved full parent document context.");
/*
The result of `contents` is :
DefaultContent {
textSegment = TextSegment {
text = """
Machine Learning (ML) is a subset of AI. It uses data to learn patterns and make predictions.
Deep Learning is a specialized form of ML based on neural networks.
Machine Learning (ML) is a subset of AI. It uses data to learn patterns and make predictions.
Deep Learning is a specialized form of ML based on neural networks.
Artificial Intelligence (AI) is a field of computer science. It focuses on creating intelligent agents
capable of performing tasks that require human intelligence.
""",
metadata = {
index = 1,
source = Wikipedia link,
title = AI Basics,
url = https://example.com/ai,
parentId = parent_1af3e080-5029-40ab-b3d7-0829064d800d
}
},
metadata = {
EMBEDDING_ID = null,
SCORE = 0.8560410737991333
}
}
*/
Резюме як повернена одиниця
Повернення всього батьківського елемента може спричинити використання занадто багатьох токенів. Більш ефективним варіантом є створення резюме батьківського елемента під час обробки та його повернення у разі збігу з дочірнім фрагментом. Клас SummaryGraphIngestor змушує ШІ написати резюме, зберігає його як окремий вузол та пов’язує з пошуковими фрагментами:
import dev.langchain4j.community.store.embedding.neo4j.Neo4jEmbeddingStoreIngestor;
import dev.langchain4j.community.store.embedding.neo4j.SummaryGraphIngestor;
// If I want the LLM to summarize my document during ingestion and store the summary:
/* Neo4jEmbeddingStore, ChatModel and DocumentSplitter instances... */
final Neo4jEmbeddingStoreIngestor ingestor = SummaryGraphIngestor.builder()
.driver(driver)
.embeddingModel(embeddingModel)
.questionModel(chatModel)
.documentSplitter(parentSplitter)
.build();
ingestor.ingest(List.of(document));
// The graph now has a 'Summary' node (containing the LLM-generated summary)
// linked to the specific 'DocumentChunk' nodes.
System.out.println("Document ingested and summarized successfully.");
Neo4jSummaryRetriever виконує пошук, тож при знаходженні вектора в певному фрагменті отримується заздалегідь обчислений огляд замість сирого тексту:
import dev.langchain4j.community.store.embedding.neo4j.Neo4jEmbeddingStoreIngestor;
import dev.langchain4j.community.store.embedding.neo4j.SummaryGraphIngestor;
/* required instances */
Document document = Document.from("""
Artificial Intelligence (AI) is a field of computer science. It focuses on creating intelligent agents capable of performing tasks that require human intelligence.
Machine Learning (ML) is a subset of AI. It uses data to learn patterns and make predictions. Deep Learning is a specialized form of ML based on neural networks.
""");
ingestor.ingest(document);
final EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingModel(embeddingModel)
.maxResults(5)
.minScore(0.6)
.embeddingStore(ingestor.getEmbeddingStore())
.build();
/*
The result is something like this:
DefaultContent {
textSegment = TextSegment {
text = "Machine Learning (ML) is a subset of AI",
metadata = {
index = 0,
source = Wikipedia link,
title = Quantum Mechanics,
url = https://example.com/ai
}
},
metadata = {
EMBEDDING_ID = null,
SCORE = 0.8425111770629883
}
}
*/
Гіпотетичні запитання
Ембеддинги чутливі до формулювання. Коротке запитання на кшталт «Хто такий Джон Доу?» за структурою сильно відрізняється від довгого абзацу про Джона Доу, тому показники схожості знижуються, а точність відповідей погіршується. У моделі LLM під час обробки даних формулюються запитання, на які має відповісти кожен фрагмент тексту. Система створює ембеддинги саме цих запитань, а не самих текстів, додаючи посилання на основний документ. Потім запит користувача порівнюється з цими ембеддингами запитань, що значно підвищує точність результатів.
import dev.langchain4j.community.store.embedding.neo4j.Neo4jEmbeddingStoreIngestor;
import dev.langchain4j.community.store.embedding.neo4j.HypotheticalQuestionGraphIngestor;
/* ... Neo4jEmbeddingStore, ChatModel and DocumentSplitter instances.. */
Neo4jEmbeddingStoreIngestor ingestor = HypotheticalQuestionGraphIngestor.builder()
.embeddingModel(embeddingModel)
.driver(driver)
.documentSplitter(splitter)
.questionModel(chatModel)
.embeddingStore(embeddingStore)
.build();
Document document = Document.from("""
Quantum mechanics studies how particles behave. It is a fundamental theory in physics.
Gradient descent and backpropagation algorithms.
Spaghetti carbonara and Italian dishes.
John Doe is a Super Saiyan.
""");
ingestor.ingest(document);
EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingModel(embeddingModel)
.maxResults(2)
.minScore(0.5)
.embeddingStore(ingestor.getEmbeddingStore())
.build();
List<Content> results = retriever.retrieve(Query.from("Who is John Doe?"));
System.out.println("Retrieved Hypothetical context: " + results);
/*
The result is something like this:
DefaultContent {
textSegment = TextSegment {
text = "John Doe is a Super Saiyan.",
metadata = {
index = 2,
source = Wikipedia link,
title = Quantum Mechanics,
url = https://example.com/ai
}
},
metadata = {
SCORE = 0.8234479427337646,
EMBEDDING_ID = 2002cabe-2a3e-4c6e-96ec-a0292e26e817
}
}
*/
Складений Cypher
Іноді топології типу «батько-дитина», «резюме» та гіпотетичні не відповідають певній корпоративній моделі даних, і вам потрібно чітко визначити, саме який Cypher має виконуватися під час завантаження та отримання даних.
final Neo4jEmbeddingStore neo4jEmbeddingStore = /* Neo4jEmbeddingStore instance */
final EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingModel(embeddingModel)
.maxResults(5)
.minScore(0.4)
.embeddingStore(neo4jEmbeddingStore)
.build();
// other required instances ...
Document doc = Document.from("""
Quantum mechanics studies how particles behave. It is a fundamental theory in physics.
Gradient descent and backpropagation algorithms.
Spaghetti carbonara and Italian dishes.
John Doe is a Super Saiyan.
""");
// Ingest the document into Neo4j as parent-child nodes
final Neo4jEmbeddingStoreIngestor ingestor = Neo4jEmbeddingStoreIngestor.builder()
.documentSplitter(parentSplitter)
.documentChildSplitter(childSplitter)
.driver(driver)
.query("CREATE (:MainDoc $metadata)") // a Cypher query template used for storing the processed segment data in Neo4j
.embeddingStore(neo4jEmbeddingStore)
.embeddingModel(embeddingModel)
.build();
ingestor.ingest(doc);
final String retrieveQuery = "Machine Learning";
List<Content> results = retriever.retrieve(Query.from(retrieveQuery));
System.out.println("Retrieved Generic context: " + results);
Хоча цей матеріал присвятований вродженим перевагам графів у Neo4j, спільнота LangChain4j узагальнила ці структурні шаблони. Вищезгадані інструменти для отримання даних ґрунтуються на ParentChildEmbeddingStoreIngestor, тому загальний механізм пошуку між батьківськими та дочірніми елементами працює з будь-яким сховищем ембеддингів LangChain4j, яке належним чином обробляє метадані. Таким чином, деякі звичайні векторні бази даних також отримують базові функції RAG у стилі графів.
import dev.langchain4j.community.store.embedding.ParentChildEmbeddingStoreIngestor;
/* Required instances */
ParentChildEmbeddingStoreIngestor ingestor = ParentChildEmbeddingStoreIngestor.builder()
.documentTransformer(documentTransformer)
.documentSplitter(documentSplitter)
.textSegmentTransformer(textSegmentTransformer)
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.documentChildSplitter(documentChildSplitter)
.childTextSegmentTransformer(childTextSegmentTransformer)
.build();
Пам’ять чату в графі
Агенти для спілкування потребують зберігання стану між взаємодіями, а ШІ без пам’яті є функцією без стану. Буфери пам’яті LangChain4j є тимчасовими та не підходять для продакшну, де додатки перезапускаються та масштабуються горизонтально. Neo4jChatMemoryStore замість цього зберігає історію чату всередині Neo4j. Це не просто набір тексту — це послідовність пов’язаних вузлів, фактично список з посиланнями у вигляді графа, що забезпечує постійне та здатне до пошуку зберігання записів про взаємодію користувача.
За замовчуванням цей буфер створює кореневий вузол Memory, ключем до якого є властивість id. Від нього через відношення LAST_MESSAGE та NEXT відходить ланцюг вузлів Message. Для створення такої структури потрібна або конфігурація сесії Neo4j, або вже існуюча інстанція Driver, причому доступні наступні опційні налаштування:
- етикет вузла пам’яті (за замовчуванням
Memory) та етикет вузла повідомлення (за замовчуваннямMessage); - ключ властивості ID пам’яті (за замовчуванням
id), який ідентифікує сеанс чату, та ключ властивості тексту повідомлення (за замовчуваннямmessage); - тип зв’язку останнього повідомлення (за замовчуванням
LAST_MESSAGE) та тип зв’язку наступного повідомлення (за замовчуваннямNEXT); - назва бази даних Neo4j (за замовчуванням
neo4j); - розмір отримання повідомлень (за замовчуванням
10), який визначає межі вікна контексту.
Щоб розділити сеанси кількох користувачів, система ізолює їх за допомогою унікального значення memoryId. Сучасні моделі LLM приймають багатомодальний вхід, і система серіалізує та зберігає складний контент, такий як зображення, а також видаляє їх.
import dev.langchain4j.store.memory.chat.neo4j.Neo4jChatMemoryStore;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ImageContent;
import java.util.List;
// 1. Initialize the store using an existing driver
Neo4jChatMemoryStore memoryStore = Neo4jChatMemoryStore.builder()
.driver(driver)
.build();
// 2. Identify the specific user sessions
String sessionId1 = "user-alice-123";
String sessionId2 = "user-bob-456";
// 3. Append standard text messages for Alice
List<ChatMessage> aliceMessages = List.of(
new UserMessage("Hi, I'm Alice."),
new AiMessage("Hello Alice!")
);
memoryStore.updateMessages(sessionId1, aliceMessages);
// 4. Append multimodal messages (text + images) for Bob
List<ChatMessage> bobMessages = List.of(
new UserMessage("What do you see in this image?", List.of(new ImageContent("https://...")))
);
memoryStore.updateMessages(sessionId2, bobMessages);
// When we retrieve or delete messages using sessionId1,
// the graph guarantees that Bob's linked list of messages remains completely untouched.
System.out.println("Isolated memory chains created for both Alice and Bob.");
// 5. Optionally delete messages
// memoryStore.deleteMessages(sessionId1);
// memoryStore.deleteMessages(sessionId2);
Збережений результат виглядає так:
Корпоративні бази даних рідко приймають загальні позначення на кшталт Memory чи Message. Для бази даних, яка вже дотримується суворих правил найменування, інструмент створення може перевизначити кожну позначку, властивість та тип взаємозв’язку, які використовує двигун.
// If I want to align the memory storage with my specific domain ontology:
Neo4jChatMemoryStore customMemoryStore = Neo4jChatMemoryStore.builder()
.driver(driver)
.memoryLabel("UserSession") // Overrides the default "Memory" label
.messageLabel("ChatTurn") // Overrides the default "Message" label
.lastMessageRelType("LATEST_CHAT") // Overrides the default "LAST_MESSAGE" rel
.nextMessageRelType("FOLLOWED_BY") // Overrides the default "NEXT" rel
.idProperty("sessionKey") // Overrides the default "id" property
.messageProperty("textContent") // Overrides the default "message" property
.build();
// Now, when the system persists a chat, it will execute domain-specific Cypher queries like:
// MERGE (m:UserSession {sessionKey: 'user-alice-123'})
// CREATE (msg:ChatTurn {textContent: 'Hi...'})
// MERGE (m)-[:LATEST_CHAT]->(msg)
System.out.println("Custom memory store initialized with domain-specific schema.");
Ця гнучкість запобігає тому, щоб пам’ять сприймалася як сторонній плагін, і робить її частиною наявної архітектури графа з чітко визначеними параметрами.
Розмір вікна
Завантаження історії з тисячами повідомлень перевищує ліміт токенів, підвищує витрати на API та погіршує якість відповідей. Параметр size у конструкторі керує «скользячим вікном» у лінкованому списку.
// If I want to heavily restrict the context window to only the most recent interactions:
Neo4jChatMemoryStore slidingWindowStore = Neo4jChatMemoryStore.builder()
.driver(driver)
.size(3) // The default is 10. We limit it to the 3 most recent messages.
.build();
// Assuming the user has sent 20 messages in this session over the past month.
List<ChatMessage> recentHistory = slidingWindowStore.getMessages("user-alice-123");
// The system efficiently traverses the graph starting from the LATEST_CHAT relationship
// and walks backwards via the FOLLOWED_BY relationships, stopping after it collects the
// limited batch of recent messages. The older historical messages remain safely in the
// database, but are not loaded into memory, saving precious tokens.
System.out.println("Loaded only the " + recentHistory.size() + " most recent messages.");
Іноді потрібна повна картина ситуації, наприклад, аналітичний звіт, який узагальнює інформацію з підтримки за місяць. Значення size у 0 або від’ємне число вказують системі пройти через весь список до першої взаємодії.
// If I want to extract the complete history for analytics or summarization:
Neo4jChatMemoryStore completeHistoryStore = Neo4jChatMemoryStore.builder()
.driver(driver)
.size(0) // 0 disables the sliding window limit
.build();
List<ChatMessage> fullHistory = completeHistoryStore.getMessages("user-alice-123");
System.out.println("Extracted the complete session history containing " + fullHistory.size() + " messages.");
У невеликих додатках або безсерверних функціях керування глобальним об’єктом Driver Neo4j може бути небажаним. Метод базової автентифікації, який є скороченням у інструменті створення, використовує безпосередньо облікові дані.
// If I want to instantiate the store directly without managing an external Driver instance:
Neo4jChatMemoryStore standaloneStore = Neo4jChatMemoryStore.builder()
.withBasicAuth("bolt://localhost:7687", "neo4j", "password")
.build();
System.out.println("Memory store connected directly via Basic Auth.");
Єдиною необхідною налаштуванням є підключення через Java Driver Neo4j. Як альтернатива, Neo4jChatMemoryStore.builder().withBasicAuth( створює внутрішню інстанцію Driver.
Оскільки пам’ять є нативною частиною Neo4j, користувачеві можна використовувати власні алгоритми обробки графів чи фонові завдання для аналізу цих вузлів, вилучення ентитетів, згаданих у розмовах, та поєднання історії чату користувача з основним графом знань, що дозволяє створювати глибоко персоналізовані AI-досвіди.
Інтеграція LangChain4j та Neo4j вже значно розвинулася. У міру покращення моделей та постійної оптимізації векторних операцій у базах даних графів, безумовно, з’являтиметься все більше змін та функцій.
Пов’язана література
- Повністю локальний пайплайн GraphRAG з використанням LangExtract, Neo4j, Qdrant та Ollama — Створення GraphRAG на локальних моделях: вилучення ентитетів за допомогою LangExtract, їх зберігання в Neo4j, індексування в Qdrant, а потім розширення підграфів для отримання відповідей, заснованих на графах.
- Практичні нотатки: локальні LLM для вилучення даних у форматі Graph RAG: перевірка на середину 2026 року — Покрокова інструкція з практичними нотатками щодо використання локальних LLM для вилучення даних у форматі Graph RAG: перевірка на середину 2026 року, включаючи контракти, перевірки та шаблони коду для команд, які впроваджують цю схему.