如何使用新功能扩展 Strus 搜索引擎





4.00/5 (5投票s)
本文介绍了 Strus 的可扩展性,通过动态加载用 C++ 编写的模块。
引言
本文将帮助您开始为 Strus 编写扩展。扩展可以单独构建并作为动态加载模块(共享对象)安装。本文的目的是使您能够自己实现、构建和安装 Strus 的扩展。
我们介绍了用于处理 postings 集合、加权函数和摘要的接口,并展示了一个实现每种类型扩展的示例模块。本文不讨论将替代的键/值存储数据库实现作为模块加载的可能性。
背景
Strus 是一组用于构建可扩展搜索引擎的库和工具。有关如何使用 Strus 构建搜索引擎的教程,请参阅 我之前的 CodeProject 文章。
备注
本文中的源代码并未实现接口的所有方法。示例已过时,但作为文档已足够。不过,下载的源代码是最新的。
目录
词汇表
- Posting:对于每个词条,我们都有一个列表,记录该词条出现在哪些文档中。列表中的每个项 — 记录词条出现在文档中(并且,稍后,通常还记录在文档中的位置) — 通常称为 posting。[nlp.stanford.edu]
- 特征 (Feature):从对象中提取并在查询处理过程中使用的信息。[people.ischool.berkeley.edu]
- 前向索引 (Forward index):前向索引存储每个文档的单词列表 [en.wikipedia.org]。
必备组件
要编译和运行此处介绍的源代码,您需要安装 `strusBase`、`strus`、`strusAnalyzer` 和 `strusModule` 的 strus 包,或者自己构建软件。对 `strusAnalyzer` 的依赖是由于 `strusModule` 依赖于它。分析器模块不是本文的主题。将会有另一篇文章介绍 strus 分析器模块。
要克隆项目或下载最新源代码,请访问 strusBase、 strus、 strusAnalyzer、 strusModule
您需要 gcc 或 clang 以及 cmake 来构建软件。strus 项目主分支的当前构建状态可以在 这里 查看。
要解决的示例问题
此处实现的示例连接运算符是查找包含至少给定数量的所有参数特征的子集的、大小(位置范围)给定的区域。示例加权函数返回此类窗口的最小大小的倒数。示例摘要器返回此窗口内的所有前向索引特征。
提出的示例函数不是 Strus 核心的一部分。它们也不被认为值得被纳入核心,因为基于最小窗口大小加权的文档排名不稳定。在此上下文中不稳定意味着,用新的查询特征扩展查询会改 shuffle 前面的结果列表,而不是简单地为匹配新特征的文档添加权重。这使得搜索作为查询细化的过程对用户来说变得困难。我选择这些示例函数是因为它们相对复杂,但另一方面又容易理解。
图示示例
这张图片标记了一个包含“building”和“Dubai”两个词条的尺寸为 7 的窗口
错误处理
要为 Strus 编写扩展模块,您必须了解 Strus 中是如何处理错误的。不幸的是,对于 C++,没有便携的方式可以在模块边界上传播异常。在模块边界上传播异常的唯一方法是使用相同的编译器并导出所有符号来编译所有涉及的源代码。[请参阅 C++ 编码标准:101 条规则、指南和最佳实践,第 62 条。不要让异常跨越模块边界传播] 但这会阻碍 Strus 库和模块作为二进制构件的部署。这就是 Strus 中的接口是无异常的原因。
Strus 使用一个用于报告错误的接口来解决这个问题,该接口向下传递给所有实现可能跨模块边界调用的接口的对象。这可能会影响 Strus 的任何接口。因此,任何接口方法都必须捕获任何可能的异常并使用在对象构造时声明的 `ErrorBufferInterface` 报告异常的错误消息。因为 Strus 中实现接口的所有对象都是由另一个接口对象通过一个以根对象接口 `StorageObjectBuilderInterface` 开头的 `create` 方法构造的,所以错误缓冲区接口不会出现在 Strus 接口中。但是您必须了解它,并将其向下传递给您的模块创建的所有对象。接口方法必须在方法中定义 try/catch 框架,并将异常内容移动到错误缓冲区对象。这通常通过预处理器宏来完成,这些宏有助于减少样板代码并提高可读性。请记住,任何可能分配内存的代码都可能抛出必须捕获的异常。
以下代码片段显示了 2 个宏的定义,一个用于返回值的函数,一个用于不返回值的函数
#define CATCH_ERROR_MAP_RETURN( HND, VALUE)\ catch( const std::bad_alloc&)\ {\ (HND).report( "out of memory in ...");\ return VALUE;\ }\ catch( const std::runtime_error& err)\ {\ (HND).report( "error in ...: %s", err.what());\ return VALUE;\ }\ #define CATCH_ERROR_MAP( HND)\ catch( const std::bad_alloc&)\ {\ (HND).report( "out of memory in window iterator");\ }\ catch( const std::runtime_error& err)\ {\ (HND).report( "error in ...: %s", err.what());\ }\
这里有一个假设 `m_errorhnd` 作为类指向 `ErrorBufferInterface` 的指针的示例函数实现
作为指向类的 `ErrorBufferInterface` 的指针。
virtual Index skipPos( const Index& posno_) { try { ... do something that might throw a bad alloc or runtime error return <RESULT>; } CATCH_ERROR_MAP_RETURN( *m_errorhnd, 0) }
当需要时,您还必须注意在调用接口函数后检查错误缓冲区。有时立即检测到错误并不重要。将错误检查视为最佳实践,尤其是在调用返回值的函数以及必须检查错误以确保一致性(例如事务提交)的函数中。允许延迟检测错误有助于避免通过远程接口调用函数时的网络往返。
国际化
Strus 核心模块使用 gettext 进行国际化(错误消息为特定语言)。此示例模块不处理此方面。请查看核心模块以了解国际化是如何处理的。
Posting 连接运算符
我们从 posting 连接运算符的实现开始。这是最难实现的接口。很少有情况您真的想自己实现一个。但是,阅读以下部分以了解 posting 连接运算符是什么很有用。
基本模型
Posting 连接运算符实现 postings 上的集合操作。Postings 集合定义为
其中 d 代表内部文档编号,p 代表词条位置。
Posting 连接运算符是对一系列子表达式 posting 集合的集合操作。集合操作可以用两个附加参数进行参数化
- 范围 (range),指定涉及的参数集合的位置的邻近条件
- 基数 (cardinality),定义用于构建结果元素的选定参数的数量。
参数 `range` 和 `cardinality` 用于更直观地构建复杂表达式。
操作的基本结构是 postings 集合的布尔代数,加上两个额外的单目运算符。一个是前驱集
另一个是后继集
前驱集和后继集构造用于将正则表达式与以词条为字母表的项关联起来,并将其转换为集合操作。
但是,用一个只有基本正则表达式用于表示模式的语言来表达一切将非常麻烦。`range` 参数可以且应该只用于定义具有更直观的邻近概念的高级表达式。例如,“返回我出现在同一句话中的三个词条的出现”,其中句子结构由分隔符表示,该分隔符是词条,因此是 postings 集合。
`cardinality` 参数则有助于构建匹配给定大小的参数词条或表达式的任何选择的模式。子集的可能选择数量趋于快速增长,而它们可能产生的表示集可以以更有效的方式计算。
如果您为 posting 连接操作定义了 `range` 和 `cardinality` 参数,您应该牢记它们与基本结构的关系。这些参数意义的严格定义也是这些参数是运算符构造函数的固定位置参数的原因。
模型表示
Postings 集合不表示为数据,而是表示为迭代器,该迭代器具有跳过到最小上界文档编号或位置的操作。提供的两个跳过操作分别命名为 `skipDoc` 和 `skipPos`。
如果您熟悉 Lucene,您会识别出这些最小上界跳过操作,它们在 Lucene 中名为 `advance` 方法。
获取 (d,p) 之后的 posting 集合中的下一个元素的实现如下(伪代码)
FUNCTION next( d, p ) p = skipPos(p+1) while (!p) do d = skipDoc(++d) if (!d) then return NULL,NULL else p = skipPos(0) end end return d,p end
但这种遍历 posting 集合的方式很少使用。在大多数情况下,您需要前进到感兴趣的文档。然后,在该文档的上下文中,您需要 `skipPos` 操作来迭代元素。
Posting 迭代器实现为一个接口,该接口要么获取存储中与词条关联的 postings,要么实现其最小上界跳过操作作为参数 posting 迭代器函数的对象。
以下伪代码说明了一个表示两个 posting 集合 `arg1` 和 `arg2` 的**交集**的迭代器的 `skipDoc` 和 `skipPos` 方法的实现。我们使用一个辅助方法 `skipDocCandidate`,它只对文档编号进行交集,从而返回一个可能不包含有效匹配的候选。然后我们使用 `skipPos` 来确保 `skipDoc` 返回一个指向非空 postings 集合的文档编号。
FUNCTION skipDocCandidate( self, d ) d1 = self->arg1->skipDocCandidate( d) d2 = self->arg2->skipDocCandidate( d) while (d1 != d2 && d1 && d2) do if (d1 < d2) then d1 = self->arg1->skipDocCandidate( d2 ) else d2 = self->arg2->skipDocCandidate( d1 ) end end return d1 end FUNCTION skipPos( self, p ) p1 = self->arg1->skipPos( p) p2 = self->arg2->skipPos( p) while (p1 != p2 && p1 && p2) do if (p1 < p2) then p1 = self->arg1->skipPos( p2 ) else p2 = self->arg2->skipPos( p1 ) end end return p1 end FUNCTION skipDoc( self, d ) d = skipDocCandidate( self, d) while (d && !skipPos( self, 0) do d = skipDocCandidate( self, d+1 ) end end
方法 `skipDoc` 迭代同时具有 `arg1` 和 `arg2` 匹配项的候选文档,直到找到包含具有相同位置的匹配项的文档。
C++ 接口
Posting 迭代器
Posting 上的迭代器表示为 迭代器接口,其中包含前进到前面章节已介绍的参数的最小上界的方法。
用于文档编号
Index skipDoc( const Index& docno)
用于位置
Index skipPos( const Index& posno)
以及用于文档候选者的辅助函数
Index skipDocCandidate( const Index& docno)
`skipDocCanditate` 方法是一个不需要返回有效结果的函数。它不能做的是遗漏一个结果。它用于减少检查的位置数量,从而减少对包含位置信息的 Datalock 的读取。例如,如果您实现了一个返回后续词条出现次数的运算符,您可以将参数的 `skipDocCandidate` 结果的交集作为您的 `skipDocCandidate` 的结果返回。这可以大大减少位置块的读取。另一方面,您必须返回有效结果的 `skipDoc` 实现可以在检查位置以确保有效结果之前,先调用自己的 `skipDocCandidate`。
`skipDocCanditate` 方法仅由迭代器内部用于 `skipDoc` 的实现。Postings 集合上的结果迭代器是通过交替调用 `skipDoc` 和 `skipPos` 来实现的。
其他需要实现的方法有
const char* featureid() const;
返回用于调试和跟踪的迭代器的唯一 ID。
Index documentFrequency() const;
返回文档频率或其上限估计值。在需要对位置块进行完整表扫描以计算实际值的情况下,会返回上限估计值。
unsigned int frequency();
返回当前访问文档中的 postings 数量。
Index docno() const;
返回当前访问文档的文档编号。
Index posno() const;
返回当前访问 posting 的位置。
Index length() const;
返回当前访问 posting 的长度。长度不是模型的一部分,但对于匹配的后处理(例如标记匹配)很有用。它不考虑在诸如序列之类的表达式中。这有悖常理,但仍然不考虑,因为它无法用纯粹的集合操作来建模。
您还需要实现一个接受参数和错误缓冲区接口作为参数的构造函数
MyPostingIterator( const std::vector< strus::Reference< strus::PostingIteratorInterface> >& args, unsigned int range_, unsigned int cardinality_, strus::ErrorBufferInterface* errorhnd_)
Posting 连接运算符
Posting 连接运算符表示为 posting 连接运算符接口,它实现了一个用于创建 posting 迭代器的虚拟构造函数方法,该迭代器是对操作结果的迭代。您可以从子表达式 posting 迭代器列表、`range`(邻近度)和 `cardinality`(输入子集排列的选择器)参数创建 posting 迭代器。将 0 作为 `cardinality` 传递意味着默认值,即需要所有参数来构建结果元素。create 方法如下所示
PostingIteratorInterface* createResultIterator( const std::vector<Reference<PostingIteratorInterface> >& argitrs, int range, unsigned int cardinality) const;
此外,还有一个用于获取运算符描述以进行内省的方法
Description getDescription() const;
返回的描述对象有一个方法
const std::string& text() const
以获取此连接运算符的描述字符串。
示例实现
下面的示例实现了 posting 连接运算符及其结果迭代器,该迭代器返回窗口的开始位置,其范围(邻近度)最大,并且包含至少由 `cardinality` 参数指定的参数 postings 数量。首先,我们定义一些辅助函数
错误处理宏
我们在前面的章节中已经介绍了 Strus 的错误处理和无异常接口。下面的宏是用于捕获异常并通过错误缓冲区接口报告异常的样板代码的助手,该接口返回一个未定义的值。函数调用者可以通过检查
传递给对象的 `ErrorBufferInterface` (`ErrorBufferInterface::hasError()`) 来检查操作是否失败。
#define CATCH_ERROR_MAP_RETURN( HND, VALUE, MSG)\ catch( const std::bad_alloc&)\ {\ (HND).report( "out of memory in window iterator");\ return VALUE;\ }\ catch( const std::runtime_error& err)\ {\ (HND).report( "error in minwin window iterator %s: %s", MSG, err.what());\ return VALUE;\ }
获取第一个/下一个文档匹配的程序
以下函数尝试查找参数的子集,其大小至少为 `cardinality`,其中每个元素都具有与 `docno` 匹配项相同的上界。它返回此类 `docno` 的最小拟合作为结果。
使用 `allowEmpty` 参数指定返回的文档是否应仅为候选元素(可能导致空集),还是仅应返回具有实际匹配的文档。
///\param[in] args argument posting iterators ///\param[in] docno result is an upper bound of this document number ///\param[in] allowEmpty allow estimates (skipDocCandidate) that may not be part of a result ///\param[in] cardinality minimum number of matching elements of args to form a result ///\param[out] candidate_set returned bitfield with matching elements of args static Index getFirstAllMatchDocnoSubset( std::vector<Reference< PostingIteratorInterface> >& args, Index docno, bool allowEmpty, std::size_t cardinality, std::vector<PostingIteratorInterface*>& candidate_set) { if (args.empty()) return 0; candidate_set.clear(); Index docno_iter = docno; for (;;) { std::vector<Reference< PostingIteratorInterface> >::iterator ai = args.begin(), ae = args.end(); std::size_t nof_matches = 0; Index match_docno = 0; // Iterate on all arguments: for (; ai != ae; ++ai) { // Try to get another candidate document number: Index docno_next = (*ai)->skipDocCandidate( docno_iter); if (docno_next) { // ... if another element found: if (match_docno) { // ... there are elements in the current candidate: if (match_docno == docno_next) { // ... it is a member of the current candidate set candidate_set.push_back( ai->get()); ++nof_matches; } else if (match_docno > docno_next) { // ... it is smaller than the members // of the current candidate set. Recreate // the current candidate set with the element // found as only member: candidate_set.clear(); candidate_set.push_back( ai->get()); match_docno = docno_next; nof_matches = 1; } } else { // ... there are no elements yet // in the current candidate set: candidate_set.clear(); candidate_set.push_back( ai->get()); match_docno = docno_next; nof_matches = 1; } } } if (nof_matches >= cardinality) { // ... we have a candidate set big enough: if (!allowEmpty) { // ... we need to check the arguments to be real matches: std::vector<PostingIteratorInterface*>::iterator ci = candidate_set.begin(), ce = candidate_set.end(); while (ci != ce) { if (match_docno != (*ci)->skipDoc( match_docno)) { --nof_matches; if (nof_matches < cardinality) break; candidate_set.erase( ci); } else { ++ci; } } if (nof_matches < cardinality) { // ... we did not get a result with the cardinality requested -> start again docno_iter = match_docno+1; candidate_set.clear(); continue; } } return match_docno; } else if (match_docno) { docno_iter = match_docno+1; } else { break; } } return 0; }
位置窗口
以下辅助类实现了一个迭代器,该迭代器遍历给定最大尺寸的窗口,这些窗口包含指定最大数量参数特征的子集。名为 `PositionWindow` 的类在本文档介绍的模块导出的所有函数中都使用。因此,它将用于加权函数和摘要器的实现。
头文件
#ifndef _STRUS_POSITION_WINDOW_HPP_INCLUDED #define _STRUS_POSITION_WINDOW_HPP_INCLUDED #include "strus/index.hpp" #include "strus/postingIteratorInterface.hpp" #include <set> namespace strus { // Sliding window to iterate on the sets of posting iterator elements // that are withing a defined proximity range: class PositionWindow { public: // Element in the sliding position window implemented as set: struct Element { PostingIteratorInterface* itr; // ... reference this element belongs to Index pos; // ... position of this element Element() :itr(0),pos(0){} Element( PostingIteratorInterface* itr_, Index pos_) :itr(itr_),pos(pos_){} Element( const Element& o) :itr(o.itr),pos(o.pos){} bool operator < ( const Element& o) const { if (pos == o.pos) { return itr < o.itr; } return pos < o.pos; } }; // Constructor that fills the sliding window implemented as set // with the argument element start positions: PositionWindow( const std::vector<PostingIteratorInterface*>& args, unsigned int range_, unsigned int cardinality_, Index firstpos_); // Check if there is any valid window: bool first(); // Skip to the next window and return true, if there is one more: bool next(); // Return the size of the current window: unsigned int size() const; // Return the starting position of the current window: unsigned int pos() const; private: // Get the top element of the current window: std::set<Element>::iterator getWinTopElement(); private: std::vector<PostingIteratorInterface*> m_args; std::set<Element> m_set; // ... set used for sliding window unsigned int m_setsize; // ... current size of m_set unsigned int m_range; // ... maximum proximity range unsigned int m_cardinality; // ... number of elements for a candidate window }; }//namespace #endif
源文件
#include "positionWindow.hpp" #include "strus/postingJoinOperatorInterface.hpp" #include "strus/postingIteratorInterface.hpp" #include "strus/errorBufferInterface.hpp" #include <cstdio> using namespace strus; // Constructor that fills the sliding window implemented as set // with the argument element start positions: PositionWindow::PositionWindow( const std::vector<PostingIteratorInterface*>& args, unsigned int range_, unsigned int cardinality_, Index firstpos_) :m_args(args) ,m_setsize(0) ,m_range(range_) ,m_cardinality(cardinality_>0?cardinality_:args.size()) { std::vector<PostingIteratorInterface*>::iterator ai = m_args.begin(), ae = m_args.end(); for (; ai != ae; ++ai) { PostingIteratorInterface* itr = *ai; Index pos = itr->skipPos( firstpos_); if (pos) { m_set.insert( Element( itr, pos)); ++m_setsize; } } } // Get the top element of the current window: std::set<PositionWindow::Element>::iterator PositionWindow::getWinTopElement() { if (m_cardinality == 0) return m_set.end(); unsigned int ii=0; std::set<Element>::iterator si = m_set.begin(), se = m_set.end(); for (; ii<m_cardinality && si != se; ++ii,++si){} return --si; } // Check if there is any valid window: bool PositionWindow::first() { // Return, if there is valid window left: return m_setsize >= m_cardinality; } // Skip to the next window and return true, if there is one more: bool PositionWindow::next() { // Calculate how many positions we can skip with the // first element without loosing any window of a size // within the defined proximity range: PostingIteratorInterface* itr = m_set.begin()->itr; std::set<Element>::iterator top = getWinTopElement(); Index posdiff = top->pos - m_set.begin()->pos; Index skipsize = posdiff > (Index)m_range ? (posdiff - (Index)m_range) : 1; // Change the position of the first element in the // sliding window by the calculated size: Index pos = m_set.begin()->pos; m_set.erase( m_set.begin()); Index newpos = itr->skipPos( pos + skipsize); if (newpos) { m_set.insert( Element( itr, newpos)); } else { --m_setsize; } // Return, if there is valid window left: return m_setsize >= m_cardinality; } // Return the size of the current window: unsigned int PositionWindow::size() const { std::set<Element>::iterator si = getWinTopElement(); return si->pos - m_set.begin()->pos; } // Return the starting position of the current window: unsigned int PositionWindow::pos() const { return (m_setsize >= m_cardinality)?m_set.begin()->pos:0; }
现在我们有了实现窗口的 posting 迭代器及其连接运算符所需的一切
Posting 迭代器
以下类实现了由本模块中实现的 posting 集合连接运算符创建的 posting 迭代器。
// Window posting iterator class: class WindowPostingIterator :public PostingIteratorInterface { public: // Constructor: WindowPostingIterator( const std::vector<Reference< PostingIteratorInterface> >& args, unsigned int range_, unsigned int cardinality_, ErrorBufferInterface* errorhnd_) :m_docno(0) ,m_posno(0) ,m_argar(args) ,m_documentFrequency(-1) ,m_range(range_) ,m_cardinality(cardinality_) ,m_errorhnd(errorhnd_) { // Initializing the featureid: std::vector<Reference< PostingIteratorInterface> >::const_iterator ai = m_argar.begin(), ae = m_argar.end(); for (int aidx=0; ai != ae; ++ai,++aidx) { if (aidx) m_featureid.push_back('='); m_featureid.append( (*ai)->featureid()); } m_featureid.append( ".win"); } // Destructor: virtual ~WindowPostingIterator(){} // The skip document method uses the getFirstAllMatchDocnoSubset introduced // and assures with a skip position call, that the result is a real match: virtual Index skipDoc( const Index& docno_) { try { m_docno = getFirstAllMatchDocnoSubset( m_argar, docno_, false, m_cardinality, m_candidate_set); while (m_docno && skipPos(0) == 0) { m_docno = getFirstAllMatchDocnoSubset( m_argar, m_docno+1, false, m_cardinality, m_candidate_set); } return m_docno; } CATCH_ERROR_MAP_RETURN( *m_errorhnd, (m_docno=0), "in skip document") } // The skip document candidate method uses the getFirstAllMatchDocnoSubset introduced: virtual Index skipDocCandidate( const Index& docno_) { try { return m_docno=getFirstAllMatchDocnoSubset( m_argar, docno_, true, m_cardinality, m_candidate_set); } CATCH_ERROR_MAP_RETURN( *m_errorhnd, (m_docno=0), "in skip document candidate") } // The skip position method uses a sliding window for finding the next match fulfilling // the condition of having a set with size cardinality of element withing a proximity range, // at a position bigger or equal to the position index passed as argument: virtual Index skipPos( const Index& posno_) { try { PositionWindow win( m_candidate_set, m_range, m_cardinality, posno_); if (!win.first()) return m_posno=0; return m_posno=win.pos(); } CATCH_ERROR_MAP_RETURN( *m_errorhnd, (m_posno=0), "in skip position") } // Return a reference to the identifier built in the constructor: virtual const char* featureid() const { return m_featureid.c_str(); } // The document frequency cannot be calculated without a fullscan of the // index for all matching documents also inspecting position. // This is considered as too expensive. So is estimating. // So we return the minimum document frequency of the arguments.: virtual Index documentFrequency() const { try { if (m_documentFrequency < 0) { std::vector<Reference< PostingIteratorInterface> >::const_iterator ai = m_argar.begin(), ae = m_argar.end(); if (ai == ae) return 0; m_documentFrequency = (*ai)->documentFrequency(); for (++ai; ai != ae; ++ai) { Index df = (*ai)->documentFrequency(); if (df < m_documentFrequency) { m_documentFrequency = df; } } } return m_documentFrequency; } CATCH_ERROR_MAP_RETURN( *m_errorhnd, 0, "in document frequency estimation") } // The frequency is calculated by iterating on all // elements of the current document and counting the number of positions: virtual unsigned int frequency() { Index idx=0; unsigned int rt = 0; for (;0!=(idx=skipPos( idx)); ++idx,++rt){} return rt; } // Return the current document number: virtual Index docno() const { return m_docno; } // Return the current position number: virtual Index posno() const { return m_posno; } // Return the length virtual Index length() const { return 1; //... this value is not correct, we should return the current window size } private: // Current document: Index m_docno; // Current position: Index m_posno; // Argument iterators: std::vector<Reference< PostingIteratorInterface> > m_argar; // Unique id of the feature expression for debugging and tracing: std::string m_featureid; // Cached Document frequency (of the rarest subexpression): mutable Index m_documentFrequency; // Required proximity range of the candicate windows: unsigned int m_range; // Required cardinality of the result: unsigned int m_cardinality; // Current set of matching document candidates: std::vector<PostingIteratorInterface*> m_candidate_set; // Buffer for error messages (for exception free interfaces): ErrorBufferInterface* m_errorhnd; };
Posting 连接运算符
以下类实现了从 posting 集合连接运算符参数创建 posting 迭代器的构造函数
// Window posting join operator class: class WindowPostingJoinOperator :public PostingJoinOperatorInterface { public: // Constructor: WindowPostingJoinOperator( ErrorBufferInterface* errorhnd_) :m_errorhnd(errorhnd_){} // Destructor: virtual ~WindowPostingJoinOperator(){} // Virtual constructor of the resulting posting iterator virtual PostingIteratorInterface* createResultIterator( const std::vector<Reference<PostingIteratorInterface> >& argitrs, int range, unsigned int cardinality) const { try { if (range < 0) { throw std::runtime_error( "no negative range allowed for window iterator"); } return new WindowPostingIterator( argitrs, (unsigned int)range, cardinality, m_errorhnd); } CATCH_ERROR_MAP_RETURN( *m_errorhnd, 0, "in create result iterator") } // Return the Description of the operator for user help and introspection: virtual Description getDescription() const { try { return Description( "iterator on windows of a maximum size (range) " "containing a defined subset of features (cardinality)"); } CATCH_ERROR_MAP_RETURN( *m_errorhnd, Description(), "in get description") } private: ErrorBufferInterface* m_errorhnd; };
模块接口
头文件
我们将创建的模块需要一个构造函数来创建 posting 集合连接运算符
#ifndef _STRUS_POSTING_JOIN_OPERATOR_WINDOW_HPP_INCLUDED #define _STRUS_POSTING_JOIN_OPERATOR_WINDOW_HPP_INCLUDED namespace strus { // Forward declarations: class PostingJoinOperatorInterface; class ErrorBufferInterface; // Window iterator constructor: PostingJoinOperatorInterface* createWindowJoinOperator( ErrorBufferInterface* errorhnd); }//namespace #endif
源文件
// Exported function constructing the PostingJoinOperatorInterface realizing an // iterator on windows of a given maximum size: PostingJoinOperatorInterface* strus::createWindowJoinOperator( ErrorBufferInterface* errorhnd) { try { return new WindowPostingJoinOperator( errorhnd); } CATCH_ERROR_MAP_RETURN( *errorhnd, 0, "in create join operator") }
加权函数
加权函数用于为文档分配权重。它们使用 posting 集合迭代器和一些数值参数作为输入来计算文档的权重。文档的权重用作查询评估中对结果文档进行排序的标准。用于计算加权函数结果的对象通过 3 个步骤创建,每个步骤实现一个参数化级别。定义加权函数的模块导出一个函数,该函数创建一个 `weighting function interface`,并使用 `error buffer interface` 进行实例化。下一个级别是创建一个 `weighting function instance interface`。此加权函数实例可以使用 `addNumericParameter` 和 `addStringParameter` 方法进行参数化。下一个也是最后一个级别是创建一个 `execution context interface`。上下文是用于将加权函数参数化与特定查询在特定存储状态下发行的对象。函数的执行上下文用于通过方法计算文档的权重
double call( const Index& docno);
以下小节展示了实现加权函数所有参数化步骤的示例,该函数计算包含至少定义数量查询特征的最小窗口的倒数。
我们从执行上下文类开始
错误处理宏
我们已经在前面介绍过 Strus 的错误处理和无异常接口,并为我们的 posting 连接运算符提供了类似的示例。我们在这里定义了相同类型的宏
#define CATCH_ERROR_MAP_RETURN( HND, VALUE, MSG)\ catch( const std::bad_alloc&)\ {\ (HND).report( "out of memory in window weighting function");\ return VALUE;\ }\ catch( const std::runtime_error& err)\ {\ (HND).report( "%s (minwin window weighting function): %s", MSG, err.what());\ return VALUE;\ } #define CATCH_ERROR_MAP( HND, MSG)\ catch( const std::bad_alloc&)\ {\ (HND).report( "out of memory in window weighting function");\ }\ catch( const std::runtime_error& err)\ {\ (HND).report( "%s (minwin window weighting function): %s", MSG, err.what());\ }
执行上下文
加权函数执行上下文使用查询中用于加权的特征进行参数化。它通过构造函数(由函数实例调用)获取全局统计信息以及访问存储和元数据表的接口。执行上下文使用我们在描述 posting 迭代器的部分中定义的 `PositionWindow` 类。
class MinWinWeightingFunctionContext :public WeightingFunctionContextInterface { public: MinWinWeightingFunctionContext( ErrorBufferInterface* errhnd_, int range_, unsigned int cardinality_) :m_errhnd(errhnd_),m_range(range_),m_cardinality(cardinality_) {} virtual ~MinWinWeightingFunctionContext(){} virtual void addWeightingFeature( const std::string& name_, PostingIteratorInterface* postingIterator_, float /*weight_*/, const TermStatistics& /*stats_*/) { try { if (name_ == "match") { m_arg.push_back( postingIterator_); } else { throw std::runtime_error( "unknown weighting feature name"); } } CATCH_ERROR_MAP( *m_errhnd, "in add weighting feature"); } virtual double call( const Index& docno) { try { // Initialize the matching features to weight for this document: std::vector<PostingIteratorInterface*>::const_iterator ai = m_arg.begin(), ae = m_arg.end(); std::vector<PostingIteratorInterface*> matches; matches.reserve( m_arg.size()); for (; ai != ae; ++ai) { if (docno == (*ai)->skipDoc( docno)) { matches.push_back( *ai); } } // Calculate the minimal window size: PositionWindow win( matches, m_range, m_cardinality, 0); unsigned int minwinsize = m_range+1; bool more = win.first(); for (;more; more = win.next()) { unsigned int winsize = win.size(); if (winsize < minwinsize) { minwinsize = winsize; } } // Return the weight depending on the minimal window size: if (minwinsize < m_range) { return 1.0/(minwinsize+1); } else { return 0.0; } } CATCH_ERROR_MAP_RETURN( *m_errhnd, 0.0, "in call"); } virtual std::string debugCall( const Index& docno) { return std::string(); // ... the source to download contains a debug function implementation. // we do not provide one here, leaving it out makes things easier. } private: ErrorBufferInterface* m_errhnd; std::vector<PostingIteratorInterface*> m_arg; int m_range; unsigned int m_cardinality; };
函数实例
加权函数执行上下文使用数值和字符串参数进行参数化。它提供虚拟构造函数来创建加权函数实例。
class MinWinWeightingFunctionInstance :public WeightingFunctionInstanceInterface { public: explicit MinWinWeightingFunctionInstance( ErrorBufferInterface* errhnd_) :m_errhnd(errhnd_),m_range(1000),m_cardinality(0){} virtual ~MinWinWeightingFunctionInstance(){} virtual void addStringParameter( const std::string& name, const std::string&) { try { throw std::runtime_error( std::string( "unknown numeric parameter ") + name); } CATCH_ERROR_MAP( *m_errhnd, "in add string parameter"); } virtual void addNumericParameter( const std::string& name, const ArithmeticVariant& value) { try { if (name == "maxwinsize") { m_range = value.toint(); if (m_range <= 0) { throw std::runtime_error("proximity range parameter negative or null"); } } else if (name == "cardinality") { if (value.type != ArithmeticVariant::UInt && value.type != ArithmeticVariant::Int) { throw std::runtime_error("illegal cardinality parameter"); } m_cardinality = value.touint(); } else { throw std::runtime_error( std::string( "unknown numeric parameter ") + name); } } CATCH_ERROR_MAP( *m_errhnd, "in add numeric parameter"); } virtual WeightingFunctionContextInterface* createFunctionContext( const StorageClientInterface* /*storage_*/, MetaDataReaderInterface* /*metadata_*/, const GlobalStatistics& /*stats_*/) const { try { return new MinWinWeightingFunctionContext( m_errhnd, m_range, m_cardinality); } CATCH_ERROR_MAP_RETURN( *m_errhnd, 0, "in create function context"); } virtual std::string tostring() const { try { std::ostringstream rt; rt << "maxwinsize=" << m_range << ", cardinality=" << m_cardinality; return rt.str(); } CATCH_ERROR_MAP_RETURN( *m_errhnd, std::string(), "in tostring"); } private: ErrorBufferInterface* m_errhnd; std::vector<PostingIteratorInterface*> m_arg; int m_range; unsigned int m_cardinality; };
加权函数
加权函数是一个未参数化的对象,它仅提供一个虚拟构造函数来创建用于参数化的函数实例。此外,它还实现了一个用于内省的 `get description` 方法。
class MinWinWeightingFunction :public WeightingFunctionInterface { public: MinWinWeightingFunction( ErrorBufferInterface* errhnd_) :m_errhnd(errhnd_) {} virtual ~MinWinWeightingFunction(){} virtual WeightingFunctionInstanceInterface* createInstance( const QueryProcessorInterface*) const { try { return new MinWinWeightingFunctionInstance( m_errhnd); } CATCH_ERROR_MAP_RETURN( *m_errhnd, 0, "in create instance"); } virtual FunctionDescription getDescription() const { typedef FunctionDescription::Parameter P; FunctionDescription rt("Calculate the document weight as the inverse " "of the minimal window size containing a subset of the document features"); rt( P::Feature, "match", "defines the query features to find in a window"); rt( P::Numeric, "maxwinsize", "the maximum size of a window to search for"); rt( P::Numeric, "cardinality", "the number of features to find at least in a window"); return rt; } private: ErrorBufferInterface* m_errhnd; };
模块接口
头文件
我们将创建的模块需要一个构造函数来创建用应用程序的错误缓冲区接口实例化的加权函数对象。
#ifndef _STRUS_WEIGHTING_FUNCTION_MINWIN_HPP_INCLUDED #define _STRUS_WEIGHTING_FUNCTION_MINWIN_HPP_INCLUDED namespace strus { // Forward declarations: class WeightingFunctionInterface; class ErrorBufferInterface; // Weighting function constructor: WeightingFunctionInterface* createWeightingFunction( ErrorBufferInterface* errorhnd); }//namespace #endif
源文件
用于创建加权函数对象的导出函数,用于计算最小窗口大小的倒数
WeightingFunctionInterface* createWeightingFunction( ErrorBufferInterface* errorhnd) { try { return new MinWinWeightingFunction( errorhnd); } CATCH_ERROR_MAP_RETURN( *errorhnd, 0, "in create function"); }
摘要器
摘要器用于从文档中提取内容。内容可以通过 posting 迭代器从特征或子表达式匹配(附加到变量)引用,这些内容作为特征参数传递给摘要器。最方便的摘要器类型是查询结果文档的摘要。但是,摘要器也用于附加属性,例如文档标题到结果元素。我们在此提供一个摘要器,它返回由包含至少最小数量查询特征的最小窗口标记的段落。
创建用于在查询中对文档进行摘要的摘要器上下文的模式与加权函数相同。用于计算摘要器结果的对象通过 3 个步骤创建,每个步骤实现一个参数化级别。定义摘要器函数的模块导出一个函数,该函数创建一个 `summarizer function interface`,并使用 `error buffer interface` 进行实例化。下一个级别是创建一个 `summarizer function instance interface`。此摘要器函数实例可以使用 `addNumericParameter` 和 `addStringParameter` 方法进行参数化。下一个也是最后一个级别是创建一个 `execution context interface`。上下文是用于将摘要器函数参数化与特定查询在特定存储状态下发行的对象。函数的执行上下文用于通过方法计算文档的摘要元素
std::vector<SummaryElement> getSummary( const Index& docno);
其中 `SummaryElement` 是一个包含字符串和权重的结构。
以下小节展示了实现摘要器函数所有参数化步骤的示例,该函数返回第一个出现的包含至少定义数量查询特征的最小窗口的文本作为摘要元素。
错误处理宏
我们已经在前面介绍过 Strus 的错误处理和无异常接口,并为我们的 posting 连接运算符和加权函数提供了类似的示例。我们在这里定义了相同类型的宏
#define CATCH_ERROR_MAP_RETURN( HND, VALUE, MSG)\ catch( const std::bad_alloc&)\ {\ (HND).report( "out of memory in minimal window summarizer function");\ return VALUE;\ }\ catch( const std::runtime_error& err)\ {\ (HND).report( "%s (minimal window summarizer function): %s", MSG, err.what());\ return VALUE;\ } #define CATCH_ERROR_MAP( HND, MSG)\ catch( const std::bad_alloc&)\ {\ (HND).report( "out of memory in minimal window summarizer function");\ }\ catch( const std::runtime_error& err)\ {\ (HND).report( "%s (minimal window summarizer function): %s", MSG, err.what());\ }
摘要器函数上下文
摘要器采用与加权函数类似的参数,用于前面描述的最小窗口。此外,我们需要前向索引的特征类型来提取内容,以及用于访问前向索引的存储。`getSummary` 方法可以与 `MinWinWeightingFunctionContext::call` 方法进行比较,不同之处在于我们不从最小窗口大小计算权重。我们连接前向索引中定义类型的所有词条,这些词条位于找到的第一个最小窗口内,并将生成的字符串作为摘要结果返回。为简单起见,摘要结果的名称硬编码为“minwin”。
class MinWinSummarizerFunctionContext :public SummarizerFunctionContextInterface { public: MinWinSummarizerFunctionContext( ErrorBufferInterface* errorhnd_, const StorageClientInterface* storage_, int range_, unsigned int cardinality_, const std::string& type_) :m_errhnd(errorhnd_) ,m_forwardindex(storage_->createForwardIterator( type_)) ,m_range(range_) ,m_cardinality(cardinality_) {} virtual ~MinWinSummarizerFunctionContext(){} virtual void addSummarizationFeature( const std::string& name_, PostingIteratorInterface* postingIterator_, const std::vector<SummarizationVariable>& /*variables_*/, float /*weight_*/, const TermStatistics& /*stats_*/) { try { if (name_ == "match") { m_arg.push_back( postingIterator_); } else { throw std::runtime_error( "unknown summarization feature name"); } } CATCH_ERROR_MAP( *m_errhnd, "in add summarization feature"); } virtual std::vector<SummaryElement> getSummary( const Index& docno) { try { // Initialize the features to weight and the forward index: m_forwardindex->skipDoc( docno); std::vector<SummaryElement> rt; std::vector<PostingIteratorInterface*>::const_iterator ai = m_arg.begin(), ae = m_arg.end(); std::vector<PostingIteratorInterface*> matches; matches.reserve( m_arg.size()); for (; ai != ae; ++ai) { if (docno == (*ai)->skipDoc( docno)) { matches.push_back( *ai); } } // Calculate the minimal window size and position: PositionWindow win( matches, m_range, m_cardinality, 0); unsigned int minwinsize = m_range+1; Index minwinpos = 0; bool more = win.first(); for (;more; more = win.next()) { unsigned int winsize = win.size(); if (winsize < minwinsize) { minwinsize = winsize; minwinpos = win.pos(); } } // Build the summary phrase and append it to the result: if (minwinsize < (unsigned int)m_range) { std::string text; Index pos = minwinpos; while (minwinsize) { if (pos == m_forwardindex->skipPos( pos)) { if (!text.empty()) text.push_back( ' '); text.append( m_forwardindex->fetch()); } else { text.append( ".."); } --minwinsize; ++pos; } rt.push_back( SummaryElement( "minwin", text)); } return rt; } CATCH_ERROR_MAP_RETURN( *m_errhnd, std::vector<SummaryElement>(), "in call"); } virtual std::string debugCall( const Index& docno) { //... we do not provide a debug function implementation here // you'll find an implementation in the sources to download return std::string(); } private: ErrorBufferInterface* m_errhnd; Reference<ForwardIteratorInterface> m_forwardindex; std::vector<PostingIteratorInterface*> m_arg; int m_range; unsigned int m_cardinality; };
摘要器函数实例
摘要器函数实例的实现与加权函数实例类似,不同之处在于我们必须处理一个额外的字符串参数“type”(要提取的词条的前向索引类型)。
class MinWinSummarizerFunctionInstance :public SummarizerFunctionInstanceInterface { public: MinWinSummarizerFunctionInstance( ErrorBufferInterface* errhnd_) :m_errhnd(errhnd_),m_range(1000),m_cardinality(0),m_type(){} virtual ~MinWinSummarizerFunctionInstance(){} virtual void addStringParameter( const std::string& name, const std::string& value) { try { if (name == "type") { m_type = value; } else { throw std::runtime_error( std::string( "unknown string parameter ") + name); } } CATCH_ERROR_MAP( *m_errhnd, "in add string parameter"); } virtual void addNumericParameter( const std::string& name, const ArithmeticVariant& value) { try { if (name == "maxwinsize") { m_range = value.toint(); if (m_range <= 0) { throw std::runtime_error("proximity range parameter negative or null"); } } else if (name == "cardinality") { if (value.type != ArithmeticVariant::UInt && value.type != ArithmeticVariant::Int) { throw std::runtime_error("illegal cardinality parameter"); } m_cardinality = value.touint(); } else { throw std::runtime_error( std::string( "unknown numeric parameter ") + name); } } CATCH_ERROR_MAP( *m_errhnd, "in add numeric parameter"); } virtual SummarizerFunctionContextInterface* createFunctionContext( const StorageClientInterface* storage_, MetaDataReaderInterface* /*metadata_*/, const GlobalStatistics& /*stats*/) const { try { return new MinWinSummarizerFunctionContext( m_errhnd, storage_, m_range, m_cardinality, m_type); } CATCH_ERROR_MAP_RETURN( *m_errhnd, 0, "in create function context"); } virtual std::string tostring() const { try { std::ostringstream rt; rt << "type=" << m_type << ", maxwinsize=" << m_range << ", cardinality=" << m_cardinality; return rt.str(); } CATCH_ERROR_MAP_RETURN( *m_errhnd, std::string(), "in tostring"); } private: ErrorBufferInterface* m_errhnd; std::vector<PostingIteratorInterface*> m_arg; int m_range; unsigned int m_cardinality; std::string m_type; };
摘要器函数
与加权对象一样,我们有一个未参数化的函数对象,带有一个描述和一个虚拟构造函数方法,用于创建可参数化的函数实例。
class MinWinSummarizerFunction :public SummarizerFunctionInterface { public: MinWinSummarizerFunction( ErrorBufferInterface* errhnd_) :m_errhnd(errhnd_) {} virtual ~MinWinSummarizerFunction(){} virtual SummarizerFunctionInstanceInterface* createInstance( const QueryProcessorInterface* /*processor*/) const { try { return new MinWinSummarizerFunctionInstance( m_errhnd); } CATCH_ERROR_MAP_RETURN( *m_errhnd, 0, "in create instance"); } virtual FunctionDescription getDescription() const { typedef FunctionDescription::Parameter P; FunctionDescription rt("Get the passage of the forward index inside the " "minimal window containing a subset of the document features"); rt( P::Feature, "match", "defines the query features to find in a window"); rt( P::Numeric, "maxwinsize", "the maximum size of a window to search for"); rt( P::Numeric, "cardinality", "the number of features to find at least in a window"); rt( P::String, "type", "forward index feature type for building the result"); return rt; } private: ErrorBufferInterface* m_errhnd; };
模块接口
我们将创建的模块需要一个构造函数来创建用应用程序的错误缓冲区接口实例化的加权函数对象。
头文件
#ifndef _STRUS_SUMMARIZER_FUNCTION_MINWIN_HPP_INCLUDED #define _STRUS_SUMMARIZER_FUNCTION_MINWIN_HPP_INCLUDED namespace strus { // Forward declarations: class SummarizerFunctionInterface; class ErrorBufferInterface; // Weighting function constructor: SummarizerFunctionInterface* createMinWinSummarizerFunction( ErrorBufferInterface* errorhnd); }//namespace #endif
源文件
SummarizerFunctionInterface* createMinWinSummarizerFunction( ErrorBufferInterface* errorhnd) { try { return new MinWinSummarizerFunction( errorhnd); } CATCH_ERROR_MAP_RETURN( *errorhnd, 0, "in create function"); }
组装好的模块
我们现在已经为 posting 集合连接运算符、加权函数和摘要器这三个组件中的每一个展示了一个示例。剩下的就是介绍如何将这些示例组件组装成一个动态加载的模块(共享对象)。Strus 的模块概念不涉及任何魔术宏来创建模块。您只需将模块入口点定义为包含所有已定义对象的数据结构。对象数组由 NULL 元组终止。它们可以是空的(仅包含 NULL 元组终止标记)。这是我们的模块源代码
#include "strus/private/dll_tags.hpp" #include "strus/storageModule.hpp" #include "window_joinop.hpp" #include "minwin_weighting.hpp" #include "minwin_summarizer.hpp" using namespace strus; static const PostingIteratorJoinConstructor postingJoinOperators[] = { {"window", createWindowJoinOperator}, {0,0} }; static const WeightingFunctionConstructor weightingFunctions[] = { {"minwin", createMinWinWeightingFunction}, {0,0} }; static const SummarizerFunctionConstructor summarizers[] = { {"minwin", createMinWinSummarizerFunction}, {0,0} }; extern "C" DLL_PUBLIC strus::StorageModule entryPoint; strus::StorageModule entryPoint( postingJoinOperators, weightingFunctions, summarizers);
构建和安装
请参阅“先决条件”部分了解所需内容。源代码中提供了一个 CMake 文件。还有一个测试程序。Strus 模块安装在 strus 库路径的 `modules` 子目录中,例如 `/usr/lib/strus/modules/modstrus_storage_example.so`。
模块可以被任何支持模块的程序或语言绑定使用。
总结
我们介绍了 Strus 核心在查询评估方面可能的三种扩展类型:posting 连接操作、加权函数和摘要器。本文未能展示每种扩展类型的全部潜力。它没有讨论可以使用这些元素构建的可能应用程序。但是,如果您阅读了本文,您可能会对 Strus 通过您自己的动态加载模块进行扩展获得初步印象。也许您很快就会实现自己的查询评估方案和一些有用的摘要器。