SPARQL 到底是怎么查知识图谱的?用 Apache Jena 写透 SELECT、FILTER、OPTIONAL 与多跳查询

前面我们已经用 Apache Jena 创建过这样的知识图谱:

张三
├── 属于 → 研发部
├── 掌握 → Go
├── 掌握 → Redis
└── 参与 → 支付系统

支付系统
├── 使用数据库 → PostgreSQL
└── 部署在 → Kubernetes

李四
└── 维护 → PostgreSQL

在这里插入图片描述

数据有了以后,下一个问题自然就是:

到底怎么查?

如果是 MySQL,我们会写:

SELECT *
FROM employee
WHERE name = '张三';

知识图谱则通常使用:

SPARQL

很多 Java 程序员第一次看到 SPARQL,会觉得:

这不就是换了一套语法的 SQL 吗?

其实两者有很大区别。

SQL 的核心思维通常是:

表
↓
JOIN
↓
字段
↓
WHERE

而 SPARQL 的核心是:

图模式匹配。

你不是先问:

我要查哪张表?

而是在描述:

我要找一种什么样的关系结构?

比如:

某个人
↓
掌握
↓
Go

SPARQL 就负责在整个 RDF Graph 中寻找:

谁满足这个图结构?

这篇我们就用 Apache Jena,从最简单的 SELECT 开始,一步一步写到:

FILTER
OPTIONAL
DISTINCT
ORDER BY
GROUP BY
聚合
多条件查询
多跳关系
Property Path
ASK
CONSTRUCT

最后再实现一个真正的:

员工技能关系查询系统。

项目地址:

https://github.com/apache/jena

一、先准备一张知识图谱

为了让后面的 SPARQL 有东西可查,我们先创建一些数据。

先定义命名空间:

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

private static final String PROP =
        NS + "property/";

private static final String EMP =
        NS + "employee/";

private static final String SKILL =
        NS + "skill/";

private static final String DEPT =
        NS + "department/";

private static final String PROJECT =
        NS + "project/";

创建 Model:

Model model =
        ModelFactory.createDefaultModel();

设置 Prefix:

model.setNsPrefix(
        "ex",
        NS
);

model.setNsPrefix(
        "prop",
        PROP
);

二、先创建几个 Property

Property name =
        model.createProperty(
                PROP + "name"
        );

Property age =
        model.createProperty(
                PROP + "age"
        );

Property hasSkill =
        model.createProperty(
                PROP + "hasSkill"
        );

Property belongsTo =
        model.createProperty(
                PROP + "belongsTo"
        );

Property worksOn =
        model.createProperty(
                PROP + "worksOn"
        );

Property usesDatabase =
        model.createProperty(
                PROP + "usesDatabase"
        );

Property maintains =
        model.createProperty(
                PROP + "maintains"
        );

三、创建部门

研发部:

Resource rd =
        model.createResource(
                DEPT + "rd"
        );

rd.addProperty(
        name,
        "研发部"
);

产品部:

Resource product =
        model.createResource(
                DEPT + "product"
        );

product.addProperty(
        name,
        "产品部"
);

四、创建技能

Resource go =
        model.createResource(
                SKILL + "go"
        );

go.addProperty(
        name,
        "Go"
);

Java:

Resource java =
        model.createResource(
                SKILL + "java"
        );

java.addProperty(
        name,
        "Java"
);

Redis:

Resource redis =
        model.createResource(
                SKILL + "redis"
        );

redis.addProperty(
        name,
        "Redis"
);

Docker:

Resource docker =
        model.createResource(
                SKILL + "docker"
        );

docker.addProperty(
        name,
        "Docker"
);

五、创建 PostgreSQL

Resource postgres =
        model.createResource(
                NS + "database/postgresql"
        );

postgres.addProperty(
        name,
        "PostgreSQL"
);

六、创建一个支付系统项目

Resource payment =
        model.createResource(
                PROJECT + "payment"
        );

payment.addProperty(
        name,
        "支付系统"
);

payment.addProperty(
        usesDatabase,
        postgres
);

这样:

支付系统
↓
usesDatabase
↓
PostgreSQL

已经形成。


七、创建张三

Resource zhangsan =
        model.createResource(
                EMP + "zhangsan"
        );

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

八、创建李四

Resource lisi =
        model.createResource(
                EMP + "lisi"
        );

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

九、创建王五

Resource wangwu =
        model.createResource(
                EMP + "wangwu"
        );

wangwu
        .addProperty(
                name,
                "王五"
        )
        .addLiteral(
                age,
                31
        )
        .addProperty(
                belongsTo,
                product
        )
        .addProperty(
                hasSkill,
                java
        );

现在整张图大概是:

                Go
                ↑
                │ hasSkill
                │
研发部 ← 张三 ─────────→ Redis
   ↑            │
   │            │ worksOn
   │            ↓
   │          支付系统
   │            │
   │            │ usesDatabase
   │            ↓
   │        PostgreSQL
   │            ↑
   │            │ maintains
   │            │
   └────────── 李四
                │
                ├── Java
                └── Redis


产品部 ← 王五
          │
          └── Java

接下来正式开始 SPARQL。


十、最简单的 SELECT

我们先做一个最简单的问题:

查询所有有姓名的实体。

SPARQL:

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

SELECT ?entity ?name
WHERE {

    ?entity
        prop:name
        ?name .
}

这里:

?entity
?name

都是:

变量。

意思就是:

帮我找出所有满足 某个东西 → name → 某个名字 的组合。


十一、Java 里怎么执行 SPARQL?

基本套路非常固定:

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

    SELECT ?entity ?name
    WHERE {

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

创建 Query:

Query query =
        QueryFactory.create(
                sparql
        );

执行:

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

    ResultSet rs =
            qexec.execSelect();

    ResultSetFormatter.out(
            rs
    );
}

这就是 Jena 查询最基本的流程:

SPARQL String
↓
Query
↓
QueryExecution
↓
ResultSet

十二、手工读取 ResultSet

真实项目中通常不会直接:

ResultSetFormatter.out()

而是自己处理。

例如:

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

    ResultSet rs =
            qexec.execSelect();

    while (
            rs.hasNext()
    ) {

        QuerySolution row =
                rs.next();

        Resource entity =
                row.getResource(
                        "entity"
                );

        Literal entityName =
                row.getLiteral(
                        "name"
                );

        System.out.println(
                entity.getURI()
                + " => "
                + entityName.getString()
        );
    }
}

结果类似:

employee/zhangsan => 张三
employee/lisi => 李四
employee/wangwu => 王五
skill/go => Go
skill/java => Java
...

十三、为什么这条查询会把技能也查出来?

因为:

?entity
    prop:name
    ?name .

没有限定:

entity 必须是 Employee。

所以只要有:

name

属性:

Employee
Skill
Department
Project
Database

都可能被匹配。

这就是:

Graph Pattern。

SPARQL 只关心:

谁满足这个模式?


十四、那怎么限定“员工”?

最规范的方法是给资源增加:

rdf:type

比如创建员工 Class:

Resource employeeClass =
        model.createResource(
                NS + "Employee"
        );

给张三:

zhangsan.addProperty(
        RDF.type,
        employeeClass
);

李四:

lisi.addProperty(
        RDF.type,
        employeeClass
);

王五:

wangwu.addProperty(
        RDF.type,
        employeeClass
);

现在:

张三
↓
rdf:type
↓
Employee

十五、查询真正的 Employee

SPARQL:

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

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

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

SELECT ?employee ?name
WHERE {

    ?employee
        rdf:type
        ex:Employee ;

        prop:name
        ?name .
}

这里出现了一个写法:

?employee
    rdf:type ex:Employee ;
    prop:name ?name .

等价于:

?employee
    rdf:type
    ex:Employee .

?employee
    prop:name
    ?name .

分号:

;

表示:

Subject 相同,继续写另一个 Predicate。


十六、这就是 SPARQL 最核心的思想

查询:

?employee
    rdf:type
    ex:Employee ;

    prop:name
    ?name .

其实是在画一个图:

        Employee
           ↑
           │ rdf:type
           │
      ?employee
           │
           │ name
           ↓
         ?name

SPARQL 会去整张 RDF Graph 里:

找满足这个结构的所有节点。

所以:

SPARQL 本质上是在做子图匹配。

理解这一句话,后面很多语法就通了。


十七、查询会 Go 的员工

目标图:

Employee
   │
   │ hasSkill
   ↓
  Go

SPARQL:

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

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

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

SELECT ?employee ?name
WHERE {

    ?employee
        a
        ex:Employee ;

        prop:name
        ?name ;

        prop:hasSkill
        skill:go .
}

这里:

a

其实就是:

rdf:type

的简写。

所以:

?employee a ex:Employee .

表示:

employee 是 Employee 类型。

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

目标图:

        Go
        ↑
        │
      employee
        │
        ↓
      Redis

SPARQL:

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

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

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

SELECT ?name
WHERE {

    ?employee
        a
        ex:Employee ;

        prop:name
        ?name ;

        prop:hasSkill
        skill:go ;

        prop:hasSkill
        skill:redis .
}

结果:

张三

十九、这里其实发生了 JOIN

如果换成 SQL,可能需要:

employee

JOIN employee_skill
JOIN skill
JOIN employee_skill
JOIN skill

但 SPARQL 中没有显式写:

JOIN

为什么?

因为:

?employee

变量在多个 Triple Pattern 中重复出现。

例如:

?employee prop:name ?name .

?employee prop:hasSkill skill:go .

?employee prop:hasSkill skill:redis .

SPARQL 引擎会自动寻找:

同时满足这些条件的 ?employee

这其实就是:

隐式 JOIN。


二十、FILTER:给匹配结果加条件

现在问:

年龄大于 27 的员工。

SPARQL:

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

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

SELECT ?name ?age
WHERE {

    ?employee
        a ex:Employee ;

        prop:name
            ?name ;

        prop:age
            ?age .

    FILTER(
        ?age > 27
    )
}

结果:

张三 28
王五 31

二十一、多个 FILTER

比如:

年龄在 27~30 岁之间。

FILTER(
    ?age >= 27
    &&
    ?age <= 30
)

完整:

SELECT ?name ?age
WHERE {

    ?employee
        prop:name ?name ;
        prop:age ?age .

    FILTER(
        ?age >= 27
        &&
        ?age <= 30
    )
}

结果:

张三 28

二十二、字符串过滤

查询:

姓名中包含“张”。

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

完整:

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

SELECT ?name
WHERE {

    ?employee
        prop:name
        ?name .

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

二十三、正则表达式

也可以:

FILTER(
    REGEX(
        STR(?name),
        "^张"
    )
)

表示:

以“张”开头。

二十四、真正容易踩坑的 OPTIONAL

假设我们再增加一个属性:

Property email =
        model.createProperty(
                PROP + "email"
        );

张三有邮箱:

zhangsan.addProperty(
        email,
        "zhangsan@example.com"
);

李四没有。

王五也没有。

现在我们问:

查询所有员工,如果有邮箱就显示邮箱。

很多 SQL 开发者第一反应可能写:

SELECT ?name ?email
WHERE {

    ?employee
        prop:name
        ?name ;

        prop:email
        ?email .
}

但是结果只有:

张三

为什么?

因为整个 Pattern 要同时满足:

有 name
+
有 email

李四没有 email:

整个匹配失败。

二十五、这时候要用 OPTIONAL

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

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

SELECT ?name ?email
WHERE {

    ?employee
        a ex:Employee ;

        prop:name
        ?name .

    OPTIONAL {

        ?employee
            prop:email
            ?email .
    }
}

结果:

张三    zhangsan@example.com

李四    -

王五    -

二十六、OPTIONAL 很像 SQL 的 LEFT JOIN

可以这么理解。

SQL:

SELECT
    e.name,
    p.email
FROM employee e

LEFT JOIN profile p
ON e.id = p.employee_id;

SPARQL:

?employee
    prop:name
    ?name .

OPTIONAL {

    ?employee
        prop:email
        ?email .
}

所以对于 SQL 程序员:

OPTIONAL ≈ LEFT JOIN。

这个类比特别好理解。


二十七、Java 中怎么判断 OPTIONAL 是否存在?

while (
        rs.hasNext()
) {

    QuerySolution row =
            rs.next();

    String name =
            row.getLiteral(
                    "name"
            ).getString();

    String emailValue =
            null;

    if (
        row.contains(
                "email"
        )
    ) {

        emailValue =
                row.getLiteral(
                        "email"
                ).getString();
    }

    System.out.println(
            name
            + " | "
            + emailValue
    );
}

输出:

张三 | zhangsan@example.com
李四 | null
王五 | null

二十八、FILTER + OPTIONAL 要特别小心

比如:

OPTIONAL {

    ?employee
        prop:email
        ?email .

    FILTER(
        CONTAINS(
            STR(?email),
            "@example.com"
        )
    )
}

这里 FILTER 只作用在:

OPTIONAL

内部。

和把 FILTER 写在外面:

OPTIONAL {

    ?employee
        prop:email
        ?email .
}

FILTER(...)

语义可能完全不同。

这是 SPARQL 新手非常容易踩的坑。

简单记:

FILTER 放在哪个 Graph Pattern 中,就对哪个匹配结果产生影响。


二十九、DISTINCT:去重

比如:

SELECT ?employee
WHERE {

    ?employee
        prop:hasSkill
        ?skill .
}

张三有三个 Skill:

Go
Redis
Docker

所以:

张三

会出现三行。

如果只想知道:

哪些员工至少有一个技能?

写:

SELECT DISTINCT ?employee
WHERE {

    ?employee
        prop:hasSkill
        ?skill .
}

三十、ORDER BY

按年龄排序:

SELECT ?name ?age
WHERE {

    ?employee
        prop:name
        ?name ;

        prop:age
        ?age .
}
ORDER BY ?age

从小到大。

倒序:

ORDER BY DESC(?age)

结果:

王五 31
张三 28
李四 26

三十一、LIMIT

只取两个:

SELECT ?name ?age
WHERE {

    ?employee
        prop:name ?name ;
        prop:age ?age .
}
ORDER BY DESC(?age)
LIMIT 2

结果:

王五
张三

三十二、OFFSET:分页

第一页:

LIMIT 10
OFFSET 0

第二页:

LIMIT 10
OFFSET 10

第三页:

LIMIT 10
OFFSET 20

是不是很像 SQL?


三十三、COUNT:统计员工数量

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

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

    ?employee
        a ex:Employee .
}

Java:

QuerySolution row =
        rs.next();

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

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

三十四、GROUP BY:统计每个技能有多少员工

这是一个很实用的例子。

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

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

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

结果可能:

Redis    2
Java     2
Go       1
Docker   1

三十五、继续 JOIN 技能名称

现在结果是:

https://example.com/company/skill/redis

不够友好。

可以继续匹配:

?skill
    prop:name
    ?skillName .

完整:

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

SELECT
    ?skillName
    (
        COUNT(?employee)
        AS ?employeeCount
    )
WHERE {

    ?employee
        prop:hasSkill
        ?skill .

    ?skill
        prop:name
        ?skillName .
}
GROUP BY ?skillName
ORDER BY DESC(?employeeCount)

结果:

Redis   2
Java    2
Go      1
Docker  1

三十六、开始进入重点:多跳查询

现在问:

张三参与的项目使用什么数据库?

图结构:

张三
↓ worksOn
支付系统
↓ usesDatabase
PostgreSQL

这就是:

两跳。

SPARQL:

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

PREFIX emp:
<https://example.com/company/employee/>

SELECT ?project ?database
WHERE {

    emp:zhangsan
        prop:worksOn
        ?project .

    ?project
        prop:usesDatabase
        ?database .
}

这就是最基础的:

Multi-Hop Query。


三十七、多跳本质还是多个 Triple Pattern JOIN

第一段:

emp:zhangsan
    prop:worksOn
    ?project .

得到:

?project = 支付系统

第二段:

?project
    prop:usesDatabase
    ?database .

再得到:

?database = PostgreSQL

共同变量:

?project

把两个 Pattern 连接起来。

所以:

Graph Traversal

在 SPARQL 中很多时候就是:

共享变量不断 JOIN。


三十八、三跳查询

现在问:

张三负责的项目使用的数据库是谁维护的?

已有:

张三
↓
支付系统
↓
PostgreSQL
↑
李四

SPARQL:

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

PREFIX emp:
<https://example.com/company/employee/>

SELECT
    ?project
    ?database
    ?maintainer
WHERE {

    emp:zhangsan
        prop:worksOn
        ?project .

    ?project
        prop:usesDatabase
        ?database .

    ?maintainer
        prop:maintains
        ?database .
}

这里非常关键。

最后一个关系:

李四
→ maintains
→ PostgreSQL

所以 Triple Pattern 写:

?maintainer
    prop:maintains
    ?database .

而不是反过来。


三十九、再把人名和系统名一起查出来

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

PREFIX emp:
<https://example.com/company/employee/>

SELECT
    ?projectName
    ?databaseName
    ?maintainerName
WHERE {

    emp:zhangsan
        prop:worksOn
        ?project .

    ?project
        prop:name
        ?projectName ;

        prop:usesDatabase
        ?database .

    ?database
        prop:name
        ?databaseName .

    ?maintainer
        prop:maintains
        ?database ;

        prop:name
        ?maintainerName .
}

结果:

支付系统
PostgreSQL
李四

这就是知识图谱特别舒服的地方。

你描述的是:

关系路径

而不是:

表怎么 JOIN。

四十、Property Path:多跳查询还能写得更短

SPARQL 1.1 提供了:

Property Path

例如:

张三
→ worksOn
→ 项目
→ usesDatabase
→ Database

可以直接:

emp:zhangsan
    prop:worksOn/prop:usesDatabase
    ?database .

完整:

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

PREFIX emp:
<https://example.com/company/employee/>

SELECT ?database
WHERE {

    emp:zhangsan
        prop:worksOn/
        prop:usesDatabase
        ?database .
}

这里:

/

表示:

沿两个关系依次向前走。


四十一、Property Path 特别像“图上的路径”

比如:

prop:worksOn/prop:usesDatabase

就是:

worksOn
↓
usesDatabase

两条边。

所以:

A p1/p2 B

可以理解成:

A
↓ p1
某个节点
↓ p2
B

四十二、反向关系 ^

假设:

李四
→ maintains
→ PostgreSQL

现在从 PostgreSQL 出发:

谁维护它?

可以写:

?database
    ^prop:maintains
    ?maintainer .

^:

反向走 Property。

所以:

?database
    ^prop:maintains
    ?maintainer .

等价于:

?maintainer
    prop:maintains
    ?database .

四十三、组合以后就更有意思了

从张三:

张三
→ 支付系统
→ PostgreSQL
← 李四

可以:

emp:zhangsan
    prop:worksOn/
    prop:usesDatabase/
    ^prop:maintains
    ?maintainer .

完整:

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

PREFIX emp:
<https://example.com/company/employee/>

SELECT ?maintainer
WHERE {

    emp:zhangsan

        prop:worksOn/
        prop:usesDatabase/
        ^prop:maintains

        ?maintainer .
}

这就是:

图路径查询。


四十四、* 和 + 更强

假设组织关系:

后端组
→ partOf
→ 研发部

研发部
→ partOf
→ 技术中心

技术中心
→ partOf
→ 公司

如果不知道到底有几层怎么办?

可以:

prop:partOf+

+:

至少走一次。

例如:

<backend-team>
    prop:partOf+
    ?parent .

结果:

研发部
技术中心
公司

四十五、* 和 + 的区别

+

表示:

1 次或多次

而:

*

表示:

0 次或多次

所以:

prop:partOf*

结果甚至可能包含:

backend-team 自己

因为:

走 0 次

也是合法路径。


四十六、| 表示“或者”

例如我们认为:

hasSkill

或者:

interestedIn

都算与技术有关。

可以:

(prop:hasSkill|prop:interestedIn)

例如:

?person
    (prop:hasSkill|prop:interestedIn)
    skill:go .

表示:

会 Go

或者:

对 Go 感兴趣

都匹配。


四十七、ASK:我只想知道“有没有”

有时候根本不需要:

ResultSet

只想问:

张三会不会 Go?

SPARQL:

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

PREFIX emp:
<https://example.com/company/employee/>

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

ASK {

    emp:zhangsan
        prop:hasSkill
        skill:go .
}

Java:

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

    PREFIX emp:
    <https://example.com/company/employee/>

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

    ASK {

        emp:zhangsan
            prop:hasSkill
            skill:go .
    }
    """;

执行:

Query query =
        QueryFactory.create(
                askSparql
        );

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

    boolean result =
            qexec.execAsk();

    System.out.println(
            result
    );
}

输出:

true

四十八、ASK 非常适合权限和规则判断

比如:

用户是否属于某部门?

服务是否依赖 Redis?

某员工是否参与某项目?

一个节点是否存在某条关系?

都可以:

ASK {
    ...
}

不需要返回一大堆数据。


四十九、CONSTRUCT:查询完直接生成新图

这也是 SPARQL 和 SQL 差异很有意思的地方。

比如:

创建一张只包含会 Redis 的员工及其技能的新图。

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

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

CONSTRUCT {

    ?employee
        prop:hasSkill
        skill:redis .

}
WHERE {

    ?employee
        prop:hasSkill
        skill:redis .
}

Java:

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

    Model result =
            qexec.execConstruct();

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

返回的不是:

Table

而是:

一个新的 RDF Graph。


五十、CONSTRUCT 很适合知识图谱裁剪

比如原知识图谱有:

1000 万 Triple

某个 Agent 只需要:

支付系统相关知识

就可以:

CONSTRUCT {
    ...
}
WHERE {
    ...
}

生成一个:

小型 Context Graph

再交给 Agent。

这个思路其实和:

GraphRAG Subgraph

非常接近。


五十一、BIND:创建一个新变量

比如年龄:

28

希望计算:

10 年后的年龄

可以:

BIND(
    ?age + 10
    AS ?ageAfter10Years
)

完整:

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

SELECT
    ?name
    ?age
    ?ageAfter10Years
WHERE {

    ?employee
        prop:name
        ?name ;

        prop:age
        ?age .

    BIND(
        ?age + 10
        AS ?ageAfter10Years
    )
}

五十二、COALESCE:处理 OPTIONAL 空值

比如:

OPTIONAL {

    ?employee
        prop:email
        ?email .
}

没有 Email 的时候变量:

?email

未绑定。

可以:

BIND(
    COALESCE(
        ?email,
        "未填写"
    )
    AS ?displayEmail
)

这样结果:

张三  zhangsan@example.com
李四  未填写
王五  未填写

五十三、一个真实一点的综合查询

现在需求:

查询研发部所有员工,显示姓名、年龄、邮箱、技能数量;邮箱没有就显示“未填写”,年龄从大到小。

SPARQL:

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

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

SELECT
    ?name
    ?age
    ?displayEmail
    (
        COUNT(?skill)
        AS ?skillCount
    )
WHERE {

    ?employee
        prop:name
        ?name ;

        prop:age
        ?age ;

        prop:belongsTo
        dept:rd .

    OPTIONAL {

        ?employee
            prop:email
            ?email .
    }

    OPTIONAL {

        ?employee
            prop:hasSkill
            ?skill .
    }

    BIND(
        COALESCE(
            ?email,
            "未填写"
        )
        AS ?displayEmail
    )
}
GROUP BY
    ?employee
    ?name
    ?age
    ?displayEmail

ORDER BY DESC(?age)

这个查询已经很接近真实系统了。


五十四、封装一个通用查询方法

Java 项目里,不要每个地方重复:

QueryFactory
QueryExecution
ResultSet

可以封装。

例如:

public class SparqlExecutor {

    private final Model model;


    public SparqlExecutor(
            Model model
    ) {

        this.model =
                model;
    }


    public void select(
            String sparql
    ) {

        Query query =
                QueryFactory.create(
                        sparql
                );

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

            ResultSet rs =
                    qexec.execSelect();

            ResultSetFormatter.out(
                    rs
            );
        }
    }
}

调用:

SparqlExecutor executor =
        new SparqlExecutor(
                model
        );

executor.select(
        """
        PREFIX prop:
        <https://example.com/company/property/>

        SELECT ?name
        WHERE {

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

五十五、真实项目不要直接字符串拼用户输入

比如:

String query =
    "SELECT * WHERE { ?s prop:name \""
    + username
    + "\" }";

这种写法和:

拼 SQL

一样,不够漂亮,也可能造成查询注入类问题。

Jena 提供:

ParameterizedSparqlString

可以参数化。

例如:

ParameterizedSparqlString pss =
        new ParameterizedSparqlString(
                """
                PREFIX prop:
                <https://example.com/company/property/>

                SELECT ?employee
                WHERE {

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

然后:

pss.setLiteral(
        "name",
        "张三"
);

不过这里需要把查询设计成合适的参数形式。

更典型的是 IRI 参数。

例如:

ParameterizedSparqlString pss =
        new ParameterizedSparqlString(
                """
                PREFIX prop:
                <https://example.com/company/property/>

                SELECT ?name
                WHERE {

                    ?employee
                        prop:hasSkill
                        ?targetSkill ;

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

设置:

pss.setIri(
        "targetSkill",
        SKILL + "redis"
);

再:

Query query =
        pss.asQuery();

这种方式比:

字符串拼接

更适合真实系统。


五十六、为什么 SPARQL 特别适合知识图谱?

因为数据库里的问题经常是:

我要查哪张表?

而知识图谱真正关心:

我要找什么关系?

比如:

找出会 Go、属于研发部、参与使用 PostgreSQL 项目的员工。

SPARQL 可以直接写成:

SELECT ?employee
WHERE {

    ?employee
        prop:hasSkill
        skill:go ;

        prop:belongsTo
        dept:rd ;

        prop:worksOn
        ?project .

    ?project
        prop:usesDatabase
        db:postgresql .
}

你会发现:

查询本身就在描述业务语义。


五十七、这和 GraphRAG 又连上了

我们前面讲 TrustGraph 时说:

GraphRAG

核心之一是:

沿 Entity Relationship 找上下文。

SPARQL 本身其实就是非常成熟的:

Graph Pattern Query Language。

比如用户:

张三负责项目的数据库维护人是谁?

可以转换成:

SELECT ?maintainer
WHERE {

    emp:zhangsan
        prop:worksOn
        ?project .

    ?project
        prop:usesDatabase
        ?database .

    ?maintainer
        prop:maintains
        ?database .
}

这实际上已经完成:

Entity
↓
Relationship
↓
Entity
↓
Relationship
↓
Entity

多跳检索。


五十八、未来 Agent 甚至可以自动生成 SPARQL

架构:

用户:
“哪些会 Redis 的研发部员工参与了支付系统?”

↓

LLM

↓

SPARQL

↓

Apache Jena / Fuseki

↓

Knowledge Graph

↓

ResultSet

↓

LLM

↓

自然语言答案

LLM 生成:

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

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

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

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

SELECT ?name
WHERE {

    ?employee
        prop:name
        ?name ;

        prop:hasSkill
        skill:redis ;

        prop:belongsTo
        dept:rd ;

        prop:worksOn
        project:payment .
}

然后 Jena 真正执行。

这就是:

Natural Language → SPARQL → Knowledge Graph

非常典型的 AI + Ontology 使用方式。


五十九、最后总结 SPARQL 最值得记住的语法

SELECT

查询变量:

SELECT ?employee ?name

Triple Pattern

描述图结构:

?employee
    prop:name
    ?name .

FILTER

过滤:

FILTER(
    ?age > 25
)

OPTIONAL

属性可能不存在:

OPTIONAL {

    ?employee
        prop:email
        ?email .
}

可以类比:

LEFT JOIN

DISTINCT

去重:

SELECT DISTINCT ?employee

ORDER BY

排序:

ORDER BY DESC(?age)

LIMIT / OFFSET

分页:

LIMIT 20
OFFSET 40

GROUP BY

聚合:

GROUP BY ?skill

COUNT

统计:

COUNT(?employee)

ASK

判断事实是否存在:

ASK {
    emp:zhangsan
        prop:hasSkill
        skill:go .
}

CONSTRUCT

生成新的 RDF Graph:

CONSTRUCT {
    ?s ?p ?o
}
WHERE {
    ?s ?p ?o
}

Property Path

沿关系走多跳:

prop:worksOn/
prop:usesDatabase

反向关系

^prop:maintains

任意层级

prop:partOf+

六十、真正理解 SPARQL,只需要记住一句话

很多教程会让你背:

SELECT
WHERE
FILTER
OPTIONAL
UNION
GROUP BY

但如果只背语法,非常容易忘。

真正应该记住的是:

SPARQL 是在描述“我想匹配什么样的图”。

例如:

?person
    prop:worksOn
    ?project .

?project
    prop:usesDatabase
    ?database .

你脑子里应该直接看到:

Person
   ↓
Project
   ↓
Database

而不是:

第一张表 JOIN 第二张表 JOIN 第三张表。

这就是从:

关系数据库思维

切换到:

图思维

最关键的一步。

Apache Jena 则负责把这些:

SPARQL

真正执行在:

Model
Dataset
TDB2
Fuseki

之上。

最终技术链:

Java
↓
Apache Jena
↓
RDF
↓
Knowledge Graph
↓
SPARQL
↓
Graph Pattern Matching
↓
Multi-Hop Query
↓
Agent / GraphRAG

如果你已经会 SQL,再学习 SPARQL,不需要把自己当成完全从零开始。

你只需要改变一个问题:

以前写 SQL 时你问:

数据在哪张表?

写 SPARQL 时,你开始问:

我要找的实体之间,到底是什么关系?

当这个思维转过来以后,SELECT、FILTER、OPTIONAL、多跳查询 基本都会顺起来。

项目地址:

https://github.com/apache/jena
posted @ 2026-09-27 09:42  JavaPub  阅读(2)  评论(0)    收藏  举报