Uche Ogbuji 继续讨论一种 RDF 查询语言,该语言比他迄今为止所讨论的基本 API 要更为复杂得多。它是下几篇问题跟踪器专栏文章中要建立的中间件的基础。
到现在为止,在对人们可能怎样使用和查询问题跟踪器 RDF 元数据的简短讨论中,我们使用了简单而基本的查询 API。现在我们转向一种更为强大的查询语言。这将有助于编写更清晰的中间件代码,也将提供合并巨型模型(比如好几篇专栏文章以前所演示的 WordNet 模型)所需的性能。
Versa:RDF 查询语言
Versa 是一种开放 RDF 查询语言,它基于开发人员对能够在其它应用程序中使用的 RDF 工具这种典型的需要。它是一种不仅仅具有 RDF 世界观的产品。Versa 着重于 RDF 模型的节点和圆弧而不是将其看作三元部分的集合。它提供核心数据模型以及一组非常丰富的用于灵活查询的函数和原语。因为 Versa 大量使用函数,它有时给人一种类 LISP 的感觉。Versa 也提供如完全的布尔逻辑和集合运算、传递运算、聚合、子串匹配以及其它核心数据类型操作之类的功能,许多其它 RDF 查询系统都不具有这些功能。我是最初的 Versa 规范的作者之一。
Versa 的核心是匹配模型图中模式的遍历表达式。下面是遍历表达式的一个示例。
all() - rdf:type -> *
all() 函数返回模型中所有资源的集合。根据 RDF 模型的约定画法,这意味着它返回所有的椭圆和圆弧(但不返回矩形)。 - 和 -> 记号形成遍历运算符,它表示您希望沿着来自每个资源的特定圆弧遍历。在这个例子中,该圆弧是 rdf:type 。 * 表示您想要该遍历的所有端点。实际上,该遍历表达式返回具有 rdf:type 谓词的所有语句的对象。
作为一个示例,让我们来看前一篇专栏文章中的样本 RDF 问题跟踪器实例,下面的清单 1 重复了该实例。
清单 1:
<?xml version='1.0'?><!DOCTYPE rdf:RDF [
<!ENTITY rdf "http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<!ENTITY rdfs "http://www.w3.org/2000/01/rdf-schema#">
<!ENTITY daml "http://www.daml.org/2001/03/daml+oil#">
<!ENTITY dc "http://purl.org/dc/elements/1.1/">
<!ENTITY foaf "http://xmlns.com/foaf/0.1/">
<!ENTITY it "http://rdfinference.org/schemata/issue-tracker/">
<!ENTITY rit "http://rdfinference.org/ril/issue-tracker/">]><rdf:RDF
xmlns:rdf="&rdf;"
xmlns:rdfs="&rdfs;"
xmlns:daml="&daml;"
xmlns:rit="&rit;"
xmlns:it="⁢"
xmlns:dc="&dc;"
xmlns:foaf="&foaf;"
xmlns="⁢">
<rdf:Description rdf:about='http://rdfinference.org/ril/ril-20010502'>
<issue rdf:resource='&rit;i2001030423'/>
<issue rdf:resource='&rit;i2001042003'/>
</rdf:Description>
<Issue rdf:about='&rit;i2001030423'>
<dc:title>Unnecessary abbreviation</dc:title>
<dc:creator rdf:resource='mailto:Alexandre.Fayolle@logilab.fr'/>
<dc:description>Is the abbreviation of rdf:type predicates needed?</dc:description>
<dc:date>2001-03-04</dc:date>
<comment rdf:parseType="Resource">
<dc:creator rdf:resource='mailto:Alexandre.Fayolle@logilab.fr'/>
<dc:description>The abbreviation in listing 8 doesn't seem needed.</dc:description>
</comment>
<action rdf:parseType="Resource">
<dc:description>Organize a vote on this topic</dc:description>
<it:assignee rdf:resource='mailto:uche.ogbuji@fourthought.com'/>
</action>
</Issue>
<Issue rdf:about='&rit;i2001042003'>
<dc:title>Inconsistent versioning</dc:title>
<dc:creator rdf:resource='mailto:Nicolas.Chauvat@logilab.fr'/>
<dc:description>RIL versioning is unclear (mix of 0.1, 0/1, 0.2 and 0/2)</dc:description>
<dc:date>2001-04-20</dc:date>
<action rdf:parseType="Resource">
<dc:description>Correct all to use the "0/1" form in the next draft.</dc:description>
<it:assignee rdf:resource='mailto:uche.ogbuji@fourthought.com'/>
</action>
</Issue>
<rdf:Description rdf:about='mailto:Alexandre.Fayolle@logilab.fr'>
<foaf:name>Alexandre Fayolle</foaf:name>
</rdf:Description>
<rdf:Description rdf:about='mailto:uche.ogbuji@fourthought.com'>
<foaf:name>Uche Ogbuji</foaf:name>
</rdf:Description>
<rdf:Description rdf:about='mailto:Nicolas.Chauvat@logilab.fr'>
<foaf:name>Nicolas Chauvat</foaf:name>
</rdf:Description></rdf:RDF>
图 1 以图形式演示了该样本。
图 1. RDF 问题跟踪器样本的图模型
如果我们对该模型运行这一查询,我们将得到一列资源(因为 rdf:type 语句的对象是资源)。如果我们正在使用一个可以有文字和资源对象的谓词,结果将是一列文字和资源。要使用 4Suite 测试它(有关 4Suite 的详细信息,请参阅以前的专栏文章),可以将清单 1 复制到文件 issues.rdf,然后在命令行中执行用红色突出显示的命令:
$4versa --rdf-file=issues.rdf "all() - rdf:type -> *"::: Using cDomletteExecuting Query:all() - rdf:type -> *With nsMapping of:vtrav --> http://rdfinference.org/versa/0/2/traverse/xml --> http://www.w3.org/XML/1998/namespacevsort --> http://rdfinference.org/versa/0/2/sort/rdfs --> http://www.w3.org/2000/01/rdf-schema#rit --> http://rdfinference.org/schemata/issue-tracker/it --> http://rdfinference.org/schemata/issue-tracker/rdf --> http://www.w3.org/1999/02/22-rdf-syntax-ns#foaf --> http://xmlns.com/foaf/0.1/versa --> http://rdfinference.org/versa/0/2/None --> http://rdfinference.org/schemata/issue-tracker/daml --> http://www.daml.org/2001/03/daml+oil#<List>
<Resource>http://rdfinference.org/schemata/issue-tracker/Issue/<Resource>
<Resource>http://rdfinference.org/schemata/issue-tracker/Issue</Resource></List>
以简单 XML 形式表示的产生的资源列表是用粗体显示的。该命令的其它输出只是为您提供一些信息。它回显正在被执行的 Versa 查询,并显示引擎知道的名称空间声明。为了方便用户,4versa 自动抓取源文件根元素内的所有名称空间声明。
细述遍历
通常,遍历表达式的格式如下:
list-expression -
list-expression ->
boolean-expression
列表表达式是返回一列资源或可以被转换成一列资源的结果的任何表达式。因此,任何返回单个资源( rdf:type )的表达式都会被转换成具有单个输入项的列表类型。您已经看到过缩写成如 rdf:type 的形式和称为限定名称(或 QNames )形式的 RDF URI。通过将第一部分扩展成 URI 库(例如 rdf 变成 http://www.w3.org/1999/02/22-rdf-syntax-ns# )然后同第二部分连接,将它们转换成完整的 URI。这样, rdf:type 就变成了 http://www.w3.org/1999/02/22-rdf-syntax-ns#type 。Versa 也允许您以完整、冗长和详细地方式拼写出 URI,这意味着您可以直接编写 @"http://www.w3.org/1999/02/22-rdf-syntax-ns#type" 。例如:
all() - @"http://www.w3.org/1999/02/22-rdf-syntax-ns#type" -> *
遍历表达式的第三部分是布尔表达式。我已经向您演示了如何使用 * 选择所有对象。您也可以对结果做更多的选择。例如,要获取模型中资源的所有日期特性,您可以使用:
all() - dc:date -> *
它产生:
<List>
<String>2001-03-04</String>
<String>2001-04-20</String></List>
要选择特定日期,您可以编写:
all() - dc:date -> eq("2001-04-20")
eq 函数将参数同上下文比较,如果它们相同,就返回 true。Versa 中上下文的思想类似于 XPath 中上下文的思想,但是更简单。在 Versa 中,上下文是一个在对表达式求值时考虑的单一值。可以使用点符号直接访问上下文。也可以使用 eq 函数比较两个显式参数,因此上面的内容可以按如下编写:
all() - dc:date -> eq(., "2001-04-20")
在遍历表达式的第三部分中,上下文是第一和第二部分的部分结果之一。例如,在上面,将每个对象都同“2001-04-20”比较,最后的结果是比较值为 true 的对象列表,在这个示例中:
<List>
<String>2001-04-20</String></List>
图 2 演示了遍历表达式的某些工作。
图 2. 遍历表达式的工作演示
这一看起来明显的查询在确定某个特定值是否是模型时十分有用。例如,如果您在上面的示例中用“2001-03-15”替换“2002-04-20”,那么结果将是一个空列表。当然您可以使用遍历表达式做更多的事情。例如,要检索三月份所有日期的资源,您可以编写:
all() - dc:date -> contains("-03-")
它产生:
<List>
<String>2001-03-04</String></List>
向后遍历
获得具有特定日期的 资源 可能更有用。要做到这一点,您需要向后操作,从 dc:date 圆弧所对应的日期到主题资源。Versa 以向后遍历的形式提供这一操作。例如:
"2001-03-04" <- dc:date - *
返回所有日期为“2001-03-04”的资源:
<List>
<Resource>http://rdfinference.org/ril/issue-tracker/i2001030423</Resource></List>
向后遍历的形式是:
list-expression <-
list-expression -
boolean-expression
其工作方式与向前遍历十分类似。这两种遍历都可以链接,这样就可以获取日期为“2001-03-04”的所有资源的标题:
("2001-03-04" <- dc:date - *) - dc:title -> *
它产生:
<List>
<String>Unnecessary abbreviation</String></List>
分配财富
到目前为止,所有查询都返回单一值。通常,您可能希望一次返回多个值。Versa 使用列表操作实现这一点,列表操作运行在遍历表达式的结果之上。处理列表的一个常用函数是 distribute ,它对列表中的每项使用一个或多个表达式。结果是列表的一个列表。可以使用如 list(rit:i2001030423, rit:i2001042003) 这样的表达式在 Versa 中直接表达列表,该表达式是两个资源的列表。下面的表达式获得这些问题中每个问题的标题和日期:
distribute(list(rit:i2001030423, rit:i2001042003), ".-dc:title->*", ".-dc:date->*")
它产生列表的列表:
<List>
<List>
<List>
<String>Unnecessary abbreviation</String>
</List>
<List>
<String>2001-03-04</String>
</List>
</List>
<List>
<List>
<String>Inconsistent versioning</String>
</List>
<List>
<String>2001-04-20</String>
</List>
</List></List>
distribute 函数的第一个参数是一个列表。依次获取列表中的每一项。第二个和后续的参数为字符串,它们被当作子查询。使用上下文的当前列表项对它们进行动态求值(如同前面所讨论的一样,通过使用点来引用)。图 3 演示了该查询是如何工作的。
图 3. distribute 的工作演示
可以对遍历表达式的结果(它们是列表)使用该技术。再举最后一个例子,我将解释如何使用 Versa 中的一个特殊快捷函数。 type 函数检索给定 RDF 类型(就象用 rdf:type 谓词表达那样)的所有资源。要获取所有已经提交问题的人的标识和姓名,可以编写:
distribute(type(it:Issue)-dc:creator->*, ".", ".-foaf:name->*")
请注意,您可以在子表达式中直接使用上下文,而无须使之成为另一个表达式的一部分。结果是:
<List>
<List>
<Resource>mailto:Nicolas.Chauvat@logilab.fr</Resource>
<List>
<String>Nicolas Chauvat</String>
</List>
</List>
<List>
<Resource>mailto:Alexandre.Fayolle@logilab.fr</Resource>
<List>
<String>Alexandre Fayolle</String>
</List>
</List></List>
这里有一个棘手的细节。 .-foaf:name->* 子表达式产生的字符串在列表中,但由 . 子表达式产生的资源却不在其中。这是因为遍历操作符总返回列表,即使结果中只有一个或没有项时也是如此。由于我们知道在我们的模型中,我们仅仅期望每个人的资源只有一个单一名称,所以我们无论如何总是忽略列表的最低一级。Versa 提供数据转换函数,其中之一是 string 函数,它将其参数转换成字符串。通过获取列表的第一(或唯一的)项的字符串值来转换列表。因此,
distribute(type(it:Issue)-dc:creator->*, ".", "string(.-foaf:name->*)")
消除了遍历子表达式周围的无关列表并产生以下结果:
<List>
<List>
<Resource>mailto:Nicolas.Chauvat@logilab.fr</Resource>
<String>Nicolas Chauvat</String>
</List>
<List>
<Resource>mailto:Alexandre.Fayolle@logilab.fr</Resource>
<String>Alexandre Fayolle</String>
</List></List>
结束语
在本文中,我解释了 Versa 的基础知识。如果您掌握了它,那么您立即可以使用它来高效地工作。Versa 有多得多的功能,但是它们大部分是以专门的函数的形式出现,通过使用它们,您将迅速获得经验。参考资料一节列举了更多关于 Versa 的参考资料。在下一篇专栏文章里,我将解释如何使 Versa 满足到目前为止在本系列中讨论的所有查询需要。