存储
以构建文章内容涉及人物间关系为例(此处人物即实体)
如下图,先使用AI来提取实体(人物)-关系
,将实体-关系存入图数据库,再将文档向量化后关联实体存入向量数据库
查询
如下图所示,用户输入一段查询文本,将其向量化后选取关联最紧密的文章相关联的实体,再利用此实体来查询图数据库中的实体关系。
(ps:理论上这种方法要查询的精确,需要将文章切分为足够小且完整的段落,否则一个文章关联实体过多就没有意义了。若文章文本巨大,可尝试先利用AI进行文本切分再提取实体关系,向量化存储;或者要求AI对每个实体写一个简介,向量化此简介;又或是将实体向量化后直接存储)
AI
市面上的ChatAI模型,或本地部署也可,此处采用智谱AI为例
向量化
curl --location --request POST 'https://open.bigmodel.cn/api/paas/v4/embeddings' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer zhipu_ai_tokens' \
--data-raw '{
"input": "贾宝玉",
"model": "embedding-2"
}'
实体关系提取
例如下图,通过提问让AI提取实体-关系,代码层面自然是使用AI产品提供的API实现。
(ps: 市面上有许多专注于 实体识别(NER)、关系抽取(RE)、事件抽取(EE)的产品,例如 KnowLM/README_ZH.md at main · zjunlp/KnowLM · GitHub)
向量数据
以 chromadb为例
Running Chroma - ChromaDB Cookbook | The Unofficial Guide to ChromaDB
docker安装
docker run -d --name chromadb -p 8100:8000 -v /root/chromadb/db_data:/chroma/chroma -e IS_PERSISTENT=TRUE -e ANONYMIZED_TELEMETRY=TRUE chromadb/chroma:latest
Java SDK
https://github.com/amikos-tech/chromadb-java-client
chroma有多种语言的sdk(Chroma Ecosystem Clients - ChromaDB Cookbook | The Unofficial Guide to ChromaDB),
此处使用java
- 导入chromadb-sdk依赖,pom.xml
<dependency>
<groupId>io.github.amikos-tech</groupId>
<artifactId>chromadb-java-client</artifactId>
<version>0.1.5</version>
</dependency>
- 由于使用智谱AI向量化接口,还需要导入智谱SDK
<dependency>
<groupId>cn.bigmodel.openapi</groupId>
<artifactId>oapi-java-sdk</artifactId>
<version>release-V4-2.1.0</version>
</dependency>
- 先简单包装下向量化接口
package com.jenson;
import com.zhipu.oapi.ClientV4;
import com.zhipu.oapi.service.v4.embedding.EmbeddingApiResponse;
import com.zhipu.oapi.service.v4.embedding.EmbeddingRequest;
import com.zhipu.oapi.service.v4.embedding.EmbeddingResult;
import java.util.concurrent.TimeUnit;
/**
* @author Jenson
* @version 1.0
* @date 2024/8/12 上午10:40
*/
public class ZhiPuUtils {
/**
* AI向量化文本
*
* @param apiKey apiKey
* @param model 模型名称
* @param content 文本内容
* @return 向量化结果
*/
public static EmbeddingResult embedding(String apiKey, String model, String content) {
ClientV4 client = new ClientV4
.Builder(apiKey)
.networkConfig(
60 * 5,
60 * 5,
60 * 5,
60 * 5,
TimeUnit.SECONDS)
.build();
EmbeddingRequest request = EmbeddingRequest.builder()
.model(model)
.input(content)
.build();
EmbeddingApiResponse embeddingApiResponse = client.invokeEmbeddingsApi(request);
return embeddingApiResponse.getData();
}
}
- 实现
tech.amikos.chromadb.EmbeddingFunction
chroma-sdk 的现有实现不包含国产AI的,得自己写一下,简单弄一下就好
package com.jenson;
import com.zhipu.oapi.service.v4.embedding.Embedding;
import com.zhipu.oapi.service.v4.embedding.EmbeddingResult;
import tech.amikos.chromadb.EmbeddingFunction;
import java.util.ArrayList;
import java.util.List;
/**
* 智谱AI向量化Chroma 函数实现
*
* @author Jenson
* @version 1.0
*/
public class ZhiPuAIEmbeddingFunction implements EmbeddingFunction {
private final String apiKey;
private final String defaultModel;
public ZhiPuAIEmbeddingFunction(String apiKey,
String defaultModel) {
this.apiKey = apiKey;
this.defaultModel = defaultModel;
}
@Override
public List<List<Float>> createEmbedding(List<String> documents) {
return this.createEmbedding(documents, this.defaultModel);
}
@Override
public List<List<Float>> createEmbedding(List<String> documents, String model) {
List<List<Float>> list = new ArrayList<>();
for (String document : documents) {
EmbeddingResult embeddingResult = ZhiPuUtils.embedding(this.apiKey, model, document);
if (embeddingResult.getError() != null) {
throw new RuntimeException("质谱AI向量化失败," + embeddingResult.getError().toString());
}
List<Embedding> embeddings = embeddingResult.getData();
List<Float> embeddingList = new ArrayList<>();
for (Double v : embeddings.get(0).getEmbedding()) {
embeddingList.add(Float.valueOf(String.valueOf(v)));
}
list.add(embeddingList);
}
return list;
}
}
- 包装chromadb的增删改查,此例只简单包装了下新增和查询
package com.jenson;
import com.google.gson.internal.LinkedTreeMap;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import tech.amikos.chromadb.Client;
import tech.amikos.chromadb.Collection;
import tech.amikos.chromadb.EmbeddingFunction;
import tech.amikos.chromadb.handler.ApiException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Chroma Db Client
*
* @author Jenson
* @version 1.0
*/
@Slf4j
public class ChromaClient {
private Client client;
private EmbeddingFunction embeddingFunction;
public ChromaClient(String basePath, EmbeddingFunction embeddingFunction) {
this.client = new Client(basePath);
this.embeddingFunction = embeddingFunction;
}
/**
* 保存数据
*
* @param collectionName 集合
* @param document 文档
* @param metadata 元数据
*/
public void save(String collectionName, String document, Map<String, String> metadata) {
Collection collection = this.getCollection(collectionName);
List<Map<String, String>> metadataList = new ArrayList<>();
if (metadata != null && !metadata.isEmpty()) {
metadataList.add(metadata);
}
try {
String id = DigestUtils.md5Hex((document).getBytes(StandardCharsets.UTF_8));
collection.add(
null,
metadataList,
Collections.singletonList(document),
Collections.singletonList(id));
} catch (ApiException e) {
log.error("wrong to add chroma document, collection : " + collectionName + ",", e);
throw new RuntimeException("wrong to add chroma document, collection : " + collectionName);
}
}
/**
* 获取集合
*
* @param collectionName 集合
* @return 集合
*/
public Collection getCollection(String collectionName) {
if (collectionName == null || collectionName.isEmpty()) {
throw new RuntimeException("function getCollection param collectionName is required");
}
Collection collection = null;
try {
Map<String, String> metadata = new LinkedTreeMap<>();
metadata.put("hnsw:space", "cosine");
metadata.put("embedding_function", embeddingFunction.getClass().getName());
collection = client.createCollection(
collectionName,
// 自定义向量空间的距离计算方法
metadata,
true,
this.embeddingFunction);
} catch (ApiException e) {
log.error("wrong to get chroma collection : " + collectionName + ",", e);
throw new RuntimeException("wrong to get chroma collection : " + collectionName);
}
return collection;
}
}
存储
package com.jenson;
import tech.amikos.chromadb.handler.ApiException;
import java.util.HashMap;
/**
* @author Jenson
* @version 1.0
* @date 2024/8/12 上午10:38
*/
public class Main {
public static void main(String[] args) throws ApiException {
ChromaClient chromaClient = new ChromaClient(
"http://localhost:8100",
new ZhiPuAIEmbeddingFunction("zhipu_apikey", "embedding-2")
);
chromaClient.save("test",
// 文章内容
"《红楼梦》书叙西方灵河岸上三生石畔的绛珠仙子,为了酬报神瑛侍者的灌溉之恩,要将毕生的泪水偿还,就随其下凡历劫。宝玉为神瑛侍者转世,林黛玉为绛珠仙子转世,这段姻缘称为“木石前盟”。远古女娲炼石补天遗下的顽石,通灵性,为贾宝玉出世时所衔的“通灵宝玉”,“通灵宝玉”历尽世间辛酸悲欢的故事,就是《石头记》,亦即《红楼梦》。\n《红楼梦》故事纷纭复杂,其较大的事件有:黛玉丧母,进京依附外祖母;宝玉母姨及其子薛蟠、女薛宝钗进驻贾府;宝玉在秦可卿卧房午觉,梦游太虚幻境,看了”金陵十二钗正册”;王熙凤毒设相思局,致贾瑞命归黄泉;秦可卿病亡,公公哭得如泪人一般;贾元春加封贤德妃,获准省亲,元春题名别院为“大观园”;宝、黛二人于沁芳闸共赏 《会真记》,宝玉以张生、莺莺喻己喻人,黛玉感极生嗔;王夫人怒逐金钏,金钏不堪受辱,投井身亡;宝玉事发,贾政痛笞宝玉;探春发起组织海棠诗社,此时邢岫烟、李纹、薛宝琴等同时入驻贾府,彼等均具诗才,大观园比前更加热闹;刘姥姥携外孙板儿进荣府,深得贾母欢心;紫鹃戏说黛玉将回苏州,宝玉呆症大发;贾琏垂涎尤氏姐妹,偷娶尤二姐;尤二姐为凤姐所害,误服虎狼药,吞金自尽;尤三姐殉情饮剑身亡;贾赦欲讨鸳鸯为妾,鸳鸯哭诉贾母,贾母申斥贾赦夫妇;王夫人、凤姐夜抄大观园,司棋、晴雯被撵;晴雯病亡,宝玉心痛如绞,作《芙蓉女儿诔》以祭;迎春嫁了“中山狼”孙绍祖,受尽凌辱而死;薛蟠吃酒打死酒店当槽被擒拿;夏金桂误饮毒药汤,自取灭亡;元妃薨逝,通灵宝玉丢失,宝玉丧魂失魄;凤姐奇设调包计,黛玉闻知宝玉娶了宝钗,魂归离恨天,宝玉于潇湘馆痛祭黛玉,紫鹃细诉黛玉临终情景;薛宝琴史湘云相继出嫁;锦衣军奉旨查抄贾府;贾母逝世,鸳鸯上吊身亡;凤姐病重,临终托刘姥姥照看巧姐;宝玉魂魄随和尚重游太虚幻境,见到众多已离人世的姐妹;宝玉、贾兰叔侄赴考,出考场,宝玉旋即迷失;贾政途遇宝玉与一僧一道飘然而去,圣上赐宝玉“文妙真人”道号;袭人嫁与蒋玉菡;贾雨村和甄士隐执手叙旧,言荣宁二府,将会兰桂齐芳,家道复初;僧道携宝玉到青埂峰下,仍将玉放在女娲补天之处,各自云游。",
// 人物(实体)列表
new HashMap<>() {{
put("names", "贾琏,夏金桂,秦可卿,蒋玉菡,贾雨村,和尚,王夫人,迎春,宝玉,刘姥姥,王熙凤,公公,尤二姐,甄士隐,金钏,贤德妃,探春,鸳鸯,贾府,外祖母,贾政,宝钗,薛宝琴,通灵宝玉,自取灭亡,贾宝玉,司棋,绛珠仙子,贾元春,邢岫烟,黛玉,史湘云,贾兰,孙绍祖,李纹,凤姐,板儿,贾瑞,酒店当槽,薛宝钗,薛蟠,紫鹃,晴雯,贾赦,林黛玉,袭人,锦衣军,神瑛侍者");
}});
}
}
查询
package com.jenson;
import tech.amikos.chromadb.Collection;
import tech.amikos.chromadb.handler.ApiException;
import tech.amikos.chromadb.model.QueryEmbedding;
import java.util.ArrayList;
import java.util.Collections;
/**
* @author Jenson
* @version 1.0
* @date 2024/8/12 上午10:38
*/
public class Main {
public static void main(String[] args) throws ApiException {
ChromaClient chromaClient = new ChromaClient(
"http://localhost:8100",
new ZhiPuAIEmbeddingFunction("zhipu_apikey", "embedding-2")
);
Collection.QueryResponse queryResponse = chromaClient.getCollection("test").query(
// 用户查询的文本
Collections.singletonList("梦游太虚幻境"),
10,
null,
null,
new ArrayList<>() {{
// include EMBEDDINGS 会报错,Expected a double but was BEGIN_ARRAY at line 1 column 300 path $.embeddings[0][0]
// add(QueryEmbedding.IncludeEnum.EMBEDDINGS);
add(QueryEmbedding.IncludeEnum.METADATAS);
add(QueryEmbedding.IncludeEnum.DOCUMENTS);
// 数字越小距离越近,关系越紧密
add(QueryEmbedding.IncludeEnum.DISTANCES);
}}
);
System.out.println(queryResponse);
}
}
图数据库
图数据库使用neo4j为例
安装
安装社区版,Neo4j Deployment Center - Graph Database & Analytics
我安装的是window版的,解压后执行 bin
下的启动文件
> neo4j console
日志中会包含以下两条
2024-08-12 08:14:30.383+0000 INFO Bolt enabled on localhost:7687.
2024-08-12 08:14:30.880+0000 INFO HTTP enabled on localhost:7474.
数据库链接端口为 7687
web端口为 7474
默认账号密码为 neo4j / neo4j
web端初次登录会被要求修改密码
Java 连接
使用java连接,安装驱动依赖
<dependency>
<groupId>org.neo4j.driver</groupId>
<artifactId>neo4j-java-driver</artifactId>
<version>5.23.0</version>
</dependency>
创建人物(实体) - 关系
若要节点不重复,可以创建唯一性约束(以某个属性为唯一)
CREATE CONSTRAINT 约束名称 FOR (n:Lable) REQUIRE n.属性 IS UNIQUE
例:
CREATE CONSTRAINT FOR (n:Person) REQUIRE n.nameIS UNIQUE
使用了拼接CQL语句的方式
使用MERGE来建立关系,可以避免创建重复关系
package com.jenson;
import org.neo4j.driver.*;
import org.neo4j.driver.Record;
import org.neo4j.driver.exceptions.ClientException;
import java.util.*;
/**
* @author Jenson
* @version 1.0
* @date 2024/8/2 上午10:36
*/
public class Main {
public static void main(String[] args) {
// 连接URI,用户名和密码
String uri = "bolt://127.0.0.1:7687";
String user = "neo4j";
String password = "Aa123456";
String entityLabel = "Person";
String relateLabel = "RELATION";
// 创建驱动实例
Driver driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password));
String s = "[绛珠仙子,神瑛侍者,恩人];[宝玉,神瑛侍者,转世];[林黛玉,绛珠仙子,转世];[宝玉,林黛玉,姻缘];[贾宝玉,通灵宝玉,拥有者];[黛玉,外祖母,依附];[薛蟠,薛宝钗,兄妹];[宝玉,秦可卿,梦中相遇];[王熙凤,贾瑞,致死];[秦可卿,公公,亲情];[贾元春,贤德妃,身份];[宝玉,黛玉,共赏];[王夫人,金钏,主仆];[宝玉,贾政,父子];[探春,邢岫烟,同住];[探春,李纹,同住];[探春,薛宝琴,同住];[刘姥姥,板儿,祖孙];[紫鹃,黛玉,主仆];[贾琏,尤二姐,婚姻];[凤姐,尤二姐,致死];[贾赦,鸳鸯,求娶];[王夫人,凤姐,夜抄];[司棋,晴雯,被撵];[宝玉,晴雯,祭奠];[迎春,孙绍祖,婚姻];[薛蟠,酒店当槽,致死];[夏金桂,自取灭亡,结果];[宝玉,通灵宝玉,丢失];[宝玉,宝钗,婚姻];[黛玉,宝玉,得知娶宝钗];[宝玉,紫鹃,询问];[薛宝琴,史湘云,出嫁];[锦衣军,贾府,查抄];[凤姐,刘姥姥,托付];[宝玉,和尚,重游];[宝玉,贾兰,叔侄];[贾政,宝玉,途遇];[袭人,蒋玉菡,婚姻];[贾雨村,甄士隐,叙旧]";
Set<String> set = new HashSet<>();
String[] split = s.split(";");
Map<String, Map<String, String>> map = new HashMap<>();
for (String string : split) {
String s0 = string;
s0 = s0.replace("[", "").replace("]", "");
String[] s1 = s0.split(",");
String a = s1[0];
String b = s1[1];
String r = s1[2];
set.add(a);
set.add(b);
Map<String, String> map2 = map.computeIfAbsent(a, k -> new HashMap<>());
map2.put(b, r);
}
// 创建会话
try (Session session = driver.session()) {
// 创建节点
for (String v : set) {
// 查询是否存在
Result result = session.run("MATCH (n:" + entityLabel + "{name:'" + v + "'}) return n");
if (result.hasNext()) {
// 已存在节点,跳过
continue;
}
try {
session.run("CREATE (:" + entityLabel + "{name:'" + v + "'})");
} catch (ClientException e) {
System.out.println(e.getMessage());
throw e;
}
}
// 创建关系
List<String> createRelList = new ArrayList<>();
map.forEach((a, v) ->
v.forEach((b, r) ->
createRelList.add("Match (a:" + entityLabel + "{name:'" + a + "'}),(b:" + entityLabel + "{name:'" + b + "'}) MERGE (a)-[r:" + relateLabel + "{name:'" + r + "'}]->(b)")));
for (String createRel : createRelList) {
session.run(createRel);
}
}
// 关闭驱动
driver.close();
}
}
查询
- 查询全部
match(n:Person) return *
- 查询指定人物的n层关系
match(a:Person)-[r1:RELATION*1..2]->(x) where a.name in ['林黛玉','宋江'] return a,r1,x