MySQL 数据不迁移,SPARQL 为什么还能查?深入 Ontop 的 R2RML Mapping 与 SPARQL→SQL

前面介绍 Ontop 时,我们说过一句很有意思的话:

不用把 MySQL 数据搬到图数据库,也可以把它“变成”知识图谱。

第一次看到这里,很多人会有疑问:

数据明明还在 MySQL

↓

为什么可以使用 SPARQL 查询?

↓

RDF 三元组到底存在哪里?

↓

SPARQL 最后是谁执行的?

答案就是 Ontop 最核心的一项技术:
在这里插入图片描述

Mapping。

更具体一点:

R2RML Mapping

它负责把:

关系数据库

中的:

表
字段
主键
外键

映射成知识图谱里的:

实体
IRI
Class
Property
Triple

最终 Ontop 再把:

SPARQL

改写成:

SQL

发送给真正的 MySQL 执行。

整个过程:

SPARQL
   ↓
Ontology / Mapping
   ↓
Ontop Query Rewriting
   ↓
SQL
   ↓
MySQL
   ↓
ResultSet
   ↓
RDF / SPARQL Result

这篇我们不泛讲 Ontop。

就深入拆一个东西:

R2RML 到底是怎么把 MySQL 表“变成”知识图谱的?

项目:

https://github.com/ontop/ontop

一、先准备一个普通得不能再普通的 MySQL

假设我们现在有一个公司后台。

三张表:

employee
department
skill

外加一张员工技能关系表:

employee_skill

数据库:

CREATE DATABASE company;

USE company;

部门表:

CREATE TABLE department (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

员工表:

CREATE TABLE employee (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    age INT,
    department_id BIGINT,

    CONSTRAINT fk_employee_department
        FOREIGN KEY (department_id)
        REFERENCES department(id)
);

技能表:

CREATE TABLE skill (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

员工技能关系:

CREATE TABLE employee_skill (
    employee_id BIGINT NOT NULL,
    skill_id BIGINT NOT NULL,

    PRIMARY KEY (
        employee_id,
        skill_id
    ),

    FOREIGN KEY (employee_id)
        REFERENCES employee(id),

    FOREIGN KEY (skill_id)
        REFERENCES skill(id)
);

加入一些数据:

INSERT INTO department
(id, name)
VALUES
(1, '研发部'),
(2, '产品部');

员工:

INSERT INTO employee
(id, name, age, department_id)
VALUES
(1001, '张三', 28, 1),
(1002, '李四', 26, 1),
(1003, '王五', 30, 2);

技能:

INSERT INTO skill
(id, name)
VALUES
(1, 'Go'),
(2, 'Java'),
(3, 'Redis'),
(4, 'Docker');

关系:

INSERT INTO employee_skill
(employee_id, skill_id)
VALUES
(1001, 1),
(1001, 3),
(1001, 4),

(1002, 2),
(1002, 3);

这就是一个非常普通的业务数据库。


二、如果不用知识图谱,我们怎么查询?

查询:

张三属于哪个部门?

SQL:

SELECT
    e.name,
    d.name AS department_name
FROM employee e
JOIN department d
    ON e.department_id = d.id
WHERE e.name = '张三';

查询:

哪些员工会 Redis?

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';

查询:

哪些研发部员工同时会 Go?

SELECT
    e.name
FROM employee e
JOIN department d
    ON e.department_id = d.id
JOIN employee_skill es
    ON e.id = es.employee_id
JOIN skill s
    ON es.skill_id = s.id
WHERE
    d.name = '研发部'
    AND s.name = 'Go';

完全没问题。

但现在如果我们希望对外暴露一个统一的:

Knowledge Graph

让用户不关心:

employee
department
employee_skill
skill

这些表。

而只看到:

Employee
Department
Skill

Employee belongsToDepartment Department

Employee hasSkill Skill

怎么办?

这就是 Ontop。


三、第一步不是搬数据,而是定义“虚拟世界”

我们希望数据库里的:

employee.id = 1001

在知识图谱里变成:

http://example.com/employee/1001

希望:

employee.name

变成:

ex:name

希望:

department

变成:

ex:Department

希望:

employee.department_id

代表:

ex:belongsToDepartment

所以最终我们想看到的是:

employee 表
        ↓
     Mapping
        ↓

<employee/1001>
    a ex:Employee ;
    ex:name "张三" ;
    ex:age 28 ;
    ex:belongsToDepartment
        <department/1> .

注意:

这些 RDF 不一定真的被写入另一个数据库。

这就是“Virtual Knowledge Graph”的关键。

它们可以只是:

通过 Mapping 规则虚拟出来的 RDF 视图。


四、R2RML 最核心的概念:TriplesMap

R2RML 中非常重要的一个东西叫:

rr:TriplesMap

它干什么?

一句话:

规定一张关系表中的每一行,应该怎么生成 RDF Triple。

你可以把它理解成一个:

Row → RDF

转换器。

最典型结构:

TriplesMap
│
├── logicalTable
│
├── subjectMap
│
└── predicateObjectMap

翻译成人话:

从哪张表取数据?

↓

这一行数据对应哪个 RDF 实体?

↓

这一行有哪些字段要变成 RDF 属性?

五、先映射 employee 表

创建:

mapping.ttl

先定义 Prefix:

@prefix rr:
    <http://www.w3.org/ns/r2rml#> .

@prefix rdf:
    <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .

@prefix xsd:
    <http://www.w3.org/2001/XMLSchema#> .

@prefix ex:
    <http://example.com/> .

然后:

<#EmployeeMapping>
    a rr:TriplesMap ;

    rr:logicalTable [
        rr:tableName "employee"
    ] ;

这里最重要:

rr:logicalTable

表示:

这个 Mapping 从哪里取数据?

现在答案是:

employee

也就是:

SELECT *
FROM employee;

可以把它理解成 Mapping 的:

数据源。


六、subjectMap:数据库的一行到底是谁?

现在有一行:

id = 1001
name = 张三
age = 28
department_id = 1

知识图谱不能只叫:

1001

最好给它一个全局 IRI:

http://example.com/employee/1001

所以:

rr:subjectMap [
    rr:template
        "http://example.com/employee/{id}" ;

    rr:class ex:Employee
] ;

这里:

rr:template

特别重要。

它相当于:

"http://example.com/employee/"
+
id

如果:

id = 1001

生成:

http://example.com/employee/1001

如果:

id = 1002

生成:

http://example.com/employee/1002

七、rr:class 做了什么?

这里:

rr:class ex:Employee

表示这个 Subject:

是 Employee 类型。

于是:

employee.id = 1001

会虚拟生成:

<http://example.com/employee/1001>
    rdf:type ex:Employee .

换句话说:

数据库行

开始变成:

知识图谱实体

了。


八、把 name 字段映射成 ex:name

继续:

rr:predicateObjectMap [

    rr:predicate ex:name ;

    rr:objectMap [
        rr:column "name"
    ]
] ;

什么意思?

非常简单。

Predicate
=
ex:name

Object 来自:

employee.name

所以数据库:

id      name

1001    张三

变成:

<employee/1001>
    ex:name
    "张三"

九、把 age 也映射出来

rr:predicateObjectMap [

    rr:predicate ex:age ;

    rr:objectMap [
        rr:column "age" ;
        rr:datatype xsd:integer
    ]
] ;

生成:

<http://example.com/employee/1001>
    ex:age
    28 .

现在一个数据库 Row:

1001
张三
28
1

已经可以虚拟成:

<employee/1001>
    a ex:Employee ;

    ex:name
        "张三" ;

    ex:age
        28 .

十、完整 Employee Mapping

组合起来:

@prefix rr:
    <http://www.w3.org/ns/r2rml#> .

@prefix ex:
    <http://example.com/> .

@prefix xsd:
    <http://www.w3.org/2001/XMLSchema#> .


<#EmployeeMapping>
    a rr:TriplesMap ;

    rr:logicalTable [
        rr:tableName "employee"
    ] ;

    rr:subjectMap [
        rr:template
            "http://example.com/employee/{id}" ;

        rr:class
            ex:Employee
    ] ;

    rr:predicateObjectMap [

        rr:predicate
            ex:name ;

        rr:objectMap [
            rr:column "name"
        ]
    ] ;

    rr:predicateObjectMap [

        rr:predicate
            ex:age ;

        rr:objectMap [
            rr:column "age" ;

            rr:datatype
                xsd:integer
        ]
    ] .

到这里先记住三个核心:

rr:logicalTable
=
从哪取

rr:subjectMap
=
这一行是谁

rr:predicateObjectMap
=
这一行有哪些属性

R2RML 最基础的结构其实就这么简单。


十一、再映射 Department

数据库:

department

id
name

Mapping:

<#DepartmentMapping>
    a rr:TriplesMap ;

    rr:logicalTable [
        rr:tableName "department"
    ] ;

    rr:subjectMap [
        rr:template
            "http://example.com/department/{id}" ;

        rr:class
            ex:Department
    ] ;

    rr:predicateObjectMap [

        rr:predicate
            ex:name ;

        rr:objectMap [
            rr:column "name"
        ]
    ] .

于是:

department

1
研发部

虚拟为:

<http://example.com/department/1>
    a ex:Department ;

    ex:name
        "研发部" .

十二、最关键的问题来了:两个表怎么连接?

现在我们已经有:

Employee

以及:

Department

但是:

张三

和:

研发部

还没有关系。

数据库是通过:

employee.department_id

连接:

department.id

也就是:

employee.department_id
=
department.id

在 RDF 里希望得到:

张三
↓
belongsToDepartment
↓
研发部

怎么映射?


十三、最简单的写法:直接使用 URI Template

因为:

employee.department_id

已经有 Department ID。

所以可以直接:

rr:predicateObjectMap [

    rr:predicate
        ex:belongsToDepartment ;

    rr:objectMap [
        rr:template
            "http://example.com/department/{department_id}" ;

        rr:termType
            rr:IRI
    ]
] ;

最终:

department_id = 1

生成:

http://example.com/department/1

于是:

<http://example.com/employee/1001>

    ex:belongsToDepartment

<http://example.com/department/1> .

图就连接起来了。


十四、这一步非常重要

数据库里原来是:

employee.department_id = 1

只是一个:

数字外键

Mapping 以后变成:

Employee
    ↓
belongsToDepartment
    ↓
Department

也就是说:

Mapping 真正完成的是“关系数据库语义化”。

不是简单把:

Column A

改个名字叫:

Property A

而是把:

ID
外键
关系表

转化成了:

实体
关系
语义

十五、R2RML 还有一种正式的跨表关联方式

除了直接使用:

URI Template

还可以:

rr:parentTriplesMap

配合:

rr:joinCondition

例如:

rr:predicateObjectMap [

    rr:predicate
        ex:belongsToDepartment ;

    rr:objectMap [

        rr:parentTriplesMap
            <#DepartmentMapping> ;

        rr:joinCondition [
            rr:child
                "department_id" ;

            rr:parent
                "id"
        ]
    ]
] ;

翻译一下。

当前:

EmployeeMapping

是 Child。

Department:

DepartmentMapping

是 Parent。

Join:

employee.department_id
=
department.id

也就是:

JOIN department d
ON employee.department_id = d.id

十六、为什么 parentTriplesMap 很重要?

因为有时候对象的 URI 生成规则很复杂。

你不希望在每个地方复制:

http://example.com/department/{id}

而是直接告诉 Ontop:

Object 请使用 DepartmentMapping 的 Subject。

于是:

DepartmentMapping

负责定义:

Department 的 IRI 怎么生成

而:

EmployeeMapping

只负责:

怎么关联过去

这种结构更加规范。


十七、再映射 Skill

技能表:

skill

id
name

Mapping:

<#SkillMapping>
    a rr:TriplesMap ;

    rr:logicalTable [
        rr:tableName "skill"
    ] ;

    rr:subjectMap [
        rr:template
            "http://example.com/skill/{id}" ;

        rr:class
            ex:Skill
    ] ;

    rr:predicateObjectMap [

        rr:predicate
            ex:name ;

        rr:objectMap [
            rr:column "name"
        ]
    ] .

现在:

skill.id = 1
name = Go

虚拟生成:

<http://example.com/skill/1>
    a ex:Skill ;

    ex:name
        "Go" .

十八、多对多关系怎么做?

这个更有意思。

数据库:

employee_skill

里面:

employee_id
skill_id

例如:

1001   1

表示:

张三会 Go

那这个关系表如何变成:

Employee
↓
hasSkill
↓
Skill

?

我们创建:

<#EmployeeSkillMapping>
    a rr:TriplesMap ;

    rr:logicalTable [
        rr:tableName
            "employee_skill"
    ] ;

Subject:

rr:subjectMap [
    rr:template
        "http://example.com/employee/{employee_id}"
] ;

然后 Object:

rr:predicateObjectMap [

    rr:predicate
        ex:hasSkill ;

    rr:objectMap [
        rr:template
            "http://example.com/skill/{skill_id}" ;

        rr:termType
            rr:IRI
    ]
] .

就这么简单。


十九、关系表甚至可以“消失”

注意。

数据库里存在:

employee_skill

表。

但是知识图谱里我们不一定需要:

EmployeeSkill

这个实体。

它可以直接转化成:

Employee
     |
     | hasSkill
     ↓
Skill

比如:

1001 1

转:

<employee/1001>
    ex:hasSkill
    <skill/1> .

这就是一个很值得理解的点:

数据库 Schema 和 Knowledge Graph Schema 不需要长得一样。

你不是在机械复制数据库。

而是在做:

Semantic Mapping。


二十、现在数据库已经“变成”了一张图

原来的:

employee
department
skill
employee_skill

变成:

              Go
              ↑
              │
           hasSkill
              │
研发部 ← 张三 ─────→ Redis
   ↑          │
   │          └────→ Docker
   │
belongsToDepartment

但是再次强调:

RDF 数据并没有真的搬走。

原始数据依然是:

MySQL

Ontop 只是在上面构建了一个:

Virtual RDF Graph

二十一、现在开始用 SPARQL 查

比如查询:

所有 Employee。

PREFIX ex:
<http://example.com/>

SELECT ?employee
WHERE {

    ?employee
        a
        ex:Employee .
}

用户看到:

SPARQL

但 Ontop 并不会去某个:

RDF 数据库

查。

它会看 Mapping。


二十二、Ontop 会发现什么?

SPARQL:

?employee
    a
    ex:Employee .

Mapping 告诉 Ontop:

ex:Employee

来自:

EmployeeMapping

而 EmployeeMapping 又告诉它:

logicalTable
=
employee

于是它可以推导:

要回答这个问题

↓

查询 employee 表即可

所以最终 SQL 大致相当于:

SELECT id
FROM employee;

然后:

id = 1001

再根据:

rr:template
"http://example.com/employee/{id}"

组装:

http://example.com/employee/1001

返回 SPARQL 客户端。


二十三、这个过程就是 SPARQL → SQL

整个链路:

SPARQL

?employee a ex:Employee

↓

查 Mapping

ex:Employee
来自 employee 表

↓

生成 SQL

SELECT id
FROM employee

↓

MySQL

1001
1002
1003

↓

套 URI Template

employee/1001
employee/1002
employee/1003

↓

SPARQL Result

这就是 Ontop 的核心魔法。

其实没有魔法。

就是:

Query Rewriting。


二十四、查询姓名会发生什么?

SPARQL:

PREFIX ex:
<http://example.com/>

SELECT ?employee ?name
WHERE {

    ?employee
        a ex:Employee ;

        ex:name
        ?name .
}

Ontop 从 Mapping 得知:

Employee
→ employee 表

ex:name
→ name 字段

大致对应 SQL:

SELECT
    id,
    name
FROM employee;

然后转换成:

employee/1001
张三

employee/1002
李四

employee/1003
王五

二十五、FILTER 怎么处理?

SPARQL:

PREFIX ex:
<http://example.com/>

SELECT ?name ?age
WHERE {

    ?employee
        a ex:Employee ;

        ex:name
            ?name ;

        ex:age
            ?age .

    FILTER(?age >= 28)
}

如果 Ontop 能够下推这个条件,那么最终 SQL 大致就是:

SELECT
    name,
    age
FROM employee
WHERE age >= 28;

这非常重要。

不是:

SELECT 全表
↓
Java 内存
↓
再判断 age

而是尽量把工作交给:

数据库。

这也是 Virtual Knowledge Graph 能用于真实数据库的重要基础。


二十六、跨表 SPARQL 怎么办?

现在查询:

每个员工属于哪个部门?

PREFIX ex:
<http://example.com/>

SELECT
    ?employeeName
    ?departmentName
WHERE {

    ?employee
        a ex:Employee ;

        ex:name
            ?employeeName ;

        ex:belongsToDepartment
            ?department .

    ?department
        ex:name
            ?departmentName .
}

SPARQL 看不到:

department_id

更看不到:

JOIN

但是 Mapping 知道。

于是 Ontop 可以把图模式:

Employee
↓
belongsToDepartment
↓
Department

还原成数据库关系:

employee.department_id
=
department.id

大致生成:

SELECT
    e.name,
    d.name
FROM employee e
JOIN department d
    ON e.department_id = d.id;

这就是非常关键的一层:

SPARQL 查询的是语义,Mapping 负责寻找数据库实现。


二十七、多对多查询更明显

查询:

哪些员工会 Redis?

SPARQL:

PREFIX ex:
<http://example.com/>

SELECT ?employeeName
WHERE {

    ?employee
        a ex:Employee ;

        ex:name
            ?employeeName ;

        ex:hasSkill
            ?skill .

    ?skill
        ex:name
            "Redis" .
}

用户完全不用知道:

employee_skill

表存在。

但是 Ontop Mapping 知道:

Employee
↓
employee_skill
↓
Skill

所以实际 SQL 大致:

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';

这就是 VKG 一个很大的价值:

把数据库物理结构藏起来,让上层只面对领域语义。


二十八、换数据库 Schema,上层 SPARQL甚至可以不改

这个才是真正有意思的地方。

假设旧系统:

employee
department

新系统表改成:

sys_user
sys_dept

字段也变了:

employee.id

变成:

sys_user.user_code

如果直接依赖 SQL:

所有查询都可能需要改。

但如果上层始终查询:

?employee
    ex:belongsToDepartment
    ?department .

理论上我们只需要调整:

Mapping。

上层 SPARQL 的领域模型可以继续保持。

这就是:

数据库 Schema

与:

Knowledge Model

之间增加了一层隔离。


二十九、logicalTable 不一定是一张表

这是 R2RML 非常实用的一点。

我们前面写:

rr:logicalTable [
    rr:tableName "employee"
] ;

但:

logicalTable

也可以来自:

SQL Query。

例如数据库数据比较复杂。

我们先写:

SELECT
    e.id,
    e.name,
    e.age,
    d.name AS department_name

FROM employee e

JOIN department d
    ON e.department_id = d.id;

可以直接放进 R2RML:

rr:logicalTable [

    rr:sqlQuery """

        SELECT
            e.id,
            e.name,
            e.age,
            d.name AS department_name

        FROM employee e

        JOIN department d
            ON e.department_id = d.id

    """
] ;

这叫:

R2RML View

三十、为什么 SQL Query Mapping 很有用?

比如数据库里:

first_name
last_name

但 Ontology 希望:

fullName

你可以直接:

SELECT
    id,

    CONCAT(
        first_name,
        ' ',
        last_name
    ) AS full_name

FROM employee;

然后 Mapping:

rr:predicateObjectMap [

    rr:predicate
        ex:fullName ;

    rr:objectMap [
        rr:column
            "full_name"
    ]
] ;

所以 Mapping 不只是:

字段改名器

它还可以使用 SQL:

JOIN
CASE WHEN
CONCAT
COUNT
GROUP BY

构造更适合知识模型的数据。


三十一、比如计算“部门人数”

数据库里根本没有:

staffCount

字段。

我们可以:

SELECT
    d.id,
    d.name,
    COUNT(e.id) AS staff_count

FROM department d

LEFT JOIN employee e
    ON e.department_id = d.id

GROUP BY
    d.id,
    d.name;

R2RML:

<#DepartmentStatsMapping>
    a rr:TriplesMap ;

    rr:logicalTable [
        rr:sqlQuery """

            SELECT
                d.id,
                d.name,
                COUNT(e.id)
                    AS staff_count

            FROM department d

            LEFT JOIN employee e
                ON e.department_id = d.id

            GROUP BY
                d.id,
                d.name

        """
    ] ;

    rr:subjectMap [
        rr:template
            "http://example.com/department/{id}" ;
    ] ;

    rr:predicateObjectMap [

        rr:predicate
            ex:staffCount ;

        rr:objectMap [
            rr:column
                "staff_count" ;

            rr:datatype
                xsd:integer
        ]
    ] .

于是知识图谱里可以出现:

研发部
↓
staffCount
↓
2

虽然数据库原表里根本没有这个字段。


三十二、所以“虚拟”不是“简单”

很多人听到:

Virtual Knowledge Graph

会觉得:

是不是只是给 SQL 包一层 SPARQL?

其实 Mapping 可以做很多事情:

表
↓
Class

主键
↓
IRI

字段
↓
Literal Property

外键
↓
Object Property

关系表
↓
Graph Edge

SQL View
↓
虚拟属性 / 虚拟实体

这已经是一层:

Semantic Abstraction。


三十三、Ontop 还支持自己的 .obda 格式

除了标准:

R2RML .ttl

Ontop 还有一个更简洁的:

.obda

格式。

同样的员工映射可以写成类似:

[PrefixDeclaration]

ex:
    http://example.com/


[MappingDeclaration] @collection [[


mappingId
    EmployeeMapping

target
    <http://example.com/employee/{id}>
        a ex:Employee ;
        ex:name {name} ;
        ex:age {age} .

source
    SELECT
        id,
        name,
        age
    FROM employee


]]

这个格式很好理解。

核心就三个东西:

mappingId

target

source

三十四、source 和 target 是什么?

source:

SELECT
    id,
    name,
    age
FROM employee

表示:

数据从哪里来。

target:

<employee/{id}>
    a Employee ;
    name {name} ;
    age {age}

表示:

这些 SQL 结果在 RDF 世界里长什么样。

所以整个 Mapping 可以直接理解:

SQL Result
     ↓
Mapping
     ↓
RDF Template

三十五、R2RML 和 OBDA 怎么选?

如果你希望:

标准化
跨工具
兼容 W3C

优先:

R2RML

如果:

主要就是使用 Ontop
希望文件容易阅读和手写

.obda 通常更加直观。

Ontop CLI 也提供:

R2RML
↔
OBDA

转换能力。

所以两个都可以学。

但如果要真正理解:

关系数据库是怎么映射成 RDF 的

我还是建议先理解:

R2RML

三十六、实际跑起来:MySQL 配置

例如:

company.properties

写:

jdbc.url=jdbc:mysql://localhost:3306/company?useCursorFetch=true

jdbc.user=root

jdbc.password=123456

jdbc.driver=com.mysql.cj.jdbc.Driver

然后需要准备对应:

MySQL JDBC Driver

给 Ontop 使用。

对于 MySQL,大数据查询时尤其值得注意:

useCursorFetch=true

否则 JDBC 可能先把整个结果集取回客户端,再继续处理。


三十七、启动 Ontop Endpoint

假设目录:

input/
├── mapping.ttl
└── company.properties

使用 Ontop CLI:

ontop endpoint \
  --mapping=input/mapping.ttl \
  --properties=input/company.properties

如果你还有:

ontology.ttl

可以再增加:

ontop endpoint \
  --ontology=input/ontology.ttl \
  --mapping=input/mapping.ttl \
  --properties=input/company.properties

默认:

8080

启动。

SPARQL Endpoint:

/sparql

三十八、查询接口已经和 MySQL 解耦了

现在其他程序只需要:

SPARQL Endpoint

不需要:

MySQL Username
MySQL Password
表结构
JOIN 关系

例如 Python:

import requests


query = """
PREFIX ex:
<http://example.com/>

SELECT ?name
WHERE {

    ?employee
        a ex:Employee ;

        ex:name ?name .
}
"""


response = requests.get(
    "http://localhost:8080/sparql",
    params = {
        "query": query
    },
    headers = {
        "Accept":
            "application/sparql-results+json"
    }
)


print(
    response.json()
)

三十九、再封装一个 Python 查询函数

import requests


ENDPOINT = (
    "http://localhost:8080/sparql"
)


def query_sparql(
    sparql: str
):

    response = requests.get(
        ENDPOINT,
        params = {
            "query": sparql
        },
        headers = {
            "Accept":
                "application/sparql-results+json"
        },
        timeout = 30
    )

    response.raise_for_status()

    return response.json()

查询会 Go 的员工:

sparql = """
PREFIX ex:
<http://example.com/>

SELECT ?name
WHERE {

    ?employee
        ex:name ?name ;

        ex:hasSkill
            ?skill .

    ?skill
        ex:name
            "Go" .
}
"""


result = query_sparql(
    sparql
)


for row in (
    result["results"]["bindings"]
):

    print(
        row["name"]["value"]
    )

结果:

张三

但 Python 从头到尾:

没写一行 SQL。


四十、怎么知道 Ontop 到底生成了什么 SQL?

这个问题特别适合学习 Ontop。

Ontop Endpoint 有开发模式。

启动时可以启用:

Development Mode

在这个模式下,它提供:

/ontop/reformulate

用于查看:

SPARQL 被改写成了什么 SQL。

例如请求:

SPARQL:

SELECT ?name
WHERE {
    ?e ex:name ?name .
}

你可以直接看到 Ontop reformulate 后的 SQL。

这对于学习:

SPARQL
↓
Mapping
↓
SQL

整个过程特别有价值。


四十一、这也是排查性能问题最重要的入口之一

如果你写了一条:

看起来很简单的 SPARQL

结果执行:

5 秒

不要第一时间怪:

SPARQL 太慢。

应该看:

Ontop 最终生成了什么 SQL?

比如:

有没有多余 JOIN?

有没有全表扫描?

WHERE 有没有下推?

索引有没有使用?

JOIN 字段有没有索引?

Mapping 的 URI Template 是否合理?

所以真正优化 Ontop,最后很多时候还是回到了:

SQL 优化。


四十二、主键为什么对 Ontop 很重要?

假设:

employee.id

是:

PRIMARY KEY

那么 Ontop 可以明确知道:

每个 id
只对应一名 Employee

这类数据库约束能够帮助查询优化。

同样:

FOREIGN KEY

可以提供:

表之间关系信息。

如果数据库约束完整,Ontop 在一些查询改写和优化场景里可以利用这些信息。

所以不要以为做了知识图谱之后:

数据库设计不重要了。

恰恰相反。


四十三、IRI Template 也非常重要

例如:

Employee

使用:

http://example.com/employee/{id}

Department:

http://example.com/department/{id}

Skill:

http://example.com/skill/{id}

这种模式非常清楚。

千万不要不同 Mapping 到处:

一会 employee/{id}

一会 users/{id}

一会 person/{id}

表示同一个东西。

否则会产生:

IRI 不一致

的问题。

在知识图谱里:

IRI 就是实体身份。

这有点像数据库里的:

Primary Key

但它是:

全局语义标识

四十四、NULL 怎么处理?

数据库很容易出现:

age = NULL

如果:

rr:objectMap

对应字段是 NULL,

通常不会生成对应 RDF Triple。

比如:

张三
name 张三
age NULL

知识图谱可能只有:

<employee/1001>
    ex:name
        "张三" .

没有:

ex:age

这和数据库:

age = NULL

的表达方式并不完全一样。

知识图谱更倾向:

没有这个事实

四十五、这也引出了一个很重要的问题

很多 Java/MySQL 程序员进入 RDF 世界以后,会下意识问:

这个属性为什么不能为空?

但 RDF 本身遵循:

Open World Assumption

缺少:

age

更多表示:

目前不知道年龄

而不一定:

年龄不存在

如果你需要严格校验:

Employee 必须有 age

那就会进入我们之前写过的:

SHACL。

所以整条技术链突然连起来了:

MySQL
↓
Ontop
↓
R2RML
↓
Virtual RDF
↓
Ontology
↓
SHACL
↓
SPARQL

四十六、这几个项目其实已经能串成一套体系

之前我们写:

Protégé

负责:

设计 Ontology

Owlready2:

Python 操作 Ontology

Apache Jena:

Java 操作 RDF / SPARQL

pySHACL:

校验 RDF 数据

而 Ontop:

把已有关系数据库接进这个语义世界。

整个架构:

                    Protégé
                       ↓
                   Ontology
                       ↓

MySQL ── R2RML ── Ontop ── SPARQL
                       │
                       ↓
                 Knowledge View
                       │
             ┌─────────┴─────────┐
             ↓                   ↓
        Apache Jena           Python
             ↓                   ↓
          Java App            AI Agent

四十七、Ontop 最适合什么场景?

我认为最适合的一类就是:

企业已经有大量关系数据库,但又想做知识图谱。

比如公司已经有:

MySQL
PostgreSQL
Oracle
SQL Server

跑了十年。

里面:

100 张表
500 张表
1000 张表

你不可能说:

我们开始做知识图谱了,把所有数据迁移到图数据库。

风险和成本都非常高。

Ontop 的思路则是:

数据库继续跑

在上面加:

Mapping
+
Ontology

形成:

Virtual Knowledge Graph

业务系统基本不用大改。


四十八、它对 GraphRAG 也很有意思

传统企业 RAG:

PDF
Word
Markdown
↓
Chunk
↓
Embedding
↓
Vector DB

但是企业真正有价值的数据很多其实在:

MySQL
ERP
CRM
订单库
客户库
资产库
研发系统

如果通过 Ontop:

MySQL
↓
R2RML
↓
Virtual Knowledge Graph
↓
SPARQL
↓
实体关系

然后再给:

Agent / GraphRAG

使用。

Agent 就可以问:

张三属于哪个部门?

他掌握哪些技术?

哪些项目需要 Go?

谁最适合加入这个项目?

而底层数据:

仍然来自正在运行的业务数据库。


四十九、最后把整个 R2RML 思路记成一张图

数据库:

employee

id
name
age
department_id

↓

rr:logicalTable

决定:

数据从哪里来

↓

rr:subjectMap

决定:

这行数据是谁

↓

rr:template

生成:

http://example.com/employee/1001

↓

rr:predicateObjectMap

决定:

name → ex:name

age → ex:age

department_id
→ ex:belongsToDepartment

↓

最终虚拟为:

Employee
│
├── name → 张三
├── age → 28
└── belongsToDepartment
        ↓
      研发部

五十、最后总结

Ontop 最值得理解的,并不是:

它支持 SPARQL。

真正核心的是:

它在关系数据库和语义知识模型之间,建立了一层 Mapping。

原来的数据库:

Table
Column
Primary Key
Foreign Key
JOIN

经过:

R2RML

被重新解释成:

Class
Property
IRI
Relation
Triple

而用户发出的:

SPARQL

又通过这套 Mapping:

SPARQL
↓
语义匹配
↓
Mapping
↓
Query Rewriting
↓
SQL
↓
Database

最终交给 MySQL 真正执行。

所以:

Ontop 并不是把 MySQL 变成图数据库。

更加准确的说法是:

Ontop 在关系数据库上构建了一层虚拟的 RDF / Knowledge Graph 视图。

数据继续待在:

MySQL

查询继续主要由:

数据库引擎

执行。

但上层看到的已经不再是:

employee.department_id

而是:

Employee
belongsToDepartment
Department

这就是从:

数据结构

走向:

数据语义

最关键的一步。

项目:

https://github.com/ontop/ontop
posted @ 2026-09-24 15:32  JavaPub  阅读(6)  评论(0)    收藏  举报