跳转到主要内容

为Zope3提供的Xapian内容索引/搜索框架

项目描述

该软件包提供了一个多线程Python应用程序的内容索引框架。它使用xapian作为索引库,并利用zope组件架构来实现灵活性。它主要作为Xapian核心搜索功能的框架包装器。

特性

  • 异步处理所有索引操作。

  • 从多个数据存储中索引/解析内容的机制。

  • 通过适配器轻松定制索引行为。

  • 事务感知的修改,在事务范围内聚合内容操作。

内容

让我们创建一些内容来进行工作。在内容与索引集成方面的唯一责任是它们实现了IIndexable标记接口。

>>> class Content( object ):
...    implements( interfaces.IIndexable )
...    __parent__ = None
...    @property
...    def __name__( self ): return self.title
...    def __init__( self, **kw): self.__dict__.update(kw)
...    def __hash__( self ): return hash(self.title)
>>>
>>> rabbit = Content( title=u"rabbit", description="furry little creatures", keywords=("skin",) )
>>> elephant = Content( title=u"elephant", description="large mammals with memory", keywords=("apple",) )
>>> snake = Content( title=u"snake", description="reptile with scales", keywords=("skin", "apple")  )
>>>

解析器

解析器允许我们从多个数据存储中索引内容。例如,我们可以有来自关系数据库的内容,来自subversion的内容,以及来自文件系统的内容,我们希望将其索引到xapian中。解析器允许我们通过标识符无歧义地识别对象,并根据标识符检索对象。解析器结构为命名实用程序,实用程序名称对应于解析策略。

一个关键要求是我们需要能够在不同的线程中异步加载内容,以便索引机制能够处理它。

为了测试目的,我们将在下面构造一个简单的解析器方案和一些示例内容。

>>> class ContentResolver( object ):
...    implements( interfaces.IResolver )
...    scheme = "" # name of resolver utility ( optionally "" for default )
...    map = dict( rabbit=rabbit, elephant=elephant, snake=snake )
...
...    def id( self, object ): return object.title
...    def resolve( self, id ): return self.map[id]
...
>>> component.provideUtility( ContentResolver() )

目录定义

任何利用此包的应用程序的核心责任之一是定义要索引的应用程序特定字段。

应用程序通过构建Xapian索引连接并添加额外字段来实现这一点

>>> import xappy
>>> indexer = xappy.IndexerConnection('tmp.idx')
>>> indexer.add_field_action('resolver', xappy.FieldActions.INDEX_EXACT )
>>> indexer.add_field_action('resolver', xappy.FieldActions.STORE_CONTENT )
>>> indexer.add_field_action('object_type', xappy.FieldActions.INDEX_EXACT )
>>> indexer.add_field_action('object_type', xappy.FieldActions.STORE_CONTENT )
>>> indexer.add_field_action('title', xappy.FieldActions.INDEX_FREETEXT )
>>> indexer.add_field_action('title', xappy.FieldActions.STORE_CONTENT )
>>> indexer.add_field_action('title', xappy.FieldActions.STORE_CONTENT )
>>> indexer.add_field_action('title', xappy.FieldActions.SORTABLE )
>>> indexer.add_field_action('description', xappy.FieldActions.INDEX_FREETEXT )
>>> indexer.add_field_action('keyword', xappy.FieldActions.INDEX_EXACT )

队列处理器

现在我们可以使用这个索引连接启动异步索引线程。请注意,我们不应尝试在此索引器中直接在应用程序线程中执行任何索引操作,因为Xapian不会进行锁定。相反,写操作将被路由到队列处理器,它在一个单独的线程/进程中执行所有对索引的修改。为了测试目的,我们还将索引刷新的时间阈值降低(默认为60秒)

为了测试目的,我们将轮询超时设置为0.1秒。

>>> from ore.xapian import queue
>>> queue.QueueProcessor.POLL_TIMEOUT = 0.1
>>> queue.QueueProcessor.FLUSH_THRESHOLD = 1

让我们开始索引队列。我们通常在ZCML中这样做,但这不是必需的,并且为了测试目的,我们将直接从Python中执行它。

>>> queue.QueueProcessor.start( indexer )
<ore.xapian.queue.QueueProcessor object at ...>

验证队列正在运行。

>>> queue.QueueProcessor.indexer_running
True

索引

内容索引通过事件集成自动提供。利用对象修改、对象添加和对象删除的事件订阅者来生成索引操作,这些操作由队列处理器异步处理。

操作

但是,为了将正确的解析器与每个对象的索引操作相关联,我们需要构建一个与解析器关联的操作工厂。通过适配可以发现对象适当操作工厂。

>>> from ore.xapian.operation import OperationFactory
>>> class MyOperationFactory( OperationFactory ):
...      resolver_id = ContentResolver.scheme
>>> component.provideAdapter( MyOperationFactory, (interfaces.IIndexable,) )

操作工厂被各种事件处理器用于为索引队列创建操作。默认实现已经为操作的创建提供了一个适当的通用实现,我们的定制仅确保工厂使用指定的解析器。

内容集成

应用程序通常会对许多不同接口和不同属性值的对象进行索引。然而,索引尝试将对象属性索引到适用于通用应用程序使用和搜索界面的公共字段集中。因此,一个常见的需求是自定义索引对象的表示。

>>> class ContentIndexer( object ):
...      implements( interfaces.IIndexer )
...      def __init__( self, context): self.ob = context
...      def document( self, connection ):
...          doc = xappy.UnprocessedDocument()
...          doc.fields.append( xappy.Field( 'title', self.ob.title ))
...          doc.fields.append( xappy.Field( 'description', self.ob.description ))
...          doc.fields.append( xappy.Field( 'object_type', self.ob.__class__.__name__ ) )
...          for kw in self.ob.keywords:
...              doc.fields.append( xappy.Field( 'keyword', kw ) )
...          return doc
>>>
>>> component.provideAdapter( ContentIndexer, (interfaces.IIndexable,) )

现在让我们生成一些事件来启动索引

>>> from zope.event import notify
>>> from zope.app.container.contained import ObjectAddedEvent
>>>
>>> notify( ObjectAddedEvent( rabbit ) )
>>> notify( ObjectAddedEvent( elephant ) )
>>> notify( ObjectAddedEvent( snake ) )

为了使索引器处理这些事件,我们需要提交事务。

>>> transaction.commit()
>>> import time
>>> time.sleep(0.1)

搜索

搜索实用工具类似于Xapian搜索连接。为了允许连接的重用并避免传递构造函数参数,我们构建了一个搜索网关,该网关作为搜索连接池的容器,并将其注册为实用工具以方便访问

>>> from ore.xapian import search
>>> search_connections = search.ConnectionHub('tmp.idx')

我们可以通过调用网关来从网关中获取搜索连接

>>> searcher = search_connections.get()
>>> query = searcher.query_parse('rabbit')
>>> results = searcher.search( query, 0, 30)
>>> len(results)
1

我们可以通过在单个搜索结果上调用object()方法来检索被索引的对象

>>> results[0].object() is rabbit
True
>>> query = searcher.query_parse('mammals')
>>> results = searcher.search( query, 0, 30 )
>>> len(results) == 1
True

我们可以搜索对象类型索引以检索同一类型的所有已索引对象。

>>> query = searcher.query_field( 'object_type', 'Content')
>>> results = searcher.search( query, 0, 30 )
>>> len(results) == 3
True

让我们尝试关键词搜索,我们已将内容对象索引在两个关键词“苹果”和“皮肤”中。

>>> query = searcher.query_field('keyword', 'apple')
>>> results = searcher.search( query, 0, 30 )
>>> print [i.object().title for i in results]
[u'snake', u'elephant']

让我们按标题排序。

>>> query = searcher.query_field('keyword', 'skin')
>>> results = searcher.search( query, 0, 30, sortby="title" )
>>> print [i.object().title for i in results]
[u'rabbit', u'snake']

内容集成重述

为了验证,让我们测试修改和删除。

>>> from zope.lifecycleevent import ObjectModifiedEvent
>>> from zope.app.container.contained import ObjectRemovedEvent

我们将给兔子一个新的描述。

>>> rabbit.description = 'hairy little animal'
>>> notify(ObjectModifiedEvent(rabbit))

并删除snake-object。

>>> notify(ObjectRemovedEvent(snake))

等待片刻,然后重新打开搜索连接。

>>> transaction.commit()
>>> time.sleep(0.1)
>>> searcher.reopen()

验证搜索结果

>>> query = searcher.query_parse('hairy')
>>> len(searcher.search(query, 0, 30))
1
>>> query = searcher.query_parse('snake')
>>> len(searcher.search(query, 0, 30))
0

清理

为了成为一个好的测试公民,我们将清理我们的队列处理线程。

>>> queue.QueueProcessor.stop()

注意事项

在索引针对关系内容时有一些注意事项,其中主要关注的是使用非索引感知应用程序,对数据库结构进行修改。

还有其他方法可以处理这种情况,如果索引队列直接移动到数据库中,那么修改应用程序可以直接将操作插入索引队列。此外,大多数数据库支持触发操作,可以直接在模式结构中执行此功能。

在使用基于数据库的操作时,额外的约束是可能丢失域模型的附加属性,或者对于其他应用程序或数据库触发器难以捕捉。

变更

0.5.0 - 2008年11月11日

  • 添加广泛的可选日志选项

  • 不要让一个坏的运算终止索引线程

  • 如果无法解析对象,则记录日志

  • 为集成测试启用同步模式

  • 允许为队列处理器提供多个zcml定义

0.4.2 - 2008年5月2日

  • 添加许可证头

0.4.1 - 2008年5月1日

  • 打包修复,不是zip安全包(包含zcml)

0.4 - 2008年4月30日

  • 事务操作缓冲区,用于馈送到操作队列。还在事务范围内对给定内容内的操作进行聚合。

  • zcml支持启动索引器

  • 增加测试覆盖率及错误修复

  • 将flush超时重命名为队列处理器上的poll超时。

  • 事务包依赖

0.3 - 2008年2月10日

首次发布

支持者

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面