Neo4j і LangChain4j: прыемнікі GraphRAG, фільтрацыя і памяць чату
Neo4j з LangChain4j для GraphRAG: адккрыцце схемы, стварэнне графа з LLM, Cypher DSL, фільтрацыя ў індэксе, выкарыстоўванне адносаў «родак-дзець» і памяць чату, апараная на графе.
Крыніцы: Mastering Neo4j & LangChain4j: GraphRAG, Persistent AI Memory and more
За межамі простага сховішча вектораў
Проста сістэма сховішча вектораў Neo4j, якая працуе ўнутры Java-програмы, ёсць толькі входны момент. Учаснікі рэпазітарыя спяльнай спяльноты LangChain4j згодна з тым дадалі функцыі, якія значна выходзяць за межы простага пошуку сэмабільнасці: аўтаматызаванае стварэнне графа, налаштаванае фільтраўванне вектораў, структурныя шаблоны GraphRAG і памяць чату, якая зберагаецца як граф. У наступных раздзелах практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна практычна
<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 выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкарыстоўванае выкары
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 перадае гэту задачу самой большай мовнай модэлі.
Клас не залежыць ад тыпу базы дадзеных, адколькі ён ператварае тэкст у вузлы і рэшткі, якія таксама можна выкарыстоўваць для іншых цэлей. Ёжы ўтрамбаваць мадэль ад галюцынацый чы аб’еднання онтологіі, напрыклад, шляхом стварэння нечаканых атрыбутаў, распрацоўшчыкі керуюць процэсам выкарыстоўвання конкрэтныя прыклады (фейў-шоты) і чыста апісаныя схемы. Аднародная спялочка таксама дадала можлівасць праеўязваць выкарыстоўваныя ентытэты назад да адпаведнага адказвальнага дакумента, дзе яны былі створаны, што дазволяе захаваць первісныя метаданы.
Рэзультат у фармаце JSON павінен адпавядаць фіксаванай структуры, у якой ёсць ключы head, head_type, relation, tail і tail_type; прыклады запытак научаюць мадэль такой структуры. Ёсць адлінак з прыкладу 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 injection. Аварыст працоўнае санітаравае кожны вхідны даны за дапамою 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 запіты часта ствараліся шляхом з’еднання необработаных стрэлок, што не рэкамендуецца праз рызык ін’екцыі коду ў Cypher і сінтаксічныя памылкі. Внутрашнія компаненты 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. Галоўная перадчынта — фільтраўванне ў самым індэксе.
Здароваець, трэба выконаць пошук на адпоўненасць плюс застосаваць абмежэнні метаданых, напрыклад, дакументы, сэмантычна блізкія да «machine learning», але опублікованыя толькі у 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.");
Пасля прыему дадзеных, retrieve шукае маленькія фрагменты і перадае цэлы адпрацоўваны тэкст 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.");
Едынавым неабяжным настройкам являецца з’язок с драйверам Neo4j для Java. Як альтэрнатыва, Neo4jChatMemoryStore.builder().withBasicAuth( стварае экземпляр драйвера ўнутршнім чынам.
Паколькі памяць ў Neo4j ёсць частынай яго структуры, спецыяльныя алгорытмы графаў або фонавыя задачы можу аналізаваць гэтыя вузлы, выдэляць аб’екты, упамінаныя ў кантактах, і прыяўляць історыю чату карыстальніка да основнага графа знанняў, чым ствараюцца глыбока персоналізаваныя AI-досвяды.
Інтэграцыя LangChain4j і Neo4j ужо значна развілася. Паколькі мадэлі становяцца крашчымі, а базы дадзенняяў графаў продовжаюць оптымізаваць вектарныя операцыі, неабяжна будуць дадзены ўсё больш змян і функцый.
Супаўзяныя матэрыялы
- Полны ўсё на месцы канал GraphRAG з LangExtract, Neo4j, Qdrant і Ollama — Стварэнне GraphRAG на месцовых мадэлях: выкарыстоўванне LangExtract для выдзеявання энтытэтаў, ўтрымванне іх у Neo4j, індэксаванне ў Qdrant, а пасля розгортанне падграфаў для атрыбутавання адпаведных адказаў.
- Практычныя прыметкі: месцовыя LLM-ы для выдзеявання даных у фармаце Graph RAG: перыякшаны тэсты для серэдзіны 2026 года — Паследовны апіс практычных прыметак: месцовыя LLM-ы для выдзеявання даных у фармаце Graph RAG: перыякшаны тэсты для серэдзіны 2026 года, укладанніе контрактаў, перакананняў і шаблонаў коду для команд, якія вікорыстоўваюць гэты падход.