使用 Python、Tornado 和 Strus 构建搜索引擎





5.00/5 (8投票s)
本教程基于 Docker 镜像,将引导您了解如何在 Tornado Web 框架中,基于 Strus 及其 Python 绑定来开发一个搜索引擎服务。
目标
本教程的目的是展示如何使用 Python、Tornado 和 Strus 来构建一个用于非平凡信息需求的搜索引擎 Web 服务,超越简单的关键词搜索。
您所需的所有先决条件都已包含在 Docker 镜像中。教程完成时间将少于一小时。
背景
Strus 是一套用于构建搜索引擎的库和工具。本文档介绍了其 Python 绑定,并将其与 Tornado Web 框架 结合使用。
更新
2017 年 10 月 5 日
本教程及其 Docker 镜像的文档和源代码已更新到 Strus 的最新版本。现在使用 Python 3.4,而不是 Python 2.7。Strus 的语言绑定不再支持 Python 2.X。
尽管此处提供的示例有效,但一些技术和最佳实践已经发生变化。尽管如此,这些示例仍能提供对 Strus 的一些见解。
此处介绍的查询评估方法 NBLNK 在 Strus Wikipedia 演示搜索中不再以相同方式实现。它已被 'accunear' 方法取代,该方法依赖于查询项的邻近性统计,并且不再构建项表达式。
必备组件
您需要安装 Docker。
至少在下载本教程的 Docker 镜像时,初次运行时需要互联网连接。
知识要求
要执行和理解本教程的步骤,需要一些关于 Python 编程的中间知识。但使用的语言概念并不复杂。如果您了解 PHP,也可能能够理解代码。
要理解 Web 服务器结果的渲染,您应该了解 HTML 的基本构造。
本教程面向具有信息检索基本概念先前知识的公众。如果您没有任何知识,也可以完成本教程,但这可能不会令人信服。在这种情况下,您有更多成熟的现成解决方案可用。
来源
本教程中介绍的源代码和本文档提供的压缩包中的源代码,都按教程中的“步骤”进行组织。因此,在步骤 6 中显示的源文件 strusIR.py,在源代码压缩包的 src/step6/strusIR.py 中,或者在 Docker 镜像的 /home/strus/src/step6/strusIR.py 中。
引言
本教程的人工数据集是一个包含 201 个国家及其所说语言的列表。此外,每个国家都被分配了一个大陆。首先,我们将使用 BM25 作为查询评估方案,对信息检索查询进行检查,搜索语言并获得国家排名列表。然后,我们将看一下使用加权实体出现在匹配文档中的示例,搜索语言并获得大陆排名列表。
此处介绍的两种加权方案都不适合示例数据集。它们有点像“杀鸡用牛刀”。例如,从句子中提取实体并与查询匹配,在只包含一个句子的文档集合中显得很傻。但我找不到合适的(包含要提取的实体的)测试数据集,因此我手工构造了一个非常简单的。
提取和加权实体的加权方案源自 Strus 演示 项目的 NBLNK 加权方案,该项目搜索了完整的维基百科(英文)。那里的 NBLNK 检索方法对出现在匹配查询的句子中的链接进行排名。它不检查所有文档,只检查 BM25 加权的 300 篇最佳文档。原始源代码 (PHP) 可以在 这里 找到。
词汇表
在本教程中,您会遇到一些在此处简要解释的术语。
-
Posting:在信息检索中,术语 posting 通常定义一个文档编号以及与之关联的信息,例如文档中的位置。它是用来描述一个术语在集合中出现情况的元素。在 Strus 中,posting 定义了一个序对 (d,p),其中 d 指代文档,p 指代位置。posting 集合描述了构成检索表达式的函数的定义域。这种结构是具有一些关于这些集合的 n 元函数的集合对 (d,p) 的 布尔代数。
-
加权函数:为文档分配数值权重(双精度浮点数)的函数。加权函数是参数化的,并使用查询表达式的 posting 集合的迭代器和一些数值元数据来计算文档的权重。
-
Summarizer:用于提取文档内容以便展示或进一步处理的函数。
-
Term expression:一个任意复杂的表达式,表示为一个树,叶子是类型化的项。表达式由类型化的项和关于项子表达式的 posting 集合的 n 元函数构成。尽管构造表达式的定义从表面上看非常通用,但它存在限制。
-
Feature:一个特征(或查询特征)是将某个term expression 与一个权重和一个集合名称关联起来,以进行寻址。引用特征的对象是weighting functions 和summarizers。
-
SearchIndex:将术语映射到出现次数或 posting 集合的索引。在信息检索理论中称为倒排索引。
-
ForwardIndex:将文档映射到术语的索引。请在此处查看定义。
步骤 1:理解示例文档集合
文档结构
在本教程中,所有示例文档都位于一个文件中,即一个多部分文档。它有一个 根标签和用于要插入文档的项的
<?xml version='1.0' encoding='UTF-8' standalone='yes'?> <list> <doc id='Sweden'> In the country Sweden on the <continent id='Europe'>Europe</continent> are the following languages spoken: Swedish, Sami, Finnish. </doc> <doc id='Switzerland'> In the country Switzerland on the <continent id='Europe'>Europe</continent> are the following languages spoken: German, French, Italian, Romansch </doc> </list>
步骤 2:启动 Docker 容器
要启动 Docker 镜像,请在 shell 中键入以下命令:
docker run -p 40080:80 -t -i patrickfrey/strus-ub1604-torntuto:v0_15 /bin/bash
您将得到类似以下的提示:
root@8cbc7f49f3cf:/home/strus#
本教程中所有后续的 shell 命令都将在该 shell 中执行。
步骤 3:创建存储
初始化存储数据库
我们将使用 Strus 的一个实用程序来创建存储。Strus 的命令行实用程序是访问存储的程序。在本教程中,我们将仅用于存储的初始创建。
现在,我们用于从 Web 服务创建存储(从而管理 Web 服务中的不同存储)的命令行命令将使示例变得更加复杂,因为会涉及同步问题。该命令如下所示:
strusCreate -s "path=storage; metadata=doclen UINT16"
您将得到:
storage successfully created.
声明了元数据元素 doclen,因为我们即将使用的查询评估方案 BM25 要求它。
步骤 4:使用 Tornado 定义 Web 服务器骨架
定义服务器骨架
我们自顶向下设计 Web 服务器,并使用 Tornado 定义服务器的骨架,暂不实现所需的请求处理程序。它看起来如下:
#!/usr/bin/python3 import tornado.ioloop import tornado.web import os import sys # [1] Request handlers: class InsertHandler(tornado.web.RequestHandler): def post(self): pass; #... insert handler implementation class QueryHandler(tornado.web.RequestHandler): def get(self): pass; #... query handler implementation # [2] Dispatcher: application = tornado.web.Application([ # /insert in the URL triggers the handler for inserting documents: (r"/insert", InsertHandler), # /query in the URL triggers the handler for answering queries: (r"/query", QueryHandler), # /static in the URL triggers the handler for accessing static # files like images referenced in tornado templates: (r"/static/(.*)",tornado.web.StaticFileHandler, {"path": os.path.dirname(os.path.realpath(sys.argv[0]))},) ]) # [3] Server main: if __name__ == "__main__": try: print( "Starting server ...\n"); application.listen(80) print( "Listening on port 80\n"); tornado.ioloop.IOLoop.current().start() print( "Terminated\n"); except Exception as e: print( e);
它由三部分组成:运行服务器的主程序 [3]。根据 URL 模式选择处理程序的应用程序调度程序 [2],以及应用程序中使用的请求处理程序列表 [1]。如果我们在一个源文件 strusServer.py 中用以下命令启动此程序:
python3 strusServer.py
我们将得到一个正在监听的 Web 服务器:
Starting server ... Listening on port 80
……但它目前对任何命令都没有反应,所以我们再次停止它。
步骤 5:定义 Tornado HTML 模板以渲染结果
Tornado 有一个模板引擎,带有一个替换语言,允许在模板作用域中执行任意 Python 命令。它还有一个继承概念。我们在示例中声明一个基本模板 search_base_html.tpl 和三个模板 search_nblnk_html.tpl、search_bm25_html.tpl 和 search_error_html.tpl,它们以不同的方式实现结果块(名为 resultblock)。
基本模板 search_base_html.tpl
这是包含页面框架的基本模板。所有其他模板都从中派生,并以不同的方式实现结果块。
<html> <head> <title>A search engine with Python, Tornado and Strus</title> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> </head> <body> <h1>A search engine with Python, Tornado and Strus</h1> {% block resultblock %} {% end %} </body> </html>
BM25 搜索模板 search_bm25_html.tpl
这是用于普通 BM25 查询的模板。对于每个结果文档,我们映射文档编号 docno、weight、title 和 abstract。
{% extends "search_base_html.tpl" %} {% block resultblock %} <table border=1> <tr> <th align='left'>Docno</th> <th align='left'>Weight</th> <th align='left'>Title</th> <th align='left'>Abstract</th> </tr> {% for result in results %} <tr> <td>{{ result['docno'] }}</td> <td>{{ "%.4f" % result['weight'] }}</td> <td>{{ result['title'] }}</td> <td>{% raw result['abstract'] %}</td> </tr> {% end %} </table> {% end %}
实体加权搜索模板 search_nblnk_html.tpl
这是用于我们获得加权提取的实体出现在匹配文档中的排名列表的模板。对于每个结果文档,我们映射weight 和title。
{% extends "search_base_html.tpl" %} {% block resultblock %} <table border=1> <tr> <th align='left'>Weight</th> <th align='left'>Title</th> </tr> {% for result in results %} <tr> <td>{{ "%.4f" % result['weight'] }}</td> <td>{{ result['title'] }}</td> </tr> {% end %} </table> {% end %}
错误模板 search_error_html.tpl
这是用于捕获错误的模板。我们在此仅映射错误消息。
{% extends "search_base_html.tpl" %} {% block resultblock %} <p><font color="red">Error: {{message}}</font></p> {% end %}
步骤 6:定义请求处理程序
信息检索引擎后端(虚拟实现)
现在,我们尝试定义请求处理程序的虚拟版本,以使我们的骨架“活”起来。我们将其放在源文件 strusIR.py 中。在本教程稍后,我们将用一个真正的信息检索引擎替换此模块。
class Backend: # Constructor creating a local Strus context with the storage configuration # string passed as argument: def __init__(self, config): pass # Insert a multipart document as described in step 1 (doing nothing for the moment): def insertDocuments( self, content): return 0 # Query evaluation scheme for a classical information retrieval query # with BM25 (returning a dummy ranked list with one element for now): def evaluateQueryText( self, querystr, firstrank, nofranks): rt = [] rt.append( { 'docno': 1, 'title': "test document", 'weight': 1.0, 'abstract': "Neque porro quisquam est qui dolorem ipsum ..." }) return rt # Query evaluation method that builds a ranked list from the best weighted entities # extracted from sentences with matches (returning an dummy list for with # one element now): def evaluateQueryEntities( self, querystr, firstrank, nofranks): rt = [] rt.append( { 'title': "test document", 'weight': 1.0 }) return rt
在我们的骨架中,现在可以在声明信息检索后端使用的导入指令后插入以下行:
#!/usr/bin/python3 import tornado.ioloop import tornado.web import os import sys import strusIR # Declare the information retrieval engine: backend = strusIR.Backend( "path=storage; cache=512M")
在进行这些更改后,我们可以替换骨架中的请求处理程序。
插入请求处理程序
插入请求处理程序接受主体为多部分文档的 POST 请求,并调用后端的方法 insertDocuments。它返回一个简单的文本字符串,头部为 OK 或 ERR,取决于结果。
# Declare the insert document handler (POST request with the multipart document as body): class InsertHandler(tornado.web.RequestHandler): def post(self): try: content = self.request.body nofDocuments = backend.insertDocuments( content) self.write( "OK %u\n" % (nofDocuments)) except Exception as e: self.write( "ERR %s\n" % (e))
查询请求处理程序
查询请求处理程序接受带有以下参数的 GET 请求:
-
q:查询字符串
-
s:查询评估方案(BM25 或 NBLNK)
-
i:返回结果排名的第一个索引
-
n:返回结果排名的最大数量
它返回一个 HTML 页面,其中结果由 Tornado 模板引擎渲染。
# Declare the query request handler: class QueryHandler(tornado.web.RequestHandler): def get(self): try: # q = query terms: querystr = self.get_argument( "q", None) # i = first rank of the result to display (for scrolling): firstrank = int( self.get_argument( "i", 0)) # n = maximum number of ranks of the result to display on one page: nofranks = int( self.get_argument( "n", 20)) # c = query evaluation scheme to use: scheme = self.get_argument( "s", "BM25") if scheme == "BM25": # The evaluation scheme is a classical BM25 (Okapi): results = backend.evaluateQueryText( querystr, firstrank, nofranks) self.render( "search_bm25_html.tpl", scheme=scheme, querystr=querystr, firstrank=firstrank, nofranks=nofranks, results=results) elif scheme == "NBLNK": # The evaluation scheme is weighting the entities in the matching documents: results = backend.evaluateQueryEntities( querystr, firstrank, nofranks) self.render( "search_nblnk_html.tpl", scheme=scheme, querystr=querystr, firstrank=firstrank, nofranks=nofranks, results=results) else: raise Exception( "unknown query evaluation scheme", scheme) except Exception as e: self.render( "search_error_html.tpl", message=e, scheme=scheme, querystr=querystr, firstrank=firstrank, nofranks=nofranks)
步骤 7:调用服务器
我们的服务器定义现已完成。
现在我们可以启动服务器并发出查询。我们启动了 Docker 镜像,将端口 40080 映射到 Docker 镜像的端口 80。因此,我们可以使用您喜欢的 Web 浏览器发出 GET 请求:
http://127.0.0.1:40080/query?q=german&i=0&n=12&s=BM25
您将从我们虚拟实现的信息检索引擎得到以下结果:
或者,如果您使用 NBLNK 和此查询字符串进行搜索:
http://127.0.0.1:40080/query?q=german&i=0&n=12&s=NBLNK
您将得到:
步骤 8:使用 Strus 定义真正的信息检索引擎
现在我们来看 strusIR.py 模块及其实现的 Backend 类。我们将一步一步地用真实实现替换虚拟实现。首先,我们必须导入以下模块:
import strus import itertools import heapq import re
分析器配置
如果我们想插入和检索文档,我们必须描述文档中的信息项到其标准化形式的映射。对查询中的项也必须进行相同的标准化,以便可以通过索引匹配查询中的项与文档中的项。
此示例分析器配置已尽可能简化。并非所有步骤都已详细解释,但您将在 Strus 的 Python 接口文档中找到所有文档和查询分析器方法:DocumentAnalyzer、QueryAnalyzer。
strusIR.Backend 方法来创建文档和查询分析器如下所示:
# Create the document analyzer for our test collection: def createDocumentAnalyzer(self): rt = self.context.createDocumentAnalyzer() # Define the sections that define a document (for multipart documents): rt.defineDocument( "doc", "/list/doc") # Define the terms to search for (inverted index or search index): rt.addSearchIndexFeature( "word", "/list/doc//()", "word", ("lc",("stem","en"),("convdia","en"))) # Define the end of sentence markers: rt.addSearchIndexFeature( "sent", "/list/doc//()", ("punctuation","en","."), "empty") # Define the placeholders that are referencable by variables: rt.addSearchIndexFeature( "continent_var", "/list/doc/continent@id", "content", "empty", "succ") # Define the original terms in the document used for abstraction: rt.addForwardIndexFeature( "orig", "/list/doc//()", "split", "orig") # Define the contents that extracted by variables: rt.addForwardIndexFeature( "continent", "/list/doc/continent@id", "content", "text", "succ") # Define the document identifier: rt.defineAttribute( "docid", "/list/doc@id", "content", "text") # Define the doclen attribute needed by BM25: rt.defineAggregatedMetaData( "doclen",("count", "word")) return rt
# Create the query analyzer according to the document analyzer configuration: def createQueryAnalyzer(self): rt = self.context.createQueryAnalyzer() rt.definePhraseType( "text", "word", "word", ["lc", ["stem", "en"], ["convdia", "en"]] ) return rt
以下结构需要进一步解释:
-
文档文本选择器表达式(defineDocument、addSearchIndexFeature、addForwardIndexFeature、defineAttribute 的第二个参数)用于寻址要处理的文档部分。选择器表达式的语法和语义取决于您使用的文档分段器。默认使用的文档分段器是基于 textwolf 库的 XML 分段器。它使用源自 XPath 缩写语法的表达式语法。另一个用于其他文档格式的分段器可能会以不同方式定义选择表达式。
-
描述归一化器、分词器、聚合器列表的函数表达式,如 Python 中的数组。例如:
("lc",("stem","en"),("convdia","en"))
描述了一系列归一化函数:小写,然后是英语词干提取,以及英语的变音符号转换,按此顺序应用。用于复杂树或列表的数组表示法用于紧凑地初始化您核心所需的功能。数组的第一个参数是函数名,其余参数描述函数的参数。在描述查询表达式时,我们也使用了类似的初始化表示法,我们将在本教程后面遇到。
-
某些特征声明中的“succ”参数。此选项告诉分析器,该元素没有自己的位置,但具有与下一个具有自己位置的元素相同的分配位置。“pred”相应地引用前一个位置。这些选项有助于声明文档中的注释,这些注释绑定到另一个项。位置对于描述结构化特征中的位置邻近关系(例如 A 紧跟 B)非常重要。
BM25 的加权方案配置
此处我们声明 strusIR.Backend 方法来创建 BM25 的加权方案,包括用于展示结果的摘要器。并非所有步骤都已详细解释,但您将在 Strus 的 Python 接口文档中找到所有查询评估方法:QueryEval。在本例中,BM25 加权方案的定义如下所示:
# Create a simple BM25 query evaluation scheme with fixed # a,b,k1 and avg document lenght and title with abstract # as summarization attributes: def createQueryEvalBM25(self): rt = self.context.createQueryEval() # Declare the sentence marker feature needed for abstracting: rt.addTerm( "sentence", "sent", "") # Declare the feature used for selecting result candidates: rt.addSelectionFeature( "selfeat") # Query evaluation scheme: rt.addWeightingFunction( "BM25", { "k1": 1.2, "b": 0.75, "avgdoclen": 20, "match": {'feature':"docfeat"} }) # Summarizer for getting the document title: rt.addSummarizer( "attribute", { "name": "docid" }, {"result":"TITLE"}) # Summarizer for abstracting: rt.addSummarizer( "matchphrase", { "type": "orig", "windowsize": 40, "sentencesize":30, "matchmark": '$<b>$</b>', "struct":{'feature':"sentence"}, "match": {'feature':"docfeat"} }, {"phrase":"CONTENT"} return rt
以下结构需要进一步解释:
-
addTerm 方法:此方法用于向查询添加项,这些项不是查询本身的一部分,但作为加权函数或摘要器的上下文信息使用。一个好的例子是结构化标记,如句子结尾。这些标记不是查询的一部分,但它们可能在加权或摘要中发挥作用。
-
addSelectionFeature 方法:在 Strus 中,加权对象与加权方式严格分开。您必须始终声明一组特征,这些特征声明了哪些文档集被考虑用于结果。这种分离在大文档集合中是必需的,以避免对结果候选进行过于昂贵的扫描。
-
加权或摘要函数的返回值可以是字符串或数值,或者通过特征集的名称来寻址特征。字符串或数值根据其类型进行识别,特征被指定为一个只有一个元素的字典。{ 'feature': <name> }。
加权文档实体匹配的加权方案配置
strusIR.Backend 方法用于创建 NBLNK 的加权方案,包括用于展示结果的摘要器,如下所示:
# Create a simple BM25 query evaluation scheme with fixed # a,b,k1 and avg document lenght and the weighted extracted # entities in the same sentence as matches as query evaluation result: def createQueryEvalNBLNK(self): rt = self.context.createQueryEval() # Declare the sentence marker feature needed for the # summarization features extracting the entities: rt.addTerm( "sentence", "sent", "") # Declare the feature used for selecting result candidates: rt.addSelectionFeature( "selfeat") # Query evaluation scheme for entity extraction candidate selection: rt.addWeightingFunction( "BM25", {"k1": 1.2, "b": 0.75, "avgdoclen": 20, "match": {'feature':"docfeat"} }) # Summarizer to extract the weighted entities: rt.addSummarizer( "accuvar", { "match": {'feature':"sumfeat"}, "var": "CONTINENT", "type": "continent", "result":"ENTITY" } ) return rt
# Create a simple BM25 query evaluation scheme with fixed # a,b,k1 and avg document lenght and the weighted extracted # entities in the same sentence as matches as query evaluation result: def createQueryEvalNBLNK(self): rt = self.context.createQueryEval() # Declare the sentence marker feature needed for the # summarization features extracting the entities: rt.addTerm( "sentence", "sent", "") # Declare the feature used for selecting result candidates: rt.addSelectionFeature( "selfeat") # Query evaluation scheme for entity extraction candidate selection: rt.addWeightingFunction( "BM25", { "k1": 1.2, "b": 0.75, "avgdoclen": 500, "match": {'feature': "docfeat"} }) # Summarizer to extract the weighted entities: rt.addSummarizer( "accuvariable", { "match": {'feature': "sumfeat"}, "var": "CONTINENT", "type": "continent", "result":"ENTITY" }) return rt
此处我们没有什么需要解释的了。声明与 BM25 类似。我们有一个不同的提取实体定义的摘要器。
strusIR.Backend 构造函数
现在我们已经声明了创建 strusIR.Backend 对象所需的所有辅助方法。strusIR.Backend 的构造函数如下所示:
# Constructor. Initializes the query evaluation schemes and the query and document analyzers: def __init__(self, config): if isinstance( config, ( int, long ) ): self.context = strus.Context( "localhost:%u" % config) self.storage = self.context.createStorageClient() else: self.context = strus.Context() self.context.addResourcePath("./resources") self.storage = self.context.createStorageClient( config ) self.queryAnalyzer = self.createQueryAnalyzer() self.documentAnalyzer = self.createDocumentAnalyzer() self.queryeval = {} self.queryeval["BM25"] = self.createQueryEvalBM25() self.queryeval["NBLNK"] = self.createQueryEvalNBLNK()
插入多部分文档的方法
插入多部分文档的方法,如步骤 1 中所述,如下所示:
# Insert a multipart document: def insertDocuments( self, content): rt = 0 transaction = self.storage.createTransaction() for doc in self.documentAnalyzer.analyzeMultiPart( content): docid = doc['attribute']['docid'] transaction.insertDocument( docid, doc) rt += 1 transaction.commit() return rt
该方法创建一个事务并插入所有已分析的文档部分。
评估 BM25 查询的方法
评估 BM25 查询的方法如下所示:
# Query evaluation scheme for a classical information retrieval query with BM25: def evaluateQueryText( self, querystr, firstrank, nofranks): queryeval = self.queryeval[ "BM25"] query = queryeval.createQuery( self.storage) terms = self.queryAnalyzer.analyzeTermExpression( [ "text", querystr ] ) if len( terms) == 0: # Return empty result for empty query: return [] selexpr = ["contains", 0, 1] for term in terms: selexpr.append( term ) query.addFeature( "docfeat", term, 1.0) query.addFeature( "selfeat", selexpr, 1.0 ) query.setMaxNofRanks( nofranks) query.setMinRank( firstrank) # Evaluate the query: results = query.evaluate() # Rewrite the results: rt = [] for pos,result in enumerate(results['ranks']): content = "" title = "" for summary in result['summary']: if summary['name'] == 'TITLE': title = summary['value'] elif summary['name'] == 'CONTENT': content = summary['value'] rt.append( { 'docno':result['docno'], 'title':title, 'weight':result['weight'], 'abstract':content }) return rt
该方法分析查询文本,并为每个项构建一个查询特征。选择特征是一个表达式,它仅选择包含所有查询项的文档(“contains”)。此方法的最大部分是重写结果排名列表,以获得扁平结构列表,从而避免将特定于实现的结构侵入表示层(Tornado 模板)。
评估匹配文档实体权重查询的方法
从我们示例文档中提取和加权实体(大陆)的方法,是通过从查询特征对(或单个项查询中的一个项)构建表达式特征。对于从查询特征对构建表达式特征,会考虑两种情况:如果其中一个项紧跟在另一个项之后,还会构建一个搜索文档句子中即时序列的表达式。从这些序列表达式构建的摘要器特征将获得最高权重。对于所有项,无论它们在查询中是否是邻居,我们都会构建搜索句子中距离 5 或 20(词数距离)的特征。对于查询中的邻居项,从这些表达式构建的特征将获得稍高的权重。
所有这些摘要器特征表达式还会在同一句子中搜索距离 50(词数距离)内的实体。一个变量附加到该实体上,该变量由累积所有实体权重的摘要器提取。
在句子中关联子表达式的表达式是“within_struct”和“sequence_struct”。两者都以前面的结构元素表达式作为第一个参数。在我们的例子中是句子分隔符。这个结构元素不能被具有最小位置和最大位置的匹配子表达式定义的区间覆盖。
在 Strus 中,变量赋值绑定到表达式匹配的一个位置。因此,提取的项必须定义表达式匹配的位置。所以我们围绕要提取的实体构建摘要器特征表达式。以下表达式显示了一个由“local languages”构建的示例特征:
sumexpr = [ "sequence_struct", 50, ["sent"], [{'variable':"CONTINENT"}, "continent_var",""], [ "within_struct", 20, ["sent"], ["word", "local"], ["word", "languages"] ] ] query.defineFeature( "sumfeat", sumexpr, 1.0 )
翻译过来意思是:我们在由句子分隔符分隔的结构内寻找一个变量“CONTINENT”分配的“continent”,后面跟着一个在句子分隔符分隔的结构内,距离不超过 50 个词(词数距离),并且在距离不超过 20 个词(词数距离)内包含“local”和“languages”的结构。
表达式 {'variable':"CONTINENT"} 是一个标记,声明字符串“CONTINENT”作为附加到其出现的结构上的变量。您可能会称之为 hack。但这已经是 Strus 绑定中使用列表或树作为对象初始化器的最后一类 hack 了。
现在我们介绍辅助方法来构建摘要器特征,最后介绍使用这些特征提取大陆的加权方案。
创建查询中邻居项摘要器特征的方法
# Helper method to define the query features created from terms # of the query string, that are following subsequently in the query: def __defineSubsequentQueryTermFeatures( self, query, term1, term2): # Pairs of terms appearing subsequently in the query are # translated into 3 query expressions: # 1+2) search for sequence inside a sentence in documents, # The summarizer extracts entities within # a distance of 50 in the same sentence # 3) search for the terms in a distance smaller than 5 inside a sentence, # The summarizer extracts entities within a distance # of 50 in the same sentence # 4) search for the terms in a distance smaller than 20 inside # The summarizer extracts entities within # a distance of 50 in the same sentence expr = [ [ "sequence_struct", 3, ["sent",''], term1, term2 ], [ "sequence_struct", 3, ["sent",''], term2, term1 ], [ "within_struct", 5, ["sent",''], term1, term2 ], [ "within_struct", 20, term1, term2 ] ] weight = [ 3.0, 2.0, 2.0, 1.5 ] ii = 0 while ii < 4: # The summarization expression attaches a variable referencing an # the entity to extract. # CONTINENT terms of type 'continent_var': sumexpr = [ "chain_struct", 50, ["sent",''], [{'variable':"CONTINENT"}, "continent_var", ""], expr[ ii] ] query.addFeature( "sumfeat", sumexpr, weight[ ii] ) sumexpr = [ "sequence_struct", -50, ["sent",''], expr[ ii], [{'variable':"CONTINENT"}, "continent_var", ""], ] query.addFeature( "sumfeat", sumexpr, weight[ ii] ) ii += 1
摘要器特征的构建看起来有点复杂。它处理了在单个句子中出现多个要提取的实体的情况。为了提取所有实体,我们必须将分配给模式匹配的位置绑定到提取的实体。简单的范围搜索(“within”)而不是两个序列(一个正向,一个反向)将只返回一个匹配。要理解这一点,请想象一个序列 W L1 L2,其中 W 是匹配项,L1 和 L2 是关联的链接。Strus 中的“within”将匹配 W L1,然后在 W 的位置停止匹配其他结构。反向序列将匹配 L1 的位置,因为它在其前面找到 W,然后是 L2 的位置,原因相同。因此,它返回 2 个匹配并提取 L1 和 L2。在这里,您清楚地看到了 Strus 模型的局限性。但这次我们通过重新排列表达式解决了这个问题。
创建查询中非邻居项摘要器特征的方法
# Helper method to define the query features created from terms # of the query string, that are not following subsequently in the query: def __defineNonSubsequentQueryTermFeatures( self, query, term1, term2): # Pairs of terms not appearing subsequently in the query are # translated into two query expressions: # 1) search for the terms in a distance smaller than 5 inside # a sentence, weight 1.6, # where d ist the distance of the terms in the query. # The summarizer extracts entities within a distance # of 50 in the same sentence # 2) search for the terms in a distance smaller than 20 inside # a sentence, weight 1.2, # where d ist the distance of the terms in the query. # The summarizer extracts entities within a distance # of 50 in the same sentence expr = [ [ "within_struct", 5, ["sent",''], term1, term2 ], [ "within_struct", 20, ["sent",''], term1, term2 ] ] weight = [ 1.6, 1.2 ] ii = 0 while ii < 2: # The summarization expression attaches a variable referencing # the entity to extract. # CONTINENT terms of type 'continent_var': sumexpr = [ "chain_struct", 50, ["sent",''], [{'variable':"CONTINENT"}, "continent_var", ""], expr[ ii] ] query.defineFeature( "sumfeat", sumexpr, weight[ ii] ) sumexpr = [ "sequence_struct", -50, ["sent",''], expr[ ii], [{'variable':"CONTINENT"}, "continent_var", ""] ] query.addFeature( "sumfeat", sumexpr, weight[ ii] ) ii += 1
为单个项查询创建摘要器特征的方法
# Helper method to define the query features created from a single # term query: def __defineSingleTermQueryFeatures( self, query, term): # Single term query: expr = [ term['type'], term['value'] ] # The summarization expression attaches a variable referencing an # the entity to extract. # CONTINENT terms of type 'continent_var': sumexpr = [ "chain_struct", 50, ["sent",''], [{'variable':"CONTINENT"}, "continent_var", ""], expr ] query.addFeature( "sumfeat", sumexpr, 1.0 ) sumexpr = [ "sequence_struct", -50, ["sent",''], expr, [{'variable':"CONTINENT"}, "continent_var", ""] ] query.addFeature( "sumfeat", sumexpr, 1.0 )
查询评估方法
现在我们终于可以根据前面介绍的用于实体提取的摘要器特征构建的 3 个辅助方法来定义查询评估方法了。
# Query evaluation method that builds a ranked list from the best weighted entities # extracted from sentences with matches: def evaluateQueryEntities( self, querystr, firstrank, nofranks): queryeval = self.queryeval[ "NBLNK"] query = queryeval.createQuery( self.storage) terms = self.queryAnalyzer.analyzeTermExpression( [ "text", querystr ] ) if len( terms) == 0: # Return empty result for empty query: return [] # Build the weighting features. Queries with more than one term are building # the query features from pairs of terms: if len( terms) > 1: # Iterate on all permutation pairs of query features and creat # combined features for summarization: for pair in itertools.permutations( itertools.takewhile( lambda x: x<len(terms), itertools.count()), 2): if pair[0] + 1 == pair[1]: self.__defineSubsequentQueryTermFeatures( query, terms[pair[0]], terms[pair[1]]) elif pair[0] < pair[0]: self.__defineNonSubsequentQueryTermFeatures( query, terms[pair[0]], terms[pair[1]]) else: self.__defineSingleTermQueryFeatures( query, terms[0] ) # Define the selector ("selfeat") as the set of documents that contain all query terms # and define the single term features for weighting and candidate evaluation ("docfeat"): selexpr = ["contains", 0, 1] for term in terms: selexpr.append( term ) query.addFeature( "docfeat", term, 1.0) query.addFeature( "selfeat", selexpr, 1.0 ) # Evaluate the ranked list for getting the documents to inspect for entities close to matches query.setMaxNofRanks( 300) query.setMinRank( 0) results = query.evaluate() # Build the table of all entities with weight of the top ranked documents: entitytab = {} for pos,result in enumerate(results['ranks']): for summary in result['summary']: if summary['name'] == 'ENTITY': weight = 0.0 if summary['value'] in entitytab: weight = entitytab[ summary['value'] ] entitytab[ summary['value']] = weight + summary['weight'] # Extract the top weighted documents in entitytab as result: heap = [] for key, value in entitytab.items(): heapq.heappush( heap, [value,key] ) topEntities = heapq.nlargest( firstrank + nofranks, heap, lambda k: k[0]) rt = [] idx = 0 maxrank = firstrank + nofranks for elem in topEntities[firstrank:maxrank]: rt.append({ 'weight':elem[0], 'title':elem[1] }) return rt
<!--EndFragment-->
完整的 strusIR 模块
我们的 strusIR 模块现在已完成。
步骤 9:重新启动服务器并使用 CURL 插入文档
现在我们已经用真正的实现替换了信息检索模块,我们可以插入文档了。首先,我们必须重新启动服务器,以便它加载我们新的 strusIR 模块。我们在后台启动它,因为之后我们需要命令行来插入文档:
python3 strusServer.py &
然后我们将得到:
Starting server ... Listening on port 80
然后,我们可以使用 CURL 命令插入文档:
curl -X POST -d @countries.xml localhost:80/insert --header "Content-Type:text/xml"
然后我们将得到:
OK 201
步骤 10:检查查询结果
现在我们有了一个真正插入了文档集合的搜索引擎,让我们用您喜欢的浏览器发出一些查询:
http://127.0.0.1:40080/query?q=spanish&i=0&n=12&s=BM25
我们将得到:
此结果显示了使用 BM25 加权方案对查询词 'spanish' 出现情况进行加权的country 文档。
如果您使用 NBLNK 和此查询字符串进行搜索:
http://127.0.0.1:40080/query?q=spanish&i=0&n=12&s=NBLNK
您将得到:
此结果显示了按权重排名的大陆,该权重是通过将查询与实体出现的句子进行匹配来计算的。在单个项查询的情况下,这是实体出现的国家数量。在我们的示例中,这是说西班牙语的国家数量。对于像“local language”这样的多项查询,使用介绍的加权方案,我们得到的权重是人为的。
从文档中提取这些实体并对其进行加权是一项琐碎的任务,但介绍的加权方案也可以应用于海量数据集,前提是摘要器检查的文档数量可以限制在合理大小。