我们已经知道,模式匹配算法是用的PHREAK。其实这个算法也是Drools以前版本所使用的一个算法:RETE(也称为RETEOO)算法。
即使从以前的章节中,我们已经了解了PHREAK是什么以及它是如何工作的,但是更详细地了解它的内部结构将给我们提供更好的、更好的性能规则的机会。了解Drools是如何在内部工作的另一个优势是,在对我们的知识资产进行故障诊断的时候,它将大大增加我们的选择。
PHREAK(与RETE相比)的一个主要缺点是,前者是一种全新的算法,它是为Drools和Drools团队开发的。这种年轻算法的缺点是缺少采用和它所提供的稀缺文档。但别慌!到目前为止,PHREAK已经证明了它是一种生产质量的算法,能够处理各种场景中的关键应用程序的复杂性。
本章的概念是介绍PHREAK算法及其特点和特性。为了更好地理解算法,将在这一章中介绍和解释不同的具体例子。
本章所涉及的主题为:
>PHREAK介绍
>PHREAK网络和节点
>PHREAK网络的具体例子
>在PHREAK的背景下,查询和反向链接推理
PS:鉴于我们所拥有的有限空间,本章的目标是介绍Drools的PHREAK算法。本章的大部分概念都被过度简化了,以便更容易地解释和学习。在理解PHREAK的最初想法之后,如果需要更深层次的理解,建议您使用Drools的文档:(http://docs.jboss.org/drools/release/6.3.0.Final/drools-docs/html_single/#ReteOO)
让我们首先介绍PHREAK算法及其组件
Drools的这个心成员的的第一个假设是,DRL文件中的规则按照它们的定义顺序进行评估,就好像每条规则都是由命令式语言构成的顺序评估结构的某种if语句。但是,对规则条件的评估既不是有效的,也不是可伸缩的。更糟糕的是,在这个场景中添加推理功能也将是一场噩梦。
在70年代中期,Charles L.Forgy博士引入了一种新的模式匹配算法,用于生产系统,称为RETE( https://en.wikipedia.org/wiki/Rete_algorithm)。为了提高速度,RETE算法牺牲了内存,比传统的模式匹配算法提供了几个数量级的改进。自从,多个产生式规则系统一直使用派生或定制版的RETE 作为内部模式匹配算法。这一章是基于Drools的RETE的实现及其最新的进化:PHREAK。
PHREAK共享了RETE中大多数的概念——特别是在Drools 2.0实现后实现的面向对象版本。我们将首先从两个算法的共同概念开始,然后重点讨论PHREAK在RETE上的改进。
与RETE一样,PHREAK可以分解规则LHS的模式和约束,从而创建一个指向节点的网络:
在PHREAK中,网络中的第一级节点由入口点(EP)节点组成。我们在《复杂的事件处理》那一部分讲过,作为分割我们事实数据来源的一种方式。在knowledge base中定义的每一个入口点,都对应了我们生成的网络的中的一个节点。
每当将一个事实数据插入会话时,它将通过插入事实数据的相应的入口点节点进入PHREAK网络;如果没有指定,Drools中的默认入口点称为DEFAULT。然后,这个事实数据将遍历网络中的每个连接节点。每个节点都将对这个事实数据进行某种程度的评估以确定它是否应该被传播到下一个节点。
PHREAK网络中的节点分为三个不同的类别,根据他们对这一事实数据的评估类型:Object Type Nodes (OTN),Alpha Nodes,Beta Nodes。PHREAK网络中的每一条可能路径都以Rule Terminal Node或者 Query Terminal Node(QTN)来结束。前者负责执行它们所代表的规则的RHS逻辑,后者与在knowledge base中出现的查询相关联。
本章的下面几节将重点介绍PHREAK网络中呈现的三种评估节点:Object Type 节点、Alpha节点和Beta节点。、
Object Type Nodes对象类型节点对正在测试的事实数据执行类型评估。这种类型评估,你可以类比为对事实数据进行的instanceof方法。只有当当前事实数据是节点所代表的类型(Java类)的实例时,才会将事实数据传播到网络中的下一个节点中。PHREAK网络将包含与它所表示的规则模式中使用的不同类的许多对象类型节点。这意味着在模式中使用同一个类的规则将在PHREAK网络中共享相同的对象类型节点。因此,当一个特定的对象类型节点被评估时,所有与之相关的规则都在同时进行评估。
PS:值得注意的是,一个单一的事实可能满足网络中的多个对象类型节点.比如:一个java.lang.String类型的事实数据会满足一个java.lang.String类型的对象类型接节点(OTN),但是它也满足另一个java.lang.Object的对象类型节点。
作为一个例子,让我们通过下面两个规则来分析一下以下所生成的PHREAK网络:
rule "Sample Rule 1"
when
$c: Customer()
then
channels["customer-channel"].send($c);
end
rule "Sample Rule 2"
when
$o: Order()
then
channels["order-channel"].send($o);
end
前面的规则定义了一个简单的模式。这种情况下的模式是两个不同的类:Customer和Order,这意味着生成的PHREAK网络将包含两个不同的对象类型节点:
前面的网络由一个Entry-point Node(入口点节点,EP)、三个Object Type Nodes(对象类型节点,OTN)和两个Rule Terminal Nodes(规则终端节点,RTN)组成。我们的网络中额外多余的对象类型节点--- InitialFactImpl 类型的那个节点---用于支持Drools中的一些特殊模式,在本章后面将介绍这些模式。
当事实数据通过knowledge base唯一的入口点被插入进来,这三个对象类型节点(OTN)将会被评估。如果传入的事实数据是Customer或者Order类型的话,那么相应的Rule Terminal Node(规则终端节点)将会被执行。重要的是要记住,Drools将规则评估阶段与规则执行阶段区分开来。规则终端节点的执行不会从相应的规则执行RHS的代码;它只会通知Drools的Agenda,为相应的规则准备一个新的匹配项。
PS:与本书绑定的代码包含一个名为phreak-inspector 的模块,用于生成本章的PHREAK图。与本章相关的代码显示了如何使用phreak-inspector,以及如何重新创建本章中的图表。
对象类型子网络(由网络中的所有对象类型节点组成)总是存在于PHREAK中,它的深度总是1:我们永远不会在一个对象类型和节点之后找到另外一个对象类型节点。
在Droolss中,一个模式可以不包含或者包含多个约束。在PHREAK网络中,每个约束模式都被表示为一个Alpha节点。这种类型的节点负责对它所表示的特定约束的评估。如果评估的结果是true,那么将对网络中的下一个节点进行评估:
rule "Sample Rule 1"
when
$c: Customer(age > 30, category == Category.GOLD)
then
channels["customer-channel"].send($c);
end
前面的规则包含有一个两个约束的单一模式:一个是age属性,另一个是catagory属性。在相应的PHREAK网络中,每个约束都将被表示为一个Alpha节点:
当一个Customer事实数据被添加进这个网络中,那么第一个Alpha节点将被评估。如果Customer事实诗句的age属性小于等于30,那么传播将停止,并且下一个Alpha节点也将不会再进行评估。但是如果Customer事实数据的年龄大于30德华,,那么接下的Alpha节点也将会被评估。如果这个的评估结果也是true的话(意味着Customer事实数据的category属性是GOLD),将在 Sample Rule 1规则的Agenda中创建一个匹配项。
PHREAK网络中的Alpha节点的顺序依赖于在DRL中定义相应约束的顺序。如果在规则中,age约束先于category约束,那么在生成的网络中,年龄的Alpha节点将先于类别的Alpha节点。、
就像对象类型节点一样,在一个knowledge base中,可以在多个规则(或者是规则的模式里)之间共享Alpha节点。如果相同而约束被超过一个模式所使用,Drools将优化PHREAK网络的创建,并且将使用一个单一的Alpha节点。
假定现在有下面的2个规则:
rule "Sample Rule 1"
when
$c: Customer(age > 30, category == Category.GOLD)
then
channels["gold-customer-channel"].send($c);
end
rule "Sample Rule 2"
when
$c: Customer(age > 30, category == Category.SILVER)
then
channels["silver-customer-channel"].send($c);
end
较早的两个规则具有相同的条件(age > 30)。这两个规则对应的PHREAK网络看起来像下面的一个:
正如我们所看到的,与复制约束相对应的Alpha节点只在网络中出现一次。除了节省一些内存,Alpha节点共享减少了knowledge base的评估时间,因为在多个规则中存在的条件只需要评估一次。
我们已经提到过,模式中的约束顺序决定了PHREAK网络中相应的Alpha节点的顺序。这个概念对于Alpha节点共享尤其重要。对于要共享的Alpha节点,约束的顺序必须在所有模式中相同。例如:接下来的连个规则在语义上是相同的是与以前的规则是一样的,但是不同的是约束的顺序不同:
rule "Sample Rule 1"
when
$c: Customer(age > 30, category == Category.GOLD)
then
channels["gold-customer-channel"].send($c);
end
rule "Sample Rule 2"
when
$c: Customer(category == Category.SILVER, age > 30)
then
channels["silver-customer-channel"].send($c);
end
如果我们分析新版本的规则产生的PHREAK网络,我们会看待一些有趣的事:
在规则中我们可以看到,现在age约束在两个规则中的优先级是不同的。相应的PHREAK网络将包含一个重复的alpha节点。这里的主要缺点是某些事实(例如, 一个SILVER Customer)将多次触发相同的逻辑约束。
为了在 knowledge base中评估规则的约束,Drools严重依赖于MVEL。默认情况下,MVEL使用一个解释器来评估约束表达式。这意味着,在大多数情况下,对约束的评估并不是在Java字节码级别上发生的,这意味着它的效率不像它所能达到的那样高。
幸运的是,Drools提供了将一个MVEL表达式编译成Java字节码的方法。因为这种编译在运行时发生,所以通常称为JIT(即时)编译。当然,这是有问题的。否则,Drools为什么不简单地将所有的约束编译成字节码呢?JIT编译的一个主要缺点是,它可能是内存密集型的,可能会产生与永久代堆相关的问题。
Drools在JIT编译过程中处理这个缺点的方法是使用一个阈值,在将一个约束在编译成字节码之前进行计算。
这里的假设是,如果一个约束从来没有被评估过,或者只被评估过几次,那么在解释模式下评估它比用jit编译它更有效。在Drools 6.3中,默认阈值是20次。
Drools有两种常见的方法可以根据应用程序的特定需求来调整默认的JIT阈值:
>。当运行我们的应用程序时通过使用 drools.jittingThreshold系统属性, -Ddrools.jittingThreshold=10,可以将阈值设置为10次
>在用于创建我们的KIE Base的KieBaeConfuguration中该设置我么你说想要的阈值:
KieBaseConfiguration kbConfig = KieServices.Factory.get().
newKieBaseConfiguration();
kbCofig.setOption(ConstraintJittingThresholdOption.get(10);
通过设置一个负阈值,可以禁用整个JIT编译。
本章到目前为止所涵盖的规则都没有限定于任何或单一模式。到目前为止,我们知道模式的类型被Drools转换为对象类型节点(OTN),并且它们所包含的每个约束都被转换为一个Alpha节点。但是,当规则由多个模式组成时,会发生什么情况呢?答案是很简单的:一个Beta节点代表了join操作符连接的一对已创建的模式。
Beta节点有两个输入和一个或多个输出,它将一直等待直到数据在两个输入中都可用,然后移动到网络中的下一个节点。
作为一个例子,让我们来评估由以下规则生成的PHREAK网络:
rule "Sample Rule 1"
when
$p: Provider(rating > 50)
$pr: ProviderRequest()
then
channels["request-channel"].send($pr);
end
规则很简单:遍历 Provider对象并要求rating属性需要大于50,然后再产生一个ProviderRequest的事实数据。规则的激活将被放在Drools的Agenda上。这个规则对应的PHREAK网络看起来像下面这样:
在前面的PHREAK网络中,我们可以很容易地识别到我们由于规则的两个模式所带来对象类型节点(OTN)的,以及用于rating属性的约束的Alpha节点。我们还可以看到,网络中的模式是由一个Beta节点连接的。因为在我们的示例规则中,Provider和ProviderRequest之间没有明确的关系,Beta节点将创建所有Provider和ProviderRequest事实数据的 的笛卡儿积。生成的每一个元组将会传播到下一个节点。在这个例子中,规则终端节点将会将规则的一个新的激活加入到Agenda中。
如果我们想要避免事实数据的完整笛卡尔积,我们可以对ProviderRequest模式的 provider属性设置一个约束:
rule "Sample Rule 1"
when
$p: Provider(rating > 50)
$pr: ProviderRequest(provider == $p)
then
channels["request-channel"].send($pr);
end
现在,规则对任何ProviderRequest都不感兴趣,但只有在与先前模式相匹配的Provider中才会出现这种情况。当一个约束涉及到一个绑定到了某些东西上的变量时,约束的评估不会在一个Alpha节点内部发生,而是在Beta节点内执行相应的连接。在我们的样例中,PHREAK将会像下面图示的这样:
在前面的网络中,Beta节点不会为它的输入的事实数据来创建完整的笛卡尔积。只有在Beat节点中匹配约束的那些元组将被转发到网络中的下一个节点。
在这一点上,Beta节点也可以在PHREAK网络中的多个规则之间共享,这一点也不奇怪。作为一个例子,让我们来看看Drools如何为以下两个规则建模PHREAK网络:
rule "Sample Rule 1"
when
$p: Provider(rating > 50)
$pr: ProviderRequest()
then
channels["provider-channel"].send($pr);
end
rule "Sample Rule 2"
when
$p: Provider(rating > 50)
$pr: ProviderRequest()
$o: Order()
then
channels["order-channel"].send($o);
end
前面两个规则中的前两种模式是相同的。我们已经知道,属于第一个模式约束的Alpha节点将在规则中共享。我们还不知道的是,Provider和ProviderRequest模式之间的连接的Beta节点也将被共享:
如我们所见,顶部的Beta节点在两个规则中共享。
需要注意的是,为了让Beta节点共享,规则中模式的顺序很重要。这个概念类似于当我们讨论Alpha节点共享时模式中的约束的顺序:如果顺序不同,Drools将不会优化它。例如,如果我们通过改变前两个模式的顺序来修改第二个规则,那么规则的语义不会改变,但是它在PHREAK中的底层实现是这样的:
rule "Sample Rule 2"
when
$pr: ProviderRequest()
$p: Provider(rating > 50)
$o: Order()
then
channels["order-channel"].send($o);
end
这个案例中的PHREAK网络看起来像下面这样:
因为在我们的规则中前两种模式的顺序不再相同,所以在相应的PHREAK网络中出现了一个新的Beta节点。但是,当涉及到Beta节点共享时,模式的顺序并不是唯一重要的事情:为了使Drools对其进行优化,之前对共享Beta节点的节点也必须是相同的。作为一个例子,让我们回到原来的规则,但是让我们将第二个规则中的Provider的rating约束更改为60.
rule "Sample Rule 2"
when
$p: Provider(rating > 60)
$pr: ProviderRequest()
$o: Order()
then
channels["order-channel"].send($o);
end
我们所做的基本上就是打破两个规则都有的共享的Alpha节点。这意味着前两种模式的Beta节点现在有了不同的输入:
前面的网络显示了两个不同的Alpha节点,它们导致了两个不同的Beta节点。
正如我们所看到的,Beta节点共享并不是特别容易实现的;在设计我们的规则时,当它们在多个规则中被重用时,我们必须始终尝试在模式和约束之间保持相同的顺序。DSL,模板,以及决策表这些都是很好的替代品,以确保这些顺序是为大型知识库所保留的。
当在模式之间使用or条件元素时,PHREAK网络会发生一件奇怪的事情。事实上,Drools并不真正理解有or条件的元素;它所做的是将其转换为在PHREAK网络中语义等价的子规则集。
作为一个例子,假设我们有一个规则来检测当一个非GOLD的Customer有一个 SuspiciousOperation 或者一个大于$100,000的Order。这条规则可以写如下:
rule "Sample Rule 1"
when
$c: Customer(category != Category.GOLD)
(
Order(customer == $c, total > 10000) or
SuspiciousOperation(customer == $c)
)
then
channels["suspicious-customer"].send($c);
end
理想情况下,我们应该使用exist条件元素来确保当一个客户存在多个Order或SuspiciousOperations时,我们没有这个规则的多重激活。但是,鉴于我们还没有在本章中介绍exist条件元素,我们将不使用它。
上述规则将由Drools转换为以下PHREAK网络:
上面的网络显示了Drools生成的两个子规则,通过一个复制的规则终端节点来证明我们拥有的一个规则。Drools的基本做法是将原始规则分成以下两种规则:
rule "Sample Rule 1.1"
when
$c: Customer(category != Category.GOLD)
Order(customer == $c, total > 10000)
then
channels["suspicious-customer"].send($c);
end
rule "Sample Rule 1.2"
when
$c: Customer(category != Category.GOLD)
SuspiciousOperation(customer == $c)
then
channels["suspicious-customer"].send($c);
end
PHREAK网络中的每一个子规则现在都是独立的:它们都可以被独立激活并被触发。这就解释了为什么在Drools中没有这样的模式。
到目前为止,我们已经介绍了PHREAK中存在的基本类型的节点,这些节点允许我们在Drools中创建简单的规则。但也有一些其他的节点有非常具体的行为,这些行为被用于一些条件元素,我们到目前为止还没有讨论过。本节将分析这些条件元素最常用的部分:not,exists, accumulate,和from.
Not条件元素是Drools中的非存在量词,它检查工作内存中一个或多个模式的缺失。
Drools提供了一个专门版本的Beta节点来实现not条件元素的必要逻辑。
作为一个例子,让我们使用以下规则
rule "Sample Rule 1"
when
$c: Customer()
not (SuspiciousOperation(customer == $c))
then
channels["clean-customer-channel"].send($c);
end
当会话内有一个Customer,并且它没有任何的SuspiciousOperation的时候,这个规则就会被激活。在这个例子中,我们可以看出not元素必须是某种Beta节点,因为它实际上是在两个模式之间执行一个连接操作。但是这个join操作并不是我们已经讨论过的。对于这个特定的节点,只有在会话中没有出现否定模式时,执行才会继续执行。
在生成的网络中,Not节点看起来像一个常规的Beta节点,但是我们现在知道它的行为不是。如果我们在会话中首先插入一个Customer事实数据,在Not节点上,它有Customer的对象类型节点和SuspiciousOperation的对象类型节点这两个输入节点,其中的Customer对象节点是有数据输入的,因为是我们手动输入的Customer,但是另一个节点是没有值的。在本例中,执行将继续执行路径中的下一个节点。
但是,如果在插入Customer之前,我么先插入一个 SuspiciousOperation事实数据,那么Not节点在评估的时候,连个输入节点都是有数据的;那么这时候,规则将在此止步。如果我们从会话中撤消我们的SuspiciousOperation,那么Not节点将会被计算为true,路径中的下一个节点将被执行。
现在让我们考虑这样一种情况,我们希望在会话中不存在任何SuspiciousOperation,而不考虑Customer。这条规则可以写成如下:
rule "Sample Rule 1"
when
not (SuspiciousOperation())
then
channels["audit-channel"].send("OK");
end
我们之前提到过,非节点是一个特殊类型的Beta节点。我们也知道Beta节点需要两个输入点。但是,在上述规则的这个场景中,我们的规则中没有其他模式可以连接到Not节点。Drools是如何解决这个问题的?在该规则的前一个版本中,Customer模式触发了Not节点的评估。换句话说,当一个Customer被插入时,对应的OTN就会被评估,然后就是Not节点。但是现在我们没有任何可以触发我们的节点的事实数据:
这个问题的答案是InitialFactImpl事实数据。InitialFactImpl事实数据是一个特殊的事实数据,在PHREAK网络中总是存在(但并不总是使用)。每次创建一个新的KIE Session时,都会自动插入InitialFactImpl。它可以允许我们在上面的这种无法触发规则情形下,某些模式,比如说我们现在介绍的Not条件元素,可以被评估。
在我们的PHREAK网络中,InitialFactImpl事实数据的含义是,一旦从它创建了一个KIE Session,就会激活Sample Rule 1规则。
Exist条件元素用于测试工作内存中一个或多个模式的存在。不管这种模式有多频繁,exists条件元素只会触发一次。就像Not条件事件一样,在Drools中,也通过一个特定版本的Beta节点来实现:Exist节点
为了演示这个节点是如何在Drools中实现的,让我们来看看前面一节中介绍的规则:当Customer有一个或多个 SuspiciousOperations时,该规则将被激活。
rule "Sample Rule 1"
when
$c: Customer()
exists SuspiciousOperation(customer == $c)
then
channels["dirty-customer-channel"].send($c);
end
如果没有exists条件元素,那么这个规则将会为客户可能拥有的每一个 SuspiciousOperations,都单独激活规则。有了Exists元素的使用,我们告诉Drools,我们只希望这个规则在每个客户只被激活一次。前面的规则生成的PHREAK网络与我们使用Not条件元素时所使用的PHREAK网络非常相似。这里的区别在于Exists节点的行为比较于Not节点的行为:
在这种情况下,Exists的节点将跟踪 SuspiciousOperations事实数据,并且只有在相应的输入中至少有一个时才会对其进行评估为真。一旦这个输入为空,存在的节点就会被评估为false。
Drools中也经常使用Exists条件元素,但是却没有任何其他模式。例如,当我们的会话中至少有一个SuspiciousOperations时被激活的规则可以如下所写:
rule "Sample Rule 1"
when
exists SuspiciousOperation()
then
channels["audit-channel"].send("FAIL");
end
在这种情况下,就像我们使用Not条件元素时一样,我们没有任何模式(或者说是在PHREAK网络中的节点)导致对现有节点的评估。这里的解决方案与前面的方法类似:InitialFactImpl事实数据。
在这种情况下,InitialFactImpl事实被用作辅助:它在Exists节点中的对应输入总是包含一个事实数据。这意味着,在这个特殊的情况下,这个节点唯一感兴趣的是,它是一个SuspiciousOperation。
Drools中的另一个非常有用的条件元素是accumulate元素。这个元素在之前讲过。它是在KIE Session中对事实进行 accumulate函数的一种方式。在PHREAK中,accumulate条件元素表示一个Beta节点的变化,我们称呼它为Accumulate节点。Accumulate节点将根据从一个输入中传入的事实数据来执行相应的accumulate函数。在应用了函数之后,执行将一直持续到path中的下一个节点。
要了解如何通过Drools对一个 accumulate条件元素进行处理,让我们分析以下规则:
rule "Sample Rule 1"
when
$c: Customer()
accumulate( Order(customer == $c), $n: count(1))
then
channels["audit-channel"].send($n);
end
这里没有新东西。规则是计算我们会话中每个Customer的所有Order。现在让我们看看相应的PHREAK网络的样子:
在这个网络中首先注意到的是新的Accumulate节点。在我们的情景中,Accumulate节点将会为每一个传入进来的Order应用count函数。然后,然后,一个包含了 accumulate函数结果的元组,以及从其他输入传递进来的的每一个客户将被传播到下一个节点。
需要注意的另一件重要的事情是,订单模式中的约束(customer==$c)并不是网络本身的一部分。在执行相应的accumulate函数之前,Accumulate节点将在内部解析任何约束。
当一个 accumulate条件元素在规则中的任何其他模式之前被使用时,InitialFactImpl事实数据再次被用作辅助工具。例如,下面的规则可以用于计算会话中的所有Order事实数据:
rule "Sample Rule 1"
when
accumulate( Order(), $n: count(1))
then
channels["audit-channel"].send($n);
end
在这种情况下,生成的PHREAK网络看起来像下面的一个:
再一次,InitialFactImpl事实数据是在没有两个显式输入节点的情况下对Beta节点进行辅助的。
因为PHREAK网络的全部内容都是关于实际情况的评估,Drolls里的from条件元素的实现就有点模糊了:这个条件元素在PHREAK网络里代表的是一个单独的节点,它的LHS和RHS都会被执行和评估。
为了说明如何在PHREAK中表示一个条件元素,下面让我们考虑以下规则:
rule "Sample Rule 1"
when
$o: Order()
$ol: OrderLine(
item.category == Category.HIGH_RANGE,
quantity > 10) from $o.getOrderLines()
then
channels["audit-channel"].send($ol);
end
对于前面的规则,当传入的事实数据Order的每一个OrderLined对象,当其包含至少10个分类为Category.HIGH_RANGE的item的时候,该规则被激活。在本例中,OrderLines本身并不是会话中的事实数据:他们是使用from条件事件从每个Order中获取的。现在,我们知道模式及其约束在PHREAK中是如何表示的。我们可能希望这个例子所生成的网络包含 OrderLine这个对象类型节点(OTN),并追随者两个Alpha节点(我们看懂它是由两个约束的):一个是catagory约束,另一个是quantoty约束。
出乎我们的意料,有from条件元素的这个模式的左边部分,并没有转换为PHREAK节点。我们之前提及过,真相其实是:我们在规则中评估的OrderLines并不是事实数据;这也是为什么在PHREAK中没有其评估路径的原因。当在前面的网络中执行From节点时,将执行右边的操作,并且在左侧的模式对每个结果对象进行评估。
现在我们已经了解了Drools是如何在knowledge bases中评估规则的,让我们来讨论另一个在本书中没有涉及到的主题:逆向链接推理。
Queries在我们之前有讲过,即理解KIE Session那一节中。他是一种从KIE Session中检出信息的方式。实际上,Queries是Drools实现backward-chaining reasoning(后向链接推理)的方式。但是在进入主题之前,鉴于我们已经讨论了PHREAK,让我们看看PHREAK网络中的常规查询是如何进行的。
对于本书的这一部分,我们将介绍一个新的Java类,它将用于在Item对象之间建立一个完整的关系。这意味着一个Item可以由其他Items组成。
Items之间的整体关系被建模为泛型类,名字为:IsPartOf。这个泛型类允许我们去定义非侵入式的关系,不仅仅是在Items之前,而是在模型中任何其他类型的对象之间的。作为一个样例,如果你想去指定car,engine和distributor Item它们之间的关系,我们可以用下面的代码片段来做:
//The constructor arguments are: name, cost and sale price.
Item car = new Item("car", 15000D, 20000D);
Item engine = new Item("engine", 5000D, 7000D);
Item distributor = new Item("distributor", 200D, 280D);
//The constructor arguments are: whole and part
IsPartOf- r1 = new IsPartOf<>(car, engine);
IsPartOf
- r2 = new IsPartOf<>(engine, distributor);
如果所有的Item和IsPartOf对象都是knowledge base中的事实数据,我们可以写下面的这样一个query来知道某个Item是否属于另一个Item:
query isItemContainedIn(Item p, Item w)
IsPartOf(whole == w, part == p)
end
前面的Query查询有以这要的限制:它不会暴露出IsPartOf关系的可传递性。换句话说,如果我们使用这个查询去询问distributor是car的一部分,那么回答是No。我们稍后再关注这个限制;现在,我们如果让PHREAK来展示上面的查询。
在上面的PHREAK网络中,我们首先发现的有趣的事是 DroolsQuery这个对象类型节点的存在。这个类被用于表示在Drools中的每一个Query查询,并且它包含一些身份信息,比如说当前的Query查询的名字,参数列表等。当在Drools中调用查询时,将使用相应的名称和参数创建一个新的实例,并将其插入到KIE Session中作为事实数据。
在DroolsQuery对象类型节点之后的是一个Alpha节点,它用于甄别Query查询的名字。他的Beta节点是与DroolsQuery和IsPartOf模式连接在一起的。这实际上是一个Beta节点,因为在IsPartOf模式中使用的w和t变量与DroolsQuery事实数据的参数绑定在一起。
网络中的最后一个节点是一个新类型的节点:一个查询终端节点。该节点将负责查询结果的生成。
Drools支持在模式中使用:=符号实现统一。这意味着同样的变量可以在多个地方使用:变量的第一次出现将把它绑定到一个值,任何其他的出现这个变量,都会被限制到相同的值。
下面的是我们讲过的<<复制事件处理>>那一节的样例,我们会尝试以unification重写它。
rule "More than 10 transactions in an hour from one client"
when
$t1: TransactionEvent($cId: customerId)
Number(intValue >= 10) from accumulate(
$t2: TransactionEvent(
this != $t1,
customerId == $cId,
this meets[1h] $t1
),
count($t2)
)
not (SuspiciousCustomerEvent(customerId == $cId, reason ==
"Many transactions"))
then
insert(new SuspiciousCustomerEvent($cId, "Many transactions"));
end
上面规则中的$cId变量绑定(定义)在第一个模式下,并在下面两个地方使用了它。那么现在使用unification的话,规则就会写成下面这样:
rule "More than 10 transactions in an hour from one client"
when
$t1: TransactionEvent($cId := customerId)
Number(intValue >= 10) from accumulate(
$t2: TransactionEvent(
this != $t1,
$cId := customerId,
this meets[1h] $t1
),
count($t2)
)
not (SuspiciousCustomerEvent($cId := customerId, reason ==
"Many transactions"))
then
insert(new SuspiciousCustomerEvent($cId, "Many
transactions"));
end
在规则的第一条模式中,变量$cId首次被使用,它将会绑定到 TransactionEvent事实数据的customerId这个属性的值上。任何其他地方对于这个变量的引用,都会被Drools转换为一个相等约束上。
对于规则来说,Drools的unification(统一)特性主要是语法糖。但是当在查询中使用unification(统一)时,事情就变得有趣了。
回到我们的 isItemContainedIn查询,让我们假设现在我们也有兴趣了解包含了指定的Item的所有的Items。下面看规则:
//Query to know if an Item is part of another
query isItemContainedIn(Item p, Item w)
IsPartOf(whole == w, part == p)
end
//Query to know all the parts of an Item
query getItemParts(Item w)
IsPartOf(whole == w, $p: part)
end
//Query to know all the Items a specific Item is part of
query getItemsFromAPart(Item p)
IsPartOf($w: whole, part == p)
end
好消息是,查询中的unification使我们有了可选参数的可能性。使用unification,前面的三个查询可以被重写为一个单独的查询.
query isItemContainedIn(Item p, Item w)
IsPartOf(w := whole, p := part)
end
当执行查询时,如果提供了两个参数,那么IsPartOf模式中的unification符号将被视为约束。对于没有提供的任何参数,unification符号将作为绑定。根据它的输入,这个查询的结果将在下表中解释:
p | w | Resulting Pattern |
---|---|---|
bound | bound | IsPartOf ( whole == w, part == p) |
bound | not bound | IsPartOf ( w: whole, part == p) |
not bound | bound | IsPartOf ( whole == w, p: part) |
not bound | not bound | IsPartOf ( w: whole, p: part) |
在查询中绑定的参数被称为input参数,非绑定的参数作为output参数。
在Java中,我们在执行Query查询时需要使用非绑定参数的方式,是为非绑定参数使用一个是 org.kie.api.runtime.rule.Variable.v的特殊的对象。
//engine and car are Item instances inserted as facts.
//Both arguments are bound
QueryResults qr1 = ksession.getQueryResults("isItemContainedIn",
engine, car);
//Argument 'p' is bound. Argument 'w' will be bound in the result of
the query to
//the corresponding values.
QueryResults qr2 = ksession.getQueryResults("isItemContainedIn",
engine, Variable.v);
与本章相关的资源包含不同的测试,显示了如何在查询中使用统一,以允许使用可选参数。hreakInspectorQueryTest就是一个很好的起点。
Drools中的位置参数,可以在不需要显式地命名它们的情况下向字段中添加相等约束。模式中的位置参数的顺序决定了它所引用的模式类的哪个字段属性。所以,举个栗子,模式IsPartOf(w == whole, p == part)可以简单地重写为IsPartOf(w, p;) 。由于有条件的参数可以与常规的约束一起使用,因此使用分号来表示位置参数部分的结束。
在一个模式中参数的位置与这个属性所代表的东西之间的映射,我们显式的使用org.kie.api.definition.type.Position注解。这个注释只能在类的字段级使用,它将获取一个指定其顺序的整数值。为了能够在IsPartOf类上使用位置参数,我们需要向下面这样标记属性:
public class IsPartOf<T> {
@Position(0)
private final T whole;
@Position(1)
private final T part;
...
}
声明类型的字段也可以用@ position注释进行注释,这不是必需的。by default the order in which the fields of a declared type are declared is used as its positional argument order。
因为@ position注释可以通过子类继承,可能会有冲突值可能出现。在这些情况下,超类中的字段将优先于子类中的优先级。
位置参数的另一个重要特性是,它们总是使用unification来解决问题;如果作为参数使用的变量还没有绑定,就会创建一个新的绑定。
既然我们已经了解了Drools中的查询的一些新技巧,我们就准备引入一个依赖于它们的新主题:逆向推理(也称为反向链接)。
自从早期开发以来,Drools一直是一个响应性的正向串行引擎。规则对会话的状态作出反应,它们的操作部分将引入或修改可用的知识,这些知识可能会导致新规则的激活和执行。在这种类型的系统中,可以处理可用的数据,直到达到目标为止。
光谱的另一端属于反向链接系统。在这里,起始点是期望的目标,系统向后工作,检查会话中的数据是否满足它。
Drools使用查询,实现了一定程度的逆向推理的方法。在你想推理的世界里,查询可以看作是需要被引擎满足的目标或子目标。但在专家级的规则体系中,比如说Drools,规则的个别条件也可以看作是子目标。Drools通过允许查询作为规则的条件来将前和后的推理结合在一起的方式。
作为一个例子,让我们假设我们的系统中有以下项目,我们知道它们之间的关系:
在代码中,前面的图可以写成
Item car = new Item("car", 15000D, 20000D);
Item engine = new Item("engine", 5000D, 7000D);
Item wheel = new Item("wheel", 50D, 75D);
Item battery = new Item("battery", 100D, 150D);
Item distributor = new Item("distributor", 200D, 280D);
IsPartOf- r1 = new IsPartOf<>(car, engine));
IsPartOf
- r2 = ksession.insert(new IsPartOf<>(car, wheel));
IsPartOf
- r3 = ksession.insert(new IsPartOf<>(engine, battery));
IsPartOf
- r4 = ksession.insert(new IsPartOf<>(engine,
distributor));
现在我们假定我们想要为包含相关Items的Orders(通过IsPartOf关系)申请一个5%的折扣。例如,一个包含引擎和电池的订单会得到折扣,但是包含一个轮子和一个经销商的订单不会。在这种情形下,应用折扣的“子目标”之一是,在订单的两个Items之间是否存在IsPartOf关系。在本章的前一节中,我们已经研究了一个查询,它将允许我们确定items之间的关系。我们所能做的就是使用我们在新规则中创建的查询,该规则将应用相应的折扣:
rule "Apply discount to orders with related items"
no-loop true
when
$o: Order()
exists (
OrderLine($item1 := item) from $o.orderLines and
OrderLine($item2 := item) from $o.orderLines and
**isItemContainedIn($item1, $item2;)**
)
then
modify ($o){ increaseDiscount(0.05) };
end
上面的规则而我们可以这样解读:当我们有一个Order,并且它包含至少连个Items(当然可能是一样的),其中的一个是另一个一部分,那么就会为其应用一个5%的折扣。在规则中特性显示的,就是exist的第三个子句,它是对于isItemContainedIn这个Query查询的调用。在这个特定的场景,当$item1和$item2变量一旦有了值时,这个Query将会被马上评估。然后,Drool会尝试看看这两个项目是否满足了IsPartOf的目标。
但是请记住,我们知道在我们的查询中有一个很大的限制:包含经销商和汽车的订单不会有任何折扣,即使它们是通过IsPartOf关系来传递的。既然我们知道查询可以作为Drools中的一个模式使用,那么有一种简单的方法可以解决这个问题:
query isItemContainedIn(Item p, Item w)
IsPartOf(w, p;)
or (IsPartOf(x, p;) and isItemContainedIn(x, w;))
end
我们的查询的新版本现在包含了一个递归调用,它将处理关系的传递性方面。
我们继续关注这个Query调用。当我们传递给它的参数是这样的时候: isItemContainedIn(engine, car),那么第一个模式将会被匹配,因为我们在这两个item之间有明确的关系。如果我们是以这样的形式来调用的Query查询: isItemContainedIn(distributor, car),那么对于这两个Item,是没有isPartOf关系的,所以第一条模式就不会匹配。但是我们现在已经在我们的查询中引入了一条新的路径;当IsPartOf(x,p;)模式被评估,x是一个未绑定变量,Drools将替换为engine这个item(因为发送机和经销商确实有一个IsPartOf关系)。现在x被绑定了,那么这个查询现在就回被递归的调用,就像 isItemContainedIn(engine,car)。递归调用确实会导致匹配(对于car和engine确实存在IsPartOf关系),这意味着原始查询也会导致一个查询。
关于查询的最后一个问题是,“如何在PHREAK中解析Query调用”。答案依赖于我们尚未引入的一种新类型的节点:查询元素节点。
之前的章节,我们了解了实时查询,以及如何将一个ViewChangedEventListener附加到它们,以便在新信息可用时实时得到通知。这基本上就是一个响应式查询元素节点的工作方式。它将自己作为一个ViewChangedEventListener注册到对应的查询中,以对之前生成的结果的新结果或修改作出反应。
下面是PHREAK网络:
这个网络被可视化地分成了两个部分:一个对应于查询,另一个对应于规则。这个网络的一些有趣的方面是:
>它包含两个查询终端节点,因为我们在查询中使用了or方法
>它包含两个查询元素节点:一个用于查询内部的递归调用,另一个用于在规则中调用查询。
>没有明确的关系没有箭头)将查询中的任何节点与规则的节点连接起来。不需要这种关系,因为查询和任何相关的查询元素节点之间的通信是使用ViewChangedEventListener完成的。
当一个PHREAK引擎启动起来,所有的规则都是unlinked的。但是一个unlinked的骨子额,Drools是不会对其进行评估的。当insert/update/delete等操作更改了KIE Session的状态,这种更改知识传播到alpha子网络,并在进入beta子网络之前排队。不像RETEOO,在Drools的PHREAK中,没有任何Beta节点作为这些操作的结果被评估。启发式决定哪些规则最有可能导致匹配,从而在它们之间强加了一个评估顺序。
只有当规则的所有节点都有要被评估的数据时,才会被认为是linked的规则。但是规则的节点一旦变为linked,规则的所有节点就不会被评估;所有链接的规则都被添加到一个队列中,按照每个规则的重要性排序。不同的Agenda组有不同的队列,并且只对激活的Agenda组队列中的规则进行评估。
从API的角度来看,RETEOO和PHREAK之间没有区别。但是在内部,PHREAK将延迟对beta子网络的评估,直到调用了fireAllRules()方法,而不是调用insert、update或delate操作。
喜欢看的自己看吧,我看不懂~
在进入本书的下一章之前,介绍一个广泛用于创建本章的实用工具类(从与这本书相关的源包)是很重要的,这个类就是 org.drools.devguide.phreakinspector.model.PhreakInspector。
本章所示的PHREAK网络图都不是手动生成的;相反,所有这些都是由一个包含我们想要展示的规则和/或查询的基库自动生成的。phreak-inspector 模块的 PhreakInspector类就是为这个目的而创建的。这个类可以从各种资源输出PHREAK网络图,包括:
>手动建立的KIE Base
>kmodule.xml文件中定义的KIE Base
>一系列的资源文件,比如说REL,DSL,决策表,等等
在与本章相关联的源包中,您将发现大多数测试实际上都使用了 PhreakInspector类。实际上,您将发现在本章中显示的所有图形都可以从测试中重新创建。PhreakInspector的基础用法如下:
KieBase kbase = //Obtain a KIE Base from somewhere.
PhreakInspector inspector = new PhreakInspector();
InputStream is = inspector.fromKieBase(kbase);
结果的图像是使用DOT语言。DOT是一种基于文本的格式,用于定义图形。有一些工具可用来显示图,Graphviz(www.graphviz.org)是最受欢迎的。
一旦我们熟悉了PhreakInspector类,我们就可以使用它去图像话任何额的KIE Base,规则和在本书中介绍的Query查询。我们甚至可以在我们自己的项目中使用这个类来更好地理解我们的KIE Base的内部表示,以便寻找改进它们的方法。