知识图谱实现思路(向量数据库+图数据库+AI大模型)

存储

以构建文章内容涉及人物间关系为例(此处人物即实体)

如下图,先使用AI来提取实体(人物)-关系,将实体-关系存入图数据库,再将文档向量化后关联实体存入向量数据库

image.png

查询

如下图所示,用户输入一段查询文本,将其向量化后选取关联最紧密的文章相关联的实体,再利用此实体来查询图数据库中的实体关系。
(ps:理论上这种方法要查询的精确,需要将文章切分为足够小且完整的段落,否则一个文章关联实体过多就没有意义了。若文章文本巨大,可尝试先利用AI进行文本切分再提取实体关系,向量化存储;或者要求AI对每个实体写一个简介,向量化此简介;又或是将实体向量化后直接存储)

image.png

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"
}'
image.png

实体关系提取

例如下图,通过提问让AI提取实体-关系,代码层面自然是使用AI产品提供的API实现。
(ps: 市面上有许多专注于 实体识别(NER)、关系抽取(RE)、事件抽取(EE)的产品,例如 KnowLM/README_ZH.md at main · zjunlp/KnowLM · GitHub

image.png

向量数据

以 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

image.png

我安装的是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 *

image.png
  • 查询指定人物的n层关系

match(a:Person)-[r1:RELATION*1..2]->(x) where a.name in ['林黛玉','宋江'] return a,r1,x

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容