索引不传递和传递的n元关系。
项目描述
关系目录
概述
关系目录可用于优化有限、预设维度的N元关系的不传递和传递搜索。
例如,您可以索引简单的双向关系,如员工到主管;主题-谓词-对象风格的RDF三元组;以及更复杂的关系,例如具有上下文和状态的主题-谓词-对象。这些可以以可变定义的传递行为进行搜索。
该目录可用于ZODB或独立使用。它是一个通用、相对无政策的工具。
通常期望它作为更专业化和受限制的工具和API的引擎使用。这样的工具有zc.relationship容器、plone.relations容器和zc.vault。该包中的文档(包括本文件),描述了其他可能的用途。
历史
这是对zc.relationship包仅限于ZODB部分的重构。具体来说,zc.relation目录在很大程度上等同于zc.relationship索引。zc.relationship 2.x线中的索引是zc.relation目录的一个几乎完全向后兼容的包装器。尽管zc.relationship将继续维护,但预期活跃的开发将进入zc.relation。
许多想法来自与Casey Duncan、Tres Seaver、Ken Manheimer等人讨论和代码。
设置关系目录
在本节中,我们将介绍以下概念。
关系是具有索引值的对象。
您可以向关系目录添加值索引以进行搜索。值可以使用可调用或接口元素识别到目录中。必须将索引值指定为目录中的单个值或集合。
关系及其值以令牌的形式存储在目录中:唯一标识符,您可以将其解析回原始值。整数是最有效的令牌,但其他也可以工作得很好。
令牌类型决定了所需的BTree模块。
您必须定义自己的令牌化和解析令牌的函数。这些函数被注册到目录中,用于关系及其每个值索引。
关系用索引(index)索引。
我们将使用一个简单的双向关系作为示例。有关更复杂的RDF风格的主题-谓词-对象设置的简要介绍,可以在文档的后面找到。
创建目录
想象一个从值到另一个值的双向关系。让我们假设我们正在模拟人员与其主管的关系:员工可能有一个主管。对于这个第一个例子,员工与主管之间的关系将是内在的:员工有一个指向主管的指针,员工对象本身表示关系。
进一步来说,为了简单起见,假设员工姓名是唯一的,可以用来表示员工。我们可以使用姓名作为我们的“令牌”。
令牌类似于关系数据库中的主键。令牌是识别对象的一种方式。它必须可靠排序,并且您必须能够编写一个可调用,在给定的上下文中可靠地将对象解析为对象。在Zope 3中,intids(zope.app.intid)和keyreferences(zope.app.keyreference)是合理的令牌示例。
如我们在下面将要看到的,你提供了一种将对象转换为标记的方法,以及将标记解析为对象的方法,用于关系和每个值索引。它们可以是完全相同的函数,也可以是完全不同的函数,具体取决于你的需求。
为了提高速度,整数是最佳标记;其次是其他不可变对象,如字符串;然后是非持久对象;最后是持久对象。选择也决定了下面的BTree模块的选择。
下面是我们的玩具Employee示例类。再次,我们将使用员工姓名作为标记。
>>> employees = {} # we'll use this to resolve the "name" tokens >>> from functools import total_ordering >>> @total_ordering ... class Employee(object): ... def __init__(self, name, supervisor=None): ... if name in employees: ... raise ValueError('employee with same name already exists') ... self.name = name # expect this to be readonly ... self.supervisor = supervisor ... employees[name] = self ... # the next parts just make the tests prettier ... def __repr__(self): ... return '<Employee instance "' + self.name + '">' ... def __lt__(self, other): ... return self.name < other.name ... def __eq__(self, other): ... return self is other ... def __hash__(self): ... ''' Dummy method needed because we defined __eq__ ... ''' ... return 1 ...
因此,我们需要定义如何将员工转换为他们的标记。我们将标记化称为“转储”函数。相反,将标记解析为对象的函数称为“加载”。
转储关系和值的函数需要几个参数。第一个参数是要标记化的对象。接下来,因为有时提供上下文有帮助,是目录。最后一个参数是将在给定搜索中共享的字典。该字典可以忽略,或用作优化(例如,存储已查找的实用工具)的缓存。
对于这个例子,我们的函数很简单:我们说标记将是员工的姓名。
>>> def dumpEmployees(emp, catalog, cache): ... return emp.name ...
如果你将关系目录持久化存储(例如,在ZODB中),请注意你提供的可调用对象必须是可序列化的——例如模块级函数。
我们还需要一种将标记转换为员工的方法,即“加载”。
“加载”函数获取要解析的标记;目录用于上下文;字典缓存用于后续调用的优化。
你可能已经注意到,在我们的Employee.__init__中,我们在全局字典employees(定义在类定义上方)中保留了一个从名称到对象的映射。我们将使用它来解析标记。
>>> def loadEmployees(token, catalog, cache): ... return employees[token] ...
现在我们知道了足够的信息来开始使用目录。我们将通过指定如何标记关系和应该使用哪种类型的BTree模块来存储标记来实例化它。
如何选择BTree模块?
如果标记是32位整数,请选择BTrees.family32.II、BTrees.family32.IF或BTrees.family32.IO。
如果标记是64位整数,请选择BTrees.family64.II、BTrees.family64.IF或BTrees.family64.IO。
如果是其他任何东西,请选择BTrees.family32.OI、BTrees.family64.OI或BTrees.family32.OO(或BTrees.family64.OO——它们是相同的)。
在这些规则中,选择在你不打算将这些结果与使用特定BTree模块的另一个源合并的情况下是有些任意的。BTree集合操作仅在同一模块内工作,所以你必须匹配模块到模块。目录默认为IF树,因为这是标准zope目录使用的。这是一个合理的选择,如果你的标记实际上与zope目录使用的标记相同,并且你想进行一些集合操作,那么可能会很有用。
在这个例子中,我们的标记是字符串,所以我们需要OO或OI变体。我们将任意选择BTrees.family32.OI。
>>> import zc.relation.catalog >>> import BTrees >>> catalog = zc.relation.catalog.Catalog(dumpEmployees, loadEmployees, ... btree=BTrees.family32.OI)
旧的zc.relationship索引实例,在新版本中是zc.relation目录的子类,过去在内部数据结构中有一个字典。我们在这里指定这一点,以便将字典转换为OOBTree的代码有机会运行。
>>> catalog._attrs = dict(catalog._attrs)
看!一个关系目录!不过,到目前为止,我们无法用它进行非常多的搜索,因为目录没有任何索引。
在这个例子中,关系本身代表员工,所以我们不需要单独索引它。
但我们确实需要一个方法来告诉目录如何找到关系的另一端,即主管。您可以使用来自zope.interface Interface的属性或方法来指定这一点,或者使用可调用对象。我们现在将使用可调用对象。这个可调用对象将接收索引关系和上下文目录。
>>> def supervisor(emp, catalog): ... return emp.supervisor # None or another employee ...
我们还需要指定如何对那些值进行标记化(存档和加载)。在这种情况下,我们可以使用与关系本身相同的功能。然而,请注意,我们可以为每个“值索引”或关系元素指定完全不同的存档和加载方式。
我们还可以指定要调用的索引名称,但默认将使用函数(或接口元素)的__name__,这对我们来说现在就足够了。
现在我们可以添加“主管”值索引。
>>> catalog.addValueIndex(supervisor, dumpEmployees, loadEmployees, ... btree=BTrees.family32.OI)
现在我们有一个索引[3]。
添加值索引可能会产生几个异常。
您必须提供存档和加载或两者都不提供。
>>> catalog.addValueIndex(supervisor, dumpEmployees, None, ... btree=BTrees.family32.OI, name='supervisor2') Traceback (most recent call last): ... ValueError: either both of 'dump' and 'load' must be None, or neither
在这个例子中,即使我们修复了它,我们也会得到一个错误,因为我们已经索引了主管函数。
>>> catalog.addValueIndex(supervisor, dumpEmployees, loadEmployees, ... btree=BTrees.family32.OI, name='supervisor2') ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ('element already indexed', <function supervisor at ...>)
您也不能在相同名称下添加不同的函数。
>>> def supervisor2(emp, catalog): ... return emp.supervisor # None or another employee ... >>> catalog.addValueIndex(supervisor2, dumpEmployees, loadEmployees, ... btree=BTrees.family32.OI, name='supervisor') ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ('name already used', 'supervisor')
最后,如果您的函数没有__name__,并且您没有提供,则无法添加索引。
>>> class Supervisor3(object): ... __name__ = None ... def __call__(klass, emp, catalog): ... return emp.supervisor ... >>> supervisor3 = Supervisor3() >>> supervisor3.__name__ >>> catalog.addValueIndex(supervisor3, dumpEmployees, loadEmployees, ... btree=BTrees.family32.OI) ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: no name specified
>>> [info['name'] for info in catalog.iterValueIndexInfo()] ['supervisor']
添加关系
现在让我们创建一些员工。除了一个以外,所有员工都将有主管。如果您还记得我们的玩具Employee类,构造函数的第一个参数是员工姓名(因此是标记),可选的第二个参数是主管。
>>> a = Employee('Alice') >>> b = Employee('Betty', a) >>> c = Employee('Chuck', a) >>> d = Employee('Diane', b) >>> e = Employee('Edgar', b) >>> f = Employee('Frank', c) >>> g = Employee('Galyn', c) >>> h = Employee('Howie', d)
这是一个层次结构的图。
Alice __/ \__ Betty Chuck / \ / \ Diane Edgar Frank Galyn | Howie
让我们使用index方法告诉目录关于这些关系。
>>> for emp in (a,b,c,d,e,f,g,h): ... catalog.index(emp) ...
我们现在已经创建了关系目录并向其中添加了关系。我们准备好了搜索!
搜索
在本节中,我们将介绍以下概念。
对关系目录的查询是用字典形成的。
查询键是要搜索的索引名称,或者在精确关系的特殊情况下,是zc.relation.RELATION常量。
查询值是要匹配的结果的标记;或者None,表示值为None的关系(或者如果是多个,则为空集合)。搜索值可以使用zc.relation.catalog.any(args)或zc.relation.catalog.Any(args)来指定多个(非None)结果以匹配给定的键。
索引有多种方法可以帮助您处理标记。通常最常用的是tokenizeQuery,尽管还有其他方法可用。
要找到与查询匹配的关系,请使用findRelations或findRelationTokens。
要找到与查询匹配的值,请使用findValues或findValueTokens。
通过使用查询工厂进行传递搜索。zc.relation.queryfactory.TransposingTransitive是一个好的通用案例工厂,允许您在层次结构中向上和向下遍历。查询工厂可以作为参数传递给搜索方法作为queryFactory,或者使用addDefaultQueryFactory安装为默认行为。
要找到查询是如何相关的,请使用findRelationChains或findRelationTokenChains。
要确定查询是否相关,请使用canFind。
循环传递关系通过处理以防止无限循环。它们在findRelationChains和findRelationTokenChains中通过zc.relation.interfaces.ICircularRelationPath标记接口来识别。
搜索方法共享以下参数
maxDepth,限制搜索的传递深度;
filter,允许代码过滤传递路径;
targetQuery,允许查询基于端点过滤传递路径;
targetFilter,允许代码基于端点过滤传递路径;以及
queryFactory,上面提到过。
您可以设置搜索索引以加快特定传递搜索。
查询,findRelations,和特殊查询值
那么谁为Alice工作?这意味着我们想要获取与Alice的主管有关的关系——员工。
向目录提问的核心是一个查询。查询被拼写为一个字典。主要思想是,字典中的键指定索引名称,值指定约束。
查询中的值总是用标记表示。目录有多个辅助工具来简化这一点,但在此我们先利用我们的标记易于理解这一事实。
>>> sorted(catalog.findRelations({'supervisor': 'Alice'})) [<Employee instance "Betty">, <Employee instance "Chuck">]
Alice是Betty和Chuck的直接(非传递)主管。
如果你想知道“谁都不向任何人汇报?”那么你需要询问一个主管为None的关系。
>>> list(catalog.findRelations({'supervisor': None})) [<Employee instance "Alice">]
Alice是唯一一个不向任何人汇报的员工。
如果你想知道“谁向Diane或Chuck汇报?”那么你使用zc.relation Any 类或 any 函数传递多个值。
>>> sorted(catalog.findRelations( ... {'supervisor': zc.relation.catalog.any('Diane', 'Chuck')})) ... # doctest: +NORMALIZE_WHITESPACE [<Employee instance "Frank">, <Employee instance "Galyn">, <Employee instance "Howie">]
Frank、Galyn和Howie各自向Diane或Chuck汇报。[4]
Any 可以进行比较。
>>> zc.relation.catalog.any('foo', 'bar', 'baz') <zc.relation.catalog.Any instance ('bar', 'baz', 'foo')> >>> (zc.relation.catalog.any('foo', 'bar', 'baz') == ... zc.relation.catalog.any('bar', 'foo', 'baz')) True >>> (zc.relation.catalog.any('foo', 'bar', 'baz') != ... zc.relation.catalog.any('bar', 'foo', 'baz')) False >>> (zc.relation.catalog.any('foo', 'bar', 'baz') == ... zc.relation.catalog.any('foo', 'baz')) False >>> (zc.relation.catalog.any('foo', 'bar', 'baz') != ... zc.relation.catalog.any('foo', 'baz')) True
findValues 和 RELATION 查询键
那么我们如何找到员工的直接主管是谁?嗯,在这种情况下,看看员工的属性!如果你可以使用一个属性,这通常在ZODB中是一个胜利。
>>> h.supervisor <Employee instance "Diane">
同样,正如我们在本例开始时提到的,主管的知识是“内在于”员工实例的。向目录提出这类问题既可能又容易,但目录语法更适合“外部”关系,比如从主管到员工的这种关系:主管对象与其员工之间的连接对主管来说是外部的,因此你可能真的需要一个目录来找到它!
然而,我们将非常简要地探讨语法,因为它引入了一对重要的搜索方法,并且它是我们第一次传递搜索的垫脚石。
那么,关系目录,Howie的上级是谁?
要提出这个问题,我们想要从关系中获取索引值:findValues。在其最简单形式中,参数是你想要的值索引名称和查询以找到具有所需值的那些关系。
关于查询?上面我们提到,查询中的键是要搜索的索引名称。然而,在这种情况下,我们不想像往常一样搜索一个或多个索引以找到匹配的关系,而是实际上指定一个关系:Howie。
我们没有值索引名称:我们是在寻找一个关系。因此,查询键应该是常量 zc.relation.RELATION。对于我们的当前示例,这意味着查询是 {zc.relation.RELATION: 'Howie'}。
>>> import zc.relation >>> list(catalog.findValues( ... 'supervisor', {zc.relation.RELATION: 'Howie'}))[0] <Employee instance "Diane">
恭喜你,你刚刚找到了一种晦涩且相对低效地编写 howie.supervisor 的方法![5]
这是同样的结果,但有标记。
>>> list(catalog.findValueTokens('supervisor', ... {zc.relation.RELATION: 'Howie'})) ['Diane']
当我们在脚注中时,我要提到你可以搜索未索引的关系。
>>> list(catalog.findRelationTokens({zc.relation.RELATION: 'Ygritte'})) [] >>> list(catalog.findRelations({zc.relation.RELATION: 'Ygritte'})) []
如果您使用 findValues 或 findValueTokens 并尝试指定一个未索引的值名称,您将得到一个 ValueError。
>>> catalog.findValues('foo') Traceback (most recent call last): ... ValueError: ('name not indexed', 'foo')
稍微有用一点,您可以使用其他查询键与 zc.relation.RELATION 一起使用。这会询问:“Betty、Alice 和 Frank 中,谁是由 Alice 监督的?”
>>> sorted(catalog.findRelations( ... {zc.relation.RELATION: zc.relation.catalog.any( ... 'Betty', 'Alice', 'Frank'), ... 'supervisor': 'Alice'})) [<Employee instance "Betty">]
只有 Betty。
令牌
如上所述,该目录提供了一些辅助工具来处理标记。最常用的是 tokenizeQuery,它接受具有对象值的查询,并使用为关系和索引值注册的“dump”函数将它们转换为标记。以下是上面遇到的查询的替代拼写。
>>> catalog.tokenizeQuery({'supervisor': a}) {'supervisor': 'Alice'} >>> catalog.tokenizeQuery({'supervisor': None}) {'supervisor': None} >>> import pprint >>> result = catalog.tokenizeQuery( ... {zc.relation.RELATION: zc.relation.catalog.any(a, b, f), ... 'supervisor': a}) # doctest: +NORMALIZE_WHITESPACE >>> pprint.pprint(result) {None: <zc.relation.catalog.Any instance ('Alice', 'Betty', 'Frank')>, 'supervisor': 'Alice'}
(如果您对最后结果中的 None 感到好奇,是的,zc.relation.RELATION 只是用于可读性的糖,代表 None。)
因此,这里有一个使用 tokenizeQuery 的实际搜索。我们将为 catalog.tokenizeQuery 创建一个别名,以稍微简化一些事情。
>>> query = catalog.tokenizeQuery >>> sorted(catalog.findRelations(query( ... {zc.relation.RELATION: zc.relation.catalog.any(a, b, f), ... 'supervisor': a}))) [<Employee instance "Betty">]
目录始终具有并行搜索方法,一个用于查找对象(如上所示),另一个用于查找标记(唯一的例外是下面描述的 canFind)。查找标记可以更高效,特别是在关系目录的结果只是找到所需结果路径中的一步时。但对于某些常见情况,查找对象更简单。以下是一些查询的快速示例,获取标记而不是对象。
您还可以在 tokenizeQuery 中使用关键字参数来拼写查询。如果您的键是 zc.relation.RELATION,则它将不起作用,但否则它可以提高可读性。以下也将看到一些此类示例。
>>> sorted(catalog.findRelationTokens(query(supervisor=a))) ['Betty', 'Chuck']>>> sorted(catalog.findRelationTokens({'supervisor': None})) ['Alice']>>> sorted(catalog.findRelationTokens( ... query(supervisor=zc.relation.catalog.any(c, d)))) ['Frank', 'Galyn', 'Howie']>>> sorted(catalog.findRelationTokens( ... query({zc.relation.RELATION: zc.relation.catalog.any(a, b, f), ... 'supervisor': a}))) ['Betty']
目录还为处理标记提供了一些其他方法。
resolveQuery:是 tokenizeQuery 的逆操作,将标记化查询转换为具有对象的查询。
tokenizeValues:返回给定索引名称值的标记的可迭代对象。
resolveValueTokens:返回给定索引名称标记的值的可迭代对象。
tokenizeRelation:返回给定关系的标记。
resolveRelationToken:返回给定标记的关系。
tokenizeRelations:返回给定关系的标记的可迭代对象。
resolveRelationTokens:返回给定标记的关系的可迭代对象。
这些方法使用较少,并且在包中的更多技术文档中有更详细的描述。
传递搜索、查询工厂和 maxDepth
因此,我们已经看到了很多单级、非传递搜索。那么,传递搜索呢?嗯,您需要告诉目录如何遍历树。在这种情况下(简单且非常常见),zc.relation.queryfactory.TransposingTransitive 将会发挥作用。
传递查询工厂只是一个可调用的函数,目录使用它来询问:“我得到了这个查询,并找到了这些结果。我应该再走一步传递,那么我接下来应该搜索什么查询?”编写工厂比我们现在想讨论的更复杂,但使用 TransposingTransitiveQueryFactory 很简单。您只需告诉它它应该反向遍历的两个查询名称。
例如,这里我们只想告诉工厂反向遍历我们使用的两个键,zc.relation.RELATION 和 ‘supervisor’。让我们创建一个工厂,在查询中进行几次传递搜索,然后,如果您愿意,您可以阅读脚注来了解发生了什么。
这是工厂。
>>> import zc.relation.queryfactory >>> factory = zc.relation.queryfactory.TransposingTransitive( ... zc.relation.RELATION, 'supervisor')
现在 factory 只是一个可调用的函数。让我们让它帮助回答几个问题。
Howie的所有直接和间接上级是谁(这在图中可以查找)?
>>> list(catalog.findValues('supervisor', {zc.relation.RELATION: 'Howie'}, ... queryFactory=factory)) ... # doctest: +NORMALIZE_WHITESPACE [<Employee instance "Diane">, <Employee instance "Betty">, <Employee instance "Alice">]
Betty间接监督的所有人,广度优先(这在图中向下查看)是谁?
>>> people = list(catalog.findRelations( ... {'supervisor': 'Betty'}, queryFactory=factory)) >>> sorted(people[:2]) [<Employee instance "Diane">, <Employee instance "Edgar">] >>> people[2] <Employee instance "Howie">
是的,看起来是对的。那么它是怎么工作的?如果你感兴趣,请阅读这个脚注。[13]
这个间接工厂实际上是您想要用于这个特定目录的唯一间接工厂,所以将其作为默认值连接起来可能是安全的。您可以使用 addDefaultQueryFactory 添加多个查询工厂以匹配不同的查询。
>>> catalog.addDefaultQueryFactory(factory)
现在所有搜索都是默认的间接搜索。
>>> list(catalog.findValues('supervisor', {zc.relation.RELATION: 'Howie'})) ... # doctest: +NORMALIZE_WHITESPACE [<Employee instance "Diane">, <Employee instance "Betty">, <Employee instance "Alice">] >>> people = list(catalog.findRelations({'supervisor': 'Betty'})) >>> sorted(people[:2]) [<Employee instance "Diane">, <Employee instance "Edgar">] >>> people[2] <Employee instance "Howie">
我们可以使用 maxDepth 强制进行非间接搜索或指定搜索深度[7]。
如果 maxDepth > 1 但没有 queryFactory,则会引发错误。
>>> catalog.removeDefaultQueryFactory(factory) >>> catalog.findRelationTokens({'supervisor': 'Diane'}, maxDepth=3) Traceback (most recent call last): ... ValueError: if maxDepth not in (None, 1), queryFactory must be available
>>> catalog.addDefaultQueryFactory(factory)
>>> list(catalog.findValues( ... 'supervisor', {zc.relation.RELATION: 'Howie'}, maxDepth=1)) [<Employee instance "Diane">] >>> sorted(catalog.findRelations({'supervisor': 'Betty'}, maxDepth=1)) [<Employee instance "Diane">, <Employee instance "Edgar">]
maxDepth 必须是 None 或正整数,否则您将收到一个值错误。
>>> catalog.findRelations({'supervisor': 'Betty'}, maxDepth=0) Traceback (most recent call last): ... ValueError: maxDepth must be None or a positive integer
>>> catalog.findRelations({'supervisor': 'Betty'}, maxDepth=-1) Traceback (most recent call last): ... ValueError: maxDepth must be None or a positive integer
我们将在本文件和其他文件中稍后介绍一些其他可用的搜索参数。需要注意的是,所有搜索方法都共享与 ``findRelations`` 相同的参数。 findValues 和 findValueTokens 只增加了指定所需值的初始参数。
我们目前已经查看过两种搜索方法:findValues 和 findRelations 方法帮助您询问什么相关。但如果我们想知道事物是如何间接相关的呢?
findRelationChains 和 targetQuery
另一种搜索方法,findRelationChains,可以帮助您发现事物是如何间接相关的。
方法名称为“查找关系链”。但什么是“关系链”?在这个API中,它是一系列间接关系。例如,Howie之上的指挥链是什么?findRelationChains 将返回每条唯一的路径。
>>> list(catalog.findRelationChains({zc.relation.RELATION: 'Howie'})) ... # doctest: +NORMALIZE_WHITESPACE [(<Employee instance "Howie">,), (<Employee instance "Howie">, <Employee instance "Diane">), (<Employee instance "Howie">, <Employee instance "Diane">, <Employee instance "Betty">), (<Employee instance "Howie">, <Employee instance "Diane">, <Employee instance "Betty">, <Employee instance "Alice">)]
仔细查看那个结果。注意结果是一个元组的可迭代对象。每个元组都是一条唯一的链,可能是后续链的一部分。在这种情况下,最后一条链是最长和最全面的。
如果我们想查看从Alice的所有路径呢?那将是一条链,对应于每个被监督的员工,因为它显示了所有可能的路径。
>>> sorted(catalog.findRelationChains( ... {'supervisor': 'Alice'})) ... # doctest: +NORMALIZE_WHITESPACE [(<Employee instance "Betty">,), (<Employee instance "Betty">, <Employee instance "Diane">), (<Employee instance "Betty">, <Employee instance "Diane">, <Employee instance "Howie">), (<Employee instance "Betty">, <Employee instance "Edgar">), (<Employee instance "Chuck">,), (<Employee instance "Chuck">, <Employee instance "Frank">), (<Employee instance "Chuck">, <Employee instance "Galyn">)]
这就是从Alice的所有路径—所有链。我们已对结果进行排序,但通常它们会是广度优先。
但如果我们只想找到从一个查询结果到另一个查询结果的路径—比如说,我们想知道从Alice到Howie的指挥链?然后我们可以指定一个 targetQuery,它指定了我们所需终端(或点)的特征。
>>> list(catalog.findRelationChains( ... {'supervisor': 'Alice'}, ... targetQuery={zc.relation.RELATION: 'Howie'})) ... # doctest: +NORMALIZE_WHITESPACE [(<Employee instance "Betty">, <Employee instance "Diane">, <Employee instance "Howie">)]
所以,Betty监督Diane,而Diane监督Howie。
请注意,targetQuery 现在我们已经将其加入到我们之前介绍过的共享搜索参数集合中,与 maxDepth 一起。
filter 和 targetFilter
现在我们可以快速查看两个共享搜索参数中的最后一个:filter 和 targetFilter。这两个类似之处在于它们都是可以基于您能编写的任何逻辑批准或拒绝搜索中给定关系的可调用对象。它们的不同之处在于 filter 停止从关系开始的所有进一步间接搜索,而 targetFilter 仅省略给定结果,但允许从它开始进一步搜索。因此,与 targetQuery 一样,targetFilter 在您想指定路径的另一端时很有用。
作为一个例子,让我们假设我们只想返回女员工。
>>> female_employees = ('Alice', 'Betty', 'Diane', 'Galyn') >>> def female_filter(relchain, query, catalog, cache): ... return relchain[-1] in female_employees ...
以下是Alice通过targetFilter间接管理的所有女性员工。
>>> list(catalog.findRelations({'supervisor': 'Alice'}, ... targetFilter=female_filter)) ... # doctest: +NORMALIZE_WHITESPACE [<Employee instance "Betty">, <Employee instance "Diane">, <Employee instance "Galyn">]
以下是Chuck管理的所有女性员工。
>>> list(catalog.findRelations({'supervisor': 'Chuck'}, ... targetFilter=female_filter)) [<Employee instance "Galyn">]
用作过滤器的相同方法只会返回直接由其他女性管理的女性——在本例中不是Galyn。
>>> list(catalog.findRelations({'supervisor': 'Alice'}, ... filter=female_filter)) [<Employee instance "Betty">, <Employee instance "Diane">]
这些可以相互组合,以及与其他搜索参数[9]一起。
例如
>>> list(catalog.findRelationTokens( ... {'supervisor': 'Alice'}, targetFilter=female_filter, ... targetQuery={zc.relation.RELATION: 'Galyn'})) ['Galyn'] >>> list(catalog.findRelationTokens( ... {'supervisor': 'Alice'}, targetFilter=female_filter, ... targetQuery={zc.relation.RELATION: 'Not known'})) [] >>> arbitrary = ['Alice', 'Chuck', 'Betty', 'Galyn'] >>> def arbitrary_filter(relchain, query, catalog, cache): ... return relchain[-1] in arbitrary >>> list(catalog.findRelationTokens({'supervisor': 'Alice'}, ... filter=arbitrary_filter, ... targetFilter=female_filter)) ['Betty', 'Galyn']
搜索索引
不设置任何额外的索引,findRelations和findValues方法的间接行为本质上依赖于findRelationChains的暴力搜索。结果是逐步计算的迭代器。例如,让我们重复“Betty管理谁?”这个问题。注意,res首先填充一个有三个成员的列表,但随后没有填充第二个列表。迭代器已耗尽。
>>> res = catalog.findRelationTokens({'supervisor': 'Betty'}) >>> unindexed = sorted(res) >>> len(unindexed) 3 >>> len(list(res)) # iterator is exhausted 0
这种方法在许多情况下可能足够,但有时这些搜索的速度是关键。在这种情况下,您可以添加一个“搜索索引”。搜索索引通过索引结果来加速一个或多个精确搜索的结果。搜索索引会影响findRelations、findValues和即将推出的canFind中带有queryFactory的搜索结果,但不会影响findRelationChains。
zc.relation包目前包括两种搜索索引,一种用于索引层次结构中的间接成员搜索,另一种用于此包中tokens.rst中探索的非间接搜索,可以优化复杂查询上的频繁搜索,或有效地改变非间接搜索的意义。将来可能会添加其他搜索索引实现和方法。
以下是一个添加指定“主管”的间接搜索索引的非常简短的示例。
>>> import zc.relation.searchindex >>> catalog.addSearchIndex( ... zc.relation.searchindex.TransposingTransitiveMembership( ... 'supervisor', zc.relation.RELATION))
zc.relation.RELATION描述了如何向上遍历链。搜索索引在searchindex.rst中有合理的详细说明。
现在我们添加了索引,我们可以再次搜索。这次的结果已经计算,所以,至少当你请求令牌时,它是可重复的。
>>> res = catalog.findRelationTokens({'supervisor': 'Betty'}) >>> len(list(res)) 3 >>> len(list(res)) 3 >>> sorted(res) == unindexed True
注意,当使用索引时,广度优先排序会丢失[10]。
本文档中我们正在考虑的情景显示了搜索索引中需要解决更新的特殊情况。例如,如果我们把Howie从Diane移到Galyn
Alice __/ \__ Betty Chuck / \ / \ Diane Edgar Frank Galyn | Howie
到Galyn
Alice __/ \__ Betty Chuck / \ / \ Diane Edgar Frank Galyn | Howie
那么搜索索引在新的位置和旧的位置都是正确的。
>>> h.supervisor = g >>> catalog.index(h) >>> list(catalog.findRelationTokens({'supervisor': 'Diane'})) [] >>> list(catalog.findRelationTokens({'supervisor': 'Betty'})) ['Diane', 'Edgar'] >>> list(catalog.findRelationTokens({'supervisor': 'Chuck'})) ['Frank', 'Galyn', 'Howie'] >>> list(catalog.findRelationTokens({'supervisor': 'Galyn'})) ['Howie'] >>> h.supervisor = d >>> catalog.index(h) # move him back >>> list(catalog.findRelationTokens({'supervisor': 'Galyn'})) [] >>> list(catalog.findRelationTokens({'supervisor': 'Diane'})) ['Howie']
传递循环(以及更新和删除关系)
间接搜索和提供的搜索索引可以处理循环。在当前示例中,循环比其他一些情况不太可能,但我们可以稍微扩展一下情况:想象一个“乔装成王的国王”,在顶层的人在工作层次结构中处于较低的位置。也许Alice为Zane工作,Zane为Betty工作,Betty为Alice工作。虽然有些牵强,但很容易画出
______ / \ / Zane / | / Alice / __/ \__ / Betty__ Chuck \-/ / \ / \ Diane Edgar Frank Galyn | Howie
也很容易创建。
>>> z = Employee('Zane', b) >>> a.supervisor = z
现在我们有一个循环。当然,我们还没有告诉目录这个情况。index可以用来重新索引Alice和Zane。
>>> catalog.index(a) >>> catalog.index(z)
现在,如果我们问谁为Betty工作,我们会得到整个树。(我们只要求令牌,这样结果就可以更小,更容易查看。)[11]
Betty、Alice和Zane的查询结果都是相同的。
>>> res1 = catalog.findRelationTokens({'supervisor': 'Betty'}) >>> res2 = catalog.findRelationTokens({'supervisor': 'Alice'}) >>> res3 = catalog.findRelationTokens({'supervisor': 'Zane'}) >>> list(res1) == list(res2) == list(res3) True
循环不会污染循环之外的索引。
>>> res = catalog.findRelationTokens({'supervisor': 'Diane'}) >>> list(res) ['Howie'] >>> list(res) # it isn't lazy, it is precalculated ['Howie']
>>> sorted(catalog.findRelationTokens({'supervisor': 'Betty'})) ... # doctest: +NORMALIZE_WHITESPACE ['Alice', 'Betty', 'Chuck', 'Diane', 'Edgar', 'Frank', 'Galyn', 'Howie', 'Zane']
如果我们问Frank的导师,它将包括Betty。
>>> list(catalog.findValueTokens( ... 'supervisor', {zc.relation.RELATION: 'Frank'})) ['Chuck', 'Alice', 'Zane', 'Betty']
由 findRelationChains 返回的路径用特殊接口和特殊元数据标记,以显示链。
>>> res = list(catalog.findRelationChains({zc.relation.RELATION: 'Frank'})) >>> len(res) 5 >>> import zc.relation.interfaces >>> [zc.relation.interfaces.ICircularRelationPath.providedBy(r) ... for r in res] [False, False, False, False, True]
这是最后一个链
>>> res[-1] # doctest: +NORMALIZE_WHITESPACE cycle(<Employee instance "Frank">, <Employee instance "Chuck">, <Employee instance "Alice">, <Employee instance "Zane">, <Employee instance "Betty">)
链的 'cycled' 属性包含创建循环的查询列表。如果你运行查询,或者多个查询,你会看到循环会重新开始的地方——路径开始重叠的地方。有时查询结果将包括多个循环和一些不是循环的路径。在这种情况下,只有一个循环查询,导致一个循环关系。
>>> len(res[4].cycled) 1>>> list(catalog.findRelations(res[4].cycled[0], maxDepth=1)) [<Employee instance "Alice">]
为了消除这种混乱 [12],我们可以取消索引 Zane,并更改和重新索引 Alice。
如果你想,看看当你反方向走时会发生什么
>>> res = list(catalog.findRelationChains({'supervisor': 'Zane'})) >>> def sortEqualLenByName(one): ... return len(one), one ... >>> res.sort(key=sortEqualLenByName) # normalizes for test stability >>> from __future__ import print_function >>> print(res) # doctest: +NORMALIZE_WHITESPACE [(<Employee instance "Alice">,), (<Employee instance "Alice">, <Employee instance "Betty">), (<Employee instance "Alice">, <Employee instance "Chuck">), (<Employee instance "Alice">, <Employee instance "Betty">, <Employee instance "Diane">), (<Employee instance "Alice">, <Employee instance "Betty">, <Employee instance "Edgar">), cycle(<Employee instance "Alice">, <Employee instance "Betty">, <Employee instance "Zane">), (<Employee instance "Alice">, <Employee instance "Chuck">, <Employee instance "Frank">), (<Employee instance "Alice">, <Employee instance "Chuck">, <Employee instance "Galyn">), (<Employee instance "Alice">, <Employee instance "Betty">, <Employee instance "Diane">, <Employee instance "Howie">)]
>>> [zc.relation.interfaces.ICircularRelationPath.providedBy(r) ... for r in res] [False, False, False, False, False, True, False, False, False] >>> len(res[5].cycled) 1 >>> list(catalog.findRelations(res[5].cycled[0], maxDepth=1)) [<Employee instance "Alice">]
>>> a.supervisor = None >>> catalog.index(a)
>>> list(catalog.findValueTokens( ... 'supervisor', {zc.relation.RELATION: 'Frank'})) ['Chuck', 'Alice']
>>> catalog.unindex(z)
>>> sorted(catalog.findRelationTokens({'supervisor': 'Betty'})) ['Diane', 'Edgar', 'Howie']
canFind
我们现在来到了最后一个搜索方法:canFind。我们已经得到了值和关系,但如果你只是想知道是否存在任何联系呢?例如,Alice 是 Howie 的主管吗?Chuck 是吗?为了回答这些问题,你可以使用 canFind 方法结合 targetQuery 搜索参数。
canFind 方法接受与 findRelations 相同的参数。然而,它只返回一个布尔值,表示搜索是否有结果。这是一个方便的功能,同时也允许一些额外的优化。
Betty 是否管理任何人?
>>> catalog.canFind({'supervisor': 'Betty'}) True
Howie 呢?
>>> catalog.canFind({'supervisor': 'Howie'}) False
Zane(不再是员工)呢?
>>> catalog.canFind({'supervisor': 'Zane'}) False
如果我们想知道 Alice 或 Chuck 是否管理 Howie,那么我们想要指定路径上两个点的特征。要询问路径另一端的问题,请使用 targetQuery。
Alice 是否是 Howie 的主管?
>>> catalog.canFind({'supervisor': 'Alice'}, ... targetQuery={zc.relation.RELATION: 'Howie'}) True
Chuck 是否是 Howie 的主管?
>>> catalog.canFind({'supervisor': 'Chuck'}, ... targetQuery={zc.relation.RELATION: 'Howie'}) False
Howie 是 Alice 的员工吗?
>>> catalog.canFind({zc.relation.RELATION: 'Howie'}, ... targetQuery={'supervisor': 'Alice'}) True
Howie 是 Chuck 的员工吗?
>>> catalog.canFind({zc.relation.RELATION: 'Howie'}, ... targetQuery={'supervisor': 'Chuck'}) False
(注意,如果你的关系描述了一个层次结构,向上搜索层次结构通常比向下搜索更有效,所以在这种情况下,第二个问题通常比第一个问题更可取。)
处理更复杂的关系
到目前为止,我们的示例使用了简单的关系,其中索引对象是关系的一端,对象上的索引值是另一端。这个例子让我们看到了 zc.relation 目录功能的基本功能。
然而,如介绍中提到的,目录支持,并且是为了支持更复杂的关系而设计的。本节将简要检查一些其他用例的示例。
在本节中,我们将看到几个上面提到但尚未演示的示例。
我们可以使用接口属性(值或可调用对象)来定义值索引。
使用接口属性会导致尝试适配关系,如果关系尚未提供接口。
在定义值索引时,我们可以使用 multiple 参数来指示索引值是一个集合。
在定义值索引时,我们可以使用 name 参数来指定查询中要使用的名称,而不是依赖于接口属性或可调用对象的名字。
在实例化目录时,family 参数允许你将关系和值索引的默认 btree family 从 BTrees.family32.IF 改为 BTrees.family64.IF。
外源双向关系
我们当前故事的简单变体是这样的:如果索引关系在两个其他对象之间——也就是说,如果关系是两个参与者的外部关系呢?
让我们想象我们有一些显示生物亲子关系的关系。我们将需要一个“人”和“亲子关系”关系。我们将定义一个 IParentage 接口,这样我们就可以看到如何使用接口来定义值索引。
>>> class Person(object): ... def __init__(self, name): ... self.name = name ... def __repr__(self): ... return '<Person %r>' % (self.name,) ... >>> import zope.interface >>> class IParentage(zope.interface.Interface): ... child = zope.interface.Attribute('the child') ... parents = zope.interface.Attribute('the parents') ... >>> @zope.interface.implementer(IParentage) ... class Parentage(object): ... ... def __init__(self, child, parent1, parent2): ... self.child = child ... self.parents = (parent1, parent2) ...
现在我们将定义导出器和加载器,然后是目录。请注意,我们正在依赖一个模式:导出必须在加载之前调用。
>>> _people = {} >>> _relations = {} >>> def dumpPeople(obj, catalog, cache): ... if _people.setdefault(obj.name, obj) is not obj: ... raise ValueError('we are assuming names are unique') ... return obj.name ... >>> def loadPeople(token, catalog, cache): ... return _people[token] ... >>> def dumpRelations(obj, catalog, cache): ... if _relations.setdefault(id(obj), obj) is not obj: ... raise ValueError('huh?') ... return id(obj) ... >>> def loadRelations(token, catalog, cache): ... return _relations[token] ... >>> catalog = zc.relation.catalog.Catalog(dumpRelations, loadRelations, family=BTrees.family64) >>> catalog.addValueIndex(IParentage['child'], dumpPeople, loadPeople, ... btree=BTrees.family32.OO) >>> catalog.addValueIndex(IParentage['parents'], dumpPeople, loadPeople, ... btree=BTrees.family32.OO, multiple=True, ... name='parent') >>> catalog.addDefaultQueryFactory( ... zc.relation.queryfactory.TransposingTransitive( ... 'child', 'parent'))
现在我们已经建立了一个完整的目录。让我们添加一些关系。
>>> a = Person('Alice') >>> b = Person('Betty') >>> c = Person('Charles') >>> d = Person('Donald') >>> e = Person('Eugenia') >>> f = Person('Fred') >>> g = Person('Gertrude') >>> h = Person('Harry') >>> i = Person('Iphigenia') >>> j = Person('Jacob') >>> k = Person('Karyn') >>> l = Person('Lee')>>> r1 = Parentage(child=j, parent1=k, parent2=l) >>> r2 = Parentage(child=g, parent1=i, parent2=j) >>> r3 = Parentage(child=f, parent1=g, parent2=h) >>> r4 = Parentage(child=e, parent1=g, parent2=h) >>> r5 = Parentage(child=b, parent1=e, parent2=d) >>> r6 = Parentage(child=a, parent1=e, parent2=c)
这是我们层次结构图中的一个。
Karyn Lee \ / Jacob Iphigenia \ / Gertrude Harry \ / /-------\ Fred Eugenia Donald / \ Charles \ / \ / Betty Alice
现在我们可以索引这些关系,并提出一些问题。
>>> for r in (r1, r2, r3, r4, r5, r6): ... catalog.index(r) >>> query = catalog.tokenizeQuery >>> sorted(catalog.findValueTokens( ... 'parent', query(child=a), maxDepth=1)) ['Charles', 'Eugenia'] >>> sorted(catalog.findValueTokens('parent', query(child=g))) ['Iphigenia', 'Jacob', 'Karyn', 'Lee'] >>> sorted(catalog.findValueTokens( ... 'child', query(parent=h), maxDepth=1)) ['Eugenia', 'Fred'] >>> sorted(catalog.findValueTokens('child', query(parent=h))) ['Alice', 'Betty', 'Eugenia', 'Fred'] >>> catalog.canFind(query(parent=h), targetQuery=query(child=d)) False >>> catalog.canFind(query(parent=l), targetQuery=query(child=b)) True
多向关系
前面的例子迅速展示了如何为一个完全外部的双向关系设置目录。相同的模式可以扩展到N向关系。例如,考虑一个四向关系,形式为SUBJECTS PREDICATE OBJECTS [in CONTEXT]。例如,我们可能想表达“(joe,)在corner_store销售(doughnuts,coffee)”,其中“(joe,)”是主题的集合,“SELLS”是谓词,“(doughnuts,coffee)”是对象的集合,“corner_store”是可选的上下文。
对于这个最后的例子,我们将整合我们之前没有例子展示的两个组件:ZODB和适配。
我们的示例ZODB方法使用OID作为令牌。在某些情况下,这可能可以,如果你永远不会支持多个数据库,并且不需要一个抽象层,以便不同的对象可以具有相同的标识符。
>>> import persistent >>> import struct >>> class Demo(persistent.Persistent): ... def __init__(self, name): ... self.name = name ... def __repr__(self): ... return '<Demo instance %r>' % (self.name,) ... >>> class IRelation(zope.interface.Interface): ... subjects = zope.interface.Attribute('subjects') ... predicate = zope.interface.Attribute('predicate') ... objects = zope.interface.Attribute('objects') ... >>> class IContextual(zope.interface.Interface): ... def getContext(): ... 'return context' ... def setContext(value): ... 'set context' ... >>> @zope.interface.implementer(IContextual) ... class Contextual(object): ... ... _context = None ... def getContext(self): ... return self._context ... def setContext(self, value): ... self._context = value ... >>> @zope.interface.implementer(IRelation) ... class Relation(persistent.Persistent): ... ... def __init__(self, subjects, predicate, objects): ... self.subjects = subjects ... self.predicate = predicate ... self.objects = objects ... self._contextual = Contextual() ... ... def __conform__(self, iface): ... if iface is IContextual: ... return self._contextual ...
(当使用zope.component时,通常不需要__conform__;然而,这个包不依赖于zope.component。)
>>> def dumpPersistent(obj, catalog, cache): ... if obj._p_jar is None: ... catalog._p_jar.add(obj) # assumes something else places it ... return struct.unpack('<q', obj._p_oid)[0] ... >>> def loadPersistent(token, catalog, cache): ... return catalog._p_jar.get(struct.pack('<q', token)) ...>>> from ZODB.tests.util import DB >>> db = DB() >>> conn = db.open() >>> root = conn.root() >>> catalog = root['catalog'] = zc.relation.catalog.Catalog( ... dumpPersistent, loadPersistent, family=BTrees.family64) >>> catalog.addValueIndex(IRelation['subjects'], ... dumpPersistent, loadPersistent, multiple=True, name='subject') >>> catalog.addValueIndex(IRelation['objects'], ... dumpPersistent, loadPersistent, multiple=True, name='object') >>> catalog.addValueIndex(IRelation['predicate'], btree=BTrees.family32.OO) >>> catalog.addValueIndex(IContextual['getContext'], ... dumpPersistent, loadPersistent, name='context') >>> import transaction >>> transaction.commit()
如上所述,dumpPersistent和loadPersistent是一个玩具,我们的谓词将作为字符串存储,一些程序员可能更喜欢在这种情况下验证字符串已经以某种方式显式注册,以防止拼写错误。显然,我们不会在这个例子中为此烦恼。
我们创建了一些对象,然后我们用这些对象建立了一些关系并对其进行了索引。
>>> joe = root['joe'] = Demo('joe') >>> sara = root['sara'] = Demo('sara') >>> jack = root['jack'] = Demo('jack') >>> ann = root['ann'] = Demo('ann') >>> doughnuts = root['doughnuts'] = Demo('doughnuts') >>> coffee = root['coffee'] = Demo('coffee') >>> muffins = root['muffins'] = Demo('muffins') >>> cookies = root['cookies'] = Demo('cookies') >>> newspaper = root['newspaper'] = Demo('newspaper') >>> corner_store = root['corner_store'] = Demo('corner_store') >>> bistro = root['bistro'] = Demo('bistro') >>> bakery = root['bakery'] = Demo('bakery')>>> SELLS = 'SELLS' >>> BUYS = 'BUYS' >>> OBSERVES = 'OBSERVES'>>> rel1 = root['rel1'] = Relation((joe,), SELLS, (doughnuts, coffee)) >>> IContextual(rel1).setContext(corner_store) >>> rel2 = root['rel2'] = Relation((sara, jack), SELLS, ... (muffins, doughnuts, cookies)) >>> IContextual(rel2).setContext(bakery) >>> rel3 = root['rel3'] = Relation((ann,), BUYS, (doughnuts,)) >>> rel4 = root['rel4'] = Relation((sara,), BUYS, (bistro,))>>> for r in (rel1, rel2, rel3, rel4): ... catalog.index(r) ...
现在我们可以提出一个简单的问题。他们在哪里卖甜甜圈?
>>> query = catalog.tokenizeQuery >>> sorted(catalog.findValues( ... 'context', ... (query(predicate=SELLS, object=doughnuts))), ... key=lambda ob: ob.name) [<Demo instance 'bakery'>, <Demo instance 'corner_store'>]
希望这些例子能给你更多关于如何使用这个工具的灵感。
附加功能
本节介绍了辅助功能。我们将学习以下内容。
可以在目录中注册监听器。当关系被添加、修改或删除时,以及当目录被清除和复制时(见下文),它们会收到通知。
clear方法清除目录中的关系。
copy方法通过复制内部数据结构来创建当前目录的副本,而不是重新索引关系,这可以是一个显著的优化机会。这复制了值索引和搜索索引;并给监听器提供了一个机会来指定在新副本中包含什么,如果有的话。
五个相关搜索方法的ignoreSearchIndex参数会导致搜索忽略搜索索引,即使存在合适的索引。
findRelationTokens()(不带参数)返回目录中所有关系令牌的BTree集合。
findValueTokens(INDEX_NAME)(其中“INDEX_NAME”应替换为索引名称)返回目录中给定索引名称的所有值令牌的BTree集合。
监听器
各种潜在客户端可能希望在目录更改时收到通知。zc.relation不依赖于zope.event,因此可以注册各种更改的监听器。让我们快速创建一个演示监听器。additions和removals参数是{value name: iterable of added or removed value tokens}的字典。
>>> def pchange(d): ... pprint.pprint(dict( ... (k, v is not None and sorted(set(v)) or v) for k, v in d.items())) >>> @zope.interface.implementer(zc.relation.interfaces.IListener) ... class DemoListener(persistent.Persistent): ... ... def relationAdded(self, token, catalog, additions): ... print('a relation (token %r) was added to %r ' ... 'with these values:' % (token, catalog)) ... pchange(additions) ... def relationModified(self, token, catalog, additions, removals): ... print('a relation (token %r) in %r was modified ' ... 'with these additions:' % (token, catalog)) ... pchange(additions) ... print('and these removals:') ... pchange(removals) ... def relationRemoved(self, token, catalog, removals): ... print('a relation (token %r) was removed from %r ' ... 'with these values:' % (token, catalog)) ... pchange(removals) ... def sourceCleared(self, catalog): ... print('catalog %r had all relations unindexed' % (catalog,)) ... def sourceAdded(self, catalog): ... print('now listening to catalog %r' % (catalog,)) ... def sourceRemoved(self, catalog): ... print('no longer listening to catalog %r' % (catalog,)) ... def sourceCopied(self, original, copy): ... print('catalog %r made a copy %r' % (catalog, copy)) ... copy.addListener(self) ...
监听器可以被安装多次。
监听器可以作为持久弱引用添加,这样,如果它们在其他地方被删除,ZODB打包将不会将目录中的引用视为防止垃圾回收的东西。
我们将以正常引用的方式将其中一种演示监听器安装到我们的新目录中,这是默认行为。然后我们将展示发送给演示监听器的示例消息。
>>> listener = DemoListener() >>> catalog.addListener(listener) # doctest: +ELLIPSIS now listening to catalog <zc.relation.catalog.Catalog object at ...> >>> rel5 = root['rel5'] = Relation((ann,), OBSERVES, (newspaper,)) >>> catalog.index(rel5) # doctest: +ELLIPSIS a relation (token ...) was added to <...Catalog...> with these values: {'context': None, 'object': [...], 'predicate': ['OBSERVES'], 'subject': [...]} >>> rel5.subjects = (jack,) >>> IContextual(rel5).setContext(bistro) >>> catalog.index(rel5) # doctest: +ELLIPSIS a relation (token ...) in ...Catalog... was modified with these additions: {'context': [...], 'subject': [...]} and these removals: {'subject': [...]} >>> catalog.unindex(rel5) # doctest: +ELLIPSIS a relation (token ...) was removed from <...Catalog...> with these values: {'context': [...], 'object': [...], 'predicate': ['OBSERVES'], 'subject': [...]}>>> catalog.removeListener(listener) # doctest: +ELLIPSIS no longer listening to catalog <...Catalog...> >>> catalog.index(rel5) # doctest: +ELLIPSIS
那些例子没有展示的两个方法只有sourceCleared和sourceCopied。我们很快会详细介绍这些。
清除方法(clear)
clear方法简单地索引目录中的所有关系。已安装的监听器将调用sourceCleared。
>>> len(catalog) 5>>> catalog.addListener(listener) # doctest: +ELLIPSIS now listening to catalog <zc.relation.catalog.Catalog object at ...>>>> catalog.clear() # doctest: +ELLIPSIS catalog <...Catalog...> had all relations unindexed>>> len(catalog) 0 >>> sorted(catalog.findValues( ... 'context', ... (query(predicate=SELLS, object=doughnuts))), ... key=lambda ob: ob.name) []
复制方法(copy)
有时你可能想复制一个关系目录。一种方法是创建一个新的目录,按照当前目录设置,然后重新索引所有相同的关系。这对程序员和计算机来说是不必要的缓慢。copy方法通过复制内部数据结构创建一个新的目录,其中包含相同语料库的索引关系。
请求搜索索引为新目录制作新的副本;并给监听器一个机会按照自己的意愿对新副本做出反应,包括安装自己,以及/或选择另一个对象作为监听器。
让我们使用搜索索引和监听器复制一个已填充的索引。注意在我们的监听器中,sourceCopied在copy过程的最后将自己添加为新副本的监听器。
>>> for r in (rel1, rel2, rel3, rel4, rel5): ... catalog.index(r) ... # doctest: +ELLIPSIS a relation ... was added... a relation ... was added... a relation ... was added... a relation ... was added... a relation ... was added... >>> BEGAT = 'BEGAT' >>> rel6 = root['rel6'] = Relation((jack, ann), BEGAT, (sara,)) >>> henry = root['henry'] = Demo('henry') >>> rel7 = root['rel7'] = Relation((sara, joe), BEGAT, (henry,)) >>> catalog.index(rel6) # doctest: +ELLIPSIS a relation (token ...) was added to <...Catalog...> with these values: {'context': None, 'object': [...], 'predicate': ['BEGAT'], 'subject': [..., ...]} >>> catalog.index(rel7) # doctest: +ELLIPSIS a relation (token ...) was added to <...Catalog...> with these values: {'context': None, 'object': [...], 'predicate': ['BEGAT'], 'subject': [..., ...]} >>> catalog.addDefaultQueryFactory( ... zc.relation.queryfactory.TransposingTransitive( ... 'subject', 'object', {'predicate': BEGAT})) ... >>> list(catalog.findValues( ... 'object', query(subject=jack, predicate=BEGAT))) [<Demo instance 'sara'>, <Demo instance 'henry'>] >>> catalog.addSearchIndex( ... zc.relation.searchindex.TransposingTransitiveMembership( ... 'subject', 'object', static={'predicate': BEGAT})) >>> sorted( ... catalog.findValues( ... 'object', query(subject=jack, predicate=BEGAT)), ... key=lambda o: o.name) [<Demo instance 'henry'>, <Demo instance 'sara'>]>>> newcat = root['newcat'] = catalog.copy() # doctest: +ELLIPSIS catalog <...Catalog...> made a copy <...Catalog...> now listening to catalog <...Catalog...> >>> transaction.commit()
现在副本有自己的内部数据结构和搜索索引的副本。例如,让我们修改关系并添加一个新关系到副本中。
>>> mary = root['mary'] = Demo('mary') >>> buffy = root['buffy'] = Demo('buffy') >>> zack = root['zack'] = Demo('zack') >>> rel7.objects += (mary,) >>> rel8 = root['rel8'] = Relation((henry, buffy), BEGAT, (zack,)) >>> newcat.index(rel7) # doctest: +ELLIPSIS a relation (token ...) in ...Catalog... was modified with these additions: {'object': [...]} and these removals: {} >>> newcat.index(rel8) # doctest: +ELLIPSIS a relation (token ...) was added to ...Catalog... with these values: {'context': None, 'object': [...], 'predicate': ['BEGAT'], 'subject': [..., ...]} >>> len(newcat) 8 >>> sorted( ... newcat.findValues( ... 'object', query(subject=jack, predicate=BEGAT)), ... key=lambda o: o.name) # doctest: +NORMALIZE_WHITESPACE [<Demo instance 'henry'>, <Demo instance 'mary'>, <Demo instance 'sara'>, <Demo instance 'zack'>] >>> sorted( ... newcat.findValues( ... 'object', query(subject=sara)), ... key=lambda o: o.name) # doctest: +NORMALIZE_WHITESPACE [<Demo instance 'bistro'>, <Demo instance 'cookies'>, <Demo instance 'doughnuts'>, <Demo instance 'henry'>, <Demo instance 'mary'>, <Demo instance 'muffins'>]
原始目录不会被修改。
>>> len(catalog) 7 >>> sorted( ... catalog.findValues( ... 'object', query(subject=jack, predicate=BEGAT)), ... key=lambda o: o.name) [<Demo instance 'henry'>, <Demo instance 'sara'>] >>> sorted( ... catalog.findValues( ... 'object', query(subject=sara)), ... key=lambda o: o.name) # doctest: +NORMALIZE_WHITESPACE [<Demo instance 'bistro'>, <Demo instance 'cookies'>, <Demo instance 'doughnuts'>, <Demo instance 'henry'>, <Demo instance 'muffins'>]
忽略搜索索引(ignoreSearchIndex)参数
可以使用findValues、findValueTokens、findRelations、findRelationTokens和canFind这五种方法来显式请求忽略任何相关的搜索索引,使用ignoreSearchIndex参数。
我们可以很容易地通过与标记相关的方法看到这一点:搜索索引的结果将是一个BTree集合,而没有搜索索引的结果将是一个生成器。
>>> res1 = newcat.findValueTokens( ... 'object', query(subject=jack, predicate=BEGAT)) >>> res1 # doctest: +ELLIPSIS LFSet([..., ..., ..., ...]) >>> res2 = newcat.findValueTokens( ... 'object', query(subject=jack, predicate=BEGAT), ... ignoreSearchIndex=True) >>> res2 # doctest: +ELLIPSIS <generator object ... at 0x...> >>> sorted(res2) == list(res1) True>>> res1 = newcat.findRelationTokens( ... query(subject=jack, predicate=BEGAT)) >>> res1 # doctest: +ELLIPSIS LFSet([..., ..., ...]) >>> res2 = newcat.findRelationTokens( ... query(subject=jack, predicate=BEGAT), ignoreSearchIndex=True) >>> res2 # doctest: +ELLIPSIS <generator object ... at 0x...> >>> sorted(res2) == list(res1) True
我们可以看到其他方法接受参数,但结果看起来与平常一样。
>>> res = newcat.findValues( ... 'object', query(subject=jack, predicate=BEGAT), ... ignoreSearchIndex=True) >>> res # doctest: +ELLIPSIS <generator object ... at 0x...> >>> list(res) == list(newcat.resolveValueTokens(newcat.findValueTokens( ... 'object', query(subject=jack, predicate=BEGAT), ... ignoreSearchIndex=True), 'object')) True>>> res = newcat.findRelations( ... query(subject=jack, predicate=BEGAT), ... ignoreSearchIndex=True) >>> res # doctest: +ELLIPSIS <generator object ... at 0x...> >>> list(res) == list(newcat.resolveRelationTokens( ... newcat.findRelationTokens( ... query(subject=jack, predicate=BEGAT), ... ignoreSearchIndex=True))) True>>> newcat.canFind( ... query(subject=jack, predicate=BEGAT), ignoreSearchIndex=True) True
findRelationTokens()
如果你不带任何参数调用findRelationTokens,你将获得目录中所有关系标记的BTree集合。这可以方便进行测试和目录的高级使用。
>>> newcat.findRelationTokens() # doctest: +ELLIPSIS <BTrees.LFBTree.LFTreeSet object at ...> >>> len(newcat.findRelationTokens()) 8 >>> set(newcat.resolveRelationTokens(newcat.findRelationTokens())) == set( ... (rel1, rel2, rel3, rel4, rel5, rel6, rel7, rel8)) True
findValueTokens(INDEX_NAME)
如果你只用索引名称调用findValueTokens,你将获得索引中该值的所有标记的BTree结构。这可以方便进行测试和目录的高级使用。
>>> newcat.findValueTokens('predicate') # doctest: +ELLIPSIS <BTrees.OOBTree.OOBTree object at ...> >>> list(newcat.findValueTokens('predicate')) ['BEGAT', 'BUYS', 'OBSERVES', 'SELLS']
结论
审查
这就结束了我们的入门示例。让我们回顾一下,然后看看你可以从这里去哪里。
关系是具有索引值的对象。
关系目录索引关系。关系可以是一对一、一对二、三对三或N对N,只要你告诉目录索引不同的值。
创建目录
关系及其值以令牌的形式存储在目录中:唯一标识符,您可以将其解析回原始值。整数是最有效的令牌,但其他也可以工作得很好。
令牌类型决定了所需的BTree模块。
如果标记是32位整数,请选择BTrees.family32.II、BTrees.family32.IF或BTrees.family32.IO。
如果标记是64位整数,请选择BTrees.family64.II、BTrees.family64.IF或BTrees.family64.IO。
如果它们是其他任何东西,请选择BTrees.family32.OI、BTrees.family64.OI或BTrees.family32.OO(或BTrees.family64.OO——它们是相同的)。
在这些规则中,除非你计划将结果与使用特定BTree模块的另一个来源合并,否则选择是相当随意的。BTree集合操作只在这些模块内部工作,所以你必须匹配模块到模块。
在实例化目录时,family 参数允许你将关系和值索引的默认 btree family 从 BTrees.family32.IF 改为 BTrees.family64.IF。
您必须定义自己的令牌化和解析令牌的函数。这些函数被注册到目录中,用于关系及其每个值索引。
你可以通过使用可调用的或接口元素将值索引添加到关系目录中以便进行搜索。值可以通过可调用的或接口元素识别给目录。
使用接口属性会导致尝试适配关系,如果关系尚未提供接口。
当定义值索引时,我们可以使用multiple参数来指示索引的值是一个集合。默认值为False。
在定义值索引时,我们可以使用 name 参数来指定查询中要使用的名称,而不是依赖于接口属性或可调用对象的名字。
你可以设置搜索索引以加快特定的搜索,通常是一致的。
可以在目录中注册监听器。当关系被添加、修改或删除时,以及当目录被清除和复制时,它们会被提醒。
目录管理
关系通过 index(relation) 进行索引,并通过 unindex(relation) 从目录中删除。 index_doc(relation_token, relation) 和 unindex_doc(relation_token) 同样适用。
clear方法清除目录中的关系。
copy方法通过复制内部数据结构来创建当前目录的副本,而不是重新索引关系,这可以是一个显著的优化机会。这复制了值索引和搜索索引;并给监听器提供了一个机会来指定在新副本中包含什么,如果有的话。
搜索目录
对关系目录的查询是用字典形成的。
查询键是要搜索的索引名称,或者在精确关系的特殊情况下,是zc.relation.RELATION常量。
查询值是要匹配的结果的标记;或者None,表示值为None的关系(或者如果是多个,则为空集合)。搜索值可以使用zc.relation.catalog.any(args)或zc.relation.catalog.Any(args)来指定多个(非None)结果以匹配给定的键。
索引有多种方法可以帮助您处理标记。通常最常用的是tokenizeQuery,尽管还有其他方法可用。
要找到与查询匹配的关系,请使用 findRelations 或 findRelationTokens。不传递任何参数调用 findRelationTokens 将返回目录中所有关系标记的 BTree 集合。
要找到与查询匹配的值,请使用 findValues 或 findValueTokens。仅传递值索引的名称调用 findValueTokens 将返回目录中该值索引的所有标记的 BTree 集合。
通过使用查询工厂进行传递搜索。zc.relation.queryfactory.TransposingTransitive是一个好的通用案例工厂,允许您在层次结构中向上和向下遍历。查询工厂可以作为参数传递给搜索方法作为queryFactory,或者使用addDefaultQueryFactory安装为默认行为。
要找到查询是如何相关的,请使用findRelationChains或findRelationTokenChains。
要确定查询是否相关,请使用canFind。
循环传递关系通过处理以防止无限循环。它们在findRelationChains和findRelationTokenChains中通过zc.relation.interfaces.ICircularRelationPath标记接口来识别。
搜索方法共享以下参数
maxDepth,限制搜索的传递深度;
filter,允许代码过滤传递路径;
targetQuery,允许查询基于端点过滤传递路径;
targetFilter,允许代码基于端点过滤传递路径;以及
queryFactory,上面提到过。
此外,findRelations、findRelationTokens、findValues、findValueTokens 和 canFind 函数中的 ignoreSearchIndex 参数会导致搜索忽略搜索索引,即使存在合适的索引。
下一步
如果您想了解更多信息,下一步取决于您喜欢如何学习。以下是 zc.relation 包中的其他一些文档。
- optimization.rst:
优化关系目录使用的最佳实践。
- searchindex.rst:
查询工厂和搜索索引:从基础知识到细节。
- tokens.rst:
本文探讨了标记的细节。所有上帝的孩子们都喜欢标记,至少如果上帝的孩子们在编写使用 zc.relation 的非玩具应用程序时是这样。它包括讨论目录提供的标记辅助程序,如何使用 zope.app.intid-like 注册表与 zc.relation 一起使用,如何合理有效地使用标记来“连接”查询结果,以及如何索引连接。它还因为使用的示例而不必要地令人印象深刻。
- interfaces.py:
合同,用于细节。
最后,真正坚持不懈的人可能也对 timeit 目录感兴趣,其中包含用于测试假设和学习的脚本。
OK,你关心查询工厂是如何工作的,所以我们将对其进行一些研究。让我们谈谈第二个问题的两个步骤的传递搜索。目录最初执行请求的初始非传递搜索:找到 Betty 的主管关系。那是 Diane 和 Edgar。
现在,对于每个结果,目录都会向查询工厂询问下一步。让我们以 Diane 为例。目录对工厂说,“给定 Betty 是主管的查询关系,我得到了 Diane 的这个结果。你有没有其他查询我应该尝试进一步查找?”。工厂也获得目录实例,以便在需要时可以使用它来回答问题。
OK,下一部分是让你的大脑头疼的部分。坚持住。
在我们的例子中,工厂看到查询是关于主管的。它的另一个关键字,它与它转置的是 zc.relation.RELATION。 工厂得到转置关键字的当前标记的结果。所以,对于我们来说,zc.relation.RELATION 的键实际上是一个无操作:结果 就是 当前标记,Diane。然后,工厂得到了它的答案:用结果 Diane 替换查询中旧的主管值 Betty。下一个传递查询应该是 {'主管','Diane'}。太棒了。
令牌和连接:zc.relation目录扩展示例
简介和设置
本文假设您已阅读介绍性的 README.rst 并想通过示例了解更多。在它里面,我们将探索一组更复杂的关系,这些关系展示了与标记一起工作的各个方面。特别是,我们将查看连接,这将给我们一个机会更深入地查看查询工厂和搜索索引,并介绍监听器这一概念。它不会解释 README 已经涉及的基本内容。
假设我们在一个系统中索引安全断言。在这个系统中,用户可能在一个组织中拥有角色。每个组织可能有多个子组织,并且可能有一个父组织。在父组织中拥有角色的用户将在所有传递连接的子关系中拥有相同的角色。
因此,我们有两种关系。一种关系将模拟组织的层次结构。我们将使用组织与其子组织之间的内在关系来实现:这反映了父组织选择并由其子组织组成的事实;子组织不选择其父母。
另一种关系将模拟(多个)用户在(单个)组织中拥有的(单个)角色。这种关系将完全是外在的。
我们可以为每种类型创建两个目录,或者我们可以将它们都放在同一个目录中。最初,我们将采用单一目录的方法来展示我们的示例。因此,这个单一目录将索引一组异质的关系。
让我们使用接口定义这两个关系。我们将包括一个访问器getOrganization,主要为了展示如何处理方法。
>>> import zope.interface >>> class IOrganization(zope.interface.Interface): ... title = zope.interface.Attribute('the title') ... parts = zope.interface.Attribute( ... 'the organizations that make up this one') ... >>> class IRoles(zope.interface.Interface): ... def getOrganization(): ... 'return the organization in which this relation operates' ... principal_id = zope.interface.Attribute( ... 'the pricipal id whose roles this relation lists') ... role_ids = zope.interface.Attribute( ... 'the role ids that the principal explicitly has in the ' ... 'organization. The principal may have other roles via ' ... 'roles in parent organizations.') ...
现在我们可以创建一些类。在README示例中,设置有点像玩具。这次我们将更实际一些。我们还将期望在ZODB中操作,包括根和事务。 [14]
在这里,我们将设置一个ZODB实例供我们使用。
>>> from ZODB.tests.util import DB >>> db = DB() >>> conn = db.open() >>> root = conn.root()
以下是我们将如何转储和加载数据:使用一个类似于intid工具的“注册”对象。 [15]
以下是一个简单的持久性键引用。请注意,它本身并不是持久的:这对于冲突解决能够工作非常重要(我们在这里没有展示,但我们正在努力使这个示例更接近实际使用)。
>>> from functools import total_ordering >>> @total_ordering ... class Reference(object): # see zope.app.keyreference ... def __init__(self, obj): ... self.object = obj ... def _get_sorting_key(self): ... # this doesn't work during conflict resolution. See ... # zope.app.keyreference.persistent, 3.5 release, for current ... # best practice. ... if self.object._p_jar is None: ... raise ValueError( ... 'can only compare when both objects have connections') ... return self.object._p_oid or '' ... def __lt__(self, other): ... # this doesn't work during conflict resolution. See ... # zope.app.keyreference.persistent, 3.5 release, for current ... # best practice. ... if not isinstance(other, Reference): ... raise ValueError('can only compare with Reference objects') ... return self._get_sorting_key() < other._get_sorting_key() ... def __eq__(self, other): ... # this doesn't work during conflict resolution. See ... # zope.app.keyreference.persistent, 3.5 release, for current ... # best practice. ... if not isinstance(other, Reference): ... raise ValueError('can only compare with Reference objects') ... return self._get_sorting_key() == other._get_sorting_key()
以下是一个简单的整数标识符工具。
>>> import persistent >>> import BTrees >>> class Registry(persistent.Persistent): # see zope.app.intid ... def __init__(self, family=BTrees.family32): ... self.family = family ... self.ids = self.family.IO.BTree() ... self.refs = self.family.OI.BTree() ... def getId(self, obj): ... if not isinstance(obj, persistent.Persistent): ... raise ValueError('not a persistent object', obj) ... if obj._p_jar is None: ... self._p_jar.add(obj) ... ref = Reference(obj) ... id = self.refs.get(ref) ... if id is None: ... # naive for conflict resolution; see zope.app.intid ... if self.ids: ... id = self.ids.maxKey() + 1 ... else: ... id = self.family.minint ... self.ids[id] = ref ... self.refs[ref] = id ... return id ... def __contains__(self, obj): ... if (not isinstance(obj, persistent.Persistent) or ... obj._p_oid is None): ... return False ... return Reference(obj) in self.refs ... def getObject(self, id, default=None): ... res = self.ids.get(id, None) ... if res is None: ... return default ... else: ... return res.object ... def remove(self, r): ... if isinstance(r, int): ... self.refs.pop(self.ids.pop(r)) ... elif (not isinstance(r, persistent.Persistent) or ... r._p_oid is None): ... raise LookupError(r) ... else: ... self.ids.pop(self.refs.pop(Reference(r))) ... >>> registry = root['registry'] = Registry()
>>> import transaction >>> transaction.commit()
在这个“转储”方法的实现中,我们只是使用缓存来向您展示您可能如何使用它。这可能对于这项工作来说有点过度,甚至可能是一个速度损失,但您可以了解这个想法。
>>> def dump(obj, catalog, cache): ... reg = cache.get('registry') ... if reg is None: ... reg = cache['registry'] = catalog._p_jar.root()['registry'] ... return reg.getId(obj) ... >>> def load(token, catalog, cache): ... reg = cache.get('registry') ... if reg is None: ... reg = cache['registry'] = catalog._p_jar.root()['registry'] ... return reg.getObject(token) ...
现在我们可以创建一个关系目录来保存这些项目。
>>> import zc.relation.catalog >>> catalog = root['catalog'] = zc.relation.catalog.Catalog(dump, load) >>> transaction.commit()
现在我们设置我们的索引。我们将从组织开始,并用它们设置目录。这部分将与README.rst中的示例类似,但将引入更多关于优化和标记的讨论。然后我们将添加关于角色的部分,并探讨查询和基于标记的“连接”。
组织
组织将保存一组组织。实际上,这在ZODB中并不容易,因为这意味着我们需要比较或散列持久性对象,这在机器之间以及时间上并不可靠。为了绕过这个问题,并仍然做一些有趣且真实的事情,我们将使用上面引入的注册标记。这也会给我们机会进一步讨论优化和标记。(如果您想以合理且透明的方式保存一组持久性对象,请尝试使用zc.set包XXX,但尚未完成。)
>>> import BTrees >>> import persistent >>> @zope.interface.implementer(IOrganization) ... @total_ordering ... class Organization(persistent.Persistent): ... ... def __init__(self, title): ... self.title = title ... self.parts = BTrees.family32.IF.TreeSet() ... # the next parts just make the tests prettier ... def __repr__(self): ... return '<Organization instance "' + self.title + '">' ... def __lt__(self, other): ... # pukes if other doesn't have name ... return self.title < other.title ... def __eq__(self, other): ... return self is other ... def __hash__(self): ... return 1 # dummy ...
好的,现在我们知道组织将如何工作。现在我们可以向目录添加一个“部分”索引。这将比我们在README中添加索引时做几件新的事情。
>>> catalog.addValueIndex(IOrganization['parts'], multiple=True, ... name="part")
那么,与README示例相比有什么不同呢?
首先,我们使用接口元素来定义要索引的值。它提供了一个接口,对象将被适配,一个默认的索引名称,以及有关是否应直接使用属性或调用的信息。
其次,我们未指定转储或加载操作。它们为空。这意味着索引值已可视为一个标记。如果索引值是一个大型集合,并且使用与索引相同的BTree家族,这可以显著优化重新索引操作——这引出了下一个差异。
第三,我们指定了multiple=True。这意味着给定关系上提供或可适配到IOrganization的值将有一个部分集合。无论实际集合是BTrees集合还是BTree的键,这些都将被视为一个集合。
最后,我们指定了用于查询的名称。我发现,当查询键为单数时,查询更易读,所以我经常将复数改写为单数。
如README中所述,我们可以添加另一个简单的转换传递查询工厂,在‘部分’和None之间切换。
>>> import zc.relation.queryfactory >>> factory1 = zc.relation.queryfactory.TransposingTransitive( ... 'part', None) >>> catalog.addDefaultQueryFactory(factory1)
让我们也添加一些搜索索引,用于向上查找……
>>> import zc.relation.searchindex >>> catalog.addSearchIndex( ... zc.relation.searchindex.TransposingTransitiveMembership( ... 'part', None))
……以及向下。
>>> catalog.addSearchIndex( ... zc.relation.searchindex.TransposingTransitiveMembership( ... None, 'part'))
请注意:向上查找的搜索索引在实际上并不是一个好的主意。该索引是为向下查找设计的[16]。
转换传递成员索引提供ISearchIndex。
>>> from zope.interface.verify import verifyObject >>> import zc.relation.interfaces >>> index = list(catalog.iterSearchIndexes())[0] >>> verifyObject(zc.relation.interfaces.ISearchIndex, index) True
让我们创建并添加一些组织。
我们将创建一个类似这样的结构[24]
Ynod Corp Mangement Zookd Corp Management / | \ / | \ Ynod Devs Ynod SAs Ynod Admins Zookd Admins Zookd SAs Zookd Devs / \ \ / / \ Y3L4 Proj Bet Proj Ynod Zookd Task Force Zookd hOgnmd Zookd Nbd
这里是Python代码。
>>> orgs = root['organizations'] = BTrees.family32.OO.BTree() >>> for nm, parts in ( ... ('Y3L4 Proj', ()), ... ('Bet Proj', ()), ... ('Ynod Zookd Task Force', ()), ... ('Zookd hOgnmd', ()), ... ('Zookd Nbd', ()), ... ('Ynod Devs', ('Y3L4 Proj', 'Bet Proj')), ... ('Ynod SAs', ()), ... ('Ynod Admins', ('Ynod Zookd Task Force',)), ... ('Zookd Admins', ('Ynod Zookd Task Force',)), ... ('Zookd SAs', ()), ... ('Zookd Devs', ('Zookd hOgnmd', 'Zookd Nbd')), ... ('Ynod Corp Management', ('Ynod Devs', 'Ynod SAs', 'Ynod Admins')), ... ('Zookd Corp Management', ('Zookd Devs', 'Zookd SAs', ... 'Zookd Admins'))): ... org = Organization(nm) ... for part in parts: ... ignore = org.parts.insert(registry.getId(orgs[part])) ... orgs[nm] = org ... catalog.index(org) ...
现在目录知道了关系。
>>> len(catalog) 13 >>> root['dummy'] = Organization('Foo') >>> root['dummy'] in catalog False >>> orgs['Y3L4 Proj'] in catalog True
此外,现在我们可以进行搜索。为此,我们可以使用目录提供的某些标记方法。最常用的是tokenizeQuery。它接受未标记的值的查询,并将它们转换为标记的值。
>>> Ynod_SAs_id = registry.getId(orgs['Ynod SAs']) >>> catalog.tokenizeQuery({None: orgs['Ynod SAs']}) == { ... None: Ynod_SAs_id} True >>> Zookd_SAs_id = registry.getId(orgs['Zookd SAs']) >>> Zookd_Devs_id = registry.getId(orgs['Zookd Devs']) >>> catalog.tokenizeQuery( ... {None: zc.relation.catalog.any( ... orgs['Zookd SAs'], orgs['Zookd Devs'])}) == { ... None: zc.relation.catalog.any(Zookd_SAs_id, Zookd_Devs_id)} True
当然,现在仅使用‘部分’进行此操作有点愚蠢,因为它不会在关系目录中更改(因为我们已讨论,转储和加载都是None)。
>>> catalog.tokenizeQuery({'part': Ynod_SAs_id}) == { ... 'part': Ynod_SAs_id} True >>> catalog.tokenizeQuery( ... {'part': zc.relation.catalog.any(Zookd_SAs_id, Zookd_Devs_id)} ... ) == {'part': zc.relation.catalog.any(Zookd_SAs_id, Zookd_Devs_id)} True
tokenizeQuery 方法非常常见,因此我们在示例中将它分配给一个变量。然后我们进行一些搜索。
所以……查找由Ynod Devs监督的关系。
>>> t = catalog.tokenizeQuery >>> res = list(catalog.findRelationTokens(t({None: orgs['Ynod Devs']})))
OK…我们使用了findRelationTokens,而不是findRelations,因此res现在是一组数字。我们如何将其转换回来?resolveRelationTokens 将做到这一点。
>>> len(res) 3 >>> sorted(catalog.resolveRelationTokens(res)) ... # doctest: +NORMALIZE_WHITESPACE [<Organization instance "Bet Proj">, <Organization instance "Y3L4 Proj">, <Organization instance "Ynod Devs">]
resolveQuery 是tokenizeQuery 的镜像:它将标记查询转换为具有“加载”值的查询。
>>> original = {'part': zc.relation.catalog.any( ... Zookd_SAs_id, Zookd_Devs_id), ... None: orgs['Zookd Devs']} >>> tokenized = catalog.tokenizeQuery(original) >>> original == catalog.resolveQuery(tokenized) True>>> original = {None: zc.relation.catalog.any( ... orgs['Zookd SAs'], orgs['Zookd Devs']), ... 'part': Zookd_Devs_id} >>> tokenized = catalog.tokenizeQuery(original) >>> original == catalog.resolveQuery(tokenized) True
同样,tokenizeRelations 是resolveRelationTokens 的镜像。
>>> sorted(catalog.tokenizeRelations( ... [orgs["Bet Proj"], orgs["Y3L4 Proj"]])) == sorted( ... registry.getId(o) for o in ... [orgs["Bet Proj"], orgs["Y3L4 Proj"]]) True
其他标记相关方法如下[17]
就其本身而言,以下是一些剩余的标记相关方法的简单示例。
这两个是tokenizeRelations 和resolveRelationTokens 的单数版本。
tokenizeRelation 返回给定关系的标记。
>>> catalog.tokenizeRelation(orgs['Zookd Corp Management']) == ( ... registry.getId(orgs['Zookd Corp Management'])) True
resolveRelationToken 返回给定标记的关系。
>>> catalog.resolveRelationToken(registry.getId( ... orgs['Zookd Corp Management'])) is orgs['Zookd Corp Management'] True
“值”部分现在展示起来有点无趣,因为我们现在唯一有的值既未标记,也未直接使用。但这里是一些有趣的no-ops展示。
tokenizeValues,返回给定索引名称值的标记的可迭代对象。
>>> list(catalog.tokenizeValues((1,2,3), 'part')) [1, 2, 3]
resolveValueTokens 返回给定索引名称标记值的可迭代对象。
>>> list(catalog.resolveValueTokens((1,2,3), 'part')) [1, 2, 3]
tokenizeValues,返回给定索引名称值的标记的可迭代对象;
resolveValueTokens,返回给定索引名称标记值的可迭代对象;
tokenizeRelation,返回给定关系的标记;以及
resolveRelationToken,它返回给定令牌的关联。
为什么我们不隐藏这些令牌,而是使API更漂亮,而要处理这些令牌呢?通过暴露它们,我们能够实现高效的连接和在其他上下文中的高效使用。例如,如果您使用相同的intid工具在其他目录中进行标记,我们的结果可以与其他目录的结果合并。同样,您可以使用对其他目录的查询结果——或者甚至是查询此目录的早期结果的“连接”——作为这里的查询值。我们将在下一节中探讨这一点。
角色
我们已经设置了组织关系。现在让我们设置角色,并实际上能够回答我们在文档开头描述的问题。
在我们的Roles对象中,角色和主体将简单地是字符串——如果这是一个真实系统,则是ids。组织将是直接的对象引用。
>>> @zope.interface.implementer(IRoles) ... @total_ordering ... class Roles(persistent.Persistent): ... ... def __init__(self, principal_id, role_ids, organization): ... self.principal_id = principal_id ... self.role_ids = BTrees.family32.OI.TreeSet(role_ids) ... self._organization = organization ... def getOrganization(self): ... return self._organization ... # the rest is for prettier/easier tests ... def __repr__(self): ... return "<Roles instance (%s has %s in %s)>" % ( ... self.principal_id, ', '.join(self.role_ids), ... self._organization.title) ... def __lt__(self, other): ... _self = ( ... self.principal_id, ... tuple(self.role_ids), ... self._organization.title, ... ) ... _other = ( ... other.principal_id, ... tuple(other.role_ids), ... other._organization.title, ... ) ... return _self <_other ... def __eq__(self, other): ... return self is other ... def __hash__(self): ... return 1 # dummy ...
现在让我们向关系目录添加值索引。
>>> catalog.addValueIndex(IRoles['principal_id'], btree=BTrees.family32.OI) >>> catalog.addValueIndex(IRoles['role_ids'], btree=BTrees.family32.OI, ... multiple=True, name='role_id') >>> catalog.addValueIndex(IRoles['getOrganization'], dump, load, ... name='organization')
这些是我们在之前《addValueIndex》中看到的一些略微不同的变体,但都是混合和匹配相同的成分。
作为提醒,这里是我们组织结构
Ynod Corp Mangement Zookd Corp Management / | \ / | \ Ynod Devs Ynod SAs Ynod Admins Zookd Admins Zookd SAs Zookd Devs / \ \ / / \ Y3L4 Proj Bet Proj Ynod Zookd Task Force Zookd hOgnmd Zookd Nbd
现在让我们创建并添加一些角色。
>>> principal_ids = [ ... 'abe', 'bran', 'cathy', 'david', 'edgar', 'frank', 'gertrude', ... 'harriet', 'ignas', 'jacob', 'karyn', 'lettie', 'molly', 'nancy', ... 'ophelia', 'pat'] >>> role_ids = ['user manager', 'writer', 'reviewer', 'publisher'] >>> get_role = dict((v[0], v) for v in role_ids).__getitem__ >>> roles = root['roles'] = BTrees.family32.IO.BTree() >>> next = 0 >>> for prin, org, role_ids in ( ... ('abe', orgs['Zookd Corp Management'], 'uwrp'), ... ('bran', orgs['Ynod Corp Management'], 'uwrp'), ... ('cathy', orgs['Ynod Devs'], 'w'), ... ('cathy', orgs['Y3L4 Proj'], 'r'), ... ('david', orgs['Bet Proj'], 'wrp'), ... ('edgar', orgs['Ynod Devs'], 'up'), ... ('frank', orgs['Ynod SAs'], 'uwrp'), ... ('frank', orgs['Ynod Admins'], 'w'), ... ('gertrude', orgs['Ynod Zookd Task Force'], 'uwrp'), ... ('harriet', orgs['Ynod Zookd Task Force'], 'w'), ... ('harriet', orgs['Ynod Admins'], 'r'), ... ('ignas', orgs['Zookd Admins'], 'r'), ... ('ignas', orgs['Zookd Corp Management'], 'w'), ... ('karyn', orgs['Zookd Corp Management'], 'uwrp'), ... ('karyn', orgs['Ynod Corp Management'], 'uwrp'), ... ('lettie', orgs['Zookd Corp Management'], 'u'), ... ('lettie', orgs['Ynod Zookd Task Force'], 'w'), ... ('lettie', orgs['Zookd SAs'], 'w'), ... ('molly', orgs['Zookd SAs'], 'uwrp'), ... ('nancy', orgs['Zookd Devs'], 'wrp'), ... ('nancy', orgs['Zookd hOgnmd'], 'u'), ... ('ophelia', orgs['Zookd Corp Management'], 'w'), ... ('ophelia', orgs['Zookd Devs'], 'r'), ... ('ophelia', orgs['Zookd Nbd'], 'p'), ... ('pat', orgs['Zookd Nbd'], 'wrp')): ... assert prin in principal_ids ... role_ids = [get_role(l) for l in role_ids] ... role = roles[next] = Roles(prin, role_ids, org) ... role.key = next ... next += 1 ... catalog.index(role) ...
现在我们可以开始进行搜索[18]。
现在我们可以更合理地显示值令牌方法。
>>> original = sorted((orgs['Zookd Devs'], orgs['Ynod SAs'])) >>> tokens = list(catalog.tokenizeValues(original, 'organization')) >>> original == sorted(catalog.resolveValueTokens(tokens, 'organization')) True
ophelia的所有角色设置是什么?
>>> sorted(catalog.findRelations({'principal_id': 'ophelia'})) ... # doctest: +NORMALIZE_WHITESPACE [<Roles instance (ophelia has publisher in Zookd Nbd)>, <Roles instance (ophelia has reviewer in Zookd Devs)>, <Roles instance (ophelia has writer in Zookd Corp Management)>]
这个答案不需要传递性:我们完成了。
下一个问题。ophelia在何处有“writer”角色?
>>> list(catalog.findValues( ... 'organization', {'principal_id': 'ophelia', ... 'role_id': 'writer'})) [<Organization instance "Zookd Corp Management">]
嗯,这是正确的非传递性。我们需要一个传递查询工厂吗?不!这是观察我们在上一节中提到的令牌连接的好机会。这实际上应该是一个两步操作:找到ophelia拥有writer的所有组织,然后找到那个组织所有传递部分。
>>> sorted(catalog.findRelations({None: zc.relation.catalog.Any( ... catalog.findValueTokens('organization', ... {'principal_id': 'ophelia', ... 'role_id': 'writer'}))})) ... # doctest: +NORMALIZE_WHITESPACE [<Organization instance "Ynod Zookd Task Force">, <Organization instance "Zookd Admins">, <Organization instance "Zookd Corp Management">, <Organization instance "Zookd Devs">, <Organization instance "Zookd Nbd">, <Organization instance "Zookd SAs">, <Organization instance "Zookd hOgnmd">]
这就对了。
下一个问题。哪些用户在“Zookd Devs”组织中拥有角色?非传递性,这很简单。
>>> sorted(catalog.findValueTokens( ... 'principal_id', t({'organization': orgs['Zookd Devs']}))) ['nancy', 'ophelia']
传递性,我们应该进行另一个连接。
>>> org_id = registry.getId(orgs['Zookd Devs']) >>> sorted(catalog.findValueTokens( ... 'principal_id', { ... 'organization': zc.relation.catalog.any( ... org_id, *catalog.findRelationTokens({'part': org_id}))})) ['abe', 'ignas', 'karyn', 'lettie', 'nancy', 'ophelia']
这有点尴尬,但它确实有效。
- 最后一个问题,这也是整个示例开始的问题。
ophelia在“Zookd Nbd”组织中有哪些角色?
>>> list(catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'ophelia'}))) ['publisher']
非传递性,这是正确的。但是,传递性,ophelia还有审阅者和作者,这是我们想要快速得到答案的。
然后,我们可以以不同的方式提出问题,再次利用连接。我们将将其设置为一个函数,因为我们将在稍后使用它而无需重复代码。
>>> def getRolesInOrganization(principal_id, org): ... org_id = registry.getId(org) ... return sorted(catalog.findValueTokens( ... 'role_id', { ... 'organization': zc.relation.catalog.any( ... org_id, ... *catalog.findRelationTokens({'part': org_id})), ... 'principal_id': principal_id})) ... >>> getRolesInOrganization('ophelia', orgs['Zookd Nbd']) ['publisher', 'reviewer', 'writer']
正如您所看到的,因此,只要两个查询中的令牌相同,使用令牌就可以实现有趣的连接。
我们已经探讨了令牌方法和令牌技术,如连接。我们讲述的示例故事可以让我们进入一些更高级的主题,例如查询工厂连接和可以增加其读取速度的搜索索引。
查询工厂连接(Query Factory Joins)
我们可以构建一个使连接自动化的查询工厂。查询工厂是一个可调用的对象,它接受两个参数:一个查询(开始搜索的那个)和目录。工厂要么返回None,表示查询工厂不能用于此查询,要么返回另一个可调用的对象,该对象接受一系列关系。关系链中的最后一个令牌是最新的。内部可调用的输出预期是BTrees.family32.OO.Bucket查询的迭代器,用于从给定关系链进一步搜索。
这是对这个问题的错误方法。
>>> def flawed_factory(query, catalog): ... if (len(query) == 2 and ... 'organization' in query and ... 'principal_id' in query): ... def getQueries(relchain): ... if not relchain: ... yield query ... return ... current = catalog.getValueTokens( ... 'organization', relchain[-1]) ... if current: ... organizations = catalog.getRelationTokens( ... {'part': zc.relation.catalog.Any(current)}) ... if organizations: ... res = BTrees.family32.OO.Bucket(query) ... res['organization'] = zc.relation.catalog.Any( ... organizations) ... yield res ... return getQueries ...
这适用于我们的当前示例。
>>> sorted(catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'ophelia'}), ... queryFactory=flawed_factory)) ['publisher', 'reviewer', 'writer']
但是,它不适用于其他类似的查询。
>>> getRolesInOrganization('abe', orgs['Zookd Nbd']) ['publisher', 'reviewer', 'user manager', 'writer'] >>> sorted(catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'abe'}), ... queryFactory=flawed_factory)) []
oops。
flawed_factory实际上是一个用于更典型关系遍历的有用模式。它从关系到关系再到关系,ophelia已经连接到了最顶层。然而,abe只有最顶层,所以没有进行遍历。
相反,我们可以创建一个修改初始查询的查询工厂。
>>> def factory2(query, catalog): ... if (len(query) == 2 and ... 'organization' in query and ... 'principal_id' in query): ... def getQueries(relchain): ... if not relchain: ... res = BTrees.family32.OO.Bucket(query) ... org_id = query['organization'] ... if org_id is not None: ... res['organization'] = zc.relation.catalog.any( ... org_id, ... *catalog.findRelationTokens({'part': org_id})) ... yield res ... return getQueries ...>>> sorted(catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'ophelia'}), ... queryFactory=factory2)) ['publisher', 'reviewer', 'writer']>>> sorted(catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'abe'}), ... queryFactory=factory2)) ['publisher', 'reviewer', 'user manager', 'writer']
与其他方法相比,这种方法的本质是及物性的:这个查询工厂修改了初始查询,然后不提供更多查询。目前目录在查询没有返回任何结果的情况下始终停止调用查询工厂,因此像flawed_factory这样的方法根本无法解决这类问题。
我们可以将这个查询工厂添加为另一个默认选项。
>>> catalog.addDefaultQueryFactory(factory2)>>> sorted(catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'ophelia'}))) ['publisher', 'reviewer', 'writer']>>> sorted(catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'abe'}))) ['publisher', 'reviewer', 'user manager', 'writer']
之前安装的查询工厂仍然可用。
>>> list(catalog.iterDefaultQueryFactories()) == [factory1, factory2] True>>> list(catalog.findRelations( ... {'part': registry.getId(orgs['Y3L4 Proj'])})) ... # doctest: +NORMALIZE_WHITESPACE [<Organization instance "Ynod Devs">, <Organization instance "Ynod Corp Management">]>>> sorted(catalog.findRelations( ... {None: registry.getId(orgs['Ynod Corp Management'])})) ... # doctest: +NORMALIZE_WHITESPACE [<Organization instance "Bet Proj">, <Organization instance "Y3L4 Proj">, <Organization instance "Ynod Admins">, <Organization instance "Ynod Corp Management">, <Organization instance "Ynod Devs">, <Organization instance "Ynod SAs">, <Organization instance "Ynod Zookd Task Force">]
查询工厂连接的搜索索引
现在我们已经编写了一个封装连接的查询工厂,我们可以使用一个加速它的搜索索引。我们迄今为止只使用了及物搜索索引。现在我们将添加一个不及物搜索索引。
不及物搜索索引通常只需要它应该索引的搜索值名称,可选的结果名称(默认为关系),以及可选的查询工厂。
由于我们正在进行的奇怪的连接技巧,我们需要使用两个额外的选项。我们需要指定在对象被索引时需要更改哪些组织和principal_id值,并指示当组织、principal_id或部分发生变化时应该更新。
getValueTokens指定需要索引的值。它获取索引、所需标记的名称、标记、生成标记更改的目录(它可能不与索引的目录相同)、包含一个字典的源字典,该字典包含将用于标记的值(如果未覆盖它们),以及包含此标记的添加值的字典(键是值名称)、包含此标记的删除值的字典,以及标记是否已被删除。该方法可以返回None,这将使索引保留其默认行为,如果未使用查询工厂,则应正常工作;或返回值的一个可迭代集合。
>>> def getValueTokens(index, name, token, catalog, source, ... additions, removals, removed): ... if name == 'organization': ... orgs = source.get('organization') ... if not removed or not orgs: ... orgs = index.catalog.getValueTokens( ... 'organization', token) ... if not orgs: ... orgs = [token] ... orgs.extend(removals.get('part', ())) ... orgs = set(orgs) ... orgs.update(index.catalog.findValueTokens( ... 'part', ... {None: zc.relation.catalog.Any( ... t for t in orgs if t is not None)})) ... return orgs ... elif name == 'principal_id': ... # we only want custom behavior if this is an organization ... if 'principal_id' in source or index.catalog.getValueTokens( ... 'principal_id', token): ... return '' ... orgs = set((token,)) ... orgs.update(index.catalog.findRelationTokens( ... {'part': token})) ... return set(index.catalog.findValueTokens( ... 'principal_id', { ... 'organization': zc.relation.catalog.Any(orgs)})) ...>>> index = zc.relation.searchindex.Intransitive( ... ('organization', 'principal_id'), 'role_id', factory2, ... getValueTokens, ... ('organization', 'principal_id', 'part', 'role_id'), ... unlimitedDepth=True) >>> catalog.addSearchIndex(index)>>> res = catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'ophelia'})) >>> list(res) ['publisher', 'reviewer', 'writer'] >>> list(res) ['publisher', 'reviewer', 'writer']>>> res = catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'abe'})) >>> list(res) ['publisher', 'reviewer', 'user manager', 'writer'] >>> list(res) ['publisher', 'reviewer', 'user manager', 'writer']
不及物搜索索引提供ISearchIndex和IListener。
>>> from zope.interface.verify import verifyObject >>> import zc.relation.interfaces >>> verifyObject(zc.relation.interfaces.ISearchIndex, index) True >>> verifyObject(zc.relation.interfaces.IListener, index) True
现在我们可以更改和删除关系——组织角色——并使索引保持正确状态。鉴于当前的组织状态——
Ynod Corp Mangement Zookd Corp Management / | \ / | \ Ynod Devs Ynod SAs Ynod Admins Zookd Admins Zookd SAs Zookd Devs / \ \ / / \ Y3L4 Proj Bet Proj Ynod Zookd Task Force Zookd hOgnmd Zookd Nbd
——首先,我们将Ynod Devs移至Zookd Devs之下,然后返回。这将短暂地赋予abe和其他人完全的权限。
>>> list(catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Y3L4 Proj'], ... 'principal_id': 'abe'}))) [] >>> orgs['Zookd Devs'].parts.insert(registry.getId(orgs['Ynod Devs'])) 1 >>> catalog.index(orgs['Zookd Devs']) >>> res = catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Y3L4 Proj'], ... 'principal_id': 'abe'})) >>> list(res) ['publisher', 'reviewer', 'user manager', 'writer'] >>> list(res) ['publisher', 'reviewer', 'user manager', 'writer'] >>> orgs['Zookd Devs'].parts.remove(registry.getId(orgs['Ynod Devs'])) >>> catalog.index(orgs['Zookd Devs']) >>> list(catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Y3L4 Proj'], ... 'principal_id': 'abe'}))) []
作为另一个示例,我们将更改abe的角色,并看到它传播到Zookd Nbd。
>>> rels = list(catalog.findRelations(t( ... {'principal_id': 'abe', ... 'organization': orgs['Zookd Corp Management']}))) >>> len(rels) 1 >>> rels[0].role_ids.remove('reviewer') >>> catalog.index(rels[0])>>> res = catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'abe'})) >>> list(res) ['publisher', 'user manager', 'writer'] >>> list(res) ['publisher', 'user manager', 'writer']
请注意,搜索索引顺序很重要。在我们的情况下,我们的不及物搜索索引依赖于我们的及物索引,因此及物索引需要先来。您希望在名称之前放置及物关系索引。目前,您负责此顺序:很难提出一个可靠的算法来猜测它。
监听器、目录管理和跨关系目录的连接
我们迄今为止的所有示例都是使用单个目录索引两种类型的关系。如果我们想有两个具有同质关系集合的目录呢?这可能感觉更干净,但也引入了一些新的问题。
让我们使用我们当前的目录来组织,删除额外的信息;并为角色创建一个新的目录。
>>> role_catalog = root['role_catalog'] = catalog.copy() >>> transaction.commit() >>> org_catalog = catalog >>> del catalog
我们需要一个稍微不同的查询工厂和稍微不同的搜索索引< cite>getValueTokens函数。我们将编写这些,然后修改我们两个目录的新世界的配置。
我们在这里编写的及物工厂是为角色目录。它需要访问组织目录。我们可以这样做——依靠实用程序,或者从上下文中找到目录。我们将使role_catalog具有一个.org_catalog属性,并依靠它。
>>> role_catalog.org_catalog = org_catalog >>> def factory3(query, catalog): ... if (len(query) == 2 and ... 'organization' in query and ... 'principal_id' in query): ... def getQueries(relchain): ... if not relchain: ... res = BTrees.family32.OO.Bucket(query) ... org_id = query['organization'] ... if org_id is not None: ... res['organization'] = zc.relation.catalog.any( ... org_id, ... *catalog.org_catalog.findRelationTokens( ... {'part': org_id})) ... yield res ... return getQueries ...>>> def getValueTokens2(index, name, token, catalog, source, ... additions, removals, removed): ... is_role_catalog = catalog is index.catalog # role_catalog ... if name == 'organization': ... if is_role_catalog: ... orgs = set(source.get('organization') or ... index.catalog.getValueTokens( ... 'organization', token) or ()) ... else: ... orgs = set((token,)) ... orgs.update(removals.get('part', ())) ... orgs.update(index.catalog.org_catalog.findValueTokens( ... 'part', ... {None: zc.relation.catalog.Any( ... t for t in orgs if t is not None)})) ... return orgs ... elif name == 'principal_id': ... # we only want custom behavior if this is an organization ... if not is_role_catalog: ... orgs = set((token,)) ... orgs.update(index.catalog.org_catalog.findRelationTokens( ... {'part': token})) ... return set(index.catalog.findValueTokens( ... 'principal_id', { ... 'organization': zc.relation.catalog.Any(orgs)})) ... return ''
如果您正在跟随代码并与原始代码进行比较,您可能会看到这种方法比关系在相同目录中时更干净。
现在我们将修复组织目录 [20]。
在我们修改它们之前,让我们看看我们制作的副本。这个副本应该目前的行为应该与原始文件相同。
>>> len(org_catalog) 38 >>> len(role_catalog) 38 >>> indexed = list(org_catalog) >>> len(indexed) 38 >>> orgs['Zookd Devs'] in indexed True >>> for r in indexed: ... if r not in role_catalog: ... print('bad') ... break ... else: ... print('good') ... good >>> org_names = set(dir(org_catalog)) >>> role_names = set(dir(role_catalog)) >>> sorted(org_names - role_names) [] >>> sorted(role_names - org_names) ['org_catalog']
>>> def checkYnodDevsParts(catalog): ... res = sorted(catalog.findRelations(t({None: orgs['Ynod Devs']}))) ... if res != [ ... orgs["Bet Proj"], orgs["Y3L4 Proj"], orgs["Ynod Devs"]]: ... print("bad", res) ... >>> checkYnodDevsParts(org_catalog) >>> checkYnodDevsParts(role_catalog)
>>> def checkOpheliaRoles(catalog): ... res = sorted(catalog.findRelations({'principal_id': 'ophelia'})) ... if repr(res) != ( ... "[<Roles instance (ophelia has publisher in Zookd Nbd)>, " + ... "<Roles instance (ophelia has reviewer in Zookd Devs)>, " + ... "<Roles instance (ophelia has writer in " + ... "Zookd Corp Management)>]"): ... print("bad", res) ... >>> checkOpheliaRoles(org_catalog) >>> checkOpheliaRoles(role_catalog)
>>> def checkOpheliaWriterOrganizations(catalog): ... res = sorted(catalog.findRelations({None: zc.relation.catalog.Any( ... catalog.findValueTokens( ... 'organization', {'principal_id': 'ophelia', ... 'role_id': 'writer'}))})) ... if repr(res) != ( ... '[<Organization instance "Ynod Zookd Task Force">, ' + ... '<Organization instance "Zookd Admins">, ' + ... '<Organization instance "Zookd Corp Management">, ' + ... '<Organization instance "Zookd Devs">, ' + ... '<Organization instance "Zookd Nbd">, ' + ... '<Organization instance "Zookd SAs">, ' + ... '<Organization instance "Zookd hOgnmd">]'): ... print("bad", res) ... >>> checkOpheliaWriterOrganizations(org_catalog) >>> checkOpheliaWriterOrganizations(role_catalog)
>>> def checkPrincipalsWithRolesInZookdDevs(catalog): ... org_id = registry.getId(orgs['Zookd Devs']) ... res = sorted(catalog.findValueTokens( ... 'principal_id', ... {'organization': zc.relation.catalog.any( ... org_id, *catalog.findRelationTokens({'part': org_id}))})) ... if res != ['abe', 'ignas', 'karyn', 'lettie', 'nancy', 'ophelia']: ... print("bad", res) ... >>> checkPrincipalsWithRolesInZookdDevs(org_catalog) >>> checkPrincipalsWithRolesInZookdDevs(role_catalog)
>>> def checkOpheliaRolesInZookdNbd(catalog): ... res = sorted(catalog.findValueTokens( ... 'role_id', { ... 'organization': registry.getId(orgs['Zookd Nbd']), ... 'principal_id': 'ophelia'})) ... if res != ['publisher', 'reviewer', 'writer']: ... print("bad", res) ... >>> checkOpheliaRolesInZookdNbd(org_catalog) >>> checkOpheliaRolesInZookdNbd(role_catalog)
>>> def checkAbeRolesInZookdNbd(catalog): ... res = sorted(catalog.findValueTokens( ... 'role_id', { ... 'organization': registry.getId(orgs['Zookd Nbd']), ... 'principal_id': 'abe'})) ... if res != ['publisher', 'user manager', 'writer']: ... print("bad", res) ... >>> checkAbeRolesInZookdNbd(org_catalog) >>> checkAbeRolesInZookdNbd(role_catalog) >>> org_catalog.removeDefaultQueryFactory(None) # doctest: +ELLIPSIS Traceback (most recent call last): ... LookupError: ('factory not found', None)
>>> org_catalog.removeValueIndex('organization') >>> org_catalog.removeValueIndex('role_id') >>> org_catalog.removeValueIndex('principal_id') >>> org_catalog.removeDefaultQueryFactory(factory2) >>> org_catalog.removeSearchIndex(index) >>> org_catalog.clear() >>> len(org_catalog) 0 >>> for v in orgs.values(): ... org_catalog.index(v)
这也展示了使用 removeDefaultQueryFactory 和 removeSearchIndex 方法 [21]。
通过移除未注册的查询工厂会导致错误。
>>> org_catalog.removeDefaultQueryFactory(factory2) # doctest: +ELLIPSIS Traceback (most recent call last): ... LookupError: ('factory not found', <function factory2 at ...>)
现在我们将设置角色目录 [22]。
对一个副本的更改不应该影响另一个副本。这意味着 role_catalog 应该仍然像以前一样工作。
>>> len(org_catalog) 13 >>> len(list(org_catalog)) 13
>>> len(role_catalog) 38 >>> indexed = list(role_catalog) >>> len(indexed) 38 >>> orgs['Zookd Devs'] in indexed True >>> orgs['Zookd Devs'] in role_catalog True
>>> checkYnodDevsParts(role_catalog) >>> checkOpheliaRoles(role_catalog) >>> checkOpheliaWriterOrganizations(role_catalog) >>> checkPrincipalsWithRolesInZookdDevs(role_catalog) >>> checkOpheliaRolesInZookdNbd(role_catalog) >>> checkAbeRolesInZookdNbd(role_catalog)
>>> role_catalog.removeValueIndex('part') >>> for ix in list(role_catalog.iterSearchIndexes()): ... role_catalog.removeSearchIndex(ix) ... >>> role_catalog.removeDefaultQueryFactory(factory1) >>> role_catalog.removeDefaultQueryFactory(factory2) >>> role_catalog.addDefaultQueryFactory(factory3) >>> root['index2'] = index2 = zc.relation.searchindex.Intransitive( ... ('organization', 'principal_id'), 'role_id', factory3, ... getValueTokens2, ... ('organization', 'principal_id', 'part', 'role_id'), ... unlimitedDepth=True) >>> role_catalog.addSearchIndex(index2)
新的 role_catalog 索引需要从 org_catalog 更新。我们将使用监听器,这是一个新概念来实现这一点。
>>> org_catalog.addListener(index2) >>> list(org_catalog.iterListeners()) == [index2] True
现在 role_catalog 应该能够回答与旧的单个目录方法相同的问题。
>>> t = role_catalog.tokenizeQuery >>> list(role_catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'abe'}))) ['publisher', 'user manager', 'writer']>>> list(role_catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'ophelia'}))) ['publisher', 'reviewer', 'writer']
我们也可以修改两个目录,同时搜索索引得到维护。
>>> list(role_catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Y3L4 Proj'], ... 'principal_id': 'abe'}))) [] >>> orgs['Zookd Devs'].parts.insert(registry.getId(orgs['Ynod Devs'])) 1 >>> org_catalog.index(orgs['Zookd Devs']) >>> list(role_catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Y3L4 Proj'], ... 'principal_id': 'abe'}))) ['publisher', 'user manager', 'writer'] >>> orgs['Zookd Devs'].parts.remove(registry.getId(orgs['Ynod Devs'])) >>> org_catalog.index(orgs['Zookd Devs']) >>> list(role_catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Y3L4 Proj'], ... 'principal_id': 'abe'}))) []>>> rels = list(role_catalog.findRelations(t( ... {'principal_id': 'abe', ... 'organization': orgs['Zookd Corp Management']}))) >>> len(rels) 1 >>> rels[0].role_ids.insert('reviewer') 1 >>> role_catalog.index(rels[0])>>> res = role_catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd Nbd'], ... 'principal_id': 'abe'})) >>> list(res) ['publisher', 'reviewer', 'user manager', 'writer']
在这里我们添加一个新组织。
>>> orgs['Zookd hOnc'] = org = Organization('Zookd hOnc') >>> orgs['Zookd Devs'].parts.insert(registry.getId(org)) 1 >>> org_catalog.index(orgs['Zookd hOnc']) >>> org_catalog.index(orgs['Zookd Devs'])>>> list(role_catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd hOnc'], ... 'principal_id': 'abe'}))) ['publisher', 'reviewer', 'user manager', 'writer']>>> list(role_catalog.findValueTokens( ... 'role_id', t({'organization': orgs['Zookd hOnc'], ... 'principal_id': 'ophelia'}))) ['reviewer', 'writer']
现在我们将移除它。
>>> orgs['Zookd Devs'].parts.remove(registry.getId(org)) >>> org_catalog.index(orgs['Zookd Devs']) >>> org_catalog.unindex(orgs['Zookd hOnc'])
TODO 确保非传递性副本看起来像我们预期的那样
您可以多次添加监听器。
>>> org_catalog.addListener(index2) >>> list(org_catalog.iterListeners()) == [index2, index2] True
现在我们将移除监听器,以显示我们能够做到这一点。
>>> org_catalog.removeListener(index2) >>> org_catalog.removeListener(index2) >>> org_catalog.removeListener(index2) ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE Traceback (most recent call last): ... LookupError: ('listener not found', <zc.relation.searchindex.Intransitive object at ...>) >>> org_catalog.removeListener(None) ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE Traceback (most recent call last): ... LookupError: ('listener not found', None)
以下是移除我们没有的搜索索引的相同示例
>>> org_catalog.removeSearchIndex(index2) ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE Traceback (most recent call last): ... LookupError: ('index not found', <zc.relation.searchindex.Intransitive object at ...>)
在《2001太空漫游》中,许多人认为选择 HAL 这个名字是因为它是 IBM 的 ROT25……我有时会作弊一点,使用 ROT1 因为听起来更好。
与搜索索引一起工作:zc.relation目录扩展示例
简介
本文件假设您已阅读 README.rst 文件,并希望通过示例了解更多。在它中,我们将探索一组关系,以展示与搜索索引和监听器一起工作的大多数方面。它不会解释其他文档已涵盖的主题。它还描述了一个高级用例。
正如我们在其他文档中看到的,关系目录支持搜索索引。它们可以返回任何搜索的结果,如所需。当然,意图是您提供索引以优化其声称的特定搜索。
searchindex 模块提供了一些搜索索引,优化指定的传递性和非传递性搜索。我们在其他文档中已经看到了它们的工作情况。我们将在这个文档中更深入地研究它们。
搜索索引通过接收通过“监听器”接口的消息来自动更新。我们也将探讨这是如何工作的。
本文件中描述的示例检查了一个类似于 zc.revision 或 zc.vault 软件包中的用例:一个关系描述了其他对象的图。因此,这是我们第一个纯粹的外部关系具体示例。
让我们构建一下这个示例故事。想象一下,我们有一个标记的图,通常是一个层次结构,这些标记是整数。关系指定了给定的整数标记与其他整数标记相关,具有包含表示或其他含义。
整数也可能有表示它们代表对象或对象的关系。
这允许我们有一个对象图,其中更改图的某一部分不需要更改其余部分。因此,zc.revision 和 zc.vault 能够有效地模拟具有多个修订版和大量元数据的图。
让我们想象一个简单的层次结构。关系有一个 token 属性和一个 children 属性;子节点指向标记。关系将通过 id 来标识自己。
>>> import BTrees >>> relations = BTrees.family64.IO.BTree() >>> relations[99] = None # just to give us a start>>> class Relation(object): ... def __init__(self, token, children=()): ... self.token = token ... self.children = BTrees.family64.IF.TreeSet(children) ... self.id = relations.maxKey() + 1 ... relations[self.id] = self ...>>> def token(rel, self): ... return rel.token ... >>> def children(rel, self): ... return rel.children ... >>> def dumpRelation(obj, index, cache): ... return obj.id ... >>> def loadRelation(token, index, cache): ... return relations[token] ...
标准的 TransposingTransitiveQueriesFactory 能够很好地处理这一点,因此我们将为我们的索引使用它。
>>> import zc.relation.queryfactory >>> factory = zc.relation.queryfactory.TransposingTransitive( ... 'token', 'children') >>> import zc.relation.catalog >>> catalog = zc.relation.catalog.Catalog( ... dumpRelation, loadRelation, BTrees.family64.IO, BTrees.family64) >>> catalog.addValueIndex(token) >>> catalog.addValueIndex(children, multiple=True) >>> catalog.addDefaultQueryFactory(factory)
现在让我们快速创建一个层次结构并将其索引化。
>>> for token, children in ( ... (0, (1, 2)), (1, (3, 4)), (2, (10, 11, 12)), (3, (5, 6)), ... (4, (13, 14)), (5, (7, 8, 9)), (6, (15, 16)), (7, (17, 18, 19)), ... (8, (20, 21, 22)), (9, (23, 24)), (10, (25, 26)), ... (11, (27, 28, 29, 30, 31, 32))): ... catalog.index(Relation(token, children)) ...
[25] 这个层次结构是任意的。以下是我们的标记指向子节点的列表
_____________0_____________ / \ ________1_______ ______2____________ / \ / | \ ______3_____ _4_ 10 ____11_____ 12 / \ / \ / \ / / | \ \ \ _______5_______ 6 13 14 25 26 27 28 29 30 31 32 / | \ / \ _7_ _8_ 9 15 16 / | \ / | \ / \ 17 18 19 20 21 22 23 24
12 个关系,标记从 0 到 11,总共有 33 个标记,包括子节点。12 个关系的 id 是从 100 到 111,对应于 0 到 11 的标记。
没有传递性搜索索引,我们可以获取所有传递性结果。这些结果是迭代器。
>>> res = catalog.findRelationTokens({'token': 0}) >>> getattr(res, '__next__') is None False >>> getattr(res, '__len__', None) is None True >>> sorted(res) [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111] >>> list(res) []>>> res = catalog.findValueTokens('children', {'token': 0}) >>> sorted(res) == list(range(1, 33)) True >>> list(res) []
[26] canFind 也支持传递性搜索,并且会使用传递性搜索索引,下面我们将看到。
>>> catalog.canFind({'token': 1}, targetQuery={'children': 23}) True >>> catalog.canFind({'token': 2}, targetQuery={'children': 23}) False >>> catalog.canFind({'children': 23}, targetQuery={'token': 1}) True >>> catalog.canFind({'children': 23}, targetQuery={'token': 2}) False
findRelationTokenChains 不会改变,但我们会将其包含在讨论和示例中,以展示这一点。
>>> res = catalog.findRelationTokenChains({'token': 2}) >>> chains = list(res) >>> len(chains) 3 >>> len(list(res)) 0
传递搜索索引
现在我们可以添加几个传递性搜索索引。我们首先来谈谈它们。
目前有一种传递性索引,它为传递性查询工厂中的关系和值搜索建立索引。
索引只能在某些条件下使用。
搜索不是关系链请求。
它没有指定最大深度。
未使用过滤器。
如果是一个值搜索,那么如果使用了目标过滤器或目标查询,就不能使用特定值索引,但仍然可以使用基本关系索引。
搜索索引的使用在很大程度上是透明的:设置它们,关系目录将使用它们来处理之前更粗糙的API调用。唯一的不同之处在于,使用索引的结果通常将是BTree结构,而不是迭代器。
当为一个关系添加传递性索引时,您必须指定查询的传递性名称(或名称),同样反向也是如此。我们现在就做这些。
>>> import zc.relation.searchindex >>> catalog.addSearchIndex( ... zc.relation.searchindex.TransposingTransitiveMembership( ... 'token', 'children', names=('children',)))
现在我们应该安装了一个搜索索引。
请注意,我们是从父(令牌)到子:这个索引主要设计用于帮助在层次结构中进行传递性成员搜索。使用它来索引父项将带来很多不必要的写入开销。
这里还可以指定一些内容:查询的静态字段进行一些过滤。在这个示例中我们不需要这些。
现在目录如何使用这个索引进行搜索?三种基本方式,具体取决于搜索的类型,关系、值或 canFind。在我们开始深入了解内部之前,让我们验证我们是否得到了预期的结果:正确的答案,而不是迭代器,而是BTree结构。
>>> res = catalog.findRelationTokens({'token': 0}) >>> list(res) [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111] >>> list(res) [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111]>>> res = catalog.findValueTokens('children', {'token': 0}) >>> list(res) == list(range(1, 33)) True >>> list(res) == list(range(1, 33)) True>>> catalog.canFind({'token': 1}, targetQuery={'children': 23}) True >>> catalog.canFind({'token': 2}, targetQuery={'children': 23}) False
[27] 注意,当我们没有索引时通过这些示例的最后一个两个 canFind 示例没有使用索引,所以我们这里不显示:它们的方向对于这个索引是不正确的。
那么这些结果是如何发生的呢?
第一个,findRelationTokens,和最后一个,canFind,是最直接的。索引找到所有与给定查询匹配的关系,非传递性。然后对于每个关系,它查找该令牌的索引传递性结果。最终结果是所有从非传递性搜索中找到的索引结果的并集。canFind 简单地将结果转换为布尔值。
findValueTokens 与上面相同,只是多了一步。在计算关系并集后,该方法返回所有找到的关系请求值的集合的并集。
当关系重新索引时,它将自行维护。
>>> rel = list(catalog.findRelations({'token': 11}))[0] >>> for t in (27, 28, 29, 30, 31): ... rel.children.remove(t) ... >>> catalog.index(rel)>>> catalog.findValueTokens('children', {'token': 0}) ... # doctest: +NORMALIZE_WHITESPACE LFSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 32]) >>> catalog.findValueTokens('children', {'token': 2}) LFSet([10, 11, 12, 25, 26, 32]) >>> catalog.findValueTokens('children', {'token': 11}) LFSet([32])>>> rel.children.remove(32) >>> catalog.index(rel)>>> catalog.findValueTokens('children', {'token': 0}) ... # doctest: +NORMALIZE_WHITESPACE LFSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26]) >>> catalog.findValueTokens('children', {'token': 2}) LFSet([10, 11, 12, 25, 26]) >>> catalog.findValueTokens('children', {'token': 11}) LFSet([])>>> rel.children.insert(27) 1 >>> catalog.index(rel)>>> catalog.findValueTokens('children', {'token': 0}) ... # doctest: +NORMALIZE_WHITESPACE LFSet([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27]) >>> catalog.findValueTokens('children', {'token': 2}) LFSet([10, 11, 12, 25, 26, 27]) >>> catalog.findValueTokens('children', {'token': 11}) LFSet([27])
当索引被复制时,搜索索引也会被复制。
>>> new = catalog.copy() >>> res = list(new.iterSearchIndexes()) >>> len(res) 1 >>> new_index = res[0] >>> res = list(catalog.iterSearchIndexes()) >>> len(res) 1 >>> old_index = res[0] >>> new_index is old_index False >>> old_index.index is new_index.index False >>> list(old_index.index.keys()) == list(new_index.index.keys()) True >>> from __future__ import print_function >>> for key, value in old_index.index.items(): ... v = new_index.index[key] ... if v is value or list(v) != list(value): ... print('oops', key, value, v) ... break ... else: ... print('good') ... good >>> old_index.names is not new_index.names True >>> list(old_index.names) == list(new_index.names) True >>> for name, old_ix in old_index.names.items(): ... new_ix = new_index.names[name] ... if new_ix is old_ix or list(new_ix.keys()) != list(old_ix.keys()): ... print('oops') ... break ... for key, value in old_ix.items(): ... v = new_ix[key] ... if v is value or list(v) != list(value): ... print('oops', name, key, value, v) ... break ... else: ... continue ... break ... else: ... print('good') ... good
辅助工具
在编写搜索索引和查询工厂时,您通常希望完全访问关系目录数据。我们已经看到了这些工具中的许多
getRelationModuleTools 获取用于处理关系的BTree工具的字典。
>>> sorted(catalog.getRelationModuleTools().keys()) ... # doctest: +NORMALIZE_WHITESPACE ['BTree', 'Bucket', 'Set', 'TreeSet', 'difference', 'dump', 'intersection', 'load', 'multiunion', 'union']
‘multiunion’ 只在BTree是I*或L*模块时存在。使用 zc.relation.catalog.multiunion 辅助函数,为给定的工具集执行最佳并集。
getValueModuleTools 对索引值做相同的事情。
>>> tools = set(('BTree', 'Bucket', 'Set', 'TreeSet', 'difference', ... 'dump', 'intersection', 'load', 'multiunion', 'union')) >>> tools.difference(catalog.getValueModuleTools('children').keys()) == set() True
>>> tools.difference(catalog.getValueModuleTools('token').keys()) == set() True
getRelationTokens 可以返回目录中的所有标记。
>>> len(catalog.getRelationTokens()) == len(catalog) True
这也恰好等同于使用空查询的 findRelationTokens。
>>> catalog.getRelationTokens() is catalog.findRelationTokens({}) True
它还可以返回与给定查询匹配的所有标记,如果没有匹配则返回 None。
>>> catalog.getRelationTokens({'token': 0}) # doctest: +ELLIPSIS <BTrees.LOBTree.LOTreeSet object at ...> >>> list(catalog.getRelationTokens({'token': 0})) [100]
这也恰好等同于带有查询、最大深度为 1 且没有其他参数的 findRelationTokens。
>>> catalog.findRelationTokens({'token': 0}, maxDepth=1) is ( ... catalog.getRelationTokens({'token': 0})) True
但是,如果没有匹配,findRelationTokens 返回一个空集(因此它始终返回一个可迭代对象)。
>>> catalog.findRelationTokens({'token': 50}, maxDepth=1) LOSet([]) >>> print(catalog.getRelationTokens({'token': 50})) None
- getValueTokens 可以返回目录中给定值名称的所有标记。
-
>>> list(catalog.getValueTokens('token')) == list(range(12)) True
这等同于使用名称仅调用 catalog.findValueTokens(或者使用空查询和最大深度为 1)。
>>> list(catalog.findValueTokens('token')) == list(range(12)) True >>> catalog.findValueTokens('token') is catalog.getValueTokens('token') True
它还可以返回给定标记的值。
>>> list(catalog.getValueTokens('children', 100)) [1, 2]
这等同于使用名称和查询 {None: token} 调用 catalog.findValueTokens。
>>> list(catalog.findValueTokens('children', {None: 100})) [1, 2] >>> catalog.getValueTokens('children', 100) is ( ... catalog.findValueTokens('children', {None: 100})) True
但是,如果没有匹配,findValueTokens 返回一个空集(因此它始终返回一个可迭代对象);而 getValueTokens 如果关系没有值(或关系是未知的),则返回 None。
>>> catalog.findValueTokens('children', {None: 50}, maxDepth=1) LFSet([]) >>> print(catalog.getValueTokens('children', 50)) None
>>> rel.children.remove(27) >>> catalog.index(rel) >>> catalog.findValueTokens('children', {None: rel.id}, maxDepth=1) LFSet([]) >>> print(catalog.getValueTokens('children', rel.id)) None
- yieldRelationTokenChains 是用于使用查询工厂的搜索的强力工具。待办事项:描述。
查询工厂知道何时不需要它——不仅当它的名称都没有被使用时,而且当两个名称都被使用时。
>>> list(catalog.findRelationTokens({'token': 0, 'children': 1})) [100]
当值与它们的标记相同时,findValues 返回与 findValueTokens 相同的结果。在这里,我们看到这是没有索引的情况。
>>> list(catalog.findValueTokens('children', {'token': 0})) == list( ... catalog.findValues('children', {'token': 0})) True
再次强调,当值与它们的标记相同时,findValues 返回与 findValueTokens 相同的结果。在这里,我们看到这是带有索引的情况。
>>> list(catalog.findValueTokens('children', {'token': 0})) == list( ... catalog.findValues('children', {'token': 0})) True
优化关系目录的使用
关于目录,有几种最佳实践和优化机会。
尽可能使用整数键的 BTree 集合。它们可以使用 BTrees 的 multiunion 来提高速度。整数的 __cmp__ 是可靠的,并且在 C 中。
永远不要使用持久对象作为键。每次需要查看它们时都会导致数据库负载,它们占用内存和对象缓存,并且它们(根据本文写作时的信息)禁用了冲突解决。Intids(或类似)是表示对象的最佳选择,其次是其他不可变的对象,如字符串,然后是 zope.app.keyreferences(或类似)。
尽可能在查询中使用多个标记值,特别是在你的传递查询工厂中。
在加载和转储标记以及在你的传递查询工厂中使用缓存。
尽可能不要加载或转储标记(值本身可能用作标记)。当你有多个标记时,这一点尤为重要:将它们存储在与 zc.relation 模块相同的模块中的 BTree 结构中,用于值。
对于某些操作,特别是当单个关系值有数百或数千个成员时,这些优化可以将一些常见情况的重索引工作速度提高约 100 倍。
最简单(也许也是最无用的)优化是,所有由单个操作生成的转储调用和所有加载调用共享一个按调用类型(转储/加载)和按索引关系值分组的缓存字典。例如,我们可以存储一个 intids 工具,这样我们只需执行一次工具查找,之后只需进行单个字典查找。这是 zc.relationship 的 index.py 中默认的 generateToken 和 resolveToken 函数所做的:看看它们作为示例。
进一步的优化是不加载或转储标记,而是使用可能是标记的值。如果标记在 C 中具有 __cmp__(或等效)功能,这将特别有用,例如内置类型如整数。要指定此行为,您需要显式将索引的属性描述的“加载”和“转储”值设置为 None。
>>> import zope.interface >>> class IRelation(zope.interface.Interface): ... subjects = zope.interface.Attribute( ... 'The sources of the relation; the subject of the sentence') ... relationtype = zope.interface.Attribute( ... '''unicode: the single relation type of this relation; ... usually contains the verb of the sentence.''') ... objects = zope.interface.Attribute( ... '''the targets of the relation; usually a direct or ... indirect object in the sentence''') ...>>> import BTrees >>> relations = BTrees.family32.IO.BTree() >>> relations[99] = None # just to give us a start>>> @zope.interface.implementer(IRelation) ... class Relation(object): ... ... def __init__(self, subjects, relationtype, objects): ... self.subjects = subjects ... assert relationtype in relTypes ... self.relationtype = relationtype ... self.objects = objects ... self.id = relations.maxKey() + 1 ... relations[self.id] = self ... def __repr__(self): ... return '<%r %s %r>' % ( ... self.subjects, self.relationtype, self.objects)>>> def token(rel, self): ... return rel.token ... >>> def children(rel, self): ... return rel.children ... >>> def dumpRelation(obj, index, cache): ... return obj.id ... >>> def loadRelation(token, index, cache): ... return relations[token] ...>>> relTypes = ['has the role of'] >>> def relTypeDump(obj, index, cache): ... assert obj in relTypes, 'unknown relationtype' ... return obj ... >>> def relTypeLoad(token, index, cache): ... assert token in relTypes, 'unknown relationtype' ... return token ...>>> import zc.relation.catalog >>> catalog = zc.relation.catalog.Catalog( ... dumpRelation, loadRelation) >>> catalog.addValueIndex(IRelation['subjects'], multiple=True) >>> catalog.addValueIndex( ... IRelation['relationtype'], relTypeDump, relTypeLoad, ... BTrees.family32.OI, name='reltype') >>> catalog.addValueIndex(IRelation['objects'], multiple=True) >>> import zc.relation.queryfactory >>> factory = zc.relation.queryfactory.TransposingTransitive( ... 'subjects', 'objects') >>> catalog.addDefaultQueryFactory(factory)>>> rel = Relation((1,), 'has the role of', (2,)) >>> catalog.index(rel) >>> list(catalog.findValueTokens('objects', {'subjects': 1})) [2]
如果您有单个关系,涉及数百或数千个对象,如果该值是存储给定属性的BTree的“倍数”类型,那么这将是一个巨大的优势。默认的BTree系列是IFBTree;IOBTree也是一个不错的选择,可能适用于某些应用程序。
>>> catalog.unindex(rel) >>> rel = Relation( ... BTrees.family32.IF.TreeSet((1,)), 'has the role of', ... BTrees.family32.IF.TreeSet()) >>> catalog.index(rel) >>> list(catalog.findValueTokens('objects', {'subjects': 1})) [] >>> list(catalog.findValueTokens('subjects', {'objects': None})) [1]
重新索引是可能出现重大改进的地方。以下操作对优化代码进行了锻炼。
>>> rel.objects.insert(2) 1 >>> catalog.index(rel) >>> list(catalog.findValueTokens('objects', {'subjects': 1})) [2] >>> rel.subjects = BTrees.family32.IF.TreeSet((3,4,5)) >>> catalog.index(rel) >>> list(catalog.findValueTokens('objects', {'subjects': 3})) [2]>>> rel.subjects.insert(6) 1 >>> catalog.index(rel) >>> list(catalog.findValueTokens('objects', {'subjects': 6})) [2]>>> rel.subjects.update(range(100, 200)) 100 >>> catalog.index(rel) >>> list(catalog.findValueTokens('objects', {'subjects': 100})) [2]>>> rel.subjects = BTrees.family32.IF.TreeSet((3,4,5,6)) >>> catalog.index(rel) >>> list(catalog.findValueTokens('objects', {'subjects': 3})) [2]>>> rel.subjects = BTrees.family32.IF.TreeSet(()) >>> catalog.index(rel) >>> list(catalog.findValueTokens('objects', {'subjects': 3})) []>>> rel.subjects = BTrees.family32.IF.TreeSet((3,4,5)) >>> catalog.index(rel) >>> list(catalog.findValueTokens('objects', {'subjects': 3})) [2]
tokenizeValues和resolveValueTokens在没有加载器和导出器的情况下工作正常——也就是说,它们什么都不做。
>>> catalog.tokenizeValues((3,4,5), 'subjects') (3, 4, 5) >>> catalog.resolveValueTokens((3,4,5), 'subjects') (3, 4, 5)
变更
2.0 (2023-04-05)
停止支持Python 2.7、3.5和3.6。[ale-rt]
修复对ZODB的依赖,我们只需依赖于BTrees包。参考#11。[ale-rt]
1.2 (2023-03-28)
根据PEP-479(修改生成器内的StopIteration处理)调整代码。参见:https://peps.pythonlang.cn/pep-0479。修复#11。[ale-rt]
1.1.post2(2018-06-18)
再次尝试通过使用正确的预期元数据语法修复PyPI页面。
1.1.post1(2018-06-18)
使用正确的ReST语法修复PyPI页面。
1.1 (2018-06-15)
添加对Python 3.5和3.6的支持。
1.0 (2008-04-23)
这是zc.relation包的初始版本。然而,它代表了另一个包,zc.relationship的重构。此包只包含关系索引的修改版本,现在称为目录。重构后的zc.relationship索引依赖于(子类)此目录。zc.relationship还维护一个向后兼容的子类。
此包仅依赖于ZODB、zope.interface和zope.testing软件,可以在标准ZODB数据库内部或外部使用。尽管如此,该软件必须存在(包在ZODB BTrees包上高度依赖)。
如果您想将旧版zc.relationship索引切换到zc.relation目录,请尝试在您的生成脚本中使用此技巧。假设旧索引是old,以下行应创建一个新的zc.relation目录,包含您的旧数据
>>> new = old.copy(zc.relation.Catalog)
为什么现在将相同的基本数据结构称为目录?因为我们暴露了修改数据结构的能力,您真正添加和删除的是索引。将索引放入索引中是没有意义的,但将索引放入目录中是有意义的。因此,名称更改应运而生。
此包中的目录与早期的zc.relationship索引存在几个不兼容性,以及许多新功能。zc.relationship包维护一个向后兼容的子类。以下讨论将zc.relation目录与zc.relationship 1.x索引进行比较。
与zc.relationship 1.x索引的不兼容性
两个主要变化是,现在的方法名称引用Relation而不是Relationship;目录的实例化方式与索引略有不同。还有一些其他变化值得注意。以下列表试图突出所有不兼容性。
- 重大不兼容性::
findRelationshipTokenSet和findValueTokenSet已重命名,语义略有不同,分别为getRelationTokens和getValueTokens。可以通过findRelationTokens(query, 1)(其中1是maxDepth)获得与findRelationTokenSet(query)完全相同的结果。可以通过findValueTokens(name, {zc.relation.RELATION: reltoken}, 1)获得与findValueTokenSet(reltoken, name)完全相同的结果。
findRelations取代了findRelatonships。新方法将在设置了默认TransitiveQueriesFactory并且maxDepth不是1时使用。它共享findRelationChains的调用签名。
isLinked现在是canFind。
目录实例化参数已从旧索引更改。
load 和 dump(以前分别称为 loadRel 和 dumpRel)现在是实例化的必选参数。
其他唯一可选参数是 btree(曾是 relFamily)和 family。现在您可以使用 addValueIndex 指定要索引的元素。
注意,与旧的实例化选项不同,addValueIndex 默认不提供加载和卸载功能。
查询工厂不同。请参阅接口中的 IQueryFactory。
它们首先获取(查询、目录、缓存),然后返回一个可调用的 getQueries,该调用获取关系链并产生查询;或不匹配时返回 None。
它们还必须处理空的关系链。通常这应该返回原始查询,但也可以用来修改原始查询。
它们不再被视为传递查询工厂,而是作为通用查询修改器。
- 中等:
目录不再继承自 zope.app.container.contained.Contained。
索引需要 ZODB 3.8 或更高版本。
- 小:
deactivateSets 已不再是实例化选项(它因 ZODB 错误而损坏,这在文档中已有描述)。
变更和新功能
目录现在提供索引某些搜索的能力。必须显式实例化和注册您想要优化的索引。这可以在搜索值、搜索关系或确定两个对象是否链接时使用。它不能用于关系链。请求索引具有更大的存储空间和较慢的写入速度与较快的搜索速度之间的权衡。注册搜索索引是在实例化之后完成的;您可以迭代当前使用的设置,并删除它们。(代码路径期望支持所有这些 API 的遗留 zc.relationship 索引实例。)
现在您可以在目录创建后指定新值,迭代使用的设置,并删除值。
目录有一个复制方法,可以快速创建新副本,而无需实际重新索引关系。
查询参数现在可以使用 zc.relation.catalog.any(1, 2, 3, 4) 或 zc.relation.catalog.Any((1, 2, 3, 4)) 来指定给定名称的多个值。
目录支持通过传递可调用函数而不是接口元素(也仍然支持)来指定索引值。
findRelations 和新方法 findRelationTokens 可以递归和递归地找到关系。findRelationTokens 在递归使用时重复了遗留 zc.relationship 索引的 findRelationTokenSet 行为。(findRelationTokenSet 仍然在 API 中,未弃用,是 findValueTokenSet 的配套。)
在 findValues 和 findValueTokens 中,query 参数现在是可选的。如果查询在布尔上下文中评估为 False,则返回所有值或值标记。值标记会显式地使用底层 BTree 存储返回。这可以直接用于其他 BTree 操作。
完全新的文档。不幸的是,仍然不够好。
该包从 zc.relationship 中大大减少了直接依赖:它现在更明显是一个 ZODB 工具,除了 zope.testing 和 zope.interface 以外,没有其他 Zope 依赖项。
监听器允许对象监听目录的消息(可以直接使用或,例如,触发事件)。
您可以使用 zc.relation.RELATION…的键搜索关系,这实际上是一个对 None 的别名。抱歉。但嘿,使用这个常量!我认为它更易读。
现在,`tokenizeQuery`(以及`resolveQuery`)接受关键字参数作为正常字典查询的替代。这可以使构建查询更加吸引人(例如,query = catalog.tokenizeQuery; res = catalog.findValues('object', query(subject='joe', predicate=OWNS)))。
项目详情
下载文件
下载适用于您平台的文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。
源分发
构建分发
zc.relation-2.0.tar.gz 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 73493dcce01b763450483086f047a226e9f5d0dd8b233d7ec602b2ff5deed963 |
|
MD5 | 782e13cb331b034a83c9ed6043d17112 |
|
BLAKE2b-256 | b6d74fa6191cc0c14688a1f5ddb5217d846fd1a060e84a3051fe9657d1bcf8cd |
zc.relation-2.0-py3-none-any.whl 的哈希值
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 41461015747508ca66becddd222838bd6246e2909cc3c9fd922b7e0705da009e |
|
MD5 | 2bdd76b98942e6fa4c785bad17818c26 |
|
BLAKE2b-256 | 04a6e10fc8a71f4d6fbcb30a3852a45848ff109bd6015909d8cfe54c8b30a507 |