5-Answer 系列-本体查询模块
前面几篇已经介绍了本体构建模块,运行本体构建模块可以构建出一个初步的本体库。
在构建出本体库之后,我们该如何对它进行操作呢?
这一篇介绍本体查询模块。
SPARQL
大多数程序员在日常开发中接触最多的应当是关系型数据库,例如 MySQL、Oracle、SQL Server 等。这些关系型数据库使用标准的结构化查询语言 SQL 来实现对数据的查询和更新等操作。
虽然不同数据库以及同一数据库不同版本对 SQL 语言规范的支持和实现都有所不同,不同数据库在 SQL 上的语法也会有细微的差别。但是 SQL 语言已经极大降低了数据库的学习成本,其非过程化、自然语言风格等特点极大提高了我们操作关系型数据库的效率。
从关系型数据库回到我们的本体库(RDF数据库),自然很容易联想到针对采用 RDF 框架的数据库也应该有一套相应的查询操作语言。
SPARQL(全称 SPARQL Protocol and RDF Query Language) 就是我们需要的针对 RDF 的查询语言,它提供了查询 RDF 数据库的能力,并在 2008 正式成为 W3C 的推荐标准语言。2013 年更新至 SPARQL 1.1,SPARQL 1.1 提供了更为强大的能力,添加了例如更新、查询嵌套等操作。
小栗子
假如现在本体库中存有如下三元组:
美人鱼 有导演 周星驰
现在如果想要查询 "美人鱼的导演是谁",那么可以通过如下代码实现:
SELECT ?name
WHERE {
美人鱼 有导演 ?name
}
其中 SELECT 和 WHERE 不必多说,值得注意的是 WHERE 子句中的内容。之前已经提到了资源描述框架中 RDF 中数据的表达单位是一个三元组——(subject,predicate,object),即主体-谓语-客体。因此在进行查询时也以三元组的形式给定查询条件,在查询条件中,三元组中三元素的任意一个都可以设定为变量。
例如现在如果要查询周星驰导演过哪些电影,可以这些设定变量:
SELECT ?movie
WHERE {
?movie 有导演 周星驰
}
以上代码就可以列出当前本体库中存有的所有周星驰导演的作品。
再进一步如果想要查询美人鱼导演的年龄,可以实现如下:
SELECT ?age
WHERE {
美人鱼 有导演 ?a.
?a 有年龄 ?age.
}
从中可以看到通过相同变量来标识同一实体,这样可实现连接的效果,查询可表示为"美人鱼 -> 导演"、"导演 -> 年龄",连接为 "美人鱼 -> 导演 -> 年龄"。
当然以上的查询语句是一种简化,是为了方便表达 SPARQL 的基本含义。再考虑上语法细节,应该实现如下:
PREFIX mymo: <http://www.semanticweb.org/narutoku/ontologies/2016/3/my-ontology#>
SELECT ?name
WHERE {
mymo:美人鱼 mymo:有导演 ?name
}
之前提到过本体应该具有共享的特点,而且 RDF 很重要的一个应用就是语义网。
因此我们构建的本体库应该要考虑到与他人共享、避免冲突等问题,这就需要为我们本体库中的实体以及关系等设定唯一标识。我们的 "美人鱼" 实体应该表达为,"标识: 美人鱼", 在 Answer 系统中这个标识就是 <http://www.semanticweb.org/narutoku/ontologies/2016/3/my-ontology#>
。
所以在构造 SPARQL 查询语句的时候需要在每个实体和关系前面添加 <http://www.semanticweb.org/narutoku/ontologies/2016/3/my-ontology#>
,为了方便可以使用 PREFIX mymo : <标识> 简化,如上代码所示。
另外在系统实际代码中是不会直接用 "美人鱼" 来查询的,因为之前以及提到过 "美人鱼" 存在同名实体和实体别名的情况,所以最终会使用实体 ID(在构建本体库生成 UUID 作为实体或关系的唯一标识)来查询。而同名实体的识别或实体别名的消歧在构建查询语句之前完成,这一部分会在语义解析模块中介绍。
以上是对 SPARQL 极短的介绍,毕竟 SPARQL 语言不是一篇博客就能介绍的完的。文章的末尾给出了一些关于 SPARQL 的参考资料。
使用 Jena
上面已经介绍了 RDF 数据查询语言 SPARQL。虽然 SPARQL 和 SQL 一样具有易学易用的特点,是非常强大的工具。但是如果在每一次的应用开发中,针对每一次的数据查询需求都要反反复复编写 SPARQL 语句还是显得有点麻烦,尤其是在一些简单但又频繁使用的操作场景,例如添加一个实体、或添加一个属性、又或向一个实体添加一个数据属性等等,这些操作需求频繁出现,重复编写 SPARQL 语句十分不必要。
这时候就轮到 Jena 出场了。Jena 是 Apache 软件基金会开源的 RDF 数据查询框架,它是对 SPARQL 语言规范的实现,是对 SPARQL 一些基本操作的封装。利用它提供的丰富的 API 就可以实现对本体库进行各种操作。
例如如果想要添加一个实体,可使用:
/**
* 创建实体
* 根据 UUID 和所属类 创建一个实体
* @param individualId 实体 ID
* @param genusClass 实体类型
* @return
*/
public Individual createIndividual(String individualId, OntClass genusClass);
给一个实体添加数据属性:
/**
* 给实体添加单个数据属性
* @param ontologyClass
* @param individualId 实体标识
* @param propertyName 属性名
* @param propertyValue 属性值
* @return
*/
public boolean addDataProperty(Individual individual, String propertyName, String propertyValue);
查询实体是否存在:
/**
* 查询实体是否存在
* @param individualName
* @return
*/
public boolean individualExist(String individualName);
Jena 还提供了其它非常多功能强大的 API,更多更详细的 API 可查询 官方 API 文档。
Jena 官方站点 也给出了 Jena 的教程等相关参考资料。
数据访问封装
有了 SPARQL 和 Jena 之后,我们就可以对本体库进行基本的操作。有了这些基本操作,我们就可以在这之上对业务层面的数据访问需求进行封装。
比如在语义解析模块中进行实体别名消歧(星爷-消歧成-周星驰)的时候就需要查询本体库中的等价实体——查询 "星爷" 是否有等价实体,查询本体库发现 "星爷" 等价实体 "周星驰",则返回 "周星驰")。我们就可以将这个同名实体的数据查询需求做一个封装。
所谓本体查询模块就是对 Answer 系统中诸如上述 "查询等价实体" 的数据访问需求的封装。
以 "查询等价实体" 为例,代码如下:
public String querySameIndividual(String individualName) { // individualName 表示实体的标识
String sameIndividual = null; // 返回的等价实体结果
String prefix = "prefix mymo: <" + Config.pizzaNs + ">\n" +
"prefix rdfs: <" + RDFS.getURI() + ">\n" +
"prefix owl: <" + OWL.getURI() + ">\n"; // 查询前缀
String QL = "SELECT ?等价实体 WHERE {?等价实体 owl:sameAs mymo:" + individualName + ".\n}"; // 查询等价实体的 SPARQL 语句,其中等价实体的关系可以用 owl:sameAs 表达
String SPARQL = prefix + QL; // 添加好前缀,构建完整查询语句
// 使用 Jena API 构建好查询对象
Query query = QueryFactory.create(SPARQL);
QueryExecution qexec = QueryExecutionFactory.create(query, model);
ResultSet results = qexec.execSelect();// 执行查询
ResultSetRewindable resultSetRewindable = ResultSetFactory.makeRewindable(results);
int numCols = resultSetRewindable.getResultVars().size();
while (resultSetRewindable.hasNext()) { // 遍历所有查询结果
QuerySolution querySolution = resultSetRewindable.next();
for (int col = 0; col < numCols;col++) {
String rVar = results.getResultVars().get(col);
RDFNode obj = querySolution.get(rVar);
sameIndividual = FmtUtils.stringForRDFNode(obj).split(":")[1]; // 获取查询结果-等价实体
}
}
return sameIndividual; // 返回查询结果
}
除了 "查询等价实体" 之外,还封装了其它一些简单操作,例如查询所有以 Subject 为主体的三元组:
/**
* 查询所有以 Subject 为主体的三元组(这里称之为断言)
* 代码还有待优化
*/
@Override
public List<Statement> getStatementsBySubject(String subject) {
List<Statement> statements = new ArrayList<Statement>();
StmtIterator stmtIter = model.listStatements(); // 列出所有的三元组
while(stmtIter.hasNext()) { // 循环所有断言
Statement statement = stmtIter.next();
String subjectName = null;
if (statement.getSubject() != null && statement.getSubject().getURI() != null) {
String[] urlFields = statement.getSubject().getURI().split("#");
if (urlFields.length > 1) {
subjectName = urlFields[1];
} else {
subjectName =urlFields[0];
}
if (subjectName != null) {
if (subjectName.equals(subject)) { // 寻找主体为参数 subject 的三元组
statements.add(statement);
}
}
}
}
return statements; // 返回最终结果
}
如上所述,本体查询模块就是对一些本体操作的封装,是系统的数据访问层。
到目前为止,本体查询模块封装的都是一些非常简单的操作,因此实际上是可以不单独构造一个模块的。现在单独划分一个模块,是为了方便之后的系统重构,以后如果实现了本体的分布式存储后者添加一些其它数据源如图数据库,那么只需要修改本体查询模块并可。
相关资料
SQARQL
下一篇
下一篇开始将介绍语义理解模块,语义理解模块的主要作用是解析自然语言,使用查询语义图表达用户的查询语义,通过实体消歧、谓语消歧等操作提高语义理解的程度,最终通过本体查询模块查询答案。
汪
汪.