企业级搜索引擎Solr 第三章 索引数据(Indexing Data)
虽然本书中假设你要建索引的内容都是有着良好结构的,比如数据库表,XML文件,CSV,但在现实中我们要保存很混乱的数据,或是二进制文件,如PDF,Microsoft Office,甚至是图片和音乐文件。
我(Eric Pugh)在首次使用Solr时,就需要处理客户在几年间产生的大量PDF和Microsoft文档。随着Solr Cell的进步,和框架的支持,对富文档进行索引不再困难了。
我们来看一个使用Solr Cell从MIDI文件中抽取卡拉OK歌词的例子。想想你可以建立你所喜欢的歌词的Solr索引,这是多令人激动呀。Solr Cell的完整参考资料在:http://wiki. apache.org/solr/ExtractingRequestHandler
Extracting text and metadata from files
每个文件格式都不同,它们所提供的元数据类型也不同,当然抽取它们内容的方法也不同。Tika提供了单个API对所有格式进行处理,以减少复杂性。
Tika支持多种数据格式。一些常用的格式都是支持的,比如Adobe PDF,Microsoft Office包括Word,Excel,PowerPoint,Visio和Outlook。也支持从其它格式中抽取数据,如从JPG,GIF和PNG中抽取数据。还支持从多个音频中抽取数据如MP3,MIDI和Wave audio。Tika它本并不解析这些文件格式。它只是一个代理,它调用多个第三方的库去解析,关于tika 0.8版的文档在http://tika.
apache.org/0.8/formats.html。
Solr Cell是一个针对Tika的简单适配器,它由一个SAX ContentHandler组成,ContentHandler处理SAX事件,并通过指定要抽取的域产生文档。
在索引二制进文件的时候,有些事要注意:
l 你可以提供任何Tika支持的文档类型给Tika,Tika会尝试确定文档正确的MIME类型,然后再调用相应的解析器。如果你已经知道了正确的MIME,你可以在stream.type参数中指定。
l Solr Cell中默认的SolrContentHandler用起来很简单。你可能发现你不止需要Solr Cell提供的功能,你还需要对数据进行一些处理来减少垃圾数据被索引。其中一种实现的方法是自定义的Solr UpdateRequestProcessor,在本章后面会讲。另一种方式是实现子类ExtractingRequestHandler的createFactory()来提供自定义的SolrContentHandler。
l 请记住如果你可能会发送很大的二进制文件,然后由Solr解析,这样速度可能会很慢。如果你只想索引元数据,那你应该用Tika来编写自己的解析器来抽取元数据,再Post给服务器,见第九章。
你可以从Tika官网上学习到更多内容:http://tika.apache.org/。
Configuring Solr
在/example/cores/karaoke/conf/solrconfig.xml文件中,有对二进制文档解析的请求处理器:
<requestHandler name="/update/extract"
class="org.apache.solr.handler.extraction.ExtractingRequestHandler">
<lst name="defaults">
<str name="map.Last-Modified">last_modified</str>
<str name="uprefix">metadata_</str>
</lst>
</requestHandler>
这里我们可以看到Tika的元数据属性Last-Modified被映射到了Solr的域last_modified,参数uprefix是指定没有匹配对应Solr域名的域的名字的前缀。
Solr Cell作为contrib模块发布,它包含大约25以上的JAR文件,它们每种能解析不同的文件格式。要使用Solr Cell,我们将Solr Cell Jar和其它的JAR放到karaoke核心中,/examples/cores/karaoke/lib,它默认不被引入solr.war,也不需要其它的核心的JAR。这些JAR文件放到lib,这时只有karaoke核心可以使用它。要将这些核心在多个核心中共享,你可以将它们加入到examples/cores/lib中,并在solr.xml中指定目录:
<solr persistent="false" sharedLib="lib">
在这个例子中,我们用标准Java包javax.audio.midi将MIDI格式的karaoke文件解析。如果你知道你所解析的文件格式是什么,你可以只加入你所需要的JAR文件,这可以让你的发布包更小,但是为了完备性,我们将所有依赖的JAR全部加入,比如pdfbox,poi和icu4j在./cores/karaoke/lib。
Solr Cell parameters
在进入示例之前,我们看一下Solr Cell的配置参数,所有参数都是可选的。
首先,Solr Cell决定文档的格式。它通常会猜的比较准,但它也可以与下面的参数配合:
l Resource.name:这个可选参数是指定文件的名字。它能帮助Tika来确定正确的MIME类型。
l Stream.type:这个可选参数允许你明确地指定文档MIME类型,它会优先于Tika的猜测结果。
Tika将所有的输入文档软换为基础的XHTML文档,在头部分中包含元数据。元数据会成为域,内容的文档会映射到content域。下面的参数更详细:
l Capture:X会被拷贝它自己的域中的HTML元素名称(比如:“p”),它可以被设置多次。
l captureAttr:设置为true来获取XHTML属性到域中。一个常见的例子是用Tika抽取从<a/>抽取href属性来索引另一个域的索引。
l xpath:允许你指定一个XPath查询,它不会将元素的文本放入content域。如果你只要得到元数据,丢弃XTHML中的正文,你可以用xpath=/xhtml:html/xhtml:head/descendant:node()。注意为每个元素使用xhtml:命名空间前缀。注意它支持一部分XPath命令。见http://tika.apache.org/0.8/api/org/apache/tika/sax/xpath/XPathParser.html。API中没有提到到它支持/descendant:node()。
l literal. [fieldname]:允许你为这个域一个特定的域值,比如,为ID域。
现在产生的域名都应该与Schema中的域名对应。下面是控制产生域名的参数:
l lowernames:将域名都小写,并将非字母数字的字符都转换成下划线。比如Last-Updated变为last_updated。
l fmap.[tikaFieldName]:将源域名转换为目标域名。比如,fmap.last_modified=timestamp映射将由Tika产生元数据域的last_modified映射到Solr Schema中timestamp域。
l uprefix:这个前缀用于域名,如果没有加前缀的名字不匹配Schema中的域名。使用这个前缀可以与动态域中进行匹配:
uprefix=meta_
<dynamicField name="meta_*" type="text_general" indexed="true"
stored="true" multiValued="true"/>
l defaultField:如果没有指定uprefix,并没有匹配Schema中的域名,那么域的值将写入这个参数指定的域名中。这样可以将所有元数据都入一个多值的域。
defaultField=meta
<field name="meta" type="text_general" indexed="true"
stored="true" multiValued="true"/>
其它的一些参数:
l boost. [fieldname]:由参数值对指定域进行Boost,来影响打分。
l extractOnly:设置为true,仅返回Tika解析的XHTML的文档结档,而不进行建索引。通常还会带上参数wt=json&indent=true来使XHTML更容易读。这个选项用来帮助调试。
l extractFormat:(当extractOnly=true时)默认为xml,它产生XHMTL结构,可以设置为text产生从文档中抽取的原始文本。
Extracting karaoke lyrics
现在我们可以将MIDI Post给Solr /update/extract请求处理器来抽取卡拉OK的歌词。一些经典的ABBA音乐在./examples/3/karaoke/目录,是从FreeKaraoke中得到http://www.freekaraoke.com,在此感谢。
要对Angel Eyes这首歌建索引,可以如下使用命令行:
>> curl 'http://localhost:8983/solr/karaoke/update/extract?fmap.content=text' -F "file=@angeleyes.kar"
不要提交你的改变:
>> curl http://localhost:8983/solr/karaoke/update?commit=true
你可以在发送一个文档的请求中加上commit=true请求参数进行提交,但是,在你建立大量文档时,这样做是低效的。
我们只有一个fmap.conent=text参数来指定源中正文的文本都到text域。在这个例子中angeleyes.kar的歌词都保存到Solr的text域中。现在我们看一下索引结果:http:// localhost:8983/solr/karaoke/select/?q=*:*。你会看到下面结果:
<result name="response" numFound="1" start="0">
<doc>
<arr name="text">
<str>
Angel Eyes by Abba sequenced by Michael Boyce
tinker@worldnet.att.netSoft karaoke@KMIDI KARAOKE
FILEWords@LENGL@TAngel Eyes@TABBA\Last night I was taking
a walk along the river/And I saw him together with a young
girl/And the look that he gave made me shiver/'Cause
he always used ...
</str>
</arr>
</doc>
</result>
你现在已经对MIDI文件中的歌词在text域中建立索引了。但是,元数据呢?Tika解析MIDI出的数据没有利用。嗯,这是动态域一展身手的地方了。每种二进制文件格式都有着很大的不同。幸运的是,Solr Cell通过使用uprefix属性让你来将元数据映射到动态域。我们在schema.xml中使用动态域:
<dynamicField name="metadata_*" type="string" indexed="true"
stored="true" multiValued="true"/>
因为有时我们想标准化地处理元数据属性,我们可以在solrconfig.xml中加入下面的配置:
<str name="uprefix">metadata_</str>
如果你提前知道一些元数据域的名字,并想进行域名映射,你可以配置:
<str name="fmap.content_type">content_type</str>
当你搜索所有文档时,你应该可以看到Angel Eyes中所有的元数据,除了content_type外,其它都以metadata_为前缀。
<str name="content_type">audio/midi</str>
<arr name="metadata_patches"><str>0</str></arr>
<arr name="metadata_stream_content_type"><str>application/octetstream
</str></arr>
<arr name="metadata_stream_size"><str>55677</str></arr>
<arr name="metadata_stream_source_info"><str>file</str></arr>
<arr name="metadata_tracks"><str>16</str></arr>
显然在大多情况下,你每次索引同一文件,你不想再产生一个新的Solr文档。如果在你的Schema中的uniqueKey域指定了如id的域,那你可以用literal.id=34参数提供一个特定的ID。每次你用相同的ID索引文件,它就会删除再插入这个文件。但是这就要求你要有管理ID的能力,可能需要第三方的支持,比如数据库。如果你想用元数据来做为ID,比如Tika提供的stream_name,那你可以用fmap.stream_name=id来映射域。要在这个例子中使用,更新./examples/cores/karaoke/schema.xml指定<uniqueKey>id</uniqueKey>。
>> curl 'http://localhost:8983/solr/karaoke/update/extract?fmap.
content=text&fmap.stream_name=id' -F "file=@angeleyes.kar"
这要求你所定义的id是字符串型,而不是数值型。
Indexing richer documents
索引对MIDI文件中的卡拉OK歌词建索引是个微不足道的例子了。我们只简单的忽略了所有的内容,然后把它们存到Solr的text域中,不对文档的结构进行任何的处理。
但是其它的文档结构,比如PDF,会更复杂,只是取得文本可能不会得到很好的搜索结果。我们看一下Take a Chance on Me,它是一个解释Monte Carlo模拟是什么的PDF文件。
打开./examples/3/karaoke/mccm.pdf,你会看到一个复杂的PDF有很多字符,背景图片,复杂的数学公式,希腊字母,和图表。尽管这个PDF很复杂,索引它和索引卡拉OK文件一样简单:
>> curl 'http://localhost:8983/solr/karaoke/update/extract?map.
content=text&commit=true' -F "file=@mccm.pdf"
如果你将文件名作为stream_name的参数进行搜索:http://localhost:8983/solr/karaoke/select/?q=stream_name:mccm.pdf,你会看到由Tika抽取出的Last_modified元数据被映射到solrconfig.xml中的last_modified域。
其中设置的lowercase=true会将Last-Modified映射到last_modified,使它符合域名的规范。
<doc>
<arr name="id">
<str>mccm.pdf</str>
</arr>
<arr name="last_modified">
<str>Sun Mar 03 15:55:09 EST 2002</str>
</arr>
<arr name="text">
<str>
Take A Chance On Me
那么对于富文档,我们将如何处理元数据和内容呢?在URL加入extractOnly=true,它会输出Solr Cell解析出的XML文档,包括元数据域,而不对文档建索引。
追加wt=json会使它更容易解析出内嵌的XML内容:
>> curl 'http://localhost:8983/solr/karaoke/update/extract?extractOnly=tr
ue&wt=json&indent=true' -F "file=@mccm.pdf"
复制粘贴JSON输出中内嵌的XML内容,用你最喜欢的HTML浏览工具查看内容。我用的是TextMate的HTML插件,另一个不错的选择在http://xmlindent.com/。
<html>
<head>
<meta name="stream_source_info" content="file">
<meta name="subject" content="Monte carlo condensed">
<meta name="Last-Modified" content="2002-03-03T20:55:09Z">
<meta name="Author" content="Andrew" n.=">
<meta name="xmpTPg:NPages" content="11">
<meta name="Creation-Date" content="2002-03-03T20:53:14Z">
<meta name="Keywords" content="ABBA">
<title>
Take A Chance On Me
</title>
</head>
<body>
<p>
"\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
</p>
<div class="page">
\n\n
<p>
Take A Chance On MeMonte Carlo Condensed MatterA very brief
guide to Monte Carlo simulation.An explanation of what I do.A chance
for far too many ABBA puns.
</p>\n
</div>
<p>
\n
</p>
<div class="page">
\n\n
<p>
What's The Name Of The Game?Simulation:'I have a dream,
a fantasy,to help me through reality'Given some model many-body
system.Simulate the evolution of the system.We can then measure
various observables.Attempt to predict properties of real systems.
Eg Equilibrium Properties All about knowing free-energies, phase
behavior, compressibility, specific heat,knowing M(E), knowing m.
</p>\n
</div>
返回的XHTML文档包含从文档在<head/>中的元数据,还有以XHTML格式表示的正文内容。
Update request processors
无论你如何选择导入数据,都要在Solr中有一个最后一步的配置点,允许它在索引导入数据之前处理数据。Solr的请求处理器是将文档放到更新请求处理器链(update request processor chain)上进行数据更新。如果你在solrconfig.xml中查找updateRequestProcessorChain你会看到示例。
它可以通过配置update.chain,选择哪个更新链来处理更新请求。它有一定用,但你可能只会用到一个链。如果没有链被指定,你会有一个默认的链,由LogUpdateProcessorFactory和RunUpdateProcessorFactory组成。下面是一些可以选择的更新请求处理器。它们名字都以UpdateProcessorFactory结尾。
l SignatureUpdateProcessorFactory:它基于你指定的域产生一个Hash ID。如果你想对你的数据去重,那么这个会很适合你。更多的信息见:http://wiki.apache.org/solr/Deduplication。
l UIMAUpdateProcessorFactory:它将文档传给Unstructured Information Management Architecture(UIMA),一个对文档进行自然语言处理的模块。更多信息见:http://wiki.apache.org/solr/SolrUIMA。
l LogUpdateProcessorFactory:它会在更新发生时记录Log信息。
l RunUpdateProcessorFactory:它是真正建索引的部分,不要忘了它,否则文档更新不会有效果!更具体地说,它将文档传给Lucene,它会根据Schema中的配置对每个域进行处理。
未来会有很多的处理器来进行其它有趣的任务,包括一个类似DIH的ScriptTransformer的处理器,见SOLR-1725。你当然也可以写你自己的处理器。Solr对这点是支持扩展的,所以你不用改动Solr本身。
UpdateRequestProcessorChain section
This allows you to configure a processing chain that processes a document before indexing. The chain consists of UpdateRequestProcessors, each doing some processing on the document. You can define multiple chains to use in different update request handlers.
Example chain, commented out in solrconfig.xml:
去重复配置:<updateRequestProcessorChain name="dedupe"> <processor class="org.apache.solr.update.processor.SignatureUpdateProcessorFactory"> <bool name="enabled">true</bool> <str name="signatureField">id</str> <bool name="overwriteDupes">false</bool> <str name="fields">name,features,cat</str> <str name="signatureClass">org.apache.solr.update.processor.Lookup3Signature</str> </processor> <processor class="solr.LogUpdateProcessorFactory" /> <processor class="solr.RunUpdateProcessorFactory" /> </updateRequestProcessorChain
<requestHandler name="/update "class="solr.XmlUpdateRequestHandler"> <lst name="defaults"> <str name="update.chain">dedupe</str> </lst> </requestHandler>