Apache jena fuseki实践踩坑笔记
最近正在跟着专栏《知识图谱-给AI装个大脑》实践KBQA,但是跟到《实践篇(四):Apache jena SPARQL endpoint及推理》的时候发现了一些问题。首先,按照作者提供的配置并没有顺利启动fuseki;其次,也完全看不懂配置文件中的配置项。所以心一横就干脆自己直接去看官网学习了,而这篇文章就是实践过程中的踩坑记录。在开始实践记录之前我要强烈吐槽一下!jena官网的资料真的是无比混乱,没有清晰的分布逻辑,你要看懂A需要先看懂B、C、D、E,等你看完E的时候你已经迷失在一堆打开的网页中了。。。。本文对应的代码和配置文件已经放在github:https://github.com/yangqinj/KBQA-for-movie
启动和使用方式
由于我的电脑上安装的是java8,而Jena4需要JAVA 11,因此我安装的jena-3.16.0版本。jena-fuseki和jena一样不需要安装,解压后即可使用。fuseki有多种运行的形式,总体来说分为两大类:有UI管理界面的webapp形式和没有UI界面的裸服务器Main形式。在这两种形式下又细分为多种不同的使用方式,而最常见的方式是有UI管理界面形式中的独立服务器方式。在《实践篇(四):Apache jena SPARQL endpoint及推理》中使用的就是这种方式,这篇笔记也主要是探索了这种使用方式。

在说明详细的命令行启动方式之前,需要了解fuseki中的两个重要路径变量:FUSEKI_HOME 和 FUSEKI_BASE。前者指向解压后的fuseki文件夹路径(如果不显式指定默认为当前执行路径),后者表示服务器的工作目录(如果不显式指定默认为 FUSEKI_HOME/run )。
非配置文件启动
首先看一下非配置文件的启动方式。这种方式不需要写配置文件,直接通过命令行参数 --file 或者 --loc 指定数据位置,即可启动一个提供只读服务的服务器。假设当前有一个RDF数据文件kg_movie.nt,可以通过如下命令启动一个服务:
./fuseki-server --file=kg_movie.nt --debug /kgmovie 其中,--file 指定RDF数据文件的位置,/kgmovie 则表示这个数据集上服务的路径。 顺利启动后会在终端会输出如下信息,可以看到 kg_movie.nt 中的数据被载入到内存中 ,这个服务器对这个数据集在 http://localhost:3030/kgmovie 上提供只读服务,可以通过query和sparql两个端点进行SPARQL查询,通过data和get两个端点读取数据。

我们尝试使用python脚本查询“周星驰出演了哪些电影?”:
from SPARQLWrapper import SPARQLWrapper, JSON
# “周星驰出演了哪些电影?”
query_string = """
PREFIX : <http://www.kgmovie.com#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT ?title WHERE {
?movie rdf:type :Movie .
?movie :starring ?person .
?person :celebrityChineseName "周星驰" .
?movie :movieTitle ?title .
}
"""
sparql = SPARQLWrapper("http://localhost:3030/kgmovie/sparql")
sparql.setQuery(query_string)
sparql.setReturnFormat(JSON)
results = sparql.query().convert()
for result in results["results"]["bindings"]:
print(result["title"]["value"])输出结果:
大话西游之大圣娶亲
九品芝麻官
唐伯虎点秋香
功夫
大话西游之月光宝盒
喜剧之王目前我们使用的是 --file 这个参数指定RDF数据文件位置,同时也可以使用 --loc 参数。这种方式需要先使用 tdbloader将RDF数据转换为TDB数据,然后通过再使用 --loc 参数指定TDB数据文件夹路径。这里需要注意的一点是,jena当前提供了TDB1和TDB2两种数据格式,如果使用tdbloader转换就对应TDB1,如果使用tdbloader2转换就对应TDB2,此时在启动fuseki-server时需要通过参数 --tdb2 说明。当前这个参数只对应3.16.0版本,当前最新版本默认是使用TDB2了。
配置文件方式启动
上述启动方式只能够启动一个提供只读服务的服务器,如果有更复杂的需求就需要使用配置文件来启动了。一个服务器的配置由多个数据服务配置组成,以及对服务器访问的一些配置(如查询超时时间等),一个数据服务对应一个数据集,一个数据服务上可以定义多个端点提供不同的读写服务。服务器配置以及数据服务配置既可以都写在 FUSEKI_BASE/config.ttl 文件中,也可以将不同数据服务配置写在 FUSEKI_BASE/configuration 目录下,该目录下的一个文件对应一个数据服务配置。
现在通过自己写配置文件的方式实现与上述命令行 --file 参数启动服务器一样的功能。首先,进入apache-jena-fuseki-xxx目录后运行./fuseki-server,然后退出。如果没有显示设置FUSEKI_BASE,那么此时在apache-jena-fuseki-xxx 目录下会生成一个run目录,run目录下生成了一个默认的服务器配置文件config.ttl,这个配置文件中还没有定义数据服务。接下来,我们在RDF数据文件kg_movie.nt上定义一个只读服务:
# Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0
## Fuseki Server configuration file.
@prefix : <#> .
@prefix fuseki: <http://jena.apache.org/fuseki#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ja: <http://jena.hpl.hp.com/2005/11/Assembler#> .
[] rdf:type fuseki:Server ;
# Example::
# Server-wide query timeout.
#
# Timeout - server-wide default: milliseconds.
# Format 1: "1000" -- 1 second timeout
# Format 2: "10000,60000" -- 10s timeout to first result,
# then 60s timeout for the rest of query.
#
# See javadoc for ARQ.queryTimeout for details.
# This can also be set on a per dataset basis in the dataset assembler.
#
# ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "30000" ] ;
# Add any custom classes you want to load.
# Must have a "public static void init()" method.
# ja:loadClass "your.code.Class" ;
# End triples.
.
:service rdf:type fuseki:Service ;
fuseki:name "kgmovie" ;
fuseki:endpoint [
fuseki:operation fuseki:query ;
fuseki:name "" ;
];
fuseki:endpoint [
fuseki:operation fuseki:query ;
fuseki:name "query" ;
];
fuseki:endpoint [
fuseki:operation fuseki:query ;
fuseki:name "sparql" ;
];
fuseki:endpoint [
# SPARQL Graph Store Protocol (read)
fuseki:operation fuseki:gsp_r ;
fuseki:name ""
] ;
fuseki:endpoint [
# SPARQL Graph Store Protocol (read)
fuseki:operation fuseki:gsp_r ;
fuseki:name "data"
] ;
fuseki:endpoint [
# SPARQL Graph Store Protocol (read)
fuseki:operation fuseki:gsp_r ;
fuseki:name "get"
] ;
fuseki:dataset :dataset_kgmovie ;
.
# 数据被载入到内存中
:dataset_kgmovie rdf:type ja:MemoryDataset ;
ja:data "kg_movie.nt"
.再次运行./fuseki-server --debug,可以看到输出的信息与命令行启动输出的信息一致。运行python查询脚本,得到了同样的结果:
大话西游之大圣娶亲
九品芝麻官
唐伯虎点秋香
功夫
大话西游之月光宝盒
喜剧之王如果需要读取TDB数据,则将 dataset_kgmovie 的配置修改为如下,并且新增一个tdb前缀定义:
@prefix tdb: <http://jena.hpl.hp.com/2008/tdb#> .
...
:dataset_kgmovie rdf:type tdb:DatasetTDB ;
tdb:location "tdb/kg_movie"
.接下来我们将数据服务配置抽出作为单独的文件。在生成默认run目录后,在configuration目录下新增如下两个配置文件, config_file.ttl定义从RDF数据文件创建一个只读数据服务, config_tdb.ttl 则定义从TDB数据目录创建一个只读数据服务。
config_file.ttl:
# Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0
## Fuseki Server configuration file.
@prefix : <#> .
@prefix fuseki: <http://jena.apache.org/fuseki#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ja: <http://jena.hpl.hp.com/2005/11/Assembler#> .
:service rdf:type fuseki:Service ;
fuseki:name "kgmovie_file" ;
fuseki:endpoint [
fuseki:operation fuseki:query ;
fuseki:name "" ;
];
fuseki:endpoint [
fuseki:operation fuseki:query ;
fuseki:name "query" ;
];
fuseki:endpoint [
fuseki:operation fuseki:query ;
fuseki:name "sparql" ;
];
fuseki:endpoint [
# SPARQL Graph Store Protocol (read)
fuseki:operation fuseki:gsp_r ;
fuseki:name ""
] ;
fuseki:endpoint [
# SPARQL Graph Store Protocol (read)
fuseki:operation fuseki:gsp_r ;
fuseki:name "data"
] ;
fuseki:endpoint [
# SPARQL Graph Store Protocol (read)
fuseki:operation fuseki:gsp_r ;
fuseki:name "get"
] ;
fuseki:dataset :dataset_file ;
.
:dataset_file rdf:type ja:MemoryDataset ;
ja:data "/Users/yangqj/Documents/Workspace/KBQA-for-movie/data/kg_movie.nt"
config_tdb.ttl:
# Licensed under the terms of http://www.apache.org/licenses/LICENSE-2.0
## Fuseki Server configuration file.
@prefix : <#> .
@prefix fuseki: <http://jena.apache.org/fuseki#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ja: <http://jena.hpl.hp.com/2005/11/Assembler#> .
@prefix tdb: <http://jena.hpl.hp.com/2008/tdb#> .
:service rdf:type fuseki:Service ;
fuseki:name "kgmovie_tdb" ;
fuseki:endpoint [
fuseki:operation fuseki:query ;
fuseki:name "" ;
];
fuseki:endpoint [
fuseki:operation fuseki:query ;
fuseki:name "query" ;
];
fuseki:endpoint [
fuseki:operation fuseki:query ;
fuseki:name "sparql" ;
];
fuseki:endpoint [
# SPARQL Graph Store Protocol (read)
fuseki:operation fuseki:gsp_r ;
fuseki:name ""
] ;
fuseki:endpoint [
# SPARQL Graph Store Protocol (read)
fuseki:operation fuseki:gsp_r ;
fuseki:name "data"
] ;
fuseki:endpoint [
# SPARQL Graph Store Protocol (read)
fuseki:operation fuseki:gsp_r ;
fuseki:name "get"
] ;
fuseki:dataset :dataset_tdb ;
.
:dataset_tdb rdf:type tdb:DatasetTDB ;
tdb:location "/Users/yangqj/Documents/Workspace/KBQA-for-movie/data/jena/tdb"
.再次运行 ./fuseki-server --debug 启动服务器,运行python脚本分别访问 kgmovie_file 和 kgmovie_tdb 端点可以得到同样的结果。
写数据服务
上述几种配置都只提供了读取数据服务,我们在config.ttl基础上增加如下配置,即可以在 kg_movie.nt 上提供写数据服务:
fuseki:endpoint [
fuseki:operation fuseki:update ;
fuseki:name "update"
] ;现在尝试往现有数据中增加电影《长津湖》,然后查询该电影的URI:
from SPARQLWrapper import SPARQLWrapper, JSON
insert_string = """
PREFIX : <http://www.kgmovie.com#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX uri_file: <file:///Users/yangqj/Documents/Workspace/KBQA-for-movie/data/kg_movie.nt#movie/>
INSERT DATA {
uri_file:1234567 rdf:type :Movie .
uri_file:1234567 :movieTitle "长津湖" .
}
"""
sparql_insert = SPARQLWrapper("http://localhost:3030/kgmovie/update")
sparql_insert.setQuery(insert_string)
sparql_insert.setMethod("POST")
results = sparql_insert.query().convert()
print(results.decode())
query_string = """
PREFIX : <http://www.kgmovie.com#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT ?movie WHERE {
?movie rdf:type :Movie .
?movie :movieTitle "长津湖" .
}
"""
sparql = SPARQLWrapper("http://localhost:3030/kgmovie/sparql")
sparql.setQuery(query_string)
sparql.setReturnFormat(JSON)
results = sparql.query().convert()
for result in results["results"]["bindings"]:
print(result["movie"]["value"])
运行脚本输出如下信息:
insert result: <html>
<head>
</head>
<body>
<h1>Success</h1>
<p>
Update succeeded
</p>
</body>
</html>
uri of the movie:
file:///Users/yangqj/Documents/Workspace/KBQA-for-movie/data/kg_movie.nt#movie/1234567
可以成功插入了数据,并且正确返回了电影的URI。
浏览器UI管理界面
我们在浏览器中输入地址 http://localhost:3030 可以进入服务器的管理界面,在这个界面可以查看所有数据集、增加数据集、进行SPARQL查询等等:



推理
Jena还提供了推理功能,主要包括OWL本体推理和规则推理两种。OWL本体推理基于OWL文件提供推理功能,我们使用Protege定义的本体文件在这里用上,在Protege中我们可以定义InverseOf、Transitive等关系,OWL推理机读取这些定义来提供推理功能。OWL推理机对应的配置如下:
:dataset rdf:type ja:RDFDataset;
ja:defaultGraph :inferenceModel
.
#### 使用OWL本体推理:成功启动,但是查询结果为空
:inferenceModel rdf:type ja:InfModel;
# 基础模型
ja:MemoryModel :tdbGraph;
# 本体文件路径
ja:content [
ja:externalContent <file:///Users/yangqj/Documents/Workspace/KBQA-for-movie/jena/apache-jena-fuseki-3.16.0/run_tdb_inf/databases/kg_movie_owl.ttl> ;
];
# OWL本体推理
ja:reasoner [
ja:reasonerURL <http://jena.hpl.hp.com/2003/OWLFBRuleReasoner> ;
];
.
:tdbGraph rdf:type tdb:GraphTDB;
tdb:location "/Users/yangqj/Documents/Workspace/KBQA-for-movie/data/tdb" ;
.
注意,原本的 ja:baseModel :tdbGraph; 需要修改为 ja:MemoryModel :tdbGraph; 不然会出现 "Exception in initialization: caught: Read-only object file" 错误。而且OWL本体文件需要修改为 .ttl 后缀,不然会出现“前言中不允许有内容” 这个错误 。启动成功后,尝试查询 “娜塔莉·波特曼出演的电影”,但是查询的结果是空,而且使用原来的 starring 谓词查询出来的结果也是空的。
PREFIX : <http://www.kgmovie.com#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT DISTINCT ?title WHERE {
?person rdf:type :Celebrity .
# 这里使用的是star谓词
?person :star ?movie .
?person :celebrityChineseName "娜塔莉·波特曼" .
?movie :movieTitle ?title .
}规则推理则是通过手动定义规则的方式来进行推理,比OWL本体推理更加灵活。我在 kg_movie_rules.ttl 中定义了两条规则,一个是 starring和star的逆关系,以及“如果一个演员出演了喜剧电影,那么他就是喜剧演员”:
<http://www.kgmovie.com#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xml: <http://www.w3.org/XML/1998/namespace> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
[ruleInverse: (?m :starring ?c) -> (?c :star ?m)]
[ruleComedian: (?m :starring ?c) (?m :isGenre ?g) (?g :genreName "喜剧") -> (?c rdf:type :Comedian)]
对应的配置如下:
:dataset rdf:type ja:RDFDataset;
ja:defaultGraph :inferenceModel
.
#### 使用规则推理:成功启动并查询到数据
:inferenceModel rdf:type ja:InfModel;
# 基础模型
ja:baseModel :tdbGraph;
# 规则推理机和规则文件路径
ja:reasoner [
ja:reasonerURL <http://jena.hpl.hp.com/2003/GenericRuleReasoner> ;
ja:rulesFrom <file:///Users/yangqj/Documents/Workspace/KBQA-for-movie/jena/apache-jena-fuseki-3.16.0/run_tdb_inf/databases/kg_movie_rules.ttl> ;
];
.
:tdbGraph rdf:type tdb:GraphTDB;
tdb:location "/Users/yangqj/Documents/Workspace/KBQA-for-movie/data/tdb" ;
.
注意这使用的还是“ja:baseModel :tdbGraph;”,使用“ja:MemoryModel :tdbGraph;”会出现查询结果为空。使用问题“娜塔莉·波特曼出演的电影”和“查询所有的喜剧演员”两个问题分别测试这两个规则,得到了正确的结果:


问题记录
1、浏览器无法显示数据集
当前我使用的是3.16.0版本,但其实最开始使用的是3.17.0版本,但是不知道为何浏览器访问无法显示数据集,降低为3.16.0版本后即可。
2、无法显示指定service
在配置文件方式启动一节,我们在 configuration 文件夹下定义了 config_file.ttl 和 config_tdb.ttl 两个配置文件分别提供数据服务 kgmovie_file 和 kgmovie_tdb。此时启动服务器默认使用 configuration 文件夹下的所有数据服务。根据官方提供的示例,可以在 config.ttl 文件中通过如下方式显示指定启动的数据服务:
fuseki:services (<#service1> <#service2>)但是经过我实际测试却会出现如下错误信息:

即使手动增加 fu:name 这个属性也还是会有这个错误,更换不同的版本也无法解决。
