主题
Neo4j
Neo4j 是一个高性能、开源的图数据库,专为管理互联数据而设计。Neo4j 的原生图模型非常适合为复杂、高度互联的领域建模,例如社交图谱、推荐系统与知识网络。借助它在 LangChain4j 中的集成,Neo4j Vector 能力可在 LangChain4j 库中使用。
Maven 依赖
xml
<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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
API
LangChain4j 为 Neo4j 集成提供了以下类:
Neo4jEmbeddingStore:实现 EmbeddingStore 接口,让你能在 Neo4j 数据库中存储并查询向量嵌入。Neo4jText2CypherRetriever:实现 ContentRetriever 接口,用于从用户问题生成并执行 Cypher 查询,从而改进从 Neo4j 数据库检索内容。它把自然语言问题翻译为 Cypher 查询,利用经由 apoc.meta.data 过程算出的 Neo4j schema。KnowledgeGraphWriter:一个从LLMGraphTransformer产出的结构化数据出发,存储 Neo4j 节点与关系的类, 而LLMGraphTransformer是一个把一个或多个非结构化文档转换为图的工具。它与数据库无关,这意味着它把文本转换为一系列 Node 与 Edge,也能用于 RedisGraph 等其他图数据库。Neo4jEmbeddingStoreIngestor:实现ParentChildEmbeddingStoreIngestor接口,执行一个多阶段转换流水线:它转换文档、把文档切分为片段、可选地对子片段施加额外转换、生成嵌入,并把父子关系与嵌入一起存入 Neo4j。Neo4jChatMemoryStore:实现ChatMemoryStore接口,在 Neo4j 图数据库中存储并检索会话消息。它借助 Neo4j 节点与关系,以高效的查询与持久化来管理聊天历史。
用法示例
Neo4jEmbeddingStore
下面是创建一个 Neo4jEmbeddingStore 实例的方式:
java
Neo4jEmbeddingStore embeddingStore = Neo4jEmbeddingStore.builder().<builderParameters>.build();1
其中 <builderParameters> 必须包含 dimension 以及 driver 或 withBasicAuth 参数,以及其他可选项。
以下是完整的 builder 列表:
| 键 | 默认值 | 描述 |
|---|---|---|
driver | 若未设 withBasicAuth 则必填 | Java Driver 实例 |
withBasicAuth | 若未设 driver 则必填 | 从 uri、user 与 password 创建一个 Java Driver 实例 |
dimension | 必填 | 向量的维度 |
config | org.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 实例,你需要提供合适的设置:
java
// ---> 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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
然后你可以以多种不同方式添加嵌入,并搜索它们:
java
// ---> 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)
);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
然后你可以搜索已存储的嵌入:
java
// ---> 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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
要借助同时利用向量与全文索引的混合搜索来获取嵌入:
java
// ---> 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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
若 FULLTEXT 索引无效,会抛出一个描述性的异常。:
java
// ---> 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 ...1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
要执行一次借助 dev.langchain4j.store.embedding.filter.Filter 类做元数据过滤的搜索:
java
// ---> 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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
要对嵌入搜索所检索到的数据执行一次用于读或写的后续查询,我们可以利用节点的 embeddingId。例如:
java
// ... 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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Spring Boot starter
要创建一个 Spring Boot starter,Neo4j starter 目前提供以下 application.properties:
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>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
配置该 starter 让我们能创建一个如下的简单 Spring Boot 项目:
java
@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();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
我们定义了可以像这里展示的那样轻松调用的 API:
shell
# 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"1
2
3
4
5
6
2
3
4
5
6
Neo4jText2CypherRetriever
下面是创建一个 Neo4jText2CypherRetriever 实例的方式:
java
Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder().<builderParameters>.build();1
以下是完整的 builder 列表:
| 键 | 默认值 | 描述 |
|---|---|---|
graph | 必填 | 见下文 |
chatModel | 必填 | 用于从自然语言问题创建 Cypher 查询的 ChatModel 实现 |
prompt | 见下面示例 | 将与 chatModel 一起使用的提示词 |
examples | 空字符串 | 用于丰富并改善结果的额外示例 |
maxRetries | 3 | 当生成 Cypher 查询失败或返回空结果时的额外重试 |
要连接 Neo4j,我们必须以这种方式利用 Neo4jGraph 类:
java
// Neo4j Java Driver connection instance
Driver driver = GraphDatabase.driver("<Bolt URL>", AuthTokens.basic("<username>", "<password>"));
Neo4jGraph neo4jGraph = Neo4jGraph.builder()
.driver(driver)
.build();1
2
3
4
5
6
2
3
4
5
6
或者像 Neo4jEmbeddingStore 那样使用 withBasicAuth:
java
Neo4jGraph neo4jGraph = Neo4jGraph.builder()
.withBasicAuth("<Bolt URL>", "<username>", "<password>")
.build();1
2
3
2
3
然后把它传给 builder:
java
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();1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
你可以通过调整诸如 sample(在上下文提示词中返回多少示例路径)与 maxRels(每个节点标签最多读取多少关系)之类的参数,进一步定制 Neo4jGraph 的行为。这些参数是可选的(默认值分别为 1000 与 100),如果你更喜欢默认行为,可以省略它们。它们在较大的图中对于控制提示词的大小与复杂度尤其有用。
此外,你可以用 Neo4jGraph 返回实体 schema,即一系列描述该图结构的模式、节点属性与关系属性:
java
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}]
*/1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
带 sample 与 maxRels 的示例
java
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();1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
这里是一个基本示例:
java
// 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"1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
上面那个会带着如下提示词字符串执行一次聊天请求:
text
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}}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
其中 question 是 "Who is the author of the book 'Dune'?",而 schema 由 apoc.meta.data 过程处理,以检索并字符串化当前的 Neo4j schema。在本例中它是
text
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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
要创建一个不带任何重试逻辑的检索器,请将 maxRetries 设为 0:
java
Neo4jText2CypherRetriever retriever = Neo4jText2CypherRetriever.builder()
.graph(graph)
.chatModel(chatModel)
.maxRetries(0) // disables retry logic
.build();1
2
3
4
5
2
3
4
5
当你想要确定性行为、且不希望检索器在 Cypher 生成失败时尝试回退查询时,这种配置很有用。它通常推荐用于性能至关重要、或失败处理由外部管理的场景。
也用 fromLLM("<question>") 方法,借助以下提示词让一个 chatModel 基于检索到的上下文与 Cypher 查询生成一个自然语言答案,其中 是从 Neo4jGraph 检索到的 schema, 是由 text-to-Cypher 生成的 Cypher 查询, 是传给 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:1
2
3
4
5
6
2
3
4
5
6
示例用法:
java
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 Herbert1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
KnowledgeGraphWriter
KnowledgeGraphWriter 是一个用于把结构化知识图谱数据写入 Neo4j 的工具类。它被设计成与 LLMGraphTransformer 产出的数据配合工作,后者从非结构化文档中抽取节点与关系。
当文本数据已被转换为图结构、并需要(连同可选的文档出处)高效地存储到 Neo4j 数据库时,这个写入器尤其有用。
特性
- 从
GraphDocument实例向 Neo4j 存储节点与关系。 - 支持可选地存储源文档的元数据与内容。
- 自动为实体创建唯一约束。
- 允许自定义标签、关系类型、ID 与文本属性。
下面是创建一个 KnowledgeGraphWriter 实例的方式:
java
KnowledgeGraphWriter writer = KnowledgeGraphWriter.builder().<builderParameters>.build();1
以下是完整的 builder 列表:
| Builder 方法 | 描述 | 默认值 |
|---|---|---|
graph(Neo4jGraph) | 设置 Neo4j 图连接。(必填) | - |
label(String) | 设置节点的实体标签。 | __Entity__ |
relType(String) | 设置实体与文档之间的关系类型。 | HAS_ENTITY |
idProperty(String) | 设置用作唯一标识符的属性名。 | id |
textProperty(String) | 设置用于存储文档文本的属性名。 | text |
constraintName(String) | 设置 Neo4j 中唯一约束的名称。 | knowledge_cons |
java
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 source1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Neo4jEmbeddingStoreIngestor
Neo4jEmbeddingStoreIngestor 是一个专门的摄入器类,旨在把嵌入及相关数据存储到 Neo4j 图数据库中。它为嵌入存储、查询模板与提示词提供了可配置选项,以支持各种知识摄入与检索工作流。
下面是创建一个 Neo4jEmbeddingStoreIngestor 实例的方式:
java
Neo4jEmbeddingStoreIngestor ingestor = Neo4jEmbeddingStoreIngestor.builder()
.<builderParameters>
.build();1
2
3
2
3
其中 <builderParameters> 把 driver 与 dimension 作为必填,外加可选的定制。
以下是完整的 builder 列表:
| 键 | 默认值 | 描述 |
|---|---|---|
driver | 必填 | Neo4j Java Driver 实例 |
retrievalQuery | 见类默认值 | 在嵌入查找期间用于检索实体的 Cypher 查询 |
entityCreationQuery | 见类默认值 | 用于创建带嵌入的实体的 Cypher 查询 |
label | "Child" | Neo4j 中用于嵌入节点的节点标签 |
indexName | "child_embedding_index" | 嵌入节点的索引名称 |
dimension | 384 | 嵌入向量的维度 |
systemPrompt | 见类默认值 | 用于 LLM 驱动任务的系统提示词 |
userPrompt | 见类默认值 | 用于 LLM 驱动任务的用户提示词 |
带必填参数的基本用法:
java
Neo4jEmbeddingStoreIngestor ingestor = Neo4jEmbeddingStoreIngestor.builder()
.driver(neo4jDriver)
.dimension(384)
.build();1
2
3
4
2
3
4
自定义检索与创建查询:
java
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();1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
使用自定义系统提示词与用户提示词:
java
Neo4jEmbeddingStoreIngestor ingestor = Neo4jEmbeddingStoreIngestor.builder()
.driver(neo4jDriver)
.dimension(384)
.systemPrompt("You are an expert knowledge base ingestor.")
.userPrompt("Please ingest the following content:")
.build();1
2
3
4
5
6
2
3
4
5
6
面向专门用例的 Neo4j 摄入器
以下类扩展 Neo4jEmbeddingStoreIngestor,提供针对特定 GraphRAG 模式定制的预配置摄入逻辑。每个摄入器都带有预定义的 Cypher 查询与提示词模板,同时仍允许 builder 级的定制。所有摄入器都从 Neo4jEmbeddingStoreIngestor 继承完整的 builder API。
SummaryGraphIngestor
为实现 全局社区摘要检索器概念,该摄入器用摘要提示词从文档中抽取简明摘要并存入图中,把它们作为标记为 "Summary"(默认)的节点存储,并链接到原始文档。
示例用法:
java
SummaryGraphIngestor ingestor = SummaryGraphIngestor.builder()
.driver(driver)
.embeddingModel(embeddingModel)
.questionModel(chatModel)
.documentSplitter(splitter)
.build();1
2
3
4
5
6
2
3
4
5
6
与 Neo4jEmbeddingStoreIngestor 不同,它有以下默认值:
query:"CREATE (:SummaryChunk $metadata)"systemPrompt:
text
You are generating concise and accurate summaries based on the information found in the text.1
userPrompt:
text
Generate a summary of the following input:
{{input}}
Summary:1
2
3
4
2
3
4
embeddingStore:
java
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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
HypotheticalQuestionGraphIngestor
通过生成并嵌入由内容片段派生的假设性问题,来实现 假设问题检索器概念。这提升了语义搜索的准确度,尤其针对间接或抽象的用户问题。当查询与文档措辞并不直接匹配时,它能增强检索。
示例用法:
java
HypotheticalQuestionGraphIngestor ingestor = HypotheticalQuestionGraphIngestor.builder()
.embeddingModel(embeddingModel)
.driver(driver)
.documentSplitter(splitter)
.questionModel(chatModel)
.embeddingStore(embeddingStore)
.build();1
2
3
4
5
6
7
2
3
4
5
6
7
与 Neo4jEmbeddingStoreIngestor 不同,它有以下默认值:
query:"CREATE (:QuestionChunk $metadata)"systemPrompt:
text
You are generating hypothetical questions based on the information found in the text.
Make sure to provide full context in the generated questions.1
2
2
userPrompt:
text
Use the given format to generate hypothetical questions from the following input:
{{input}}
Hypothetical questions:1
2
3
4
2
3
4
embeddingStore:
java
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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ParentChildGraphIngestor
为实现 父子检索器概念。它在对子节点做语义搜索、但结果锚定到父文档的场景中很有用。该摄入器存储带嵌入的子块,并默认用 :HAS_CHILD 关系把它们链接到父节点。非常适合在引用更广泛文档上下文的同时检索相关片段。
java
ParentChildGraphIngestor ingestor = ParentChildGraphIngestor.builder()
.embeddingModel(embeddingModel)
.driver(driver)
.documentSplitter(parentSplitter)
.documentChildSplitter(childSplitter)
.build();1
2
3
4
5
6
2
3
4
5
6
与 Neo4jEmbeddingStoreIngestor 不同,它有以下默认值:
query:"CREATE (:ParentChunk $metadata)"embeddingStore:
java
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();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Neo4jChatMemoryStore
Neo4jChatMemoryStore 是一个专门的聊天记忆实现,在 Neo4j 图数据库中存储并检索会话消息。它借助 Neo4j 节点与关系,以高效的查询与持久化来管理聊天历史。
下面是创建一个 Neo4jChatMemoryStore 实例的方式:
java
Neo4jChatMemoryStore chatMemoryStore = Neo4jChatMemoryStore.builder()
.<builderParameters>
.build();1
2
3
2
3
其中 <builderParameters> 把 driver 作为必填,以及用于标签与节点属性名的可选属性。
以下是完整的 builder 列表:
| 键 | 默认值 | 描述 |
|---|---|---|
driver | 必填 | Neo4j Java Driver 实例 |
label | "ChatMessage" | Neo4j 中用于聊天消息节点的标签 |
idProperty | "id" | 消息 ID 的属性名 |
conversationIdProperty | "conversationId" | 标识该会话的属性名 |
timestampProperty | "timestamp" | 消息时间戳的属性名 |
示例
带必填参数的基本用法:
java
Neo4jChatMemoryStore chatMemoryStore = Neo4jChatMemoryStore.builder()
.driver(neo4jDriver)
.build();1
2
3
2
3
自定义节点标签与属性:
java
Neo4jChatMemoryStore chatMemoryStore = Neo4jChatMemoryStore.builder()
.driver(neo4jDriver)
.label("Message")
.idProperty("messageId")
.conversationIdProperty("convId")
.timestampProperty("timeSent")
.build();1
2
3
4
5
6
7
2
3
4
5
6
7
简单流程示例
以下是 Neo4jEmbeddingStore 与 Neo4jText2CypherRetriever API 使用流程的几个示例。
Neo4jEmbeddingStore:
java
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.
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
Neo4jText2CypherRetriever:
java
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"
}
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30