跳到主要内容

Neo4j

Neo4j 是一款高性能的开源图数据库,专为管理互联数据而设计。 Neo4j 的原生图模型非常适合对社会图谱、推荐系统、知识网络等复杂且高度互联的领域建模。 通过与 LangChain4j 的集成,可在 Langchain4j 库中使用 Neo4j Vector 能力。

Maven 依赖

<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-neo4j</artifactId>
<version>${latest version here}</version>
</dependency>

<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-neo4j-retriever</artifactId>
<version>${latest version here}</version>
</dependency>

<!-- if we want to use the Spring Boot starter -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-neo4j-spring-boot-starter</artifactId>
<version>${latest version here}</version>
</dependency>

API

LangChain4j 为 Neo4j 集成提供了以下类:

  • Neo4jEmbeddingStore:实现 EmbeddingStore 接口,支持在 Neo4j 数据库中存储与查询向量嵌入。
  • Neo4jText2CypherRetriever:实现 ContentRetriever 接口,根据用户问题生成并执行 Cypher 查询,从而改进从 Neo4j 数据库的内容检索。它将自然语言问题翻译为 Cypher 查询, 并利用通过 apoc.meta.data 过程计算得到的 Neo4j schema。
  • KnowledgeGraphWriter:从 LLMGraphTransformer 产生的结构化数据出发,将 Neo4j 节点与关系写入数据库的类。 LLMGraphTransformer 可将一份或多份非结构化文档转换为图;它与具体数据库无关,因此可将文本转换为 Nodes 与 Edges 的集合,也可用于 RedisGraph 等其他图数据库。
  • Neo4jEmbeddingStoreIngestor:实现 ParentChildEmbeddingStoreIngestor 接口,执行多阶段转换流水线:转换文档、拆分为片段、可选地对子片段做额外转换、生成嵌入,并将父子关系与嵌入一并存入 Neo4j。
  • Neo4jChatMemoryStore:实现 ChatMemoryStore 接口,在 Neo4j 图数据库中存储与检索对话消息。它支持使用 Neo4j 节点与关系高效查询并持久化聊天历史。

用法示例

Neo4jEmbeddingStore

以下展示如何创建 Neo4jEmbeddingStore 实例:

Neo4jEmbeddingStore embeddingStore = Neo4jEmbeddingStore.builder().<builderParameters>.build();

其中 <builderParameters> 必须包含 dimension,以及 driverwithBasicAuth 之一,并可附带其他可选参数。

完整 builder 参数列表如下:

默认值说明
driver未设置 withBasicAuth 时必填Java Driver 实例
withBasicAuth未设置 driver 时必填uriuserpassword 创建 Java Driver 实例
dimension必填向量维度
configorg.neo4j.driver.SessionConfig.forDatabase("<databaseName>")SessionConfig 实例
label"Document"标签名
embeddingProperty"embedding"嵌入属性名
idProperty"id"ID 属性名
metadataPrefix""元数据前缀
textProperty"text"文本属性名
indexName"vector"向量索引名
databaseName"neo4j"数据库名
retrievalQuery"RETURN properties(node) AS metadata, node.idProperty AS idProperty, node.textProperty AS textProperty, node.embeddingProperty AS embeddingProperty, score"检索查询

因此,创建 Neo4jEmbeddingStore 实例时需要提供合适的配置:

// ---> MINIMAL EMBEDDING <---
Neo4jEmbeddingStore minimalEmbedding = Neo4jEmbeddingStore.builder()
.withBasicAuth(NEO4J_CONNECTION_STRING, USERNAME, ADMIN_PASSWORD)
.dimension(384)
.build();

// ---> CUSTOM EMBEDDING <---
Neo4jEmbeddingStore customEmbeddingStore = Neo4jEmbeddingStore.builder()
.withBasicAuth(NEO4J_CONNECTION_STRING, USERNAME, ADMIN_PASSWORD)
.dimension(384)
.indexName(CUSTOM_INDEX)
.metadataPrefix(CUSTOM_METADATA_PREF)
.label(CUSTOM_LABEL)
.embeddingProperty(CUSTOM_PROP)
.idProperty(CUSTOM_ID)
.textProperty(CUSTOM_TEXT)
.build();

然后你可以用多种方式添加嵌入并进行搜索:

// ---> ADD MINIMAL EMBEDDING <---
Embedding embedding = embeddingModel.embed("embedText").content();
String id = minimalEmbedding.add(embedding); // output: id of the embedding

// ---> ADD MINIMAL EMBEDDING WITH ID <---
String id = randomUUID();
Embedding embedding = embeddingModel.embed("embedText").content();
minimalEmbedding.add(id, embedding);

// ---> ADD EMBEDDING WITH SEGMENT <---
TextSegment segment = TextSegment.from(randomUUID());
Embedding embedding = embeddingModel.embed(segment.text()).content();
String id = minimalEmbedding.add(embedding, segment);

// ---> ADD EMBEDDING WITH SEGMENT AND METADATA <---
TextSegment segment = TextSegment.from(randomUUID(), Metadata.from(METADATA_KEY, "test-value"));
Embedding embedding = embeddingModel.embed(segment.text()).content();
String id = minimalEmbedding.add(embedding, segment);

// ---> ADD MULTIPLE EMBEDDINGS <---
Embedding firstEmbedding = embeddingModel.embed("firstEmbedText").content();
Embedding secondEmbedding = embeddingModel.embed("secondEmbedText").content();
List<String> ids = minimalEmbedding.addAll(asList(firstEmbedding, secondEmbedding));

// ---> ADD MULTIPLE EMBEDDINGS WITH SEGMENTS <---
TextSegment firstSegment = TextSegment.from("firstText");
Embedding firstEmbedding = embeddingModel.embed(firstSegment.text()).content();
TextSegment secondSegment = TextSegment.from("secondText");
Embedding secondEmbedding = embeddingModel.embed(secondSegment.text()).content();
List<String> ids = minimalEmbedding.addAll(
asList(firstEmbedding, secondEmbedding),
asList(firstSegment, secondSegment)
);

然后可以搜索已存储的嵌入:

// ---> SEARCH EMBEDDING WITH MAX RESULTS <---
String id = minimalEmbedding.add(embedding);
final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(embedding)
.maxResults(10)
.build();
final List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(request).matches();

// ---> SEARCH EMBEDDING WITH MIN SCORE <---
Embedding embedding = embeddingModel.embed("embedText").content();
String id = minimalEmbedding.add(embedding);
final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(embedding)
.maxResults(10)
.minScore(0.15)
.build();
final List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(request).matches();

// ---> SEARCH EMBEDDING WITH CUSTOM METADATA PREFIX <---
String metadataCompleteKey = CUSTOM_METADATA_PREF + METADATA_KEY;
TextSegment segment = TextSegment.from(randomUUID(), Metadata.from(METADATA_KEY, "test-value"));
Embedding embedding = embeddingModel.embed(segment.text()).content();
String id = customEmbeddingStore.add(embedding, segment);
final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(embedding)
.maxResults(10)
.build();
final List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(request).matches();

// ---> SEARCH EMBEDDING WITH CUSTOM ID PROPERTY <---
String metadataCompleteKey = CUSTOM_METADATA_PREF + METADATA_KEY;
TextSegment segment = TextSegment.from(randomUUID(), Metadata.from(METADATA_KEY, "test-value"));
Embedding embedding = embeddingModel.embed(segment.text()).content();
String id = embeddingStore.add(embedding, segment);
final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(embedding)
.maxResults(10)
.build();
final List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(request).matches();

// ---> SEARCH MULTIPLE EMBEDDING <---
List<String> ids = minimalEmbedding.addAll(asList(firstEmbedding, secondEmbedding));
final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(firstEmbedding)
.maxResults(10)
.build();
final List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(request).matches();

// ---> SEARCH MULTIPLE EMBEDDING WITH SEGMENTS <---
List<String> ids = minimalEmbedding.addAll(
asList(firstEmbedding, secondEmbedding),
asList(firstSegment, secondSegment)
);
final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(firstEmbedding)
.maxResults(10)
.build();
final List<EmbeddingMatch<TextSegment>> relevant = embeddingStore.search(request).matches();

要同时利用向量索引与全文索引进行混合搜索以获取嵌入:

// ---> ADDS EMBEDDING AND FULLTEXT WITH ID <---
embeddingStore = Neo4jEmbeddingStore.builder()
.withBasicAuth("<Bolt URL>", "<username>", "<password>")
.dimension(384)
.fullTextIndexName("movie_text")
.fullTextQuery("Matrix")
.autoCreateFullText(true)
.label(LABEL_TO_SANITIZE)
.build();

List<Embedding> embeddings =
embeddingModel.embedAll(List.of(TextSegment.from("test"))).content();
embeddingStore.addAll(embeddings);

final Embedding queryEmbedding = embeddingModel.embed("Matrix").content();

final EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(1)
.build();

final List<EmbeddingMatch<TextSegment>> matches =
embeddingStore.search(embeddingSearchRequest).matches();

// ---> SEARCH EMBEDDING WITH AUTOCREATED FULLTEXT <---
final String fullTextIndexName = "movie_text";
final String label = "Movie";
final String fullTextSearch = "Matrix";
embeddingStore = Neo4jEmbeddingStore.builder()
.withBasicAuth("<Bolt URL>", "<username>", "<password>")
.dimension(384)
.label(label)
.indexName("movie_vector_idx")
.fullTextIndexName(fullTextIndexName)
.fullTextQuery(fullTextSearch)
.build();

若 FULLTEXT 索引无效,将抛出带描述信息的异常:

// ---> ERROR HANDLING WITH INVALID FULLTEXT <---
Neo4jEmbeddingStore embeddingStore = Neo4jEmbeddingStore.builder()
.withBasicAuth("<Bolt URL>", "<username>", "<password>")
.dimension(384)
.fullTextIndexName("full_text_with_invalid_retrieval")
.fullTextQuery("Matrix")
.autoCreateFullText(true)
.fullTextRetrievalQuery("RETURN properties(invalid) AS metadata")
.label(LABEL_TO_SANITIZE)
.build();

List<Embedding> embeddings = embeddingModel.embedAll(List.of(TextSegment.from("test"))).content();
embeddingStore.addAll(embeddings);

final Embedding queryEmbedding = embeddingModel.embed("Matrix").content();

final EmbeddingSearchRequest embeddingSearchRequest = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(3)
.build();
embeddingStore.search(embeddingSearchRequest).matches();
// This search will throw a ClientException: ... Variable `invalid` not defined ...

要使用 dev.langchain4j.store.embedding.filter.Filter 类执行带元数据过滤的搜索:

// ---> ADD EMBEDDING WITH ID AND RETRIEVE WITH OR WITHOUT PREFILTER <---
final List<TextSegment> segments = IntStream.range(0, 10)
.boxed()
.map(i -> {
if (i == 0) {
final Map<String, Object> metas =
Map.of("key1", "value1", "key2", 10, "key3", "3", "key4", "value4");
final Metadata metadata = new Metadata(metas);
return TextSegment.from(randomUUID(), metadata);
}
return TextSegment.from(randomUUID());
})
.toList();

final List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
embeddingStore.addAll(embeddings, segments);

final And filter = new And(
new And(new IsEqualTo("key1", "value1"), new IsEqualTo("key2", "10")),
new Not(new Or(new IsIn("key3", asList("1", "2")), new IsNotEqualTo("key4", "value4"))));

TextSegment segmentToSearch = TextSegment.from(randomUUID());
Embedding embeddingToSearch =
embeddingModel.embed(segmentToSearch.text()).content();
final EmbeddingSearchRequest requestWithFilter = EmbeddingSearchRequest.builder()
.maxResults(5)
.minScore(0.0)
.filter(filter)
.queryEmbedding(embeddingToSearch)
.build();
final EmbeddingSearchResult<TextSegment> searchWithFilter = embeddingStore.search(requestWithFilter);
final List<EmbeddingMatch<TextSegment>> matchesWithFilter = searchWithFilter.matches();

final EmbeddingSearchRequest requestWithoutFilter = EmbeddingSearchRequest.builder()
.maxResults(5)
.minScore(0.0)
.queryEmbedding(embeddingToSearch)
.build();
final EmbeddingSearchResult<TextSegment> searchWithoutFilter = embeddingStore.search(requestWithoutFilter);
final List<EmbeddingMatch<TextSegment>> matchesWithoutFilter = searchWithoutFilter.matches();

要对嵌入搜索检索到的数据执行后续读写查询,可利用节点的 embeddingId。 例如:

// ... Neo4jEmbeddingStore instance creation ...
// ... add embeddings....

final List<EmbeddingMatch<TextSegment>> results = embeddingStore.search(/*dev.langchain4j.store.embedding.EmbeddingSearchRequest instance*/)
.matches();

// retrieve the ids to execute the follow-up
List<String> nodeIds = results.stream().map(dev.langchain4j.store.embedding.EmbeddingMatch:embeddingId).toList();

String cypher = """
MATCH (d:Document)
WHERE d.id IN $ids
// -- here the follow-up query, for example
WITH (d)-[:CONNECTED_TO]->(o:OtherLabel)
RETURN o.id
""";

// run the follow-up query
Map<String, Object> params = Map.of("ids", nodeIds);
final List<Record> list = session.run(cypher, params).list();

Spring Boot starter

要创建 Spring Boot starter,Neo4j starter 目前提供如下 application.properties


# the builder.dimension(dimension) method
langchain4j.community.neo4j.dimension=<dimension>
# the builder.withBasicAuth(uri, username, password) method
langchain4j.community.neo4j.auth.uri=<boltURI>
langchain4j.community.neo4j.auth.user=<username>
langchain4j.community.neo4j.auth.password=<password>
# the builder.label(label) method
langchain4j.community.neo4j.label=<label>
# the builder.indexName(indexName) method
langchain4j.community.neo4j.indexName=<indexName>
# the builder.metadataPrefix(metadataPrefix) method
langchain4j.community.neo4j.metadataPrefix=<metadataPrefix>
# the builder.embeddingProperty(embeddingProperty) method
langchain4j.community.neo4j.embeddingProperty=<embeddingProperty>
# the builder.idProperty(idProperty) method
langchain4j.community.neo4j.idProperty=<idProperty>
# the builder.textProperty(textProperty) method
langchain4j.community.neo4j.textProperty=<textProperty>
# the builder.databaseName(databaseName) method
langchain4j.community.neo4j.databaseName=<databaseName>
# the builder.retrievalQuery(retrievalQuery) method
langchain4j.community.neo4j.retrievalQuery=<retrievalQuery>
# the builder.awaitIndexTimeout(awaitIndexTimeout) method
langchain4j.community.neo4j.awaitIndexTimeout=<awaitIndexTimeout>

配置 Starter 后,可以创建如下所示的简单 Spring Boot 项目:

@SpringBootApplication
public class SpringBootExample {

public static void main(String[] args) {
SpringApplication.run(SpringBootExample.class, args);
}

@Bean
public AllMiniLmL6V2EmbeddingModel embeddingModel() {
return new AllMiniLmL6V2EmbeddingModel();
}

}

@RestController
@RequestMapping("/api/embeddings")
public class EmbeddingController {

private final EmbeddingStore<TextSegment> store;
private final EmbeddingModel model;

public EmbeddingController(EmbeddingStore<TextSegment> store, EmbeddingModel model) {
this.store = store;
this.model = model;
}

// add embeddings
@PostMapping("/add")
public String add(@RequestBody String text) {
TextSegment segment = TextSegment.from(text);
Embedding embedding = model.embed(text).content();
return store.add(embedding, segment);
}

// search embeddings
@PostMapping("/search")
public List<String> search(@RequestBody String query) {
Embedding queryEmbedding = model.embed(query).content();
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(5)
.build();
return store.search(request).matches()
.stream()
.map(i -> i.embedded().text()).toList();
}
}

我们定义了可轻松调用的 API,示例如下:

# to create a new embedding 
# and store it with a label "Spring Boot"
curl -X POST localhost:8083/api/embeddings/add -H "Content-Type: text/plain" -d "embeddingTest"

# to search the first 5 embeddings
curl -X POST localhost:8083/api/embeddings/search -H "Content-Type: text/plain" -d "querySearchTest"

Neo4jText2CypherRetriever

以下展示如何创建 Neo4jText2CypherRetriever 实例:

Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder().<builderParameters>.build();

完整 builder 参数列表如下:

默认值说明
graph必填见下文
chatModel必填用于从自然语言问题创建 Cypher 查询的 ChatModel 实现
prompt见下方示例将与 chatModel 一起使用的提示
examples空字符串用于丰富并改进结果的额外示例
maxRetries3若 Cypher 查询失败或返回空结果时的额外重试次数

要连接 Neo4j,需按如下方式使用 Neo4jGraph 类:

// Neo4j Java Driver connection instance
Driver driver = GraphDatabase.driver("<Bolt URL>", AuthTokens.basic("<username>", "<password>"));

Neo4jGraph neo4jGraph = Neo4jGraph.builder()
.driver(driver)
.build();

或像 Neo4jEmbeddingStore 一样使用 withBasicAuth

Neo4jGraph neo4jGraph = Neo4jGraph.builder()
.withBasicAuth("<Bolt URL>", "<username>", "<password>")
.build();

然后将其传给 builder:

Neo4jGraph neo4jGraph = /* Neo4jGraph instance */;

// ChatModel instance, e.g. OpenAiChatModel
ChatModel chatModel = OpenAiChatModel.builder()
.apiKey(OPENAI_API_KEY)
.modelName(GPT_4_O_MINI)
.build();

// Neo4jText2CypherRetriever instance
Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder()
.graph(neo4jGraph)
.chatModel(chatModel)
.build();

你还可以通过调整 sample(在上下文提示中返回多少示例路径)和 maxRels(每个节点标签最多读取多少条关系)等参数进一步自定义 Neo4jGraph 行为。 这些参数是可选的(默认分别为 1000100),若偏好默认行为可省略。 它们对于控制大型图中提示的大小与复杂度特别有用。

此外,你可以使用 Neo4jGraph 返回实体 schema, 即描述图结构的模式、节点属性与关系属性列表:

final Neo4jGraph.StructuredSchema structuredSchema = graph.getStructuredSchema();

List<String> patterns = structuredSchema.patterns();
List<String> nodesProperties = structuredSchema.nodesProperties();
List<String> relationshipsProperties = structuredSchema.relationshipsProperties();

/*
Example outputs:
`patterns`: [(:Person)-[:WROTE]->(:Book)]
`nodesProperties`: [:Book {title: STRING}, :Person {name: STRING}]
`relationshipsProperties`: [:WROTE {year: 1986}]
*/

使用 samplemaxRels 的示例

Neo4jGraph neo4jGraph = Neo4jGraph.builder()
.driver(driver)
.sample(3L) // Sample up to 3 example paths from the graph schema
.maxRels(8L) // Explore a maximum of 8 relationships from the start node
.build();

Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder()
.graph(neo4jGraph)
.chatModel(chatModel)
.build();

以下是基本示例:


// create dataset, for example:
// CREATE (book:Book {title: 'Dune'})<-[:WROTE {when: date('1999')}]-(author:Person {name: 'Frank Herbert'})");


// create a Neo4jGraph instance
Neo4jGraph neo4jGraph = Neo4jGraph.builder()
.driver(/*<Neo4j Driver instance>*/)
.build();

// create a Neo4jText2CypherRetriever instance
Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder()
.graph(neo4jGraph)
.chatModel(chatModel)
.build();

Query query = new Query("Who is the author of the book 'Dune'?");

// retrieve result
List<Content> contents = retriever.retrieve(query);

System.out.println(contents.get(0).textSegment().text());
// example output: "Frank Herbert"

上面的示例会使用如下提示字符串执行聊天请求:

Task:Generate Cypher statement to query a graph database.
Instructions
Use only the provided relationship types and properties in the schema.
Do not use any other relationship types or properties that are not provided.
Schema:

Node properties are the following:
:Book {title: STRING}
:Person {name: STRING}

Relationship properties are the following:
:WROTE {when: DATE}

The relationships are the following:
(:Person)-[:WROTE]->(:Book)

Note: Do not include any explanations or apologies in your responses.
Do not respond to any questions that might ask anything else than for you to construct a Cypher statement.
Do not include any text except the generated Cypher statement.
The question is: {{question}}

其中 question 为 "Who is the author of the book 'Dune'?", 而 schema 由 apoc.meta.data 过程处理,用于检索并字符串化当前 Neo4j schema。 在本例中为

Node properties are the following:
:Book {title: STRING}
:Person {name: STRING}

Relationship properties are the following:
:WROTE {when: DATE}

The relationships are the following:
(:Person)-[:WROTE]->(:Book)
----

We can also change the default prompt if needed:
[source,java]
----
Neo4jGraph neo4jGraph = /* Neo4jGraph instance */

Neo4jText2CypherRetriever.builder()
.neo4jGraph(neo4jGraph)
.promptTemplate("<custom prompt>")
.build();

要创建没有任何重试逻辑的检索器,将 maxRetries 设为 0

Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder()
.graph(graph)
.chatModel(chatModel)
.maxRetries(0) // disables retry logic
.build();

当你希望行为确定、且不希望在 Cypher 生成失败时由检索器尝试回退查询时,此配置很有用。通常推荐用于性能关键或失败处理由外部管理的场景。

也可使用 fromLLM("<question>") 方法,借助带如下提示的 chatModel,基于检索到的上下文与 Cypher 查询生成自然语言答案;其中 {{context}} 是从 Neo4jGraph 检索的 schema,{{cypher}} 是 text-to-Cypher 生成的 Cypher 查询,{{question}} 是传给 fromLLM() 的参数。

Based on the following context and the generated Cypher,
write an answer in natural language to the provided user's question:
Context: {{context}}
Generated Cypher: {{cypher}}
Question: {{question}}
Cypher query:

用法示例:

Neo4jText2CypherRetriever neo4jContentRetriever = Neo4jText2CypherRetriever.builder()
.graph(graph)
.chatModel(OPEN_AI_CHAT_MODEL)
.build();

Query query = new Query("Who is the author of the book 'Dune'?");

String response = neo4jContentRetriever.fromLLM(query);
// example output: the author of the book 'Dune' is Frank Herbert

KnowledgeGraphWriter

KnowledgeGraphWriter 是将结构化知识图谱数据写入 Neo4j 的工具类。它设计为与 LLMGraphTransformer 产生的数据配合使用;后者从非结构化文档中抽取节点与关系。

当文本数据已被转换为图结构、需要高效存入 Neo4j(包括可选的文档溯源信息)时,该写入器特别有用。

功能

  • GraphDocument 实例将节点与关系存入 Neo4j。
  • 支持可选地存储源文档元数据与内容。
  • 自动为实体创建唯一约束。
  • 允许自定义标签、关系类型、ID 与文本属性。

以下展示如何创建 KnowledgeGraphWriter 实例:

KnowledgeGraphWriter writer = KnowledgeGraphWriter.builder().<builderParameters>.build();

完整 builder 参数列表:

Builder 方法说明默认值
graph(Neo4jGraph)设置 Neo4j 图连接。(必填)-
label(String)设置节点的实体标签。__Entity__
relType(String)设置实体与文档之间的关系类型。HAS_ENTITY
idProperty(String)设置用作唯一标识的属性名。id
textProperty(String)设置用于存储文档文本的属性名。text
constraintName(String)设置 Neo4j 中唯一性约束的名称。knowledge_cons
Neo4jGraph graph = Neo4jGraph.builder()
.withBasicAuth("bolt://localhost:7687", "neo4j", "password")
.build();

KnowledgeGraphWriter writer = KnowledgeGraphWriter.builder()
.graph(graph)
.label("Entity")
.relType("MENTIONS")
.idProperty("id")
.textProperty("text")
.build();

List<GraphDocument> graphDocuments = ... // obtained from LLMGraphTransformer
writer.addGraphDocuments(graphDocuments, true); // set to true to include document source

Neo4jEmbeddingStoreIngestor

Neo4jEmbeddingStoreIngestor 是专门用于在 Neo4j 图数据库中存储嵌入及相关数据的摄取类。它提供可配置的嵌入存储、查询模板与提示选项,以支持多种知识摄取与检索工作流。

以下展示如何创建 Neo4jEmbeddingStoreIngestor 实例:

Neo4jEmbeddingStoreIngestor ingestor = Neo4jEmbeddingStoreIngestor.builder()
.<builderParameters>
.build();

其中 <builderParameters> 必须包含 driverdimension,并可进行可选自定义。

完整 builder 参数列表如下:

默认值说明
driver必填Neo4j Java Driver 实例
retrievalQuery见类默认值嵌入查找时用于检索实体的 Cypher 查询
entityCreationQuery见类默认值创建带嵌入实体的 Cypher 查询
label"Child"Neo4j 中嵌入节点使用的节点标签
indexName"child_embedding_index"嵌入节点的索引名
dimension384嵌入向量的维度
systemPrompt见类默认值LLM 驱动任务的系统提示
userPrompt见类默认值LLM 驱动任务的用户提示

使用必填参数的基本用法:

Neo4jEmbeddingStoreIngestor ingestor = Neo4jEmbeddingStoreIngestor.builder()
.driver(neo4jDriver)
.dimension(384)
.build();

自定义检索与创建查询:

Neo4jEmbeddingStoreIngestor ingestor = Neo4jEmbeddingStoreIngestor.builder()
.driver(neo4jDriver)
.dimension(384)
.retrievalQuery("MATCH (doc:Document) WHERE doc.id = $id RETURN doc")
.entityCreationQuery("CREATE (doc:Document {id: $id, embedding: $embedding})")
.label("Document")
.indexName("document_embedding_index")
.build();

使用自定义系统与用户提示:

Neo4jEmbeddingStoreIngestor ingestor = Neo4jEmbeddingStoreIngestor.builder()
.driver(neo4jDriver)
.dimension(384)
.systemPrompt("You are an expert knowledge base ingestor.")
.userPrompt("Please ingest the following content:")
.build();

面向特定场景的 Neo4j Ingestor

以下类扩展 Neo4jEmbeddingStoreIngestor,为特定 GraphRAG 模式提供预配置的摄取逻辑。每个 ingestor 都带有预定义的 Cypher 查询与提示模板,同时仍允许 builder 级自定义。 所有 ingestor 都继承 Neo4jEmbeddingStoreIngestor 的完整 builder API。

SummaryGraphIngestor

用于实现 Global Community Summary Retriever 概念 该 ingestor 使用摘要提示从文档中抽取并存储简洁摘要,默认将其存为标签为 "Summary" 的节点,并链接到原始文档。

用法示例:

SummaryGraphIngestor ingestor = SummaryGraphIngestor.builder()
.driver(driver)
.embeddingModel(embeddingModel)
.questionModel(chatModel)
.documentSplitter(splitter)
.build();

Neo4jEmbeddingStoreIngestor 不同,它具有以下默认值:

  • query"CREATE (:SummaryChunk $metadata)"
  • systemPrompt
You are generating concise and accurate summaries based on the information found in the text.
  • userPrompt
Generate a summary of the following input:
{{input}}

Summary:
  • embeddingStore
private static final String DEFAULT_RETRIEVAL = """
MATCH (node)<-[:HAS_SUMMARY]-(parent)
WITH parent, max(score) AS score, node // deduplicate parents
RETURN parent.text AS text, score, properties(node) AS metadata
ORDER BY score DESC
LIMIT $maxResults""";

private static final String DEFAULT_PARENT_QUERY = """
UNWIND $rows AS row
MATCH (p:SummaryChunk {parentId: $parentId})
CREATE (p)-[:HAS_SUMMARY]->(u:%1$s {%2$s: row.%2$s})
SET u += row.%3$s
WITH row, u
CALL db.create.setNodeVectorProperty(u, $embeddingProperty, row.%4$s)
RETURN count(*)""";

EmbeddingStore defaultEmbeddingStore = Neo4jEmbeddingStore.builder()
.driver(driver)
.retrievalQuery(DEFAULT_RETRIEVAL)
.entityCreationQuery(DEFAULT_PARENT_QUERY)
.label("Summary")
.indexName("summary_embedding_index")
.dimension(384)
.build();

HypotheticalQuestionGraphIngestor

用于实现 Hypothetical Question Retriever 概念:从内容块派生并嵌入假设性问题。这可提高语义搜索准确性,尤其适用于间接或抽象的用户问题。 当查询措辞与文档不完全直接匹配时,能增强检索效果。

用法示例:

HypotheticalQuestionGraphIngestor ingestor = HypotheticalQuestionGraphIngestor.builder()
.embeddingModel(embeddingModel)
.driver(driver)
.documentSplitter(splitter)
.questionModel(chatModel)
.embeddingStore(embeddingStore)
.build();

Neo4jEmbeddingStoreIngestor 不同,它具有以下默认值:

  • query"CREATE (:QuestionChunk $metadata)"
  • systemPrompt
You are generating hypothetical questions based on the information found in the text.
Make sure to provide full context in the generated questions.
  • userPrompt
Use the given format to generate hypothetical questions from the following input:
{{input}}

Hypothetical questions:
  • embeddingStore
private static final String DEFAULT_RETRIEVAL = """
MATCH (node)<-[:HAS_QUESTION]-(parent)
WITH parent, max(score) AS score, node // deduplicate parents
RETURN parent.text AS text, score, properties(node) AS metadata
ORDER BY score DESC
LIMIT $maxResults""";

private static final String DEFAULT_PARENT_QUERY = """
UNWIND $rows AS question
MATCH (p:QuestionChunk {parentId: $parentId})
WITH p, question
CREATE (q:%1$s {%2$s: question.%2$s})
SET q += question.%3$s
MERGE (q)<-[:HAS_QUESTION]-(p)
WITH q, question
CALL db.create.setNodeVectorProperty(q, $embeddingProperty, question.%4$s)
RETURN count(*)""";

EmbeddingStore defaultEmbeddingStore = Neo4jEmbeddingStore.builder()
.driver(driver)
.retrievalQuery(DEFAULT_RETRIEVAL_QUERY)
.entityCreationQuery(DEFAULT_PARENT_QUERY)
.label("Child")
.indexName("child_embedding_index")
.dimension(384)
.build();

ParentChildGraphIngestor

用于实现 Parent-Child Retriever 概念。 适用于在子节点上做语义搜索、但结果锚定到父文档的场景。 该 ingestor 存储带嵌入的子块,并默认通过 :HAS_CHILD 关系将它们链接到父节点。适合在引用更广文档上下文的同时检索相关片段。

ParentChildGraphIngestor ingestor = ParentChildGraphIngestor.builder()
.embeddingModel(embeddingModel)
.driver(driver)
.documentSplitter(parentSplitter)
.documentChildSplitter(childSplitter)
.build();

Neo4jEmbeddingStoreIngestor 不同,它具有以下默认值:

  • query"CREATE (:ParentChunk $metadata)"

  • embeddingStore

private static final String DEFAULT_RETRIEVAL = """
MATCH (node)<-[:HAS_CHILD]-(parent)
WITH parent, collect(node.text) AS chunks, max(score) AS score
RETURN parent.text + reduce(r = "", c in chunks | r + "\n\n" + c) AS text,
score,
properties(parent) AS metadata
ORDER BY score DESC
LIMIT $maxResults""";

private static final String DEFAULT_PARENT_QUERY = """
UNWIND $rows AS row
MATCH (p:ParentChunk {parentId: $parentId})
CREATE (p)-[:HAS_CHILD]->(u:%1$s {%2$s: row.%2$s})
SET u += row.%3$s
WITH row, u
CALL db.create.setNodeVectorProperty(u, $embeddingProperty, row.%4$s)
RETURN count(*)""";

EmbeddingStore defaultEmbeddingStore = Neo4jEmbeddingStore.builder()
.driver(driver)
.retrievalQuery(DEFAULT_RETRIEVAL)
.entityCreationQuery(DEFAULT_PARENT_QUERY)
.label("Child")
.indexName("child_embedding_index")
.dimension(384)
.build();

Neo4jChatMemoryStore

Neo4jChatMemoryStore 是专门的聊天记忆实现,在 Neo4j 图数据库中存储与检索对话消息。它支持使用 Neo4j 节点与关系高效查询并持久化聊天历史。

以下展示如何创建 Neo4jChatMemoryStore 实例:

Neo4jChatMemoryStore chatMemoryStore = Neo4jChatMemoryStore.builder()
.<builderParameters>
.build();

其中 <builderParameters> 必须包含 driver,以及标签与节点属性名等可选属性。

完整 builder 参数列表如下:

默认值说明
driver必填Neo4j Java Driver 实例
label"ChatMessage"Neo4j 中聊天消息节点使用的标签
idProperty"id"消息 ID 的属性名
conversationIdProperty"conversationId"标识对话的属性名
timestampProperty"timestamp"消息时间戳的属性名

示例

使用必填参数的基本用法:

Neo4jChatMemoryStore chatMemoryStore = Neo4jChatMemoryStore.builder()
.driver(neo4jDriver)
.build();

自定义节点标签与属性:

Neo4jChatMemoryStore chatMemoryStore = Neo4jChatMemoryStore.builder()
.driver(neo4jDriver)
.label("Message")
.idProperty("messageId")
.conversationIdProperty("convId")
.timestampProperty("timeSent")
.build();

简单流程示例

以下是 Neo4jEmbeddingStoreNeo4jText2CypherRetriever API 使用流程的若干示例。

  • Neo4jEmbeddingStore
private static final EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();

public static void minimalEmbedding() {
try (Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5.26")) {
neo4j.start();

EmbeddingStore<TextSegment> minimalEmbedding = Neo4jEmbeddingStore.builder()
.withBasicAuth(neo4j.getBoltUrl(), "neo4j", neo4j.getAdminPassword())
.dimension(384)
.build();


TextSegment segment1 = TextSegment.from("I like football.", Metadata.from("test-key-1", "test-value-1"));
Embedding embedding1 = embeddingModel.embed(segment1).content();

TextSegment segment2 = TextSegment.from("The weather is good today.", Metadata.from("test-key-2", "test-value-2"));
Embedding embedding2 = embeddingModel.embed(segment2).content();

TextSegment segment3 = TextSegment.from("I like basketball.", Metadata.from("test-key-3", "test-value-3"));
Embedding embedding3 = embeddingModel.embed(segment3).content();
minimalEmbedding.addAll(
List.of(embedding1, embedding2, embedding3),
List.of(segment1, segment2, segment3)
);

Embedding queryEmbedding = embeddingModel.embed("What are your favourite sports?").content();
final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(2)
.minScore(0.15)
.build();
List<EmbeddingMatch<TextSegment>> relevant = minimalEmbedding.search(request).matches();
relevant.forEach(match -> {
System.out.println(match.score()); // 0.8144289255142212
System.out.println(match.embedded().text()); // I like football. || I like basketball.
});
}
}

public static void customEmbeddingStore() {
try (Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5.26")) {
neo4j.start();

Neo4jEmbeddingStore customEmbeddingStore = Neo4jEmbeddingStore.builder()
.withBasicAuth(neo4j.getBoltUrl(), "neo4j", neo4j.getAdminPassword())
.dimension(384)
.indexName("customidx")
.label("CustomLabel")
.embeddingProperty("customProp")
.idProperty("customId")
.textProperty("customText")
.build();

TextSegment segment1 = TextSegment.from("I like football.");
Embedding embedding1 = embeddingModel.embed(segment1).content();
customEmbeddingStore.add(embedding1, segment1);

TextSegment segment2 = TextSegment.from("The weather is good today.");
Embedding embedding2 = embeddingModel.embed(segment2).content();
customEmbeddingStore.add(embedding2, segment2);

Embedding queryEmbedding = embeddingModel.embed("What is your favourite sport?").content();
final EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(queryEmbedding)
.maxResults(1)
.build();
List<EmbeddingMatch<TextSegment>> relevant = customEmbeddingStore.search(request).matches();
EmbeddingMatch<TextSegment> embeddingMatch = relevant.get(0);

System.out.println(embeddingMatch.score()); // 0.8144289255142212
System.out.println(embeddingMatch.embedded().text()); // I like football.
}
}
  • Neo4jText2CypherRetriever
    private final ChatModel chatModel;

public void Neo4jText2CypherRetriever() {
try (
Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5.16.0")
.withoutAuthentication()
.withLabsPlugins("apoc")
) {
neo4jContainer.start();
try (Driver driver = GraphDatabase.driver(neo4jContainer.getBoltUrl(), AuthTokens.none())) {
try (Neo4jGraph graph = Neo4jGraph.builder().driver(driver).build()) {
try (Session session = driver.session()) {
session.run("CREATE (book:Book {title: 'Dune'})<-[:WROTE]-(author:Person {name: 'Frank Herbert'})");
}
graph.refreshSchema();

Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder()
.graph(graph)
.chatModel(chatModel)
.build();

Query query = new Query("Who is the author of the book 'Dune'?");

List<Content> contents = retriever.retrieve(query);

System.out.println(contents.get(0).textSegment().text()); // "Frank Herbert"
}
}
}
}

示例源码