Java 程序员怎么做知识图谱?用 Apache Jena 从 0 写一个 RDF + SPARQL 项目

在这里插入图片描述

很多 Java 程序员第一次接触知识图谱,第一反应通常是:

这东西是不是必须先学 Neo4j?

其实不是。

知识图谱真正应该先理解的是:

RDF
Triple
URI
SPARQL
Ontology

而如果你本身就是 Java 程序员,有一个非常值得学习的开源项目:

Apache Jena

GitHub:

https://github.com/apache/jena

Apache Jena 是一个 Java 生态下非常完整的语义 Web 和知识图谱框架。

你可以用它:

创建 RDF
读取 RDF
修改 RDF
查询 RDF
执行 SPARQL
操作 Ontology
执行 RDFS / OWL 推理
保存知识图谱
启动 SPARQL Server

如果用我们熟悉的 Java 后端思维理解:

JDBC
负责操作关系数据库

MyBatis
负责操作 SQL 数据

Apache Jena
负责操作 RDF / Knowledge Graph

今天我们不一上来讲各种晦涩的本体理论。

直接写代码。

最终做一个:

员工技能知识图谱

能够查询:

张三是什么员工?

张三会什么技术?

哪些员工会 Go?

哪些人属于研发部?

哪些员工同时会 Go 和 Redis?

一、先理解 RDF:其实就是三个东西

传统数据库里,一条数据可能是:

用户表

id = 1
name = 张三
age = 28

RDF 不太一样。

RDF 最核心的结构叫:

Triple

也就是:

Subject
Predicate
Object

翻译一下:

主语
关系
宾语

比如:

张三
会
Go

就是一个三元组:

张三 → 会 → Go

再比如:

张三 → 属于 → 研发部

再比如:

张三 → 类型 → BackendEngineer

这样大量三元组连接起来:

           Go
           ↑
           │ 掌握
           │
研发部 ← 张三 → BackendEngineer
  ↑
  │
属于

慢慢就形成了一个:

Graph。

所以知识图谱本质上可以先简单理解成:

大量实体和关系组成的一张图。


二、创建一个 Jena 项目

当前使用:

Java 21+
Apache Jena 6.2.0
Maven

新建:

jena-demo

目录:

jena-demo
├── pom.xml
└── src
    └── main
        └── java
            └── com
                └── javapub
                    └── JenaDemo.java

pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="
         http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.javapub</groupId>
    <artifactId>jena-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <project.build.sourceEncoding>
            UTF-8
        </project.build.sourceEncoding>

        <jena.version>6.2.0</jena.version>
    </properties>

    <dependencies>

        <!-- RDF + SPARQL -->
        <dependency>
            <groupId>org.apache.jena</groupId>
            <artifactId>jena-arq</artifactId>
            <version>${jena.version}</version>
        </dependency>

        <!-- 本地持久化三元组数据库 -->
        <dependency>
            <groupId>org.apache.jena</groupId>
            <artifactId>jena-tdb2</artifactId>
            <version>${jena.version}</version>
        </dependency>

    </dependencies>

</project>

然后:

mvn clean package

依赖正常下载,就可以开始了。


三、第一段 Jena 代码:创建一个 RDF Model

Jena 中非常重要的一个概念叫:

Model

可以先把它理解成:

一张 RDF 图。

创建:

import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.ModelFactory;

public class JenaDemo {

    public static void main(String[] args) {

        Model model =
                ModelFactory.createDefaultModel();

        System.out.println(
                "RDF 三元组数量:" + model.size()
        );
    }
}

运行:

RDF 三元组数量:0

因为现在还是一个空图。

可以理解:

Model

≈

一张空白知识图谱

四、创建第一个实体:张三

RDF 里的实体一般使用:

URI

进行唯一标识。

例如我们定义自己的命名空间:

String NS =
        "https://example.com/company/";

然后:

Resource zhangsan =
        model.createResource(
                NS + "employee/zhangsan"
        );

完整:

import org.apache.jena.rdf.model.*;

public class JenaDemo {

    public static void main(String[] args) {

        Model model =
                ModelFactory.createDefaultModel();

        String NS =
                "https://example.com/company/";

        Resource zhangsan =
                model.createResource(
                        NS + "employee/zhangsan"
                );

        System.out.println(
                zhangsan.getURI()
        );
    }
}

输出:

https://example.com/company/employee/zhangsan

这里:

zhangsan

就是一个:

Resource

也就是 RDF 中的资源。


五、给张三增加姓名

现在定义:

name

属性。

Property name =
        model.createProperty(
                NS + "property/name"
        );

然后:

zhangsan.addProperty(
        name,
        "张三"
);

完整:

Model model =
        ModelFactory.createDefaultModel();

String NS =
        "https://example.com/company/";

Resource zhangsan =
        model.createResource(
                NS + "employee/zhangsan"
        );

Property name =
        model.createProperty(
                NS + "property/name"
        );

zhangsan.addProperty(
        name,
        "张三"
);

现在知识图谱里已经出现了一个三元组:

张三实体
   ↓
name
   ↓
"张三"

对应:

Subject

https://example.com/company/employee/zhangsan
Predicate

https://example.com/company/property/name
Object

"张三"

这就是 RDF。


六、Object 不一定是字符串

很多小白第一次接触 RDF,会以为:

Object

就是数据库字段值。

其实不是。

Object 可以是:

字符串
数字
日期
另外一个 Resource

比如:

张三
属于
研发部

这里:

研发部

不是字符串。

它也应该是一个实体。

先创建:

Resource rdDepartment =
        model.createResource(
                NS + "department/rd"
        );

创建关系:

Property belongsTo =
        model.createProperty(
                NS + "property/belongsTo"
        );

然后:

zhangsan.addProperty(
        belongsTo,
        rdDepartment
);

现在:

张三
  │
  │ belongsTo
  ↓
研发部

变成了:

两个节点之间的关系。

这就是知识图谱开始真正像“图”的地方。


七、再增加一个 Skill

创建:

Resource go =
        model.createResource(
                NS + "skill/go"
        );

创建:

Property hasSkill =
        model.createProperty(
                NS + "property/hasSkill"
        );

添加:

zhangsan.addProperty(
        hasSkill,
        go
);

现在:

张三
   ├── belongsTo → 研发部
   │
   └── hasSkill → Go

八、再增加 Redis、Docker

非常简单:

Resource redis =
        model.createResource(
                NS + "skill/redis"
        );

Resource docker =
        model.createResource(
                NS + "skill/docker"
        );

然后:

zhangsan
        .addProperty(hasSkill, go)
        .addProperty(hasSkill, redis)
        .addProperty(hasSkill, docker);

注意。

RDF 一个属性:

hasSkill

完全可以对应多个值。

于是:

张三
├── hasSkill → Go
├── hasSkill → Redis
└── hasSkill → Docker

这和数据库:

employee_skill

中间表的思路有点类似。

但 RDF 天然就是图结构。


九、增加年龄:使用 Literal

现在增加:

age

属性。

Property age =
        model.createProperty(
                NS + "property/age"
        );

不要写:

zhangsan.addProperty(
        age,
        "28"
);

因为这样:

28

其实是字符串。

更加规范:

zhangsan.addLiteral(
        age,
        28
);

现在:

age → 28

会成为带数据类型的 Literal。


十、什么是 Resource、Property、Literal?

到这里可以暂停一下。

Jena 最基础的几个对象:

Model
Resource
Property
Literal
Statement

可以这么理解。

Model

整个 RDF 图。

Model model =
        ModelFactory.createDefaultModel();

Resource

实体。

例如:

张三
研发部
Go
Redis

Java:

Resource zhangsan =
        model.createResource(
                NS + "employee/zhangsan"
        );

Property

关系 / 属性。

例如:

name
age
hasSkill
belongsTo

Java:

Property hasSkill =
        model.createProperty(
                NS + "property/hasSkill"
        );

Literal

普通值。

例如:

"张三"
28
true
2026-09-23

Statement

真正的一条:

Subject
Predicate
Object

也就是一个三元组。


十一、手工创建一个 Statement

比如:

张三
hasSkill
Go

可以:

Statement statement =
        model.createStatement(
                zhangsan,
                hasSkill,
                go
        );

然后:

model.add(statement);

完整:

Statement statement =
        model.createStatement(
                zhangsan,
                hasSkill,
                go
        );

model.add(statement);

其实:

zhangsan.addProperty(
        hasSkill,
        go
);

本质上也是在创建三元组。


十二、遍历整个知识图谱

现在我们已经有了一堆 Triple。

怎么查看?

StmtIterator iterator =
        model.listStatements();

while (iterator.hasNext()) {

    Statement stmt =
            iterator.nextStatement();

    System.out.println(
            stmt.getSubject()
    );

    System.out.println(
            stmt.getPredicate()
    );

    System.out.println(
            stmt.getObject()
    );

    System.out.println(
            "----------------"
    );
}

你会看到:

Subject
Predicate
Object

一条一条输出。

这也是理解 RDF 最好的方式之一。


十三、把 RDF 输出成 Turtle

RDF 可以有很多序列化格式。

比如:

RDF/XML
Turtle
N-Triples
JSON-LD

我比较推荐小白先看:

Turtle

因为最好读。

Jena 可以这样输出:

import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.riot.RDFFormat;

RDFDataMgr.write(
        System.out,
        model,
        RDFFormat.TURTLE_PRETTY
);

输出类似:

<https://example.com/company/employee/zhangsan>
        <https://example.com/company/property/name>
                "张三" ;

        <https://example.com/company/property/age>
                28 ;

        <https://example.com/company/property/belongsTo>
                <https://example.com/company/department/rd> ;

        <https://example.com/company/property/hasSkill>
                <https://example.com/company/skill/go> ,
                <https://example.com/company/skill/redis> ,
                <https://example.com/company/skill/docker> .

突然是不是清楚很多了?


十四、设置 Prefix,让 RDF 更好看

现在 URI 太长。

可以:

model.setNsPrefix(
        "company",
        NS
);

或者:

model.setNsPrefix(
        "emp",
        NS + "employee/"
);

model.setNsPrefix(
        "skill",
        NS + "skill/"
);

model.setNsPrefix(
        "prop",
        NS + "property/"
);

输出时就会更容易阅读。


十五、把知识图谱保存成文件

现在我们的数据只在:

内存

中。

程序退出就没了。

可以写文件:

import java.io.FileOutputStream;

try (
        FileOutputStream out =
                new FileOutputStream(
                        "company.ttl"
                )
) {

    RDFDataMgr.write(
            out,
            model,
            RDFFormat.TURTLE_PRETTY
    );
}

现在目录里出现:

company.ttl

这个文件就是一个:

RDF Knowledge Graph。


十六、读取 Turtle 文件

下次启动程序:

Model model =
        ModelFactory.createDefaultModel();

RDFDataMgr.read(
        model,
        "company.ttl"
);

然后:

System.out.println(
        model.size()
);

就能看到 Triple 数量。


十七、终于进入重点:SPARQL

如果 RDF 是:

知识图谱的数据模型

那么:

SPARQL 就像知识图谱里的 SQL。

数据库:

SELECT *
FROM employee;

知识图谱:

SELECT ?employee
WHERE {
    ?employee ?p ?o .
}

你会发现:

SQL
查询 Table

SPARQL
查询 Graph

十八、查询所有员工姓名

假设我们有:

张三
name
"张三"

SPARQL:

PREFIX prop:
<https://example.com/company/property/>

SELECT ?employee ?name
WHERE {

    ?employee
        prop:name
        ?name .
}

Java:

String queryString = """
    PREFIX prop:
    <https://example.com/company/property/>

    SELECT ?employee ?name
    WHERE {

        ?employee
            prop:name
            ?name .
    }
    """;

执行:

import org.apache.jena.query.*;

try (
        QueryExecution qexec =
                QueryExecution.create(
                        queryString,
                        model
                )
) {

    ResultSet resultSet =
            qexec.execSelect();

    while (resultSet.hasNext()) {

        QuerySolution solution =
                resultSet.next();

        System.out.println(
                solution.get("employee")
        );

        System.out.println(
                solution.get("name")
        );
    }
}

这已经是一个完整 SPARQL 查询了。


十九、为什么 ?employee 前面有问号?

SPARQL 里的:

?employee
?name
?skill

都是:

变量。

比如:

?employee prop:name ?name .

意思:

帮我找出所有满足“某个员工有某个名字”的数据。

类似 SQL:

SELECT id, name
FROM employee;

二十、查询所有会 Go 的员工

这就开始有知识图谱味道了。

PREFIX prop:
<https://example.com/company/property/>

PREFIX skill:
<https://example.com/company/skill/>

SELECT ?employee
WHERE {

    ?employee
        prop:hasSkill
        skill:go .
}

Java:

String query = """
    PREFIX prop:
    <https://example.com/company/property/>

    PREFIX skill:
    <https://example.com/company/skill/>

    SELECT ?employee
    WHERE {

        ?employee
            prop:hasSkill
            skill:go .
    }
    """;

try (
        QueryExecution qexec =
                QueryExecution.create(
                        query,
                        model
                )
) {

    ResultSet rs =
            qexec.execSelect();

    while (rs.hasNext()) {

        QuerySolution row =
                rs.next();

        System.out.println(
                row.getResource("employee")
        );
    }
}

二十一、同时查询姓名

现在结果只是 URI。

我们希望:

张三

一起出来。

SPARQL:

PREFIX prop:
<https://example.com/company/property/>

PREFIX skill:
<https://example.com/company/skill/>

SELECT ?employee ?name
WHERE {

    ?employee
        prop:name
        ?name .

    ?employee
        prop:hasSkill
        skill:go .
}

注意这里出现了:

两个 Triple Pattern
?employee → name → ?name

以及:

?employee → hasSkill → Go

共同的:

?employee

把两个关系串了起来。

这就是 SPARQL 查询图结构的核心。


二十二、再创建一个员工李四

现在添加:

Resource lisi =
        model.createResource(
                NS + "employee/lisi"
        );

lisi
        .addProperty(
                name,
                "李四"
        )
        .addLiteral(
                age,
                26
        )
        .addProperty(
                belongsTo,
                rdDepartment
        )
        .addProperty(
                hasSkill,
                model.createResource(
                        NS + "skill/java"
                )
        )
        .addProperty(
                hasSkill,
                redis
        );

现在:

张三
├── Go
├── Redis
└── Docker

李四
├── Java
└── Redis

二十三、查询会 Redis 的所有员工

PREFIX prop:
<https://example.com/company/property/>

PREFIX skill:
<https://example.com/company/skill/>

SELECT ?name
WHERE {

    ?employee
        prop:name
        ?name ;

        prop:hasSkill
        skill:redis .
}

结果:

张三
李四

二十四、查询同时会 Go 和 Redis 的人

这是非常典型的图查询。

PREFIX prop:
<https://example.com/company/property/>

PREFIX skill:
<https://example.com/company/skill/>

SELECT ?name
WHERE {

    ?employee
        prop:name
        ?name .

    ?employee
        prop:hasSkill
        skill:go .

    ?employee
        prop:hasSkill
        skill:redis .
}

结果:

张三

换句话说,我们正在问:

谁
同时存在:

hasSkill → Go

和

hasSkill → Redis

这就是图查询非常自然的地方。


二十五、查询某个部门所有员工

例如:

研发部

SPARQL:

PREFIX prop:
<https://example.com/company/property/>

PREFIX department:
<https://example.com/company/department/>

SELECT ?employee ?name
WHERE {

    ?employee
        prop:name
        ?name ;

        prop:belongsTo
        department:rd .
}

二十六、增加 FILTER

我们想查询:

年龄 > 27

的员工。

PREFIX prop:
<https://example.com/company/property/>

SELECT ?name ?age
WHERE {

    ?employee
        prop:name
        ?name ;

        prop:age
        ?age .

    FILTER(?age > 27)
}

结果可能:

张三 28

和 SQL:

SELECT name, age
FROM employee
WHERE age > 27;

是不是非常像?


二十七、模糊搜索姓名

SPARQL 也可以:

FILTER(
    CONTAINS(
        STR(?name),
        "张"
    )
)

完整:

PREFIX prop:
<https://example.com/company/property/>

SELECT ?employee ?name
WHERE {

    ?employee
        prop:name
        ?name .

    FILTER(
        CONTAINS(
            STR(?name),
            "张"
        )
    )
}

二十八、COUNT 统计

统计员工人数:

PREFIX prop:
<https://example.com/company/property/>

SELECT (
    COUNT(?employee)
    AS ?count
)
WHERE {

    ?employee
        prop:name
        ?name .
}

Java:

try (
        QueryExecution qexec =
                QueryExecution.create(
                        query,
                        model
                )
) {

    ResultSet rs =
            qexec.execSelect();

    if (rs.hasNext()) {

        QuerySolution row =
                rs.next();

        long count =
                row.getLiteral(
                        "count"
                ).getLong();

        System.out.println(
                "员工人数:" + count
        );
    }
}

二十九、GROUP BY:统计每项技能有多少员工

例如:

Go      1
Java    1
Redis   2
Docker  1

SPARQL:

PREFIX prop:
<https://example.com/company/property/>

SELECT ?skill (
    COUNT(?employee)
    AS ?count
)
WHERE {

    ?employee
        prop:hasSkill
        ?skill .
}
GROUP BY ?skill
ORDER BY DESC(?count)

这和 SQL:

GROUP BY

也非常相似。


三十、ASK:判断某个关系存不存在

有时候不想 SELECT。

只想问:

张三会不会 Go?

可以:

PREFIX prop:
<https://example.com/company/property/>

PREFIX skill:
<https://example.com/company/skill/>

ASK {

    <https://example.com/company/employee/zhangsan>
        prop:hasSkill
        skill:go .
}

Java:

try (
        QueryExecution qexec =
                QueryExecution.create(
                        askQuery,
                        model
                )
) {

    boolean result =
            qexec.execAsk();

    System.out.println(
            "张三会 Go:" + result
    );
}

输出:

张三会 Go:true

三十一、CONSTRUCT:查询以后生成新的 RDF

这就非常有意思了。

比如:

把所有会 Go 的员工提取成一个新的知识图谱。

PREFIX prop:
<https://example.com/company/property/>

PREFIX skill:
<https://example.com/company/skill/>

CONSTRUCT {

    ?employee
        prop:hasSkill
        skill:go .

}
WHERE {

    ?employee
        prop:hasSkill
        skill:go .
}

Java:

try (
        QueryExecution qexec =
                QueryExecution.create(
                        constructQuery,
                        model
                )
) {

    Model resultModel =
            qexec.execConstruct();

    RDFDataMgr.write(
            System.out,
            resultModel,
            RDFFormat.TURTLE_PRETTY
    );
}

也就是说:

SELECT
返回表格结果

CONSTRUCT
返回一个新的 RDF Graph

三十二、SPARQL UPDATE:新增数据

SPARQL 不只是查询。

也能修改。

例如:

PREFIX prop:
<https://example.com/company/property/>

PREFIX skill:
<https://example.com/company/skill/>

INSERT DATA {

    <https://example.com/company/employee/lisi>
        prop:hasSkill
        skill:docker .
}

Java:

import org.apache.jena.update.UpdateAction;

String update = """
    PREFIX prop:
    <https://example.com/company/property/>

    PREFIX skill:
    <https://example.com/company/skill/>

    INSERT DATA {

        <https://example.com/company/employee/lisi>
            prop:hasSkill
            skill:docker .
    }
    """;

UpdateAction.parseExecute(
        update,
        model
);

现在:

李四

也会 Docker 了。


三十三、DELETE:删除一个关系

PREFIX prop:
<https://example.com/company/property/>

PREFIX skill:
<https://example.com/company/skill/>

DELETE DATA {

    <https://example.com/company/employee/lisi>
        prop:hasSkill
        skill:docker .
}

Java:

UpdateAction.parseExecute(
        deleteQuery,
        model
);

你会发现:

SPARQL 已经越来越像 SQL 了。


三十四、内存 Model 有一个问题

现在:

ModelFactory.createDefaultModel();

创建的是内存模型。

程序关闭:

数据没了。

真实项目当然不能这样。

Apache Jena 还有:

TDB2

它是 Jena 自己的持久化 RDF 数据库。

可以把它理解成:

专门保存 Triple 的本地数据库。

三十五、创建一个 TDB2 数据库

代码:

import org.apache.jena.query.Dataset;
import org.apache.jena.tdb2.TDB2Factory;

Dataset dataset =
        TDB2Factory.connectDataset(
                "./data/tdb"
        );

第一次运行会创建:

data/
└── tdb/

以后数据就保存在磁盘。


三十六、往 TDB2 写数据

Jena 的 Dataset 支持事务。

写操作:

dataset.begin(
        ReadWrite.WRITE
);

try {

    Model model =
            dataset.getDefaultModel();

    Resource employee =
            model.createResource(
                    NS + "employee/wangwu"
            );

    employee.addProperty(
            name,
            "王五"
    );

    dataset.commit();

} finally {

    dataset.end();
}

这种方式已经非常像数据库事务了。


三十七、更推荐用 Txn

Jena 提供:

Txn

让代码更简单。

import org.apache.jena.system.Txn;

Txn.executeWrite(
        dataset,
        () -> {

            Model model =
                    dataset.getDefaultModel();

            Resource wangwu =
                    model.createResource(
                            NS + "employee/wangwu"
                    );

            wangwu
                    .addProperty(
                            name,
                            "王五"
                    )
                    .addProperty(
                            hasSkill,
                            go
                    );
        }
);

读取:

Txn.executeRead(
        dataset,
        () -> {

            Model model =
                    dataset.getDefaultModel();

            System.out.println(
                    model.size()
            );
        }
);

真实项目一定要有:

事务意识。

三十八、从文件批量导入 RDF

比如:

company.ttl

已经有大量数据。

可以:

Txn.executeWrite(
        dataset,
        () -> {

            RDFDataMgr.read(
                    dataset,
                    "company.ttl"
            );
        }
);

这样数据就进入 TDB2。


三十九、从 TDB2 执行 SPARQL

非常简单。

Txn.executeRead(
        dataset,
        () -> {

            String query = """
                PREFIX prop:
                <https://example.com/company/property/>

                SELECT ?name
                WHERE {

                    ?employee
                        prop:name
                        ?name .
                }
                """;

            try (
                    QueryExecution qexec =
                            QueryExecution.create(
                                    query,
                                    dataset
                            )
            ) {

                ResultSet rs =
                        qexec.execSelect();

                ResultSetFormatter.out(rs);
            }
        }
);

这时候:

TDB2

就有点类似:

MySQL

而:

SPARQL

类似:

SQL

四十、那 Fuseki 又是什么?

如果 TDB2 只是:

本地 Java 程序里的知识图谱数据库

那么:

Fuseki

可以把知识图谱通过 HTTP 暴露出来。

例如:

Java App
Python App
AI Agent
前端
其他服务器

都可以请求:

SPARQL Endpoint

架构:

        Java
         │
Python ─ Fuseki ─ AI Agent
         │
       TDB2
         │
      RDF Graph

Apache Jena 官方也将 Fuseki 定位为可通过 HTTP 暴露 RDF 数据和 SPARQL Endpoint 的服务器组件。


四十一、甚至可以把 Fuseki 嵌入 Java

概念代码:

Dataset dataset =
        DatasetFactory.createTxnMem();

FusekiServer server =
        FusekiServer.create()
                .add(
                        "/knowledge",
                        dataset
                )
                .port(3030)
                .build();

server.start();

然后你的数据集就可以作为:

/knowledge

服务提供出去。


四十二、Jena 还能做 Ontology

到现在为止,我们主要用的是:

RDF

但 Jena 还可以操作:

RDFS
OWL
Ontology

例如:

import org.apache.jena.ontology.*;

OntModel ontModel =
        ModelFactory.createOntologyModel(
                OntModelSpec.OWL_MEM
        );

创建:

Employee

Class:

OntClass employeeClass =
        ontModel.createClass(
                NS + "Employee"
        );

再创建:

BackendEngineer
OntClass backendClass =
        ontModel.createClass(
                NS + "BackendEngineer"
        );

然后:

backendClass.addSuperClass(
        employeeClass
);

现在:

BackendEngineer
      ↓
subClassOf
      ↓
Employee

四十三、创建 ObjectProperty

例如:

hasSkill
ObjectProperty hasSkillProperty =
        ontModel.createObjectProperty(
                NS + "hasSkill"
        );

设置 Domain:

hasSkillProperty.addDomain(
        employeeClass
);

设置 Range:

OntClass skillClass =
        ontModel.createClass(
                NS + "Skill"
        );

hasSkillProperty.addRange(
        skillClass
);

得到:

Employee
    │
    │ hasSkill
    ↓
Skill

这就是 Ontology 建模。


四十四、创建 DatatypeProperty

例如:

员工姓名
DatatypeProperty employeeName =
        ontModel.createDatatypeProperty(
                NS + "employeeName"
        );

Domain:

employeeName.addDomain(
        employeeClass
);

Range 可以指定:

string

这已经和我们上一篇 Protégé 讲的:

Class
Object Property
Data Property
Individual

完全对应起来了。

不同点只是:

Protégé
图形界面建模

而:

Apache Jena
Java 代码建模

四十五、Jena 还能做推理

这是知识图谱开始真正有意思的地方。

假设:

BackendEngineer
subClassOf
Employee

现在我们只告诉系统:

张三
a
BackendEngineer

理论上系统可以推理:

张三
也是
Employee

这就是:

Inference

Jena 自带:

RDFS Reasoner
OWL Reasoner
Rule Reasoner

等推理能力。


四十六、一个简单的 RDFS 推理例子

先建立基础 Model:

Model base =
        ModelFactory.createDefaultModel();

定义:

Resource employee =
        base.createResource(
                NS + "Employee"
        );

Resource backend =
        base.createResource(
                NS + "BackendEngineer"
        );

告诉系统:

BackendEngineer
是
Employee
的子类
base.add(
        backend,
        RDFS.subClassOf,
        employee
);

创建:

Resource zhangsan =
        base.createResource(
                NS + "zhangsan"
        );

告诉:

张三
是
BackendEngineer
base.add(
        zhangsan,
        RDF.type,
        backend
);

然后:

InfModel infModel =
        ModelFactory.createRDFSModel(
                base
        );

现在判断:

boolean result =
        infModel.contains(
                zhangsan,
                RDF.type,
                employee
        );

System.out.println(result);

结果:

true

但注意:

我们从来没有直接写:

张三
a
Employee

这是 Jena:

推理出来的。


四十七、这就是 Ontology 和普通数据库最大的差异之一

数据库里:

张三
职位:
BackendEngineer

如果你想知道:

张三是不是 Employee?

通常需要程序自己判断。

而 Ontology:

BackendEngineer
subClassOf
Employee

再结合:

张三
type
BackendEngineer

Reasoner 可以自动推出:

张三
type
Employee

整个过程:

事实
+
规则
↓
Reasoner
↓
新的事实

四十八、一个稍完整的 Employee Knowledge Graph

现在我们把这些代码组合一下。

package com.javapub;

import org.apache.jena.query.*;
import org.apache.jena.rdf.model.*;
import org.apache.jena.riot.RDFDataMgr;
import org.apache.jena.riot.RDFFormat;

public class EmployeeKnowledgeGraph {

    private static final String NS =
            "https://example.com/company/";

    public static void main(
            String[] args
    ) {

        Model model =
                ModelFactory.createDefaultModel();

        model.setNsPrefix(
                "company",
                NS
        );

        Property name =
                model.createProperty(
                        NS + "property/name"
                );

        Property age =
                model.createProperty(
                        NS + "property/age"
                );

        Property hasSkill =
                model.createProperty(
                        NS + "property/hasSkill"
                );

        Property belongsTo =
                model.createProperty(
                        NS + "property/belongsTo"
                );


        // ---------- 部门 ----------

        Resource rd =
                model.createResource(
                        NS + "department/rd"
                );


        // ---------- 技能 ----------

        Resource go =
                model.createResource(
                        NS + "skill/go"
                );

        Resource java =
                model.createResource(
                        NS + "skill/java"
                );

        Resource redis =
                model.createResource(
                        NS + "skill/redis"
                );

        Resource docker =
                model.createResource(
                        NS + "skill/docker"
                );


        // ---------- 张三 ----------

        Resource zhangsan =
                model.createResource(
                        NS + "employee/zhangsan"
                );

        zhangsan
                .addProperty(
                        name,
                        "张三"
                )
                .addLiteral(
                        age,
                        28
                )
                .addProperty(
                        belongsTo,
                        rd
                )
                .addProperty(
                        hasSkill,
                        go
                )
                .addProperty(
                        hasSkill,
                        redis
                )
                .addProperty(
                        hasSkill,
                        docker
                );


        // ---------- 李四 ----------

        Resource lisi =
                model.createResource(
                        NS + "employee/lisi"
                );

        lisi
                .addProperty(
                        name,
                        "李四"
                )
                .addLiteral(
                        age,
                        26
                )
                .addProperty(
                        belongsTo,
                        rd
                )
                .addProperty(
                        hasSkill,
                        java
                )
                .addProperty(
                        hasSkill,
                        redis
                );


        // ---------- 输出 RDF ----------

        RDFDataMgr.write(
                System.out,
                model,
                RDFFormat.TURTLE_PRETTY
        );


        // ---------- SPARQL ----------

        String query = """
            PREFIX prop:
            <https://example.com/company/property/>

            PREFIX skill:
            <https://example.com/company/skill/>

            SELECT ?name
            WHERE {

                ?employee
                    prop:name
                    ?name .

                ?employee
                    prop:hasSkill
                    skill:redis .
            }
            """;

        try (
                QueryExecution qexec =
                        QueryExecution.create(
                                query,
                                model
                        )
        ) {

            ResultSet resultSet =
                    qexec.execSelect();

            System.out.println(
                    "\n会 Redis 的员工:"
            );

            while (
                    resultSet.hasNext()
            ) {

                QuerySolution row =
                        resultSet.next();

                System.out.println(
                        row.getLiteral(
                                "name"
                        ).getString()
                );
            }
        }
    }
}

运行结果:

会 Redis 的员工:

张三
李四

到这里,其实我们已经自己实现了一个:

最小知识图谱应用。


四十九、如果换成传统数据库会怎么设计?

MySQL 可能要:

employee

department

skill

employee_skill

其中:

employee

和:

skill

需要:

employee_skill

中间表。

查询:

SELECT e.name
FROM employee e
JOIN employee_skill es
    ON e.id = es.employee_id
JOIN skill s
    ON es.skill_id = s.id
WHERE s.name = 'Redis';

而 RDF:

SELECT ?name
WHERE {

    ?employee
        prop:name
        ?name ;

        prop:hasSkill
        skill:redis .
}

不是说:

知识图谱一定比数据库好。

而是:

当你的业务核心开始变成“大量复杂关系”的时候,图模型往往会更加自然。


五十、知识图谱适合什么场景?

比如:

企业知识管理

可以:

员工
→ 参与
→ 项目

项目
→ 使用
→ 技术

员工
→ 掌握
→ 技术

员工
→ 属于
→ 部门

例如:

软件资产管理
服务
→ 依赖
→ Redis

服务
→ 调用
→ 用户服务

服务
→ 部署在
→ Kubernetes

例如:

AI Agent
用户
→ 喜欢
→ Go

用户
→ 创建
→ ShiyuAdmin

ShiyuAdmin
→ 使用
→ GORM

ShiyuAdmin
→ 使用
→ Redis

Agent 就可以沿着关系寻找上下文。


例如:

GraphRAG

传统 RAG:

问题
↓
向量搜索
↓
相似文本

GraphRAG:

问题
↓
实体
↓
关系
↓
相关实体
↓
相关文档
↓
LLM

这里:

Apache Jena

就可以成为底层知识图谱处理工具之一。


五十一、Jena、Neo4j、Protégé 到底什么关系?

可以这么理解。

Protégé

偏:

Ontology 建模工具

适合:

画 Class
Property
Individual
OWL

Apache Jena

偏:

Java 知识图谱开发框架

适合:

Java + RDF
Java + SPARQL
Java + OWL
Java + Reasoner
Java + TDB

Neo4j

偏:

Property Graph Database

使用:

Cypher

更加偏工程图数据库。


Jena TDB2

则是:

RDF Triple Store

更加靠近:

RDF
Semantic Web
SPARQL
OWL

这一套标准。


五十二、我建议 Java 程序员这样学

不要一上来研究:

Description Logic
OWL DL
SHACL
SWRL

先按这个顺序:

第一步
Triple

↓

第二步
Resource / Property / Literal

↓

第三步
Model

↓

第四步
Turtle

↓

第五步
SPARQL SELECT

↓

第六步
FILTER / GROUP BY / ASK

↓

第七步
TDB2

↓

第八步
Fuseki

↓

第九步
Ontology

↓

第十步
Reasoner

把这个流程走一遍以后,再看:

Ontology
Knowledge Graph
GraphRAG

你会发现一下子清楚很多。


最后总结

Apache Jena 最适合 Java 程序员的一点就是:

它没有要求你突然离开熟悉的 Java 世界。

我们依然是在写:

Model
Resource
Property
Dataset
QueryExecution

然后慢慢把思维从:

Table
Row
Column

切换到:

Entity
Relation
Triple
Graph

从:

SQL

切换到:

SPARQL

最终:

Java
+
Apache Jena
+
RDF
+
SPARQL
+
TDB2
+
Fuseki
+
Ontology
+
Reasoner

就组成了一套比较完整的 Java 知识图谱技术栈。

如果只记住一句话:

Apache Jena,就是 Java 程序员进入 RDF、SPARQL 和知识图谱世界的一扇门。

项目地址:

https://github.com/apache/jena

而如果你已经会:

Java
Spring Boot
MySQL

那么学 Jena 时真正需要改变的并不是编程语言。

而是:

从“表结构思维”,慢慢切换到“实体 + 关系 + 图”的思维。

posted @ 2026-09-23 13:22  JavaPub  阅读(13)  评论(0)    收藏  举报