Zope 3 目录的扩展
项目描述
zc.catalog 是 Zope 3 目录的扩展,Zope 3 的索引和搜索工具。zc.catalog 包含对 Zope 3 目录的许多扩展,例如一些新索引、改进的通配符和词干支持,以及替代目录实现。
<nav class="contents" id="contents" role="doc-toc">内容
变化
3.0 (2019-03-21)
在 Python 3.4 生命周期结束时放弃对它的支持。
添加对 Python 3.7 和 3.8a2 的支持。
2.0.1 (2017-06-15)
为zopyx.txng3.ext词干分析器添加 Python 3 兼容性。见#4。
2.0.0 (2017-05-09)
添加对 Python 3.4、3.5、3.6 和 PyPy 的支持。请注意, zopyx.txng3.ext词干分析器在 Python 3 上不可用。
删除对 zope.app.zcmlfiles 和 zope.app.testing 等的测试依赖。
1.6 (2013-07-04)
使用 Python 的doctest模块而不是弃用的 zope.testing.doctest。
将zope.intid移至依赖项。
1.5.1 (2012-01-20)
修复范围目录的searchResults方法以在使用本地 uid 源时工作。
用 zope.password替换了对zope.app.authentication的测试依赖。
删除了 zope.app.server测试依赖项。
1.5 (2010-10-19)
包的configure.zcml不再包含浏览器子包的 configure.zcml。
这与browser和test_browser extras_require 一起,将浏览器视图注册与主代码分离。因此,不需要注册 ZMI 视图的项目不再引入 zope.app.* 依赖项。
要为您的项目启用 ZMI 视图,您必须做两件事:
将zc.catalog [browser]列为install_requires。
让您的项目的configure.zcml包含zc.catalog.browser 子包。
仅当浏览器测试的依赖项可用时才包含浏览器测试。
Python2.7 测试修复。
1.4.5 (2010-10-05)
删除对 zope.app.dublincore 的隐式测试依赖,这是一开始就不需要的。
1.4.4 (2010-07-06)
修复了最近的机械化(>=2.0) 发生的测试失败。
1.4.3 (2010-03-09)
首先尝试从 zopyx.txng3.ext 包中导入词干分析器,从 3.3.2 开始,该包包含稳定性和内存泄漏修复。
1.4.2 (2010-01-20)
通过添加 zope.login 修复使用 ZTK 时缺少的测试依赖项。
1.4.1 (2009-02-27)
为 ValueIndex 添加类似 FieldIndex 的排序支持。
添加对 NormalizationWrapper 的排序索引支持。
1.4.0 (2009-02-07)
修复了 ValueIndex addform 和 addMenuItem 中的错字
使用zope.container而不是zope.app.container。
使用zope.keyreference而不是zope.app.keyreference。
使用zope.intid而不是zope.app.intid。
使用zope.catalog而不是zope.app.catalog。
1.3.0 (2008-09-10)
添加了挂钩点以允许将范围目录与本地 UID 源一起使用。
1.2.0 (2007-11-03)
更新了包元数据。
zc.catalog 现在可以使用 ZODB 3.8 提供的 64 位 BTree(“L”)。
Albertas Agejavas ( alga @ pov . lt ) 包含了新的 CallableWrapper,因为当典型的 Zope 3 index-by-adapter 故事 (zope.app.catalog.attribute) 是不必要的麻烦时,您只想使用可调用的。请参阅 callablewrapper.txt。这也可以用于基于 zope.index 接口的其他索引。
范围现在有一个 __len__。当前实现遵循标准的 BTree len 实现,并共享其性能特征:它需要唤醒所有桶,但如果所有桶都处于唤醒状态,则它是一个相当快的操作。
一个简单的 ISelfPoulatingExtent 被添加到 extentcatalog 模块中,对于该模块,填充是无操作的。这对于用作组件实现细节的目录直接有用,其中对象由您自己的调用而不是通常的订阅者显式索引。作为其他自填充范围的基础,它也可能稍微有用。
1.1.1 (2007-3-17)
当其中一个值没有结果时,'all_of' 将返回所有结果。Nando Quintana 报告并提供了测试和修复。
1.1 (2007-01-06)
删除的功能
扩展目录中的事件排队已完全删除。子事务对 1.0 中引入的代码造成了重大问题。其他解决方案也存在重大问题,这种排队的胜利是值得怀疑的。以下是为使排队工作而被拒绝的方法的简要说明:
_p_invalidate(在 1.0 中使用)。并非真正设计用于事务中,而是恢复到最后一个保存点,而不是事务的开始。可以使用monkeypatch 保存点来迭代预提交事务钩子,但这听起来太糟糕了。
_p_resolve冲突。要求应用软件存在于 ZEO 甚至 ZRS 安装中,这与我们的软件部署目标背道而驰。还会导致无用的空队列重复写入数据库,但这不是最重要的。
用于队列的单独存储或事务管理器的模糊手动想法。从未在讨论中脱颖而出。
1.0 (2007-01-05)
已修复的错误
调整 extentcatalog 测试以触发(并讨论和测试)排队行为。
修复了由于排队代码导致的过多冲突错误的问题。
更新词干以使用最新版本的 TextIndexNG 扩展。
当 TextIndexNG 的扩展不可用时省略了词干测试,因此没有它测试通过。由于 TextIndexNG 的扩展是可选的,这似乎是合理的。
在 extentcatalog 中删除了 zapi 的使用。
0.2 (2006-11-22)
添加的功能
在 Cheeseshop 上首次发布。
价值指数
valueindex 是一个类似于标准 Zope 字段索引但更灵活的索引。索引允许搜索包含任何一组值的文档;在一组值之间;任何(非无)值;和任何空值。
此外,索引支持允许检查索引值的接口。
它尽可能无策略,旨在成为具有更多策略的索引的引擎,并且本身也很有用。
在创建时,索引没有 wordCount,没有 documentCount,并且正如预期的那样,相当空。
>>> from zc.catalog.index import ValueIndex >>> index = ValueIndex() >>> index.documentCount() 0 >>> index.wordCount() 0 >>> index.maxValue() # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError:... >>> index.minValue() # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError:... >>> list(index.values()) [] >>> len(index.apply({'any_of': (5,)})) 0
索引支持索引任何值。给定索引中的所有值必须在 Python 版本中一致地排序。
>>> data = {1: 'a', ... 2: 'b', ... 3: 'a', ... 4: 'c', ... 5: 'd', ... 6: 'c', ... 7: 'c', ... 8: 'b', ... 9: 'c', ... } >>> for k, v in data.items(): ... index.index_doc(k, v) ...
索引后,统计数据和值与新输入的内容相匹配。
>>> list(index.values()) ['a', 'b', 'c', 'd'] >>> index.documentCount() 9 >>> index.wordCount() 4 >>> index.maxValue() 'd' >>> index.minValue() 'a' >>> list(index.ids()) [1, 2, 3, 4, 5, 6, 7, 8, 9]
该索引支持四种类型的查询。第一个是'any_of'。它接受一个可迭代的值,并返回一个包含任何值的文档 ID 的可迭代。结果没有加权。
>>> list(index.apply({'any_of': ('b', 'c')})) [2, 4, 6, 7, 8, 9] >>> list(index.apply({'any_of': ('b',)})) [2, 8] >>> list(index.apply({'any_of': ('d',)})) [5] >>> bool(index.apply({'any_of': (42,)})) False
另一个查询是'any',如果键是None,则返回所有具有任何值的索引文档ID。如果键是范围,则返回范围和所有具有任何值的文档 ID 的交集。
>>> list(index.apply({'any': None})) [1, 2, 3, 4, 5, 6, 7, 8, 9]>>> from zc.catalog.extentcatalog import FilterExtent >>> extent = FilterExtent(lambda extent, uid, obj: True) >>> for i in range(15): ... extent.add(i, i) ... >>> list(index.apply({'any': extent})) [1, 2, 3, 4, 5, 6, 7, 8, 9] >>> limited_extent = FilterExtent(lambda extent, uid, obj: True) >>> for i in range(5): ... limited_extent.add(i, i) ... >>> list(index.apply({'any': limited_extent})) [1, 2, 3, 4]
'between' 参数有 1 到 4 个值。第一个是最小值,默认为None,表示没有最小值;第二个是最大值,默认为None,表示没有最大值;next 是一个布尔值,是否应排除最小值,默认为 False;最后一个是是否应排除最大值的布尔值,默认为 False。结果没有加权。
>>> list(index.apply({'between': ('b', 'd')})) [2, 4, 5, 6, 7, 8, 9] >>> list(index.apply({'between': ('c', None)})) [4, 5, 6, 7, 9] >>> list(index.apply({'between': ('c',)})) [4, 5, 6, 7, 9] >>> list(index.apply({'between': ('b', 'd', True, True)})) [4, 6, 7, 9]
对 between 使用无效(在 Python 3 上不可比较)参数不会产生任何结果:
>>> list(index.apply({'between': (1, 5)})) []
'none' 参数接受一个范围并返回未索引范围内的 id;它旨在用于返回没有(或为空)值的 docid。
>>> list(index.apply({'none': extent})) [0, 10, 11, 12, 13, 14]
尝试一次使用多个这些会产生错误。
>>> index.apply({'between': (5,), 'any_of': (3,)}) ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError:...
不使用它们只会返回 None。
>>> index.apply({}) # returns None
无效的查询名称会导致 ValueErrors。
>>> index.apply({'foo': ()}) ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError:...
取消索引文档时,应更新搜索和统计信息。
>>> index.unindex_doc(5) >>> len(index.apply({'any_of': ('d',)})) 0 >>> index.documentCount() 8 >>> index.wordCount() 3 >>> list(index.values()) ['a', 'b', 'c'] >>> list(index.ids()) [1, 2, 3, 4, 6, 7, 8, 9]
重新索引具有更改值的文档也会反映在后续搜索和统计检查中。
>>> list(index.apply({'any_of': ('b',)})) [2, 8] >>> data[8] = 'e' >>> index.index_doc(8, data[8]) >>> index.documentCount() 8 >>> index.wordCount() 4 >>> list(index.apply({'any_of': ('e',)})) [8] >>> list(index.apply({'any_of': ('b',)})) [2] >>> data[2] = 'e' >>> index.index_doc(2, data[2]) >>> index.documentCount() 8 >>> index.wordCount() 3 >>> list(index.apply({'any_of': ('e',)})) [2, 8] >>> list(index.apply({'any_of': ('b',)})) []
重新索引现在值为 None 的文档会导致它从统计信息中删除。
>>> data[3] = None >>> index.index_doc(3, data[3]) >>> index.documentCount() 7 >>> index.wordCount() 3 >>> list(index.ids()) [1, 2, 4, 6, 7, 8, 9]
这会影响确定索引中和不在索引中的 id 的两种方式(有值和没有值)。
>>> list(index.apply({'any': None})) [1, 2, 4, 6, 7, 8, 9] >>> list(index.apply({'any': extent})) [1, 2, 4, 6, 7, 8, 9] >>> list(index.apply({'none': extent})) [0, 3, 5, 10, 11, 12, 13, 14]
values 方法可用于检查给定文档 id 的索引值。对于 valueindex,给定 doc_id 的“值”将始终具有 0 或 1 的长度。
>>> index.values(doc_id=8) ('e',)
containsValue 方法提供了一种确定值成员资格的方法。
>>> index.containsValue('a') True >>> index.containsValue('q') False
排序值索引
值索引支持排序,就像 zope.index.field.FieldIndex。
>>> index.clear()>>> index.index_doc(1, 9) >>> index.index_doc(2, 8) >>> index.index_doc(3, 7) >>> index.index_doc(4, 6) >>> index.index_doc(5, 5) >>> index.index_doc(6, 4) >>> index.index_doc(7, 3) >>> index.index_doc(8, 2) >>> index.index_doc(9, 1)>>> list(index.sort([4, 2, 9, 7, 3, 1, 5])) [9, 7, 5, 4, 3, 2, 1]
我们还可以指定reverse参数来反转结果:
>>> list(index.sort([4, 2, 9, 7, 3, 1, 5], reverse=True)) [1, 2, 3, 4, 5, 7, 9]
根据 IIndexSort,我们可以通过指定limit 参数来限制结果:
>>> list(index.sort([4, 2, 9, 7, 3, 1, 5], limit=3)) [9, 7, 5]
如果我们传递一个没有被这个索引索引的 id,它就不会被包含在结果中。
>>> list(index.sort([2, 10])) [2]
设置索引
setindex 是一种类似于传统关键字索引但比传统关键字索引更通用的索引。索引的值应该是可迭代的;索引允许搜索包含任何一组值的文档;所有一组值;或在一组值之间。
此外,索引支持允许检查索引值的接口。
它尽可能无策略,旨在成为具有更多策略的索引的引擎,并且本身也很有用。
在创建时,索引没有 wordCount,没有 documentCount,并且正如预期的那样,相当空。
>>> from zc.catalog.index import SetIndex >>> index = SetIndex() >>> index.documentCount() 0 >>> index.wordCount() 0 >>> index.maxValue() # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError:... >>> index.minValue() # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError:... >>> list(index.values()) [] >>> len(index.apply({'any_of': (5,)})) 0
索引支持索引任何值。给定索引中的所有值必须在 Python 版本中一致地排序。实际上,在 Python 3 中,这意味着值需要是同质的。
>>> data = {1: ['a', '1'], ... 2: ['b', 'a', '3', '4', '7'], ... 3: ['1'], ... 4: ['1', '4', 'c'], ... 5: ['7'], ... 6: ['5', '6', '7'], ... 7: ['c'], ... 8: ['1', '6'], ... 9: ['a', 'c', '2', '3', '4', '6',], ... } >>> for k, v in data.items(): ... index.index_doc(k, v) ...
索引后,统计数据和值与新输入的内容相匹配。
>>> list(index.values()) ['1', '2', '3', '4', '5', '6', '7', 'a', 'b', 'c'] >>> index.documentCount() 9 >>> index.wordCount() 10 >>> index.maxValue() 'c' >>> index.minValue() '1' >>> list(index.ids()) [1, 2, 3, 4, 5, 6, 7, 8, 9]
该索引支持五种类型的查询。第一个是'any_of'。它接受一个可迭代的值,并返回一个包含任何值的文档 ID 的可迭代。结果是加权的。
>>> list(index.apply({'any_of': ('b', '1', '5')})) [1, 2, 3, 4, 6, 8] >>> list(index.apply({'any_of': ('b', '1', '5')})) [1, 2, 3, 4, 6, 8] >>> list(index.apply({'any_of': ('42',)})) [] >>> index.apply({'any_of': ('a', '3', '7')}) # doctest: +ELLIPSIS BTrees...FBucket([(1, 1.0), (2, 3.0), (5, 1.0), (6, 1.0), (9, 2.0)])
使用无效(在 Python 3 上不可比较)参数将被忽略:
>>> list(index.apply({'any_of': (1,)})) [] >>> list(index.apply({'any_of': (1, '1')})) [1, 3, 4, 8]
另一个查询是“任何”。如果键为 None,则返回所有具有任何值的索引文档 ID。如果键是范围,则返回范围和所有具有任何值的文档 ID 的交集。
>>> list(index.apply({'any': None})) [1, 2, 3, 4, 5, 6, 7, 8, 9]>>> from zc.catalog.extentcatalog import FilterExtent >>> extent = FilterExtent(lambda extent, uid, obj: True) >>> for i in range(15): ... extent.add(i, i) ... >>> list(index.apply({'any': extent})) [1, 2, 3, 4, 5, 6, 7, 8, 9]>>> limited_extent = FilterExtent(lambda extent, uid, obj: True) >>> for i in range(5): ... limited_extent.add(i, i) ... >>> list(index.apply({'any': limited_extent})) [1, 2, 3, 4]
'all_of' 参数也接受一个可迭代的值,但返回一个包含所有值的文档 ID 的可迭代。结果没有加权。
>>> list(index.apply({'all_of': ('a',)})) [1, 2, 9] >>> list(index.apply({'all_of': ('3', '4')})) [2, 9] >>> list(index.apply({'all_of': (3, '4')})) [] >>> list(index.apply({'all_of': ('3', 4)})) []
这些测试说明了两个已修复的相关报告错误。
>>> list(index.apply({'all_of': ('z', '3', '4')})) [] >>> list(index.apply({'all_of': ('3', '4', 'z')})) []
'between' 参数有 1 到 4 个值。第一个是最小值,默认为None,表示没有最小值;第二个是最大值,默认为None,表示没有最大值;next 是一个布尔值,是否应排除最小值,默认为 False;最后一个是是否应排除最大值的布尔值,默认为 False。结果是加权的。
>>> list(index.apply({'between': ('1', '7')})) [1, 2, 3, 4, 5, 6, 8, 9] >>> list(index.apply({'between': ('b', None)})) [2, 4, 7, 9] >>> list(index.apply({'between': ('b',)})) [2, 4, 7, 9] >>> list(index.apply({'between': ('1', '7', True, True)})) [2, 4, 6, 8, 9] >>> index.apply({'between': ('2', '6')}) # doctest: +ELLIPSIS BTrees...FBucket([(2, 2.0), (4, 1.0), (6, 2.0), (8, 1.0), (9, 4.0)])
使用无效(在 Python 3 上不可比较)参数不会产生任何结果:
>>> list(index.apply({'between': (1, 7)})) []
'none' 参数接受一个范围并返回未索引范围内的 id;它旨在用于返回没有(或为空)值的 docid。
>>> list(index.apply({'none': extent})) [0, 10, 11, 12, 13, 14]
尝试一次使用多个这些会产生错误。
>>> index.apply({'all_of': ('5',), 'any_of': ('3',)}) ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError:...
不使用它们只会返回 None。
>>> index.apply({}) # returns None
无效的查询名称会导致 ValueErrors。
>>> index.apply({'foo': ()}) ... # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError:...
取消索引文档时,应更新搜索和统计信息。
>>> index.unindex_doc(6) >>> len(index.apply({'any_of': ('5',)})) 0 >>> index.documentCount() 8 >>> index.wordCount() 9 >>> list(index.values()) ['1', '2', '3', '4', '6', '7', 'a', 'b', 'c'] >>> list(index.ids()) [1, 2, 3, 4, 5, 7, 8, 9]
重新索引具有新附加值的文档也会反映在后续搜索和统计检查中。
>>> data[8].extend(['5', 'c']) >>> index.index_doc(8, data[8]) >>> index.documentCount() 8 >>> index.wordCount() 10 >>> list(index.apply({'any_of': ('5',)})) [8] >>> list(index.apply({'any_of': ('c',)})) [4, 7, 8, 9]
使用添加和删除重新索引文档也是如此。
>>> 2 in set(index.apply({'any_of': ('7',)})) True >>> 2 in set(index.apply({'any_of': ('2',)})) False >>> data[2].pop() '7' >>> data[2].append('2') >>> index.index_doc(2, data[2]) >>> 2 in set(index.apply({'any_of': ('7',)})) False >>> 2 in set(index.apply({'any_of': ('2',)})) True
重新索引不再具有任何值的文档会导致它从统计信息中删除。
>>> del data[2][:] >>> index.index_doc(2, data[2]) >>> index.documentCount() 7 >>> index.wordCount() 9 >>> list(index.ids()) [1, 3, 4, 5, 7, 8, 9]
这会影响确定索引中和不在索引中的 id 的两种方式(有值和没有值)。
>>> list(index.apply({'any': None})) [1, 3, 4, 5, 7, 8, 9] >>> list(index.apply({'none': extent})) [0, 2, 6, 10, 11, 12, 13, 14]
values 方法可用于检查给定文档 id 的索引值。
>>> set(index.values(doc_id=8)) == set(['1', '5', '6', 'c']) True
containsValue 方法提供了一种确定值成员资格的方法。
>>> index.containsValue('5') True >>> index.containsValue(5) False >>> index.containsValue('20') False
归一化指数
index 模块提供了一个规范化包装器、一个 DateTime 规范化器以及一个使用 DateTime 规范化器规范化的集合索引和值索引。
规范化包装器实现了完整的索引接口——zope.index.interfaces.IInjection、zope.index.interfaces.IIndexSearch、zope.index.interfaces.IStatistics 和 zc.catalog.interfaces.IIndexValues——并委托所有行为到包装的索引,在索引看到它们之前使用规范器规范化值。
规范化包装器当前仅支持 zc.catalog.interfaces.ISetIndex 和 zc.catalog.interfaces.IValueIndex 提供的查询。
规范器接口需要以下方法,如接口中所定义:
- 默认值(值):
“””规范化或检查输入值的约束;引发错误或返回要索引的值。”””
- 定义任何(值,索引):
“””规范化“any_of”搜索的查询值;返回一系列值。”””
- 定义全部(值,索引):
“”“规范化“all_of”搜索的查询值;返回查询的值”””
- def 最小值(值,索引):
“””将查询值归一化为范围的最小值;返回查询的值”””
- def 最大值(值,索引):
“””将查询值归一化为范围最大值;返回查询的值”””
DateTime 规范化器执行以下规范化和验证。每当需要时区时,它会尝试从当前交互中获取请求并将其调整为 zope.interface.common.idatetime.ITZInfo;失败(没有请求或没有适配器)它使用系统本地时区。
输入值必须是带有时区的日期时间。它们被归一化为创建归一化器时指定的分辨率:0 的分辨率将值归一化为天;分辨率为 1 到小时;2 到分钟;3到秒;和 4 到微秒。
“任何”值可以是时区感知日期时间、时区原始日期时间或日期。如上所述,日期将转换为找到的时区中给定日期的开始到结束的任何值。timezone-naive datetimes 获取找到的时区。
“所有”值可能是时区感知日期时间或时区原始日期时间。timezone-naive datetimes 获取找到的时区。
“最小”值可以是时区感知日期时间、时区原始日期时间或日期。如上所述,日期将转换为找到的时区中给定日期的开始。timezone-naive datetimes 获取找到的时区。
“最大”值可以是时区感知日期时间、时区原始日期时间或日期。如上所述,日期将转换为找到的时区中给定日期的结尾。timezone-naive datetimes 获取找到的时区。
让我们先看一下 DateTime 规范化器,然后将它与规范化包装器以及 value 和 set 索引进行集成。
索引值用“值”解析。
>>> from zc.catalog.index import DateTimeNormalizer >>> n = DateTimeNormalizer() # defaults to minutes >>> import datetime >>> import pytz >>> naive_datetime = datetime.datetime(2005, 7, 15, 11, 21, 32, 104) >>> date = naive_datetime.date() >>> aware_datetime = naive_datetime.replace( ... tzinfo=pytz.timezone('US/Eastern')) >>> n.value(naive_datetime) Traceback (most recent call last): ... ValueError: This index only indexes timezone-aware datetimes. >>> n.value(date) Traceback (most recent call last): ... ValueError: This index only indexes timezone-aware datetimes. >>> n.value(aware_datetime) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, tzinfo=<DstTzInfo 'US/Eastern'...>)
如果我们指定不同的分辨率,结果会有所不同。
>>> another = DateTimeNormalizer(1) # hours >>> another.value(aware_datetime) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 0, tzinfo=<DstTzInfo 'US/Eastern'...>)
请注意,更改索引值的分辨率可能会产生令人惊讶的结果,因为查询不会更改它们的分辨率。因此,如果您使用比规范器更精细的日期时间索引某些内容,那么搜索该日期时间将找不到 doc_id。
“any_of”查询中的值用“any”解析。'any' 应该返回一系列值。它需要一个索引,我们将在这里进行模拟。
>>> class DummyIndex(object): ... def values(self, start, stop, exclude_start, exclude_stop): ... assert not exclude_start and exclude_stop ... six_hours = datetime.timedelta(hours=6) ... res = [] ... dt = start ... while dt < stop: ... res.append(dt) ... dt += six_hours ... return res ... >>> index = DummyIndex() >>> tuple(n.any(naive_datetime, index)) # doctest: +ELLIPSIS (datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>),) >>> tuple(n.any(aware_datetime, index)) # doctest: +ELLIPSIS (datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>),) >>> tuple(n.any(date, index)) # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS (datetime.datetime(2005, 7, 15, 0, 0, tzinfo=<...Local...>), datetime.datetime(2005, 7, 15, 6, 0, tzinfo=<...Local...>), datetime.datetime(2005, 7, 15, 12, 0, tzinfo=<...Local...>), datetime.datetime(2005, 7, 15, 18, 0, tzinfo=<...Local...>))
“all_of”查询中的值用“all”解析。
>>> n.all(naive_datetime, index) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>) >>> n.all(aware_datetime, index) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>) >>> n.all(date, index) # doctest: +ELLIPSIS Traceback (most recent call last): ... ValueError: ...
'between' 查询中的最小值以及其他方法中的最小值用 'minimum' 解析。它们还采用可选的排除布尔值,指示是否要排除最小值。对于日期时间,只有传入日期才会有所不同。
>>> n.minimum(naive_datetime, index) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>) >>> n.minimum(naive_datetime, index, exclude=True) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>)>>> n.minimum(aware_datetime, index) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>) >>> n.minimum(aware_datetime, index, True) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>)>>> n.minimum(date, index) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 0, 0, tzinfo=<...Local...>) >>> n.minimum(date, index, True) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 23, 59, 59, 999999, tzinfo=<...Local...>)
'between' 查询中的最大值以及其他方法中的最大值都用 'maximum' 解析。它们还采用可选的排除布尔值,指示是否要排除最大值。对于日期时间,只有传入日期才会有所不同。
>>> n.maximum(naive_datetime, index) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>) >>> n.maximum(naive_datetime, index, exclude=True) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Local...>)>>> n.maximum(aware_datetime, index) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>) >>> n.maximum(aware_datetime, index, True) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, 32, 104, tzinfo=<...Eastern...>)>>> n.maximum(date, index) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 23, 59, 59, 999999, tzinfo=<...Local...>) >>> n.maximum(date, index, True) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 0, 0, tzinfo=<...Local...>)
现在让我们在真实索引的上下文中检查这些规范化器。
>>> from zc.catalog.index import DateTimeValueIndex, DateTimeSetIndex >>> setindex = DateTimeSetIndex() # minutes resolution >>> data = [] # generate some data >>> def date_gen( ... start=aware_datetime, ... count=12, ... period=datetime.timedelta(hours=10)): ... dt = start ... ix = 0 ... while ix < count: ... yield dt ... dt += period ... ix += 1 ... >>> gen = date_gen() >>> count = 0 >>> while True: ... try: ... next_ = [next(gen) for i in range(6)] ... except StopIteration: ... break ... data.append((count, next_[0:1])) ... count += 1 ... data.append((count, next_[1:3])) ... count += 1 ... data.append((count, next_[3:6])) ... count += 1 ... >>> print(data) # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [(0, [datetime.datetime(2005, 7, 15, 11, 21, 32, 104, ...<...Eastern...>)]), (1, [datetime.datetime(2005, 7, 15, 21, 21, 32, 104, ...<...Eastern...>), datetime.datetime(2005, 7, 16, 7, 21, 32, 104, ...<...Eastern...>)]), (2, [datetime.datetime(2005, 7, 16, 17, 21, 32, 104, ...<...Eastern...>), datetime.datetime(2005, 7, 17, 3, 21, 32, 104, ...<...Eastern...>), datetime.datetime(2005, 7, 17, 13, 21, 32, 104, ...<...Eastern...>)]), (3, [datetime.datetime(2005, 7, 17, 23, 21, 32, 104, ...<...Eastern...>)]), (4, [datetime.datetime(2005, 7, 18, 9, 21, 32, 104, ...<...Eastern...>), datetime.datetime(2005, 7, 18, 19, 21, 32, 104, ...<...Eastern...>)]), (5, [datetime.datetime(2005, 7, 19, 5, 21, 32, 104, ...<...Eastern...>), datetime.datetime(2005, 7, 19, 15, 21, 32, 104, ...<...Eastern...>), datetime.datetime(2005, 7, 20, 1, 21, 32, 104, ...<...Eastern...>)])] >>> data_dict = dict(data) >>> for doc_id, value in data: ... setindex.index_doc(doc_id, value) ... >>> list(setindex.ids()) [0, 1, 2, 3, 4, 5] >>> set(setindex.values()) == set( ... setindex.normalizer.value(v) for v in date_gen()) True
对于搜索,我们实际上将使用一个请求和交互,以及一个返回东部时区的适配器。这使得示例较少依赖于他们使用的机器。
>>> import zope.security.management >>> import zope.publisher.browser >>> import zope.interface.common.idatetime >>> import zope.publisher.interfaces >>> request = zope.publisher.browser.TestRequest() >>> zope.security.management.newInteraction(request) >>> from zope import interface, component >>> @interface.implementer(zope.interface.common.idatetime.ITZInfo) ... @component.adapter(zope.publisher.interfaces.IRequest) ... def tzinfo(req): ... return pytz.timezone('US/Eastern') ... >>> component.provideAdapter(tzinfo) >>> n.all(naive_datetime, index).tzinfo is pytz.timezone('US/Eastern') True>>> set(setindex.apply({'any_of': (datetime.date(2005, 7, 17), ... datetime.date(2005, 7, 20), ... datetime.date(2005, 12, 31))})) == set( ... (2, 3, 5)) True
请注意,此搜索使用的是标准化值。
>>> set(setindex.apply({'all_of': ( ... datetime.datetime( ... 2005, 7, 16, 7, 21, tzinfo=pytz.timezone('US/Eastern')), ... datetime.datetime( ... 2005, 7, 15, 21, 21, tzinfo=pytz.timezone('US/Eastern')),)}) ... ) == set((1,)) True >>> list(setindex.apply({'any': None})) [0, 1, 2, 3, 4, 5] >>> set(setindex.apply({'between': ( ... datetime.datetime(2005, 4, 1, 12), datetime.datetime(2006, 5, 1))}) ... ) == set((0, 1, 2, 3, 4, 5)) True >>> set(setindex.apply({'between': ( ... datetime.datetime(2005, 4, 1, 12), datetime.datetime(2006, 5, 1), ... True, True)}) ... ) == set((0, 1, 2, 3, 4, 5)) True
'between' 搜索应该很好地处理日期。
>>> set(setindex.apply({'between': ( ... datetime.date(2005, 7, 16), datetime.date(2005, 7, 17))}) ... ) == set((1, 2, 3)) True >>> len(setindex.apply({'between': ( ... datetime.date(2005, 7, 16), datetime.date(2005, 7, 17))}) ... ) == len(setindex.apply({'between': ( ... datetime.date(2005, 7, 15), datetime.date(2005, 7, 18), ... True, True)}) ... ) True
删除文档照常工作。
>>> setindex.unindex_doc(1) >>> list(setindex.ids()) [0, 2, 3, 4, 5]
Value、Minvalue 和 Maxvalue 可以采用无时区的日期时间和日期。
>>> setindex.minValue() # doctest: +ELLIPSIS datetime.datetime(2005, 7, 15, 11, 21, ...<...Eastern...>) >>> setindex.minValue(datetime.date(2005, 7, 17)) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 17, 3, 21, ...<...Eastern...>)>>> setindex.maxValue() # doctest: +ELLIPSIS datetime.datetime(2005, 7, 20, 1, 21, ...<...Eastern...>) >>> setindex.maxValue(datetime.date(2005, 7, 17)) # doctest: +ELLIPSIS datetime.datetime(2005, 7, 17, 23, 21, ...<...Eastern...>)>>> list(setindex.values( ... datetime.date(2005, 7, 17), datetime.date(2005, 7, 17))) ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE [datetime.datetime(2005, 7, 17, 3, 21, ...<...Eastern...>), datetime.datetime(2005, 7, 17, 13, 21, ...<...Eastern...>), datetime.datetime(2005, 7, 17, 23, 21, ...<...Eastern...>)]>>> zope.security.management.endInteraction() # TODO put in tests tearDown
排序
规范化包装器提供 zope.index.interfaces.IIndexSort 接口,如果它的上游索引提供它。例如,DateTimeValueIndex 将提供 IIndexSort,因为 ValueIndex 提供排序。它还将排序方法委托给值索引。
>>> from zc.catalog.index import DateTimeValueIndex >>> from zope.index.interfaces import IIndexSort>>> ix = DateTimeValueIndex() >>> IIndexSort.providedBy(ix.index) True >>> IIndexSort.providedBy(ix) True >>> ix.sort.__self__ is ix.index True
但它不适用于不进行排序的索引,例如 DateTimeSetIndex。
>>> ix = DateTimeSetIndex() >>> IIndexSort.providedBy(ix.index) False >>> IIndexSort.providedBy(ix) False >>> ix.sort Traceback (most recent call last): ... AttributeError: 'SetIndex' object has no attribute 'sort'
范围目录
范围目录与普通目录非常相似,只是它只索引可添加到其范围的项目。范围既是一个过滤器,也是一个可以与其他结果集合并的集合。过滤是我们将在下面讨论的附加功能;我们将从一个仅支持第二个用例的简单“什么都不做”范围开始。
我们在这里创建文本需要的状态。
>>> import zope.keyreference.persistent >>> import zope.component >>> import zope.intid >>> import zope.component >>> import zope.interface.interfaces >>> import zope.component.persistentregistry >>> from ZODB.MappingStorage import DB >>> import transaction>>> zope.component.provideAdapter( ... zope.keyreference.persistent.KeyReferenceToPersistent, ... adapts=(zope.interface.Interface,)) >>> zope.component.provideAdapter( ... zope.keyreference.persistent.connectionOfPersistent, ... adapts=(zope.interface.Interface,))>>> site_manager = None >>> def getSiteManager(context=None): ... if context is None: ... if site_manager is None: ... return zope.component.getGlobalSiteManager() ... else: ... return site_manager ... else: ... try: ... return zope.interface.interfaces.IComponentLookup(context) ... except TypeError as error: ... raise zope.component.ComponentLookupError(*error.args) ... >>> def setSiteManager(sm): ... global site_manager ... site_manager = sm ... if sm is None: ... zope.component.getSiteManager.reset() ... else: ... zope.component.getSiteManager.sethook(getSiteManager) ... >>> def makeRoot(): ... db = DB() ... conn = db.open() ... root = conn.root() ... site_manager = root['components'] = ( ... zope.component.persistentregistry.PersistentComponents()) ... site_manager.__bases__ = (zope.component.getGlobalSiteManager(),) ... site_manager.registerUtility( ... zope.intid.IntIds(family=btrees_family), ... provided=zope.intid.interfaces.IIntIds) ... setSiteManager(site_manager) ... transaction.commit() ... return root ...>>> @zope.component.adapter(zope.interface.Interface) ... @zope.interface.implementer(zope.interface.interfaces.IComponentLookup) ... def getComponentLookup(obj): ... return obj._p_jar.root()['components'] ... >>> zope.component.provideAdapter(getComponentLookup)
为了显示工作中的范围目录,我们需要一个 intid 实用程序、一个索引和一些要索引的项目。我们将在真正的 ZODB 和真正的 intid 实用程序中执行此操作。
>>> import zc.catalog >>> import zc.catalog.interfaces >>> from zc.catalog import interfaces, extentcatalog >>> from zope import interface, component >>> from zope.interface import verify >>> import persistent >>> import BTrees.IFBTree>>> root = makeRoot() >>> intid = zope.component.getUtility( ... zope.intid.interfaces.IIntIds, context=root) >>> TreeSet = btrees_family.IF.TreeSet>>> from zope.container.interfaces import IContained >>> @interface.implementer(IContained) ... class DummyIndex(persistent.Persistent): ... __parent__ = __name__ = None ... def __init__(self): ... self.uids = TreeSet() ... def unindex_doc(self, uid): ... if uid in self.uids: ... self.uids.remove(uid) ... def index_doc(self, uid, obj): ... self.uids.insert(uid) ... def clear(self): ... self.uids.clear() ... def apply(self, query): ... return [uid for uid in self.uids if uid <= query] ... >>> class DummyContent(persistent.Persistent): ... def __init__(self, name, parent): ... self.id = name ... self.__parent__ = parent ...>>> extent = extentcatalog.Extent(family=btrees_family) >>> verify.verifyObject(interfaces.IExtent, extent) True >>> root['catalog'] = catalog = extentcatalog.Catalog(extent) >>> verify.verifyObject(interfaces.IExtentCatalog, catalog) True >>> index = DummyIndex() >>> catalog['index'] = index >>> transaction.commit()
现在我们已经建立了一个带有索引和范围的目录。我们可以在一定程度上添加一些数据:
>>> matches = [] >>> for i in range(100): ... c = DummyContent(i, root) ... root[i] = c ... doc_id = intid.register(c) ... catalog.index_doc(doc_id, c) ... matches.append(doc_id) >>> matches.sort() >>> sorted(extent) == sorted(index.uids) == matches True
我们可以得到范围的大小。
>>> len(extent) 100
取消索引目录中的对象应该像往常一样简单地将其从目录和索引中删除。
>>> matches[0] in catalog.extent True >>> matches[0] in catalog['index'].uids True >>> catalog.unindex_doc(matches[0]) >>> matches[0] in catalog.extent False >>> matches[0] in catalog['index'].uids False >>> doc_id = matches.pop(0) >>> sorted(extent) == sorted(index.uids) == matches True
清除目录会清除范围和包含的索引。
>>> catalog.clear() >>> list(catalog.extent) == list(catalog['index'].uids) == [] True
更新所有索引和单个索引都会更新范围。
>>> catalog.updateIndexes() >>> matches.insert(0, doc_id) >>> sorted(extent) == sorted(index.uids) == matches True>>> index2 = DummyIndex() >>> catalog['index2'] = index2 >>> index2.__parent__ == catalog True >>> index.uids.remove(matches[0]) # to confirm that only index 2 is touched >>> catalog.updateIndex(index2) >>> sorted(extent) == sorted(index2.uids) == matches True >>> matches[0] in index.uids False >>> matches[0] in index2.uids True >>> res = index.uids.insert(matches[0])
但是,为什么首先要有一个范围呢?它允许索引对完整索引数据的可靠集合进行操作;因此,它允许 zc.catalog 中的索引执行 NOT 操作。
范围本身提供了许多合并功能,以允许其值与其他 BTrees.IFBTree 数据结构合并。这些包括交集、并集、差异和反向差异。给定一个名为“extent”的范围和另一个名为“data”的 IFBTree 数据结构,交集可以拼写为“extent & data”或“data & extent”;union 可以拼写为“extent | 数据”或“数据 | 程度”; 差异可以拼写为“范围 - 数据”;和反向差异可以拼写为“数据-范围”。联合和交叉点是加权的。
>>> extent = extentcatalog.Extent(family=btrees_family) >>> for i in range(1, 100, 2): ... extent.add(i, None) ... >>> alt_set = TreeSet() >>> _ = alt_set.update(range(0, 166, 33)) # return value is unimportant here >>> sorted(alt_set) [0, 33, 66, 99, 132, 165] >>> sorted(extent & alt_set) [33, 99] >>> sorted(alt_set & extent) [33, 99] >>> sorted(extent.intersection(alt_set)) [33, 99] >>> original = set(extent) >>> union_matches = original.copy() >>> union_matches.update(alt_set) >>> union_matches = sorted(union_matches) >>> sorted(alt_set | extent) == union_matches True >>> sorted(extent | alt_set) == union_matches True >>> sorted(extent.union(alt_set)) == union_matches True >>> sorted(alt_set - extent) [0, 66, 132, 165] >>> sorted(extent.rdifference(alt_set)) [0, 66, 132, 165] >>> original.remove(33) >>> original.remove(99) >>> set(extent - alt_set) == original True >>> set(extent.difference(alt_set)) == original True
我们可以将我们自己的实例化 UID 实用程序传递给 extentcatalog.Catalog。
>>> extent = extentcatalog.Extent(family=btrees_family) >>> uidutil = zope.intid.IntIds() >>> cat = extentcatalog.Catalog(extent, uidutil) >>> cat["index"] = DummyIndex() >>> cat.UIDSource is uidutil True>>> cat._getUIDSource() is uidutil True
目录的searchResults方法返回的 ResultSet 实例使用我们的 UID 实用程序。
>>> obj = DummyContent(43, root) >>> uid = uidutil.register(obj) >>> cat.index_doc(uid, obj) >>> res = cat.searchResults(index=uid) >>> res.uidutil is uidutil True>>> list(res) == [obj] True
searchResults也可能返回 None。
>>> cat.searchResults() is None True
当目录具有其 uid 源集时调用updateIndex和updateIndexes也可以。
>>> cat.clear() >>> uid in cat.extent False
uid 实用程序中的所有对象都已编入索引。
>>> cat.updateIndexes() >>> uid in cat.extent True>>> len(cat.extent) 1>>> obj2 = DummyContent(44, root) >>> uid2 = uidutil.register(obj2) >>> cat.updateIndexes() >>> len(cat.extent) 2>>> uid2 in cat.extent True>>> uidutil.unregister(obj2)>>> cat.clear() >>> uid in cat.extent False >>> cat.updateIndex(cat["index"]) >>> cat.extent 中的 uid 真的
对于自填充范围,调用updateIndex或updateIndexes意味着只有 id 在范围内的对象被更新/重新索引;如果存在,目录将使用其 uid 源按 id 查找对象。
>>> extent = extentcatalog.NonPopulatingExtent(family=btrees_family) >>> cat = extentcatalog.Catalog(extent, uidutil) >>> cat["index"] = DummyIndex()>>> extent.add(uid, obj) >>> uid in cat["index"].uids False>>> cat.updateIndexes() >>> uid in cat["index"].uids True>>> cat.clear() >>> uid in cat["index"].uids False>>> uid in cat.extent False>>> cat.extent.add(uid, obj) >>> cat.updateIndex(cat["index"]) >>> uid in cat["index"].uids True
从 intid 实用程序中注销先前测试的对象:
>>> intid = zope.component.getUtility( ... zope.intid.interfaces.IIntIds, context=root) >>> for doc_id in matches: ... intid.unregister(intid.queryObject(doc_id))
具有过滤范围的目录
正如本文开头所讨论的,extent 不仅可以帮助进行索引操作,还可以充当过滤器,以便给定目录可以回答有关 intid 中包含的对象子集的问题。
过滤器范围仅存储与给定过滤器匹配的对象。
>>> def filter(extent, uid, ob): ... assert interfaces.IFilterExtent.providedBy(extent) ... # This is an extent of objects with odd-numbered uids without a ... # True ignore attribute ... return uid % 2 and not getattr(ob, 'ignore', False) ... >>> extent = extentcatalog.FilterExtent(filter, family=btrees_family) >>> verify.verifyObject(interfaces.IFilterExtent, extent) True >>> root['catalog1'] = catalog = extentcatalog.Catalog(extent) >>> verify.verifyObject(interfaces.IExtentCatalog, catalog) True >>> index = DummyIndex() >>> catalog['index'] = index >>> transaction.commit()
现在我们已经建立了一个带有索引和范围的目录。如果我们创建一些内容并要求目录对其进行索引,则只有与过滤器匹配的内容才会在范围和索引中。
>>> matches = [] >>> fails = [] >>> i = 0 >>> while True: ... c = DummyContent(i, root) ... root[i] = c ... doc_id = intid.register(c) ... catalog.index_doc(doc_id, c) ... if filter(extent, doc_id, c): ... matches.append(doc_id) ... else: ... fails.append(doc_id) ... i += 1 ... if i > 99 and len(matches) > 4: ... break ... >>> matches.sort() >>> sorted(extent) == sorted(index.uids) == matches True
如果一个内容对象被编入索引,用于匹配过滤器但不再匹配,则应将其从范围和索引中删除。
>>> matches[0] in catalog.extent True >>> obj = intid.getObject(matches[0]) >>> obj.ignore = True >>> filter(extent, matches[0], obj) False >>> catalog.index_doc(matches[0], obj) >>> doc_id = matches.pop(0) >>> doc_id in catalog.extent False >>> sorted(extent) == sorted(index.uids) == matches True
取消索引不在目录中的对象应该是无操作的。
>>> fails[0] in catalog.extent False >>> catalog.unindex_doc(fails[0]) >>> fails[0] in catalog.extent False >>> sorted(extent) == sorted(index.uids) == matches True
更新所有索引和单个索引都会更新范围。
>>> index2 = DummyIndex() >>> catalog['index2'] = index2 >>> index2.__parent__ == catalog True >>> index.uids.remove(matches[0]) # to confirm that only index 2 is touched >>> catalog.updateIndex(index2) >>> sorted(extent) == sorted(index2.uids) True >>> matches[0] in index.uids False >>> matches[0] in index2.uids True >>> res = index.uids.insert(matches[0])
如果更新单个索引并且对象不再是范围的成员,则会从所有索引中删除它。
>>> matches[0] in catalog.extent True >>> matches[0] in index.uids True >>> matches[0] in index2.uids True >>> obj = intid.getObject(matches[0]) >>> obj.ignore = True >>> catalog.updateIndex(index2) >>> matches[0] in catalog.extent False >>> matches[0] in index.uids False >>> matches[0] in index2.uids False >>> doc_id = matches.pop(0) >>> (matches == sorted(catalog.extent) == sorted(index.uids) ... == sorted(index2.uids)) True
自填充范围
一个范围可能知道如何填充自己;如果可以使用比 IIntIds 实用程序中可用的项目更少的项目初始化目录,这将特别有用,这些项目也在最近的 Zope 3 站点内(基本 Zope 3 目录中编码的策略)。
这样的范围必须实现ISelfPopulatingExtent接口,该接口需要两个属性。让我们使用FilterExtent类作为实现这种范围的基础,使用选择内容项 0 的方法(在上面创建和注册):
>>> class PopulatingExtent( ... extentcatalog.FilterExtent, ... extentcatalog.NonPopulatingExtent): ... ... def populate(self): ... if self.populated: ... return ... self.add(intid.getId(root[0]), root[0]) ... super(PopulatingExtent, self).populate()
基于此范围创建目录会忽略数据库中的对象:
>>> def accept_any(extent, uid, ob): ... return True >>> extent = PopulatingExtent(accept_any, family=btrees_family) >>> catalog = extentcatalog.Catalog(extent) >>> index = DummyIndex() >>> catalog['index'] = index >>> root['catalog2'] = catalog >>> transaction.commit()
此时,我们的范围仍然是 u