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 时真正需要改变的并不是编程语言。
而是:

浙公网安备 33010602011771号