Solr笔记--转载
Solr 是一种可供企业使用的、基于 Lucene 的搜索服务器,它支持层面搜索、命中醒目显示和多种输出格式。在这篇分两部分的文章中,Lucene Java™ 的提交人 Grant Ingersoll 将介绍 Solr 并向您展示如何轻松地将其表现优异的全文本搜索功能加入到 Web 应用程序中。
一旦用户需要某种信息,就可以立即搜索到这些信息,这种要求再也不是可有可无的了。随着 Google 和类似的复杂搜索引擎的出现,用户希望得到高质量的搜索结果,帮助他们快速、轻易地找到所需的信息。经理对您的在线购物站点同样抱有很高的期望,要求它能够提供一个可伸缩、高度可用且易于维护的搜索解决方案,并且安装这个解决方案不应太昂贵。对于您而言,只是希望事业进步,让老板和客户满意,以及保持头脑清醒。
使用 Apache Solr 可以满足所有的这些要求,它是一种开放源码的、基于 Lucene Java 的搜索服务器,易于加入到 Web 应用程序中。Solr 提供了层面搜索、命中醒目显示并且支持多种输出格式(包括 XML/XSLT 和 JSON 格式)。它易于安装和配置,而且附带了一个基于 HTTP 的管理界面。您可以坚持使用 Solr 的表现优异的基本搜索功能,也可以对它进行扩展从而满足企业的需要。Solr 还拥有一个活跃的开发者群体,如有需要,您可以随时向他们寻求帮助。
Solr 的历史
Solr 最初由 CNET Networks 开发,2006 年初,Apache Software Foundation 在 Lucene 顶级项目的支持下得到了 Solr。Solr 于 2007 年 1 月酝酿成熟,在整个项目孵化期间,Solr 稳步地积累各种特性并吸引了一个稳定的用户群体、贡献者和提交人。Solr 现在是 Lucene(Apache 的基于 Java 的全文本搜索引擎库)的一个子项目。
安装和配置
要开始使用 Solr,需安装以下软件:
Java 1.5 或更高版本。
Ant 1.6.x 或更高版本。
Web 浏览器,用来查看管理页面。建议使用 Firefox;相比之下使用 Internet Explorer 可能要复杂些。
servlet 容器,如 Tomcat 5.5。本文中的示例假定您的 Tomcat 在 8080 端口上运行,这是 Tomcat 的默认设置。如果运行的是其他 servlet 容器或在其他的端口上运行,则可能要修改给出的 URL 才能访问示例应用程序和 Solr。我已经假定示例应用程序的各个部分都运行在 Tomcat 的本地主机上。另外还要注意 Solr 以打包的方式与 Jetty 一起提供。
要下载和安装所有这些应用程序,请参阅 参考资料。
设置 Solr
一旦搭建好运行环境,就可以从 Apache Mirrors Web 站点 下载 Solr 1.1 版。接下来,执行以下操作:
停止 servlet 容器。
在命令行选择希望在其中执行操作的目录,从中输入 mkdir dw-solr。
输入 cd dw-solr。
将 Solr 下载版本复制到当前目录中并解压缩。即可得到 apache-solr-1.1.0-incubating 目录。不用注意 incubating 标记;Solr 早已孵化成熟。
将 Solr WAR 文件复制到 servlet 容器的 webapps 目录中。
下载示例应用程序,将其复制到当前目录,然后解压缩,即可在当前工作目录中得到一个 solr 目录。本文将一直把它用作 Solr 的主目录。
可以通过以下三种方式之一设置 Solr 的主位置:
设置 java 系统属性 solr.solr.home (没错,就是 solr.solr.home)。
配置 java:comp/env/solr/home 的一个 JNDI 查找指向 solr 目录。
在包含 solr 目录的目录中启动 servlet 容器。(默认的 Solr 主目录是当前工作目录下的 solr。)
将示例 WAR 文件(位于 dw-solr/solr/dist/dw.war 中)复制到 servlet 容器的 webapps 目录中,方法与复制 Solr WAR 文件相同。WAR 文件的 Java 的代码位于 dw-solr/solr/src/java 中,而 JSP 和其他 Web 文件位于 dw-solr/solr/src/webapp 中。
要验证所有程序都正常运行,请启动 servlet 容器并将浏览器指向 http://localhost:8080/solr/admin/。如果一切顺利,您应该看到类似图 1 所示的页面。如果没有出现管理页面,查看容器日志中的错误消息。另外,确保从 dw-solr 目录启动 servlet 容器,以便可以正确地设置 Solr 的主位置。
图 1. 一个 Solr 管理屏幕示例
关于 Lucene
要了解 Solr 就必须熟悉 Lucene。 Lucene 是一个基于 Java 的高性能文本搜索引擎库,最初由 Doug Cutting 编写,后来被捐赠给 Apache Software Foundation。很多应用程序都利用 Lucene 来增强自身的搜索功能,因为 Lucene 高速、易用和具有活跃社区的特点。Solr 构建在这些功能之上,使 Lucene 可供企业使用,并具有最小的编程需求。
Solr 基础
因为 Solr 包装并扩展了 Lucene,所以它们使用很多相同的术语。更重要的是,Solr 创建的索引与 Lucene 搜索引擎库完全兼容。通过对 Solr 进行适当的配置,某些情况下可能需要进行编码,Solr 可以阅读和使用构建到其他 Lucene 应用程序中的索引。此外,很多 Lucene 工具(如 Luke)也可以使用 Solr 创建的索引。
在 Solr 和 Lucene 中,使用一个或多个 Document 来构建索引。Document 包括一个或多个 Field。Field 包括名称、内容以及告诉 Solr 如何处理内容的元数据。例如,Field 可以包含字符串、数字、布尔值或者日期,也可以包含您想添加的任何类型。Field 可以使用大量的选项来描述,这些选项告诉 Solr 在索引和搜索期间如何处理内容。我将在本文中稍后详细讨论这些选项。现在,查看一下表 1 中列出的重要属性的子集:
表 1. 字段属性
属性名称 |
说明 |
indexed |
Indexed Field 可以进行搜索和排序。您还可以在 indexed Field 上运行 Solr 分析过程,此过程可修改内容以改进或更改结果。下一节提供了关于 Solr 的分析过程的更多信息。 |
stored |
stored Field 内容保存在索引中。这对于检索和醒目显示内容很有用,但对于实际搜索则不是必需的。例如,很多应用程序存储指向内容位置的指针而不是存储实际的文件内容。 |
Solr 的分析过程
您可以在对应用程序内容索引之前运行 Solr 的分析过程来修改这些内容。在 Solr 和 Lucene 中,Analyzer 包括一个 Tokenizer 和一个或多个 TokenFilter。Tokenizer 负责生成 Token,后者在多数情况下对应要索引的词。TokenFilter 从 Tokenizer 接受 Token 并且可以在索引之前修改或删除 Token。例如,Solr 的 WhitespaceTokenizer 根据空白断词,而它的 StopFilter 从搜索结果中删除公共词。其他类型的分析包括词干提取、同义词扩展和大小写折叠。如果应用程序要求以某种特殊方式进行分析,则 Solr 所拥有的一个或多个断词工具和筛选器可以满足您的要求。
您还可以在搜索操作期间对查询应用分析。一个总体规则是:应该对查询和要索引的文档运行相同的分析。不熟悉这些概念的用户常犯的一个错误就是:对文档标记进行词干提取,但不对查询标记进行词干提取,通常导致零搜索匹配。Solr 的 XML 配置使它可以轻易地使用简单声明创建 Analyzer,本文稍后会对此作出展示。
有关 Solr 和 Lucene 的分析工具,以及索引结构和其他功能的更多信息,请参阅 参考资料。
示例应用程序
以下各节将使用实际的示例应用程序向您介绍 Solr 的功能。该示例应用程序是一个基于 Web 的博客界面,您可以使用它来记录条目、给条目指派元数据,然后索引和搜索条目。在索引和搜索过程的每一步,您都可以选择显示发送到 Solr 的命令。
要查看示例应用程序,请将浏览器指向 http://localhost:8080/dw/index.jsp。如果一切设置正确(如 “设置 Solr” 描述的那样),则您可以看到一个题为 “Sample Solr Blog Search” 的简单用户界面,在标题的正下方有一些菜单项。当您浏览本文的两个部分时,将会了解到菜单中的所有主题。
索引操作
在 Solr 中,通过向部署在 servlet 容器中的 Solr Web 应用程序发送 HTTP 请求来启动索引和搜索。Solr 接受请求,确定要使用的适当 SolrRequestHandler,然后处理请求。通过 HTTP 以同样的方式返回响应。默认配置返回 Solr 的标准 XML 响应。您也可以配置 Solr 的备用响应格式。我将在本文的第 2 部分向您展示如何定制请求和响应处理。
索引就是接受输入(本例中是博客条目、关键字和其他元数据)并将它们传递给 Solr,从而在 HTTP Post XML 消息中进行索引的过程。您可以向 Solr 索引 servlet 传递四个不同的索引请求:
add/update 允许您向 Solr 添加文档或更新文档。直到提交后才能搜索到这些添加和更新。
commit 告诉 Solr,应该使上次提交以来所做的所有更改都可以搜索到。
optimize 重构 Lucene 的文件以改进搜索性能。索引完成后执行一下优化通常比较好。如果更新比较频繁,则应该在使用率较低的时候安排优化。一个索引无需优化也可以正常地运行。优化是一个耗时较多的过程。
delete 可以通过 id 或查询来指定。按 id 删除将删除具有指定 id 的文档;按查询删除将删除查询返回的所有文档。
一个索引示例
浏览到 http://localhost:8080/dw/index.jsp 可以查看索引过程的更多细节。首先为表单中的每个字段填入适当的条目并按 Submit 按钮。示例应用程序接受条目、创建 Solr 请求并显示请求以便在下一个屏幕上查看。清单 1 包含了一个 add 命令的例子,当您按下 Submit 按钮时向 Solr 发送这个命令。
清单 1. Solr add 命令样例
<add>
<doc>
<field name=”url”>http://localhost/myBolg/solr-rocks.html</field>
<field name=”title”>Solr Search is Simply Great</field>
<field name=”keywords”>solr,lucene,enterprise,search</field>
<field name=”creationDate”>2007-01-06T05:04:00.000Z</field>
<field name=”rating”>10</field>
<field name=”content”>Solr is a readlly great open source search server.It scales,It’s easy to configure and the Solr community is really supportive.</field>
<field name=”pulblished”>on</field>
</doc>
</add>
清单 1 的 <doc> 中的每个 field 条目告诉 Solr 应该将哪些 Field 添加到所创建文档的 Lucene 索引中。可以向 add 命令添加多个 <doc>。稍后我将解释 Solr 如何处理这些 field。现在,知道包含清单 1 中指定的七个 field 的文档将会被索引就足够了。
当您在 “Solr XML Command” 页面提交命令时,结果将发往 Solr 进行处理。HTTP POST 将命令发往在 http://localhost:8080/solr/update 运行的 Solr Update Servlet。如果一切进展顺利,则会随 <result status="0"/> 返回一个 XML 文档。Solr 使用相同的 URL 自动更新文档(示例应用程序中的 URL 是 Solr 识别文档以前是否被添加过所使用的惟一 id)。
实践索引
现在再添加几个文档并提交这些文档,以便在下一节中有文档可供搜索。一旦您熟悉 add 命令的语法后,就可以取消选择 Index 按钮旁边的 “Display XML...” 复选框,跳过 “Solr XML Command” 页面。本文附带的 样例文件 包含了一个很多这些示例中使用的索引版本。
您可以通过在 http://localhost:8080/dw/delete.jsp 页面输入文档的 URL、提交并查看命令,然后将命令提交到 Solr 来删除文档。有关索引和删除命令的更多信息,请参阅 参考资料 中的 “Solr Wiki” 参考。
搜索命令
添加文档后,就可以搜索这些文档了。Solr 接受 HTTP GET 和 HTTP POST 查询消息。收到的查询由相应的 SolrRequestHandler 进行处理。出于本文的讨论目的,我们将使用默认的 StandardRequestHandler。在本文的第 2 部分,我将向您展示如何为其他的 SolrRequestHandler 配置 Solr。
要查看搜索运行,返回到示例应用程序并浏览到 http://localhost:8080/dw/searching.jsp。此屏幕应该与索引屏幕非常类似,只是增加了几个搜索相关的选项。与索引类似,可以向各种输入字段中输入值,选择搜索参数并将查询提交给示例应用程序。示例应用程序醒目显示了一些 Solr 中更常见的查询参数。这些参数如下所示:
关于查询语法的一点注意
用于 StandardRequestHandler 的 Solr 查询语法与 Lucene QueryParser 支持的查询语法相同,只是前者加入了一些排序支持。示例应用程序对输入的值几乎没有进行验证,而且没有演示如查询增强、短语、范围筛选等功能,所有这些功能在 Solr 和 Lucene 中都有效。有关 Lucene QueryParser 的更多信息,请参阅 参考资料。在本文的第 2 部分,我将介绍管理界面中的一些有助于调试查询语法和结果的工具。
布尔运算符:默认情况下,用于合并搜索条目的布尔运算符是 OR。将它设为 AND 要求匹配的文档中出现所有的条目。
结果数目:指定返回的最大结果数目。
开始:结果集中开始的偏移量。此参数可用于分页。
醒目显示:醒目显示匹配文档字段的条目。参考 清单 2 底部的 <lst> 节点。醒目显示的条目标记为 <em>。
一旦输入和提交值后,博客应用程序就返回一个可以立即提交给 Solr 的查询字符串。提交字符串后,如果一切正常并且存在匹配文档,则 Solr 返回一个 XML 响应,其中包含了结果、醒目显示的信息和一些有关查询的元数据。清单 2 给出了一个示例搜索结果:
清单 2. 搜索结果示例
<response>
<lst name=”responseHeader”>
<int name=”status”>0</int>
<int name=”OTime”>6</int>
<lst name=”params”>
<str name=”rows”>10</str>
<str name=”start”>0</str>
<str name=”fl”>*,score</str>
<str name=”hl”>true</str>
<str name=”q”>content:”faceted bowsing”</str>
</lst>
</lst>
<result name=”response” numFound=”1” start=”0” maxScore=”1.058217”>
<doc>
<float name=”score”>1.058217</float>
<arr name=”all”>
<str>http://localhost/myBlog/solr-rocks-again.html</str>
<str>Solr is Great</str>
<str>solr,lucene,enterprise,search,greatness</str>
<str>Solr has some really great features,like faceted browsing and replication</str>
</arr>
<arr name=”content”>
<str>Solr has some really great features,like faceted browsing and replication</str>
</arr>
<date name=”creationDate”>2007-01-07T05:04:00.000Z</date>
<arr name=”keywords”>
<str>solr,lucene,enterprise,search,greatness</date>
</arr>
<int name=”ration”8</int>
<str name=”title”>Solr is Great</str>
<str name=”url”>http://localhost/myBlog/solr-rocks-again.html</str>
</doc>
</result>
<lst name=”highlighting”>
<lst name=”http://localhost/myBlog/solr-rocks-again.html”>
<arr name=”content”>
<str>Solr has some really great features,like <em>faceted</em>
<em>browsing</em>and replication</str>
</arr>
</lst>
<./lst>
</response>.
一条查询消息可以包含大量的参数,表 2 中对醒目显示的那些参数进行了描述。参阅 参考资料 中的 “Solr Wiki” 参考可以查看参数的完整清单。
表 2. 醒目显示的查询参数
参数 |
描述 |
示例 |
q |
Solr 中用来搜索的查询。有关该语法的完整描述,请参阅 参考资料 中的 “Lucene QueryParser Syntax”。可以通过追加一个分号和已索引且未进行断词的字段(下面会进行解释)的名称来包含排序信息。默认的排序是 score desc,指按记分降序排序。 |
q=myField:Java AND otherField:developerWorks; date asc 此查询搜索指定的两个字段并根据一个日期字段对结果进行排序。 |
start |
将初始偏移量指定到结果集中。可用于对结果进行分页。默认值为 0。 |
start=15 返回从第 15 个结果开始的结果。 |
rows |
返回文档的最大数目。默认值为 10。 |
rows=25 |
fq |
提供一个可选的筛选器查询。查询结果被限制为仅搜索筛选器查询返回的结果。筛选过的查询由 Solr 进行缓存。它们对提高复杂查询的速度非常有用。 |
任何可以用 q 参数传递的有效查询,排序信息除外。 |
hl |
当 hl=true 时,在查询响应中醒目显示片段。默认为 false。参看醒目显示参数上的 Solr Wiki 部分可以查看更多选项(见 参考资料)。 |
hl=true |
fl |
作为逗号分隔的列表指定文档结果中应返回的 Field 集。默认为 “*”,指所有的字段。“score” 指还应返回记分。 |
*,score |
层面浏览
最近,似乎所有流行的购物站点都添加了便利的条件列表,帮助用户根据制造商、价格和作者缩小搜索结果的范围。这些列表是层面浏览的结果,层面浏览是一种分类方式,用于对已经返回到有意义的、已证实存在的种类的结果进行分类。层面用于帮助用户缩小搜索结果的范围。
通过浏览到 http://localhost:8080/dw/facets.jsp 可以查看层面的运行。在这个页面上,您需要关注两种输入形式:
一个文本区域,您可以在其中输入查询,根据索引中的 all 字段发布此查询。将 all 字段看作一连串已索引的所有其他字段。(稍后将对此作详细介绍。)
一个可以用于分层面的字段下拉列表。此处并未列出所有的已索引字段,其原因马上就会得到解释。
输入一个查询并从下拉列表中选择一个层面字段,然后单击 Submit 与生成的查询一起传递给 Solr。博客应用程序解析结果并返回类似图 2 中的结果:
图 2. 层面浏览结果示例
在图 2 中,您可以在顶部看见所有非零值的层面计数,底部则是匹配所提交查询的两个搜索结果。单击示例中的层面链接提交原始查询,另外将 Facet 关键字作为一个新关键字。如果原始查询是 q=Solr 而层面字段是 keywords,并且单击图 2 中的 replication 链接,则新查询将会是 q=Solr AND keywords:replication。
运行层面不需要打开它或在 Solr 中进行配置,但是可能需要按照新的方式对应用程序内容进行索引。在已索引的字段中完成分层,层面对未进行断词的非小写词最为有效。(因此我并未包含 content 字段或 Facet Field 下拉列表中添加到文档的其他字段。)Facet 字段通常不需要存储,因为分层面的总体思想就是显示人类可读的值。
另外还要注意 Solr 没有在层面中创建类别;必须由应用程序自身在索引期间进行添加,正如在索引应用程序时给文档指派关键字一样。如果存在层面字段,Solr 就提供了查明这些层面及其计数的逻辑。
Solr 模式
迄今为止,我已向您介绍了 Solr 的特性,但没有实际解释如何配置这些特性。本文的剩余部分主要介绍配置,首先介绍 Solr 模式(schema.xml)然后向您展示它如何与 Solr 的特性相关联。
在编辑器中,最好是支持 XML 标记醒目显示的编辑器,打开位于 <INSTALL_DIR>/dw-solr/solr/conf 中的 schema.xml 文件。首先要注意的是大量的注释。如果您以前使用过开放源码的软件,您将会为模式文件中的文档及整个 Solr 中的文档欣喜不已。因为 schema.xml 的注释非常全面,所以我主要介绍文件的一些关键属性,具体细节可查阅文档。首先,注意 <schema> 标记中模式(dw-solr)的名称。Solr 为每个部署支持一个模式。将来它可能支持多个模式,但是目前只允许使用一个模式。(参阅 参考资料 中的 “Solr Wiki” 参考,了解如何简单地配置 Tomcat 和其他容器,以便为每个容器使用多个部署。)
模式可以组织为三部分:
类型
字段
其他声明
<types> 部分是一些常见的可重用定义,定义了 Solr(和 Lucene)如何处理 Field。在示例模式中,有 13 个字段类型,按名称从 string 到 text。<types> 部分顶部声明的字段类型(如 sint 和 boolean)用于存储 Solr 中的原始类型。在很大程度上,Lucene 只处理字符串,因此需要对整型、浮点型、日期型和双精度型进行特殊处理才能用于搜索。使用这些字段类型会警告 Solr 使用适当的特殊处理索引内容,不需要人为干涉。
类型
本文前面的内容中,我简要介绍了 Solr 的分析过程的基础。仔细观察一下 text 字段类型声明,您可以看见 Solr 管理分析的细节。清单 3 给出了 text 字段类型声明:
清单 3. 文本字段类型声明
<fieldtype name=”text” class=”solr.TextField” positionIncrementGap=”100”>
<analyzer type=”index”>
<tokenizer class=”solr.WhitespaceTokenizerFactory”/>
<filter class=”solr.StopFilterFactory”> ignoreCase=”true” words=”stopwords.txt”/>
<filter class=”solr.WordDelimiterFilterFactory” generateWordParts=”1”
generateNumbers=”1” catenateWords=”1” catenateNumberParts=”1” catennateWords=”1” catenateNumbers=”1” catenateAll=”0”/>
<filter class=”solr.LowerCaseFilterFactory”/>
<filter class=”sor.EnglishPorterFilterFactory” protected=”protwords.txt” />
<filter class=”solr.RemoveDuplicateTokenFilterFactory”/>
</analyzer>
<analyzer type=”query”>
<tokenizer class=”solr.WhitespaceTokenizerFactory”/>
<filter class=”solr.SynoymFilterFactory” synonyms=”synonyms.txt” ignoreCase=”true” expand=”true”/>
<filter class=”solr.WordDelimiterFilterFactory” gnerateWordParts=”1” generateNumberParts=”1” catenateWords=”0” cnetenateNumbers=”0” catenateAll=”0” />
<filter class=”solr.LowerCaseFilterFactory”/>.
<filter class=”solr.EnglishPorterFilterFactory” protected=”protwords.txt” />
<filter class=”solr.RemoveDuplicatesTokenFilterFactory” />
</analyzer>
<fieldtype>
类属性
在 Solr 模式中的很多实例中,类属性简化为类似 solr.TextField 的样子。这是 org.apache.solr.schema.TextField 的简略表达形式。在本文的第 2 部分中您将看到,类路径中扩展 org.apache.solr.schema.FieldType 类的任何有效类都可能使用到。
首先,注意我在清单 3 中声明了两个不同的 Analyzer。虽然 Analyzer 对于索引和搜索并非完全相同,但是它们只是在查询分析期间的同义词添加方面有所差别。词干提取、停止词删除以及相似的操作都被应用于标记,然后才进行索引和搜索,导致使用相同类型的标记。接下来,注意我首先声明断词工具,然后声明使用的筛选器。示例应用程序的 Solr 配置按以下步骤进行设置:
根据空白进行断词,然后删除所有的公共词(StopFilterFactory)
使用破折号处理特殊的大小写、大小写转换等等。(WordDelimiterFilterFactory);将所有条目处理为小写(LowerCaseFilterFactory)
使用 Porter Stemming 算法进行词干提取(EnglishPorterFilterFactory)
删除所有的副本(RemoveDuplicatesTokenFilterFactory)
示例分析加入了很多用于改进搜索结果的常见方法,但不应被看作分析文本的惟一方式。每个应用程序都可能有一些自己的分析需求,这个示例或者甚至是 Solr 或 Lucene 中的任何现有 Analyzer 都可能没有涉及相应需求。请参阅 参考资料 中的 “More Info On Solr Analysis”,了解关于分析的更多选项以及如何使用其他 Analyzer 的信息。
字段
继续介绍模式的 <fields> 部分,查看 Solr 如何处理索引和搜索期间使用的 8 个(实际上是 7 个,外加一个 all)字段。清单 4 中重复了这些字段:
清单 4. 博客应用程序的声明字段
<field name="url" type="string" indexed="true" stored="true"/>
<field name="title" type="text" indexed="true"
stored="true"/>
<field name="keywords" type="text_ws"
indexed="true" stored="true"
multiValued="true"
omitNorms="true"/>
<field name="creationDate" type="date"
indexed="true" stored="true"/>
<field name="rating" type="sint"
indexed="true" stored="true"/>
<field name="published" type="boolean"
indexed="true" stored="true"/>
<field name="content" type="text"
indexed="true" stored="true" />
<!-- catchall field,
containing many of the other searchable text fields
(implemented
via copyField further on in this schema) -->
<field name="all" type="text" indexed="true"
stored="true" multiValued="true"/>
理解字段类型后,您可以清晰地看见如何处理每个字段。例如,url 字段是一个经过索引、存储和未经分析的 string 字段。同时,使用 清单 3 中声明的 Analyzer 来分析 text 字段。all 字段如何处理?all 字段是一个 text 字段,如 title 或 content 一样,但是它包含了连接在一起的几个字段的内容,便于使用备用搜索机制(记住层面搜索使用的是 all 字段)。
对于字段的属性,在 表 1 中您已经了解了 indexed 和 stored 的意义。multiValued 属性是一个特殊的例子,指 Document 可以拥有一个相同名称添加了多次的 Field。比如在我们的示例中,可以多次添加 keywords。omitNorms 属性告诉 Solr(和 Lucene)不要存储规范。省略规范对于节省不影响记分的 Field 的内存非常有用,比如那些用于计算层面的字段。
在结束 <fields> 部分之前,简要介绍一下字段声明下方的 <dynamicField> 声明。动态字段是一些特殊类型的字段,可以在任何时候将这些字段添加到任何文档中,由字段声明定义它们的属性。动态字段和普通字段之间的关键区别在于前者不需要在 schema.xml 中提前声明名称。Solr 将名称声明中的 glob-like 模式应用到所有尚未声明的引入的字段名称,并根据其 <dynamicField> 声明定义的语义来处理字段。例如,<dynamicField name="*_i" type="sint" indexed="true" stored="true"/> 指一个 myRating_i 字段被 Solr 处理为 sint,尽管并未将其声明为字段。这种处理比较方便,例如,当需要用户定义待搜索内容的时候。
其他声明
最后,schema.xml 文件的最后部分包含了字段和查询相关的各种声明。最重要的声明是 <uniqueKey>url</uniqueKey>。该声明用于告诉 Solr 先前声明的 url 字段是用于确定何时添加或更新文档的惟一标识符。defaultSearchField 是查询条目没有前缀任何字段时 Solr 在查询中使用的 Field。示例所使用的查询与 q=title:Solr 类似。如果您输入 q=Solr,则应用默认搜索字段。最终结果与 q=all:Solr 相同,因为 all 是博客应用程序中的默认搜索字段。
<copyField> 机制让您能够创建 all 字段而无需将文档的所有内容手工添加到单独的字段。复制字段是以多种方式索引相同内容的简便方法。例如,如果您希望提供区分大小写的精确匹配和忽略大小写的匹配,则可以使用一个复制字段自动分析收到的内容。然后严格按照收到的内容进行索引(所有的字母使用小写)。
下一步:用于企业的 Solr
目前为止,您已经安装了 Solr 并学习了如何使用它在示例应用程序中索引和搜索文档。您也了解了 Solr 中层面浏览如何工作,并学习了如何使用 Solr 的 schema.xml 文件声明索引结构。本文附带的 示例应用程序 演示了这些功能和介绍了如何为 Solr 格式化命令。
在文章的第 2 部分,我将会介绍一些特性,它们将 Solr 从一个简单的搜索界面扩展成一个可供企业使用的搜索解决方案。您将学习 Solr 的管理界面和高级配置选项,以及性能相关的特性(如缓存、复制和日志记录)。我还将简要讨论扩展 Solr 以满足企业需求的一些方法。同时,充分利用示例应用程序,帮助自己熟悉 Solr 的基本功能。
配置和管理
本部分介绍了可用于监视和控制 Solr 功能性的诸多选项,首先来看看 Solr 的 Administration Start Page,该页可在 http://localhost:8080/solr/admin/ 找到。一旦找到了起始页,在继续之前,请务必花些时间熟悉一下上面的各种菜单选项。在起始页中,根据这些选项所提供的信息的不同对它们进行了分组:
Solr 给出了有关这种活动模式(请参见 第 1 部分)、配置以及当前部署的统计数据的详细信息。 App server 给出了容器的当前状态,包括 threading 信息以及所有 Java 系统属性的列表。
Make a Query 提供了调试查询所需的快捷界面以及到功能更加全面的查询界面的链接。
Assistance 提供了到外部资源的有用链接以便理解和解决使用 Solv 可能遇到的一些问题。
如下的章节详细介绍了这些菜单选项并重点突出了其中的管理特性。
要使用 Solr 的配置选项,可以单击初始页上的 CONFIG 链接,这会显示当前的 solrconfig.xml 文件。您可以在 示例应用程序 的 dw-solr/solr/conf 目录找到该文件。现在,让我们先来看看与索引和查询处理有关的一些常见的配置选项,而与 缓存、复制 和 扩展 Solr 有关的配置选项则留到后面的章节再介绍。
索引配置
mainIndex 标记段定义了控制 Solr 索引处理的低水平的 Lucene 因素。Lucene 基准发布(位于 Lucene 源代码的 contrib/benchmark 之下)包含了很多可用来对这些因素的更改效果进行基准测试的工具。此外,请参阅 参考资料 一节中的 “Solr 性能因素” 来了解与各种更改相关的性能权衡。表 1 概括了可控制 Solr 索引处理的各种因素:
表 1. 对性能因素进行索引
因素 |
描述 |
useCompoundFile |
通过将很多 Lucene 内部文件整合到单一一个文件来减少使用中的文件的数量。这可有助于减少 Solr 使用的文件句柄数目,代价是降低了性能。除非是应用程序用完了文件句柄,否则 false 的默认值应该就已经足够。 (设置为混合索引格式)writer.setUseCompoundFile(); |
mergeFactor |
决定低水平的 Lucene 段被合并的频率。较小的值(最小为 2)使用的内存较少但导致的索引时间也更慢。较大的值可使索引时间变快但会牺牲较多的内存。 |
maxBufferedDocs |
在合并内存中文档和创建新段之前,定义所需索引的最小文档数。段 是用来存储索引信息的 Lucene 文件。较大的值可使索引时间变快但会牺牲较多的内存。 |
maxMergeDocs |
控制可由 Solr 合并的 Document 的最大数。较小的值 (< 10,000) 最适合于具有大量更新的应用程序。 |
maxFieldLength |
对于给定的 Document,控制可添加到 Field 的最大条目数,进而截断该文档。如果文档可能会很大,就需要增加这个数值。然而,若将这个值设置得过高会导致内存不足错误。 |
unlockOnStartup |
unlockOnStartup 告知 Solr 忽略在多线程环境中用来保护索引的锁定机制。在某些情况下,索引可能会由于不正确的关机或其他错误而一直处于锁定,这就妨碍了添加和更新。将其设置为 true 可以禁用启动锁定,进而允许进行添加和更新。 |
查询处理配置
在 <query> 部分,有一些与 缓存 无关的特性,这一点您需要知道。首先,<maxBooleanClauses> 标记定义了可组合在一起形成一个查询的子句数量的上限。对于大多数应用程序而言,默认的 1024 就应该已经足够;然而,如果应用程序大量使用了通配符或范围查询,增加这个限值将能避免当值超出时,抛出 TooManyClausesException。
通配符和范围查询
通配符和范围查询是可自动扩展以包括所有可能匹配查询条件的条目的 Lucene 查询。通配符查询允许使用 * 和 ? 通配符运算符,而范围查询则要求匹配文档必须要在指定的范围之内。例如,若查找 b*,可能导致潜在的数千个不同项都组合进这个查询,进而会导致 TooManyClausesException。
接下来,若应用程序预期只会检索 Document 上少数几个 Field,那么可以将 <enableLazyFieldLoading> 属性设置为 true。懒散加载的一个常见场景大都发生在应用程序返回和显示一系列搜索结果的时候,用户常常会单击其中的一个来查看存储在此索引中的原始文档。初始的显示常常只需要显示很短的一段信息。若考虑到检索大型 Document 的代价,除非必需,否则就应该避免加载整个文档。
最后,<query> 部分负责定义与在 Solr 中发生的事件相关的几个选项。首先,作为一种介绍的方式,Solr(实际上是 Lucene)使用称为 Searcher 的 Java 类来处理 Query 实例。Searcher 将索引内容相关的数据加载到内存中。根据索引、CPU 以及可用内存的大小,这个过程可能需要较长的一段时间。要改进这一设计和显著提高性能,Solr 引入了一种 “温暖” 策略,即把这些新的 Searcher 联机以便为现场用户提供查询服务之前,先对它们进行 “热身”。<query> 部分中的 <listener> 选项定义 newSearcher 和 firstSearcher 事件,您可以使用这些事件来指定实例化新搜索程序或第一个搜索程序时应该执行哪些查询。如果应用程序期望请求某些特定的查询,那么在创建新搜索程序或第一个搜索程序时就应该反注释这些部分并执行适当的查询。
solrconfig.xml 文件的剩余部分,除 <admin> 之外,涵盖了与 缓存、复制 和 扩展或定制 Solr 有关的项目。admin 部分让您可以定制管理界面。有关配置 admin 节的更多信息,请参看 Solr Wiki 和 solrconfig.xml 文件中的注释。
监视、记录和统计数据
在 http://localhost:8080/solr/admin 的管理页,有几个菜单条目可以让 Solr 管理员监视 Solr 过程。表 2 给出了这些条目:
表 2. 用于监视、记录和统计数据的 Solr 管理选项
菜单名 |
Admin URL |
描述 |
Statistics |
http://localhost:8080/solr/admin/stats.jsp |
Statistics 管理页提供了与 Solr 性能相关的很多有用的统计数据。这些数据包括: 关于何时加载索引以及索引中有多少文档的信息。 关于用来服务查询的 SolrRequestHandler 的有用信息。 涵盖索引过程的数据,包括添加、删除、提交等的数量。 缓存实现和 hit/miss/eviction 信息。 |
Info |
http://localhost:8080/solr/admin/registry.jsp |
有关正在运行的 Solr 的版本以及在当前实现中进行查询、更新和缓存所使用的类的详细信息。此外,还包括文件存于 Solr subversion 存储库的何处的信息以及对该文件功能的一个简要描述。 |
Distribution |
http://localhost:8080/solr/admin/distributiondump.jsp |
显示与索引发布和复制有关的信息。更多信息,请参见 “发布和复制” 一节。 |
Ping |
http://localhost: 8080/solr/admin/ping |
向服务器发出 ping 请求,包括在 solrconfig.xml 文件的 admin 部分定义的请求。 |
Logging |
http:// localhost:8080/solr/admin/logging.jsp |
让您可以动态更改当前应用程序的日志记录等级。更改日志记录等级对于调试在执行过程中可能出现的问题非常有用。 |
Java properties |
http: //localhost:8080/solr/admin/get-properties.jsp |
显示当前系统正在使用的所有 Java 系统属性。Solr 支持通过命令行的系统属性替换。有关实现此特性的更多信息,请参见 solrconfig.xml 文件。 |
Thread dump |
http://localhost:8080/solr/admin/threaddump.jsp |
thread dump 选项显示了在 JVM 中运行的所有线程的堆栈跟踪信息。 |
调试此分析过程
经常地,当创建搜索实现时,您都会输入一个应该匹配特定文档的搜索,但它不会出现在结果中。在大多数情况下,故障都是由如下两个因素之一引起的:
查询分析和文档分析不匹配(虽然不推荐,但对文档的分析可能会与对查询的分析不同)。
Analyzer 正在修改不同于预期的一个或多个条目。
可以使用位于 http://localhost:8080/solr/admin/analysis.jsp 的 Solr 分析管理功能来深入调查这两个问题。Analysis 页可接受用于查询和文档的文本片段以及能确定文本该如何分析并返回正被修改的文本的逐步结果的 Field 名称。图 1 显示了分析句子 “The Carolina Hurricanes are the reigning Stanley Cup champions, at least for a few more weeks” 以及相关的查询 “Stanley Cup champions” 的部分结果,正如为示例应用程序 schema.xml 中指定的 content Field 分析的那样:
图 1. 对分析进行调试
分析屏幕显示了每个条件在被上述表结果 Tokenizer 或 TokenFilter 处理后的结果。比如,StopFilterFactory 会删除字 The、are 和 the。EnglishPorterFilterFactory 会将字 champions 提取为 champion,将 Hurricanes 提取为 hurrican。紫色的醒目显示表明在特定文档中查询条件在何处有匹配。
查询测试
admin 页的 Make a Query 部分提供了可输入查询并查看结果的搜索框。这个输入框接受 第 1 部分 中讨论到的 Lucene 查询解析器语法,而 Full Interface 链接则提供了对更多搜索特性的控制,比如返回的结果的数量、在结果集中应该包括哪些字段以及如何格式化输出。此外,该界面还可用来解释文档的计分以更好地理解哪些条件得到了匹配以及这些条件是如何得分的。要实现这一目的,可以查看 Debug: enable 选项并滚动到搜索结果的底端来查看相关解释。
智能缓存
智能缓存是让 Solr 得以成为引人瞩目的搜索服务器的一个关键性能特征。例如,Solr 在提供缓存服务之前可通过使用旧缓存中的信息来自热缓存,以便在服务于现有用户的同时改进性能。Solr 提供了四种不同的缓存类型,所有四种类型都可在 solrconfig.xml 的 <query> 部分中配置。表 3 根据在 solrconfig.xml 文件中所用的标记名列出了这些缓存类型:
表 3. Solr 缓存类型
缓存标记名 |
描述 |
能否自热? |
filterCache |
通过存储一个匹配给定查询的文档 id 的无序集,过滤器让 Solr 能够有效提高查询的性能。缓存这些过滤器意味着对 Solr 的重复调用可以导致结果集的快速查找。更常见的场景是缓存一个过滤器,然后再发起后续的精炼查询,这种查询能使用过滤器来限制要搜索的文档数。 |
可以 |
queryResultCache |
为查询、排序条件和所请求文档的数量缓存文档 id 的有序 集合。 |
可以 |
documentCache |
缓存 Lucene Document,使用内部 Lucene 文档 id(以便不与 Solr 惟一 id 相混淆)。由于 Lucene 的内部 Document id 可以因索引操作而更改,这种缓存不能自热。 |
不可以 |
Named caches |
命名缓存是用户定义的缓存,可被 Solr 定制插件 所使用。 |
可以,如果实现了 org.apache.solr.search.CacheRegenerator 的话。 |
每个缓存声明都接受最多四个属性:
class 是缓存实现的 Java 名。
size 是最大的条目数。
initialSize 是缓存的初始大小。
autoWarmCount 是取自旧缓存以预热新缓存的条目数。如果条目很多,就意味着缓存的 hit 会更多,只不过需要花更长的预热时间。
而对于所有缓存模式而言,在设置缓存参数时,都有必要在内存、CPU 和磁盘访问之间进行均衡。统计信息管理页 对于分析缓存的 hit-to-miss 比例以及微调缓存大小的统计数据都非常有用。而且,并非所有应用程序都会从缓存受益。实际上,一些应用程序反而会由于需要将某个永远也用不到的条目存储在缓存中这一额外步骤而受到影响。
发布和复制
对于收到大量查询的应用程序,单一一个 Solr 服务器恐怕不足以满足性能上的需求。因而,Solr 提供了跨多个服务器复制 Lucene 索引的机制,这些服务器必须是负载均衡的查询服务器的一部分。复制过程由 solrconfig.xml 文件启动的事件侦听程序和几个 shell 脚本(位于示例应用程序的 dw-solr/solr/bin)处理。
在复制架构中,一个 Solr 服务器充当主服务器,负责向一个或多个处理查询请求的从服务器提供索引的副本(称为 snapshot)。索引命令发送到主服务器,查询则发送到从服务器。主服务器可以手动创建快照,也可以通过配置 olrconfig.xml 的 <updateHandler> 部分(请参见清单 1)来触发接收到 commit 和/或 optimize 事件时的快照创建。无论是手动创建还是事件驱动的创建,都会在主服务器上调用 snapshooter 脚本,这会在名为 snapshot.yyyymmddHHMMSS(其中的 yyyymmddHHMMSS 代表实际创建快照的时间)的服务器上创建一个目录。之后,从服务器使用 rsync 来只复制 Lucene 索引中的那些已被更改的文件。
清单 1. 更新句柄侦听程序
|
清单 1 显示了在收到 commit 事件后,在主服务器上创建快照所需的配置。同样的配置也同样适用处理 optimize 事件。在这个示例配置中,在 commit 完成后,Solr 调用位于 solr/bin 目录的 snapshooter 脚本,传入指定的参数和环境变量。wait 实参告知 Solr 在继续之前先等待线程返回。有关执行 snapshooter 和其他配置脚本的详细信息,请参见 Solr 网站上的 “Solr Collection and Distribution Scripts” 文档(请参见 参考资料)。
在从服务器上,使用 snappuller shell 脚本从主服务器上检索快照。snappuller 从主服务器上检索了所需文件后,snapinstaller shell 脚本就可用来安装此快照并告知 Solr 有一个新的快照可用。根据快照创建的频率,最好是安排系统定期执行这些步骤。在主服务器上,rsync 守护程序在从服务器获得快照之前必须先行启动。rsyn 守护程序可用 rsyncd-enable shell 脚本启用,然后再用 rsyncd-start 命令实际启动。在从服务器上,snappuller-enable shell 脚本必须在调用 snappuller shell 脚本之前运行。 排除发布故障
虽然,我们已经竭尽全力地对索引更新的发布进行了优化,但还是有几个常见的场景会为 Solr 带来问题:
优化大型索引可能会非常耗时,而且应该在索引更新不是很频繁的情况下才进行。优化会导致多个 Lucene 索引文件合并成一个单一文件。这就意味者从服务器必须要复制整个索引。然而,这种方式的优化还是比在每个从服务器上进行优化要好很多。这些服务器可能与主服务器不同步,导致新副本再次被检索。
如果从主服务器中获取新快照的频率过高,则从服务器的性能可能会降低,这种降低源于使用 snappuller 复制更改的开销以及在安装新索引时的缓存预热。有关频繁的索引更新方面的性能均衡的详细信息,请参见 参考资料 中的 “Solr Performance Factors”。
最终,向从服务器添加、提交和获取更改的频繁程度完全取决于您自己的业务需求和硬件能力。仔细测试不同的场景将会帮助您定义何时需要创建快照以及何时需要从主服务器中获取这些快照。有关设置和执行 Solr 发布和复制的更多信息,请参看 参考资料 中的 “Solr Collection and Distribution” 文档。
定制 Solr
Solr 提供了几个插件点,您可以在这里添加定制功能来扩展或修改 Solr 处理。此外,由于 Solr 是开源的,所以如果需要不同的功能,您尽可以更改源代码。有两种方式可以向 Solr 添加插件:
打开 Solr WAR,在 WEB-INF/lib 目录下添加新的库,重新打包这些文件,然后将 WAR 文件部署到 servlet 容器。
将 JAR 放入 Solr Home lib 目录,然后启动 servlet 容器。这种方法使用了定制 ClassLoader 且有可能不适用于某些 servlet 容器。
接下来的几个章节突出介绍了可能希望扩展 Solr 的几个领域。
请求处理
若现有的功能不能满足业务需求,Solr 允许应用程序实现其自身的请求处理功能。比如,您可能想要支持您自己的查询语言或想要将 Solr 与您的用户配置文件相集成来提供个性化的效果。SolrRequestHandler 接口定义了实现定制请求处理所需的方法。实际上,除了 第 1 部分 所使用的那些默认的 “标准” 请求处理程序之外,Solr 还定义了其他几个请求处理程序:
默认的 StandardRequestHandler 使用 Lucene Query Parser 语法处理查询,添加了排序和层面浏览。
DisMaxRequestHandler 被设计用来通过更为简单的语法来跨多个 Field 进行搜索。它也支持排序(使用与标准处理程序稍有不同的语法)和层面浏览。
IndexInfoRequestHandler 可以检索有关索引的信息,比如索引中的文档数或 Field 数。
请求处理程序是由请求中的 qt 参数指定的。Solr servlet 使用参数值来查找给定的请求处理程序并将输入用于请求处理程序的处理。请求处理程序的声明和命名通过 solrconfig.xml 中的 <requestHandler> 标记指定。要添加其他的内容,只需实现定制的 SolrRequestHandler 线程安全的实例即可,将其添加到 上述 定义好的 Solr,并将其包括到 如前所述 的类路径中,之后就可以通过 HTTP GET 或 POST 方法开始向其发送请求了。
响应处理
与请求处理类似,也可以定制响应输出。必须要支持老式的搜索输出或必须要使用二进制或加密输出格式的应用程序可以通过实现 QueryResponseWriter 来输出所需的格式。然而,在添加您自己的 QueryResponseWriter 之前,需要先深入研究一下 Solr 所自带的实现,如表 4 所示:
表 4. Solr 的查询响应书写器
查询响应书写器 |
描述 |
XMLResponseWriter |
这个最为常用的响应格式以 XML 格式输出结果,如 第 1 部分 的博客应用程序所示。 |
XSLTResponseWriter |
XSLTResponseWriter 将 XMLResponseWriter 的输出转换成指定的 XSLT 格式。请求中的 tr 参数指定了要使用的 XSLT 转换的名称。指定的转换必须存在于 Solr Home 的 conf/xslt 目录。 |
JSONResponseWriter |
用 JavaScript Object Notation (JSON) 格式输出结果。JSON 是一种简单、人类可读的数据转换格式,而且非常易于机器解析。 |
RubyResponseWriter |
RubyResponseWriter 是对 JSON 格式的扩展以便在 Ruby 中安全地使用结果。若有兴趣将 Ruby 和 Solr 结合使用,可以参考 参考资料 中给出的到 acts_as_solr 和 Flare 的链接。 |
PythonResponseWriter |
对 JSON 输出格式的扩展以便在 Python eval 方法中安全地使用。 |
QueryResponseWriter 通过 <queryResponseWriter> 标记及其附属属性被添加至 Solr 的 solrconfig.xml 文件。响应的类型通过 wt 参数在请求中指定。默认值是 “标准”,即在 solrconfig.xml 中设定为 XMLResponseWriter。最后要强调的是,QueryResponseWriter 的实例必须提供用来创建响应的 write() 和 getContentType() 方法的线程安全的实现。
Analyzer、Tokenizer、TokenFilter 和 FieldType
借助新的 Analyzer、Tokenizer、TokenFilter 可以定制 Solr 的索引输出以提供新的分析功能。自身需要 Tokenizer 或 TokenFilter 的应用程序必须实现其自身的 TokenizerFactory 和 TokenFilterFactory,这两者使用 <tokenizer> 或 <filter> 标记(作为 <analyzer> 标记的一部分)在 schema.xml 中声明。如果您从之前的应用程序中已经获得了一个 Analyzer,那么就可以在 <analyzer> 标记的 class 属性中声明它并进行使用。您无需创建新的 Analyzer,除非是想要在其他 Lucene 应用程序中使用这些分析器 —— 在 schema.xml 中使用 <analyzer> 标记声明 Analyzer 真是容易呀!
如果应用程序有特定的数据需求,您可能需要添加一个 FieldType 来处理数据。比如,可以添加一个 FieldType 来处理来自旧的应用程序的二进制字段,在 Solr 中应该可以搜索到这个应用程序。只需使用 <fieldtype> 声明将 FieldType 添加到 schema.xml 并确保它在类路径中可用。
性能考虑
虽然 Solr 可以开箱即用,但还是有几个技巧可有助于让它更易于使用。与任何应用程序一样,仔细考虑您对数据访问的具体业务需求任重而道远。比如,添加的已索引 Field 越多,对内存的需求就越多、索引就越大、优化该索引所需的时间也越长。同样的,检索已存储的 Field 会因为太多的 I/O 处理而减慢服务器的速度。使用懒散字段加载或在他处存储大型内容可以为搜索请求释放 CPU 资源。
在搜索层面上,您应该考虑所支持的查询类型。很多应用程序都不需要 Lucene Query Parser 语法的全部,尤其是使用通配符和其他高级查询类型的情况下就更是如此。若能分析日志和确保常用的查询被缓存,将会非常有帮助。为一般的查询使用 Filter 对于减少服务器的负载也非常有用。与任何应用程序一样,全面地测试应用程序可确保 Solr 能够满足您的性能需求。有关 Lucene(和 Solr)性能的更多信息,请参阅 参考资料 中给出的 ApacheCon Europe 的 “Advanced Lucene” 幻灯片演示。
Solr 前景光明
构建于 Lucene 的速度和强大功能之上,Solr 本身就证明了它完全可以成为企业级的搜索解决方案。它吸引了大量活跃的社区使用者,这些使用者已经将它用到了各种大型的企业环境。Solr 也获得了开发人员的衷心支持,他们还一直在寻找提高它的途径。
在这个包含两部分的文章,您了解了 Solr,包括它开箱即用的索引和搜索功能以及用来配置其功能的 XML 模式。另外,您还浏览了让 Solr 得以成为企业架构的理想选择的配置和管理特性。最后,您还获悉了采用 Solr 时的性能考虑以及可用来扩展它的架构。
怎么跑起来!
1》 首先下载好solr,我用的是 solr1.3,下载地址:
windows版本
http://labs.xiaonei.com/apache-mirror/lucene/solr/1.3.0/apache-solr-1.3.0.zip
linux版本
http://labs.xiaonei.com/apache-mirror/lucene/solr/1.3.0/apache-solr-1.3.0.tgz
2》准备运行容器,我用的是tomcat6.0.20.如果是玩的话,也可以不用准备专门的容易,你只需解压好solr的下载包,找到 example文件夹,然后运行 start.jar。具体指令:java -jar start.jar做应用的时候,不建议采用该方式。该方式内部包含了jetty!
3》有关tomcat的使用,不是本文的重点,如果你有问题,请参见tomcat的使用。解压好tomcat,将solr包下面的dist文件夹中的apache-solr-1.3.0.war 拷贝到 tomcat的webapps,并且改名为 solr.war。
4》新建一个 solr-tomcat文件夹,我是把这个文件夹建立在C盘根目录,你可以采用同样方式,也可以不这样,放哪由你决定。建立好该文件夹以后,在把它下面在建立一个solr文件夹,把solr包里面的example\solr文件夹下面的所有文件放入到 solr里面。
5》最后一步 配置 solr.home,可以有三种方式。
1)基于当前路径的方式
这种情况需要在c:\solr-tomcat\目录下去启动tomcat,Solr查找./solr,因此在启动时候需要切换到c:\solr-tomcat\
2)基于环境变量
windows在环境变量中建立solr.home,值为c:\solr-tomcat
linux在当前用户的环境变量中(.bash_profile)或在catalina.sh中添加如下环境变量
export JAVA_OPTS="$JAVA_OPTS -Dsolr.solr.home=/opt/solr-tomcat/solr"
3)基于JNDI
在tomcat的conf文件夹建立Catalina文件夹,然后在Catalina文件夹中在建立localhost文件夹,在该文件夹下面建立solr.xml,其中内容:
Xml代码
<Context docBase="c:\tomcat\webapps\solr.war" debug="0" crossContext="true" >
<Environment name="solr/home" type="java.lang.String" value="c:/solr-tomcat/solr" override="true" />
</Context>
问题描述:
个人发现的一个问题,就是如果配置好JNDI的话,然后在tomcat的bin文件夹下面启动 tomcat的话,会在tomcat的bin下面建立solr文件夹,这个文件夹中主要存放的索引文件。本来这些东西应该放入c:\solr-tomcat\solr。如果你不想出现这种情况的话,请使用基于当前路径的方式。
6》打开浏览器,请看看能否访问该服务。如果能够访问,恭喜您跑起来了。
这篇文章,主要说的是 怎么在solr中加入中文分词,参考了一些文章,但是还是花了不少时间才搞出的。可能是大侠们太牛了,所以很多细节东西都没有写出来!但是要指出的是很多文章都是抄来抄去的!
入正题:
在上面的一个文章中,已经使solr跑起来了,在上面的基础上,加入中文分词。我用的是paoding分词器!
1》请下好paoding分词器,下载地址:
http://code.google.com/p/paoding/downloads/list,在这里要非常感谢paoding作者:qieqie
在使用paoding的时候要注意:paoding的dic位置,也就是字典的位置,有两种办法解决:
1) 在系统环境变量中加入PAODING_DIC_HOME这个变量,值为paoding压缩包下面的dic的解压位置。
2)paoding-analysis.jar里面有个paoding-dic-home.properties文件,指定dic也可,但是要重新编译这个jar包,我采用的后面一种办法,只要每次固定好dic位置,部署还不是很不方便,设置环境变量我比较反感
2》建立文件
Java代码
package com.yeedoo.slor.tokenizer;
import java.io.Reader;
import java.util.Map;
import net.paoding.analysis.analyzer.PaodingTokenizer;
import net.paoding.analysis.analyzer.TokenCollector;
import net.paoding.analysis.analyzer.impl.MaxWordLengthTokenCollector;
import net.paoding.analysis.analyzer.impl.MostWordsTokenCollector;
import net.paoding.analysis.knife.PaodingMaker;
import org.apache.lucene.analysis.TokenStream;
import org.apache.solr.analysis.BaseTokenizerFactory;
public class ChineseTokenizerFactory extends BaseTokenizerFactory {
// 最多切分 默认模式
public static final String MOST_WORDS_MODE = "most-words";
// 按最大切分
public static final String MAX_WORD_LENGTH_MODE = "max-word-length";
private String mode = null;
public void setMode(String mode) {
if (mode == null || MOST_WORDS_MODE.equalsIgnoreCase(mode) || "default".equalsIgnoreCase(mode)) {
this.mode = MOST_WORDS_MODE;
} else if (MAX_WORD_LENGTH_MODE.equalsIgnoreCase(mode)) {
this.mode = MAX_WORD_LENGTH_MODE;
} else {
throw new IllegalArgumentException("不合法的分析器Mode参数设置:" + mode);
}
}
public void init(Map<String,String> args) {
super.init(args);
setMode(args.get("mode").toString());
}
public TokenStream create(Reader input) {
return new PaodingTokenizer(input, PaodingMaker.make(), createTokenCollector());
}
private TokenCollector createTokenCollector() {
if (MOST_WORDS_MODE.equals(mode))
return new MostWordsTokenCollector();
if (MAX_WORD_LENGTH_MODE.equals(mode))
return new MaxWordLengthTokenCollector();
throw new Error("never happened");
}
}
运行solr是个很简单的事,如何让solr高效运行你的项目,这个就不容易了。要考虑的因素太多。这里很重要一个就是对solr的配置要了解。懂得配置文件每个配置项的含义,这样操作起来就会如鱼得水!
在solr里面主要的就是solr的主目录下面的schema.xml,solrConfig.xml(c:\solr-tomcat\solr\conf\)。
我们首先来说说这个schema.xml。
schema.xml,这个相当于数据表配置文件,它定义了加入索引的数据的数据类型的。主要包括types、fields和其他的一些缺省设置。
1》首先需要在types结点内定义一个FieldType子结点,包括name,class,positionIncrementGap等等一些参数,name就是这个FieldType的名称,class指向org.apache.solr.analysis包里面对应的class名称,用来定义这个类型的行为。在FieldType定义的时候最重要的就是定义这个类型的数据在建立索引和进行查询的时候要使用的分析器analyzer,包括分词和过滤。在例子中text这个FieldType在定义的时候,在index的analyzer中使用 solr.WhitespaceTokenizerFactory这个分词包,就是空格分词,然后使用 solr.StopFilterFactory,solr.WordDelimiterFilterFactory,solr.LowerCaseFilterFactory,solr.EnglishPorterFilterFactory,solr.RemoveDuplicatesTokenFilterFactory 这几个过滤器。在向索引库中添加text类型的索引的时候,Solr会首先用空格进行分词,然后把分词结果依次使用指定的过滤器进行过滤,最后剩下的结果才会加入到索引库中以备查询。Solr的analysis包并没有带支持中文的包,在第二篇文章中详细讲了怎样添加paoding中文分词器,详情请参见http://lianj-lee.javaeye.com/blog/424474
2》接下来的工作就是在fields结点内定义具体的字段(类似数据库中的字段),就是filed,filed定义包括name,type(为之前定义过的各种FieldType),indexed(是否被索引),stored(是否被储存),multiValued(是否有多个值)等等。
例:
Xml代码
<fields>
<field name="id" type="integer" indexed="true" stored="true" required="true" />
<field name="name" type="text" indexed="true" stored="true" />
<field name="summary" type="text" indexed="true" stored="true" />
<field name="author" type="string" indexed="true" stored="true" />
<field name="date" type="date" indexed="false" stored="true" />
<field name="content" type="text" indexed="true" stored="false" />
<field name="keywords" type="keyword_text" indexed="true" stored="false" multiValued="true" />
<field name="all" type="text" indexed="true" stored="false" multiValued="true"/>
</fields>
field的定义相当重要,有几个技巧需注意一下,对可能存在多值得字段尽量设置multiValued属性为true,避免建索引是抛出错误;如果不需要存储相应字段值,尽量将stored属性设为false。
3》建议建立了一个拷贝字段,将所有的全文字段复制到一个字段中,以便进行统一的检索:
Xml代码
<field name="all" type="text" indexed="true" stored="false" multiValued="true"/>
并在拷贝字段结点处完成拷贝设置:
Xml代码 <copyField source="name" dest="all"/>
<copyField source="summary" dest="all"/>
4》除此之外,还可以定义动态字段,所谓动态字段就是不用指定具体的名称,只要定义字段名称的规则,例如定义一个 dynamicField,name 为*_i,定义它的type为text,那么在使用这个字段的时候,任何以_i结尾的字段都被认为是符合这个定义的,例如:name_i,gender_i,school_i等。
Solr Multicore 是 solr 1.3 的新特性。其目的一个solr实例,可以有多个搜索应用。
下面着手来将solr给出的一个example跑出来,在《利用SOLR搭建企业搜索平台 之一(运行solr)》这篇文章里面已经讲了怎样来运行solr,这篇文章是基于《利用SOLR搭建企业搜索平台 之一(运行solr)》,有不明白的请参见http://lianj-lee.javaeye.com/blog/424383
1》找到solr下载包中的example文件夹,在它的下面有个multicore文件夹,将这个文件夹下面的所有东西copy到 c:\solr-tomcat\solr下面。
注意:有一个 solr.xml(这只是默认文件,当然也可以指定别的文件),如:
Xml代码
<?xml version="1.0" encoding="UTF-8" ?>
<solr persistent="false">
<cores adminPath="/admin/cores">
<core name="core0" instanceDir="core0" />
<core name="core1" instanceDir="core1" />
</cores>
</solr>
这个文件是告诉solr应该加载哪些core,cores里有 core0,core1。core0(可以类比以前的solr.home)/conf目录下有schema.xml与solrconfig.xml,可以把实际应用的复制过来。现示例就用官方的了。
2》启动tomcat,访问应用,就可以看到有 Admin core0 和 Admin core1
3》采用上面的默认solr.xml,索引文件将存放在同一个目录下面,在这里将存放在C:\solr-tomcat\solr\data,如果你想更改目录,或者两个应用存放在不同的目录,请参见下面的xml。
Xml代码
<core name="core0" instanceDir="core0">
<property name="dataDir" value="/data/core0" />
</core>
给core添加子元素 property,property的两个属性就不说了,一看就能明白!
solr.core.name -- The core's name as defined in solr.xml
solr.core.instanceDir -- The core's instance directory (i.e. the directory under which that core's conf/ and data/ directory are located)
solr.core.dataDir -- The core's data directory (i.e. the directory under which that core's index directory are located)
solr.core.configName -- The name of the core's config file (solrconfig.xml by default)
solr.core.schemaName -- The name of the core's schema file (schema.xml by default)
4》solr.xml具体含义:
1)solr
The <solr> tag accepts two attributes:
persistent - By default, should runtime core manipulation be saved in solr.xml so that it is available after a restart.
sharedLib - Path to a directory containing .jar files that are added to the classpath of every core. The path is relative to solr.home (where solr.xml sits)
2)cores
The <cores> tag accepts two attribute:
adminPath - Relative path to access the CoreAdminHandler for dynamic core manipulation. For example, adminPath="/admin/cores" configures access via http://localhost:8983/solr/admin/cores. If this attribute is not specified, dynamic manipulation is unavailable.
3)core
The <core> tag accepts two attributes:
name - The registered core name. This will be how the core is accessed.
instanceDir - The solr.home directory for a given core.
dataDir - The data directory for a given core. The default is <instanceDir>/data . It can take an absolute path or a relative path w.r.t instanceDir . Solr1.4
4)property
The <property> tag accepts two attributes:
name - The name of the property
value - The value of the property
相信很多人,在准备提交数据让solr建立索引的那刻,很纳闷,尽管看了不少网上的一些文章,但是我想依然还是有不少不理解的地方。
比如提交一个xml,采用post方式,尽管有些文章说了可以采用httpclient。但是我那个时候,还不是很理解,当然现在看来其实也没有什么了。但是对于一个刚入门solr的初学者,我想讲讲关于solr1.3的 solrj( sorlr J 目前使用二进制的格式作为默认的格式。对于solr1.2的用户通过显示的设置才能使用XML格式。)!
先上一个例子:
Java代码
public static final String SOLR_URL = "http://localhost/solr/core0";
public static void commit() {
Date date = new Date();
SolrServer solr = null;
try {
solr = new CommonsHttpSolrServer(SOLR_URL);
} catch (MalformedURLException e1) {
e1.printStackTrace();
}
for (int i = 0; i < 10000; i++) {
SolrInputDocument sid = new SolrInputDocument();
sid.addField("id", i);
sid.addField("name", "struts+hibernate+spring 开发大全" + i);
sid.addField("summary", "三种框架的综合应用" + i);
sid.addField("author", "李良杰" + i);
sid.addField("date", new Date());
sid.addField("content", "高级应用类书籍" + i);
sid.addField("keywords", "SSH" + i);
try {
solr.add(sid);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (SolrServerException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(i);
if (i == 999)
System.out.println((new Date().getTime() - date.getTime()) / 60000 + "分钟");
}
try {
solr.commit();
} catch (SolrServerException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
在做solr查询的时候,solr提供了很多参数来扩展它自身的强大功能!以下是使用频率最高的一些参数!
具体请看:
1.常用
q - 查询字符串,必须的。查询语句(类似SQL) 相关详细的操作还需lucene 的query 语法
fl - 指定返回那些字段内容,用逗号或空格分隔多个。
start - 返回第一条记录在完整找到结果中的偏移位置,0开始,一般分页用。
rows - 指定返回结果最多有多少条记录,配合start来实现分页。
sort - 排序,格式:sort=<field name>+<desc|asc>[,<field name>+<desc|asc>]… 。示例:(score desc, price asc)表示先 “score” 降序, 再 “price” 升序,默认是相关性降序。
wt - (writer type)指定输出格式,可以有 xml, json, php, phps, 后面 solr 1.3增加的,要用通知我们,因为默认没有打开。
fl表示索引显示那些field(*表示所有field, score 是solr 的一个匹配热度)
q.op 表示q 中 查询语句的 各条件的逻辑操作 AND(与) OR(或)
hl 是否高亮
hl.fl 高亮field
hl.snippets 不太清楚(反正是设置高亮3就可以了)
hl.simple.pre 高亮前面的格式
hl.simple.post 高亮后面的格式
facet 是否启动统计
facet.field 统计field
fq - (filter query)过虑查询,作用:在q查询符合结果中同时是fq查询符合的,例如:q=mm&fq=date_time:[20081001 TO 20091031],找关键字mm,并且date_time是20081001到20091031之间的。官方文档:http://wiki.apache.org/solr/CommonQueryParameters#head-6522ef80f22d0e50d2f12ec487758577506d6002
2.不常用
q.op - 覆盖schema.xml的defaultOperator(有空格时用"AND"还是用"OR"操作逻辑),一般默认指定
df - 默认的查询字段,一般默认指定
qt - (query type)指定那个类型来处理查询请求,一般不用指定,默认是standard。
3.其它
indent - 返回的结果是否缩进,默认关闭,用 indent=true|on 开启,一般调试json,php,phps,ruby输出才有必要用这个参数。
version - 查询语法的版本,建议不使用它,由服务器指定默认值。
1》solr 做索引时报 Lock obtain timed out: SingleInstanceLock: write.lock
有个频繁做索引的应用,它同时也对外提供搜索服务。大部分是 solr 1.3 的默认配置。solr 做索引,有时候报:
Xml代码
2009-7-13 9:48:06 org.apache.solr.common.SolrException log
严重: org.apache.lucene.store.LockObtainFailedException: Lock obtain timed out: SingleInstanceLock: write.lock
at org.apache.lucene.store.Lock.obtain(Lock.java:85)
at org.apache.lucene.index.IndexWriter.init(IndexWriter.java:1140)
at org.apache.lucene.index.IndexWriter.<init>(IndexWriter.java:938)
at org.apache.solr.update.SolrIndexWriter.<init>(SolrIndexWriter.java:116)
...
是写锁取不到。但重启 solr 又可以正常做,主要是运行时间长了就报这个错了。还是看下配置吧。
看到 solr 1.3 默认的配置是:
Xml代码
<indexDefaults>
<!-- ... -->
<!--
This option specifies which Lucene LockFactory implementation to use.
single = SingleInstanceLockFactory - suggested for a read-only index
or when there is no possibility of another process trying
to modify the index.
native = NativeFSLockFactory
simple = SimpleFSLockFactory
(For backwards compatibility with Solr 1.2, 'simple' is the default
if not specified.)
-->
<lockType>single</lockType>
</indexDefaults>
引用Bory.Chanhttp://blog.chenlb.com/2009/04/apply-solr-collapsing-patch-remove-duplicate-result.html
打上SOLR-236_collapsing.patch补丁,实现 solr 搜索结果折叠、除去重复的搜索结果,可以实现类似google搜索结果的“站内的其它相关信息 ”。solr collapsing patch 是用 hash 某个字段来实现折叠重复结果的。下面我演示下应用这个补丁并搜索试用下。
其实 solr 上已经有了这功能的实现:solr 1.3 collapse patch, 请看:https://issues.apache.org/jira/browse/SOLR-236,我这里下载是了新的:https://issues.apache.org/jira/secure/attachment/12403590/SOLR-236_collapsing.patch。
下载好后就需要打上补丁了,先准备一份源码在D:/apache-solr-1.3.0目录下。没有可以去下载:http: //archive.apache.org/dist/lucene/solr/1.3.0/apache-solr-1.3.0.zip。把SOLR- 236_collapsing.patch文件放在D:/apache-solr-1.3.0目录下, 打补丁有我知道的有两种:用linux工具 patch(windows 下有 cygwin);用 ant 的 patch。
windows cygwin 的 patch:
Html代码
D:\apache-solr-1.3.0>patch -p0 < SOLR-236_collapsing.patch
patching file src/test/org/apache/solr/search/TestDocSet.java
patching file src/java/org/apache/solr/search/CollapseFilter.java
patching file src/java/org/apache/solr/search/DocSet.java
patching file src/java/org/apache/solr/search/NegatedDocSet.java
patching file src/java/org/apache/solr/search/SolrIndexSearcher.java
patching file src/java/org/apache/solr/common/params/CollapseParams.java
patching file src/java/org/apache/solr/handler/component/CollapseComponent.java
solr的一些查询语法
1. 首先假设我的数据里fields有:name, tel, address 预设的搜寻是name这个字段, 如果要搜寻的数据刚好就是 name 这个字段,就不需要指定搜寻字段名称.
2. 查询规则:
如欲查询特定字段(非预设字段),请在查询词前加上该字段名称加 “:” (不包含”号) 符号,
例如: address:北京市海淀区上地软件园 tel:88xxxxx1
1>. q代表query input
2>. version代表solr版本(建议不要变动此变量)
3>. start代表显示结果从哪一笔结果资料开始,预设为0代表第一笔, rows是说要显示几笔数据,预设为10笔
(因为有时查询结果可能有几百笔,但不需要显示所有结果,所以预设是从第一笔开始到第十笔)
所以若要显示第10到30笔就改为:
http: //localhost:8080/solr/select/?indent=on&version=2.2&q=address:北京市海淀区上地软件园+tel:88xxxxx1&version=2.2&start=10&rows= 20&indent=on
(indent代表输出的xml要不要缩行.预设为开启 on)
3. 另外,要限定输出结果的内容可用 “fl=” 加上你要的字段名称,如以下这个范例:
http: //localhost:8080/solr/select/?indent=on&version=2.2&q=text:北京+ OR+text:亿度&start=0&rows=10&fl=name,address,tel
在fl=之后加上了name,adress,tel
所以结果会如下:
<result
name=”response” numFound=”1340″ start=”0″>
<doc>
<str name=”name”>北京亿度</str>
<str name=”address”>北京市海淀区上地软件园</str>
<str name=”tel”>88xxxxxx1</str>
</doc>
<doc>
<str name=”name”>北京亿度</str>
<str name=”address”/>
<str name=”tel”>88xxxxxx1</str>
</doc>
</result>
5. 查询 name 或 address:直接输入查询词, 如: 亿度
送出的内容即为:
name:亿度 AND address:海淀
6. 若要搜寻联集结果,请在词与词间空格或加上大写 “OR” (不包含”号).
例如: text:海淀 OR text:亿度
text:海淀 OR 亿度
或
海淀 亿度
或
name:亿度 OR tel:88xxxxxx1
或
name:亿度 tel:88xxxxxx1
5. 若要搜寻交集结果,请在词与词间加上大写 “AND” 或 “+” (不包含”号).
例如: text:海淀 AND 亿度
或
+text:海淀 +text:亿度
或
name:亿度 AND tel:88xxxxxx1
或
name: ( +亿度 +海淀)
6. 排除查询
在要排除的词前加上 “-” (不包含”号) 号
例如: 海淀 -亿度
搜寻结果不会有包含亿度的词的结果在内
7. Group 搜寻
使用 “()” 来包含一个group
如希望搜寻在店名字段内同时有 “台北”(不包含”号) 及 “火车站”(不包含”号)
8. 增加权重: 如要搜寻 “北京 加油站”(不包含”号) 但因为回传太多笔资料内有 “中华”(不包含”号) 或 “加油站”(不包含”号) 的结果,
所以想要把有包含 “加油站”(不包含”号)的数据往前排,可使用 “^”(不包含”号)符号在后面加上愈增加的权重数,
像是 “2″,则可以这样做:
北京 加油站^2
会同时搜寻含有北京或加油站的结果,并把加油站这个词加权所以搜寻时会先判断加油站这一个词在
搜寻结果中的比重,甚至假设一笔数据内加油站出现过两次以上的就更加会有优先权.
查询时在查询词后加上 “^” (不包含”号) 再加上权重分数
例如: 亿度 AND “北京”^2
或
亿度^2 OR 北京
9. Wildcard 搜寻使用 “*” 符号; 如果输入 “中国*银” (不包含”号), 结果会有中国信托商业银行, 中国输出入银行图书阅览室, 中国商银证券
中国及银之间可夹任何长短字词.
solr不可谓是个好东西啊,越往下挖掘,他的各种功能逐渐的展现在我的面前,对于solr的架构人员,不得不令人佩服啊。
写程序 可以将数据读出100条,如果你的内存够大,可以是1000条甚至更多,然后放入Collection中,批量提交至solr。或者读取数据写入xml文件中,再将该文件提交到solr等等。但是,在我看到那一篇文章的时候,原来还有这么巧妙的招。
一.首先准备好solr的dataimport功能需要的东西,在solr的下载包中。分别在:
1》Solr-1.3.0\dist\apache-solr-dataimporthandler-1.3.0.jar
2》E:\education\search\Solr-1.3.0\example\example-DIH\solr\
3》你是哪种数据库,提供该数据库的jdbc驱动。
二.先把E:\education\search\Solr-1.3.0 \example\example-DIH\solr\下面的东西拷贝到solr的HOME目录,然后删除rss,这个是另外一个功能是导入rss订阅信息到solr中,确实很强,这都想到了。将jar文件,实际就两个拷贝到tomcat的webapps下面的solr的WEB-INF的lib文件夹下面。
三.更改solr Home目录下的conf/solrconfig.xml,其实就是提交一个solrRequestHandler,代码如下:
Xml代码
<requestHandler name="/dataimport" class="org.apache.solr.handler.dataimport.DataImportHandler">
<lst name="defaults">
<str name="config">C:\solr-tomcat\solr\db\conf\db-data-config.xml</str>
</lst>
</requestHandler>
在经过使用了庖丁以后,这里说说怎么将目前很火很流行的IK集成进SOLR,其实方法真的很简单,比paoding方便不少。
1》请先去IK下载地址,主要就是一个IKAnalyzer3.1.1Stable.jar。
Java代码 package com.yeedoo.slor.tokenizer;
import java.io.Reader;
import org.apache.lucene.analysis.TokenStream;
import org.apache.solr.analysis.BaseTokenizerFactory;
import org.wltea.analyzer.lucene.IKAnalyzer;
public class ChineseTokenizerFactory extends BaseTokenizerFactory {
@Override
public TokenStream create(Reader reader) {
return new IKAnalyzer().tokenStream("text", reader);
}
}
从代码就可以看得出来真的很方便!将它打包放入solr.war中同时还有IK的jar包。如果你不想打包,请去附件下载已经打好的包。
2》配置文件
Xml代码 <fieldType name="text" class="solr.TextField" positionIncrementGap="100">
<analyzer type="index">
<tokenizer class="com.yeedoo.slor.tokenizer.ChineseTokenizerFactory" />
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" enablePositionIncrements="true" />
<filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" catenateNumbers="1" catenateAll="0" splitOnCaseChange="1" />
<filter class="solr.LowerCaseFilterFactory" />
<filter class="solr.EnglishPorterFilterFactory" protected="protwords.txt" />
<filter class="solr.RemoveDuplicatesTokenFilterFactory" />
</analyzer>
<analyzer type="query">
<tokenizer class="com.yeedoo.slor.tokenizer.ChineseTokenizerFactory" />
<filter class="solr.SynonymFilterFactory" synonyms="synonyms.txt" ignoreCase="true" expand="true" />
<filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" />
<filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="0" catenateNumbers="0" catenateAll="0" splitOnCaseChange="1" />
<filter class="solr.LowerCaseFilterFactory" />
<filter class="solr.EnglishPorterFilterFactory" protected="protwords.txt" />
<filter class="solr.RemoveDuplicatesTokenFilterFactory" />
</analyzer>
</fieldType>
3》如果在启动tomcat没有报错的情况下,可以去solr的admin那里尝试看看该分词器的效果,效果如下图:
Lucene 和 Solr 专家 Grant Ingersoll 将逐步向您介绍空间搜索的基础知识,并演示如何利用这些功能来增强您的下一个位置感知应用程序。
不管是通过支持 GPS 的智能手机查找最近的咖啡馆,还是通过社交站点查找附近的朋友,或是查看特定城市中运输某种商品的所有货车,越来越多的人和企业都使用位置感知的搜索服务。创建位置感知搜索服务通常属于昂贵的专用解决方案的一部分,并且一般由地理空间专家完成。不过,很流行的开源搜索库 Apache Lucene 和强大的 Lucene 搜索服务器 Apache Solr 最近添加了空间位置功能。
地理位置在空间搜索中至关重要!地理位置不仅在地产中至尊为王,将其用在搜索中还能帮助位于特定位置的用户快速找到有用的信息。例如,如果您是企业名录提供商(比如一个 “黄页” 站点),当用户需要找一位水管维修员时,该站点必须返回在用户住所附近的维修员。如果您运营的是一个旅游站点,那么您必须让旅游者能够搜索到他们所在的位置附近的名胜,从而帮助他们丰富旅游行程。如果您要构建一个社交网络站点,那么最好使用位置信息来帮助用户与朋友联系。位置感知设备(比如汽车导航系统和支持 GPS 的摄像机)和大量免费地图数据的普及为构建能够为终端用户搜索高级结果的 Geographical Information Systems (GIS) 提供了各种机会。
空间信息还可以被利用到搜索领域之外,但在本文中我将主要关注如何通过 Apache Lucene 和 Apache Solr 利用空间信息来改进搜索应用程序。为什么要使用搜索引擎?并不是因为它是许多很好(甚至免费)的 GIS 工具中的必要组成部分。不过,将应用程序构建在搜索引擎的基础上能够提供几个强大的功能,这是其他传统途径无法实现的。搜索系统在合并结构化和非结构化方面非常强劲,这允许用户输入自由形式的查询,比如在搜索免费文本的描述和标题的同时根据地理位置数据限制或修改结果。例如,旅游站点可以实现这样一个特性,它让用户能够在一秒之内找到马萨诸塞州波士顿市的所有 24 小时提供服务并且配有舒适床具的四星级宾馆。有些搜索系统(比如 Apache Solr)还提供对结果集进行分类、突出显示和拼写检查的功能,从而让应用程序能够帮助用户高效地查找所需的结果。
我首先简单介绍 Lucene 的一些关键概念,深入的细节留给读者自己探索。接下来,我将介绍一些基础的地理空间搜索概念。GIS 是一个广泛的领域,本文难以对其进行详尽的描述,因此我仅关注一些查找服务、人和其他日常事项所需的基础概念。本文的末尾是关于使用 Lucene 和 Solr 索引和搜索空间信息的方法的讨论。我将通过一个真实但很简单的例子来阐述这些概念,并且使用来自 OpenStreetMap (OSM) 项目的数据。
回顾关键的 Lucene 概念
Apache Lucene 是一个基于 Java™ 的高性能搜索库。Apache Solr 是一个使用 Lucene 通过 HTTP 来提供搜索、分类等功能的搜索服务器。它们都使用价格适中的 Apache Software License。
从本质上看,Solr 和 Lucene 都将内容表示为文档。文档由一个或多个字段 和一个表明文档的重要性的可选增强(boost)值 组成。字段由需要索引和储存的实际内容、告诉 Lucene 如何处理该内容的元数据和表明该字段的重要性的增强值组成。由您决定以何种方式将内容表示为文档和字段,这取决于您希望怎样搜索或访问文档中的信息。在每个内容单元中,您可以使用一对一的关系,也可以使用一对多的关系。例如,我可以选择用一个包含几个字段(比如 title、keywords 和 body)的文档来表示一个 Web 页面。如果是一本书,我则选择将它的每一页表示为一个独立的文档。稍后您将看到,这一区分在为搜索编码空间数据时非常重要。可以为字段中的内容建立索引,或者原样储存供应用程序使用。如果为内容建立了索引,应用程序就可以使用它。还可以分析建立了索引的内容来生成词汇(通常称为令牌)。词汇是在搜索过程中查找和使用的基础。词汇通常是一个词,但这不是必要的。
在查询方面,Lucene 和 Solr 为表达用户查询(从基础的关键字查询到短语和通配符查询)提供丰富的功能。Lucene 和 Solr 还通过应用一个或多个对空间搜索非常重要的过滤器来提供限制空间的能力。范围查询 和范围过滤器 是限制空间的关键机制。在范围查询(或过滤器)中,用户声明需要将所有搜索到的文档限制在使用自然排序的两个值之间。例如,通常使用范围查询来查找发生在过去一年或上一个月的所有文档。在处理过程中,Lucene 必须枚举文档中的词汇以识别在范围之内的所有文档。如我在稍后展示的一样,正确地设置范围查询是提升空间搜索应用程序的查询性能的关键因素之一。
Lucene 和 Solr 还提供函数查询 的概念,它允许您使用字段的值(比如经度和纬度)作为记录机制的一部分,而不是仅仅使用组成主要的记录机制的内部数据集合。该功能在后文我演示使用 Solr 的一些基于距离的函数时用到。
地理空间搜索概念
在构建空间搜索应用程序时,最重要的是识别需要添加到应用程序中的空间数据。这些数据通常以某些地理编码的形式出现,比如纬度、经度和海拔,或以邮政编码或街道地址的形式出现。编码系统的格式越规范,它在您的系统中的使用就越容易。例如,民歌 “Over the River and Through the Woods”(其中有这样的歌词:“to Grandmother's house we go”)就将很多空间信息编码到了歌词中。但这些信息在 GIS 系统中就没有多大用处,因为我们不知道小河和森林的位置。该信息与到外婆家的详细方向(包含出发地址和到达地址的)相比,您将了解到为什么正确编码地址如此重要。(有趣的是,能够提取和编码更常用的方向和地理实体 —— 比如渡过小河 或在棕色房子附近 —— 并根据它们进行推断的系统也是非常有用的,但这不属于本文的讨论范围)。
除了用于识别地理位置的原始地理编码数据之外,许多 GIS 系统还可以添加与实际位置相关的信息。例如,导航系统可以使用在地图上按顺序列出的一系列位置来创建一条从 A 点到 B 点的路线。或者气象学家可以将降雨或恶劣的天气数据添加到特定区域的地图上,从而允许用户搜索到特定区域的降雨量。居住地点相邻的人通常将小的区域合并起来,从而形成 ZIP 编码、地区编码,甚至是城镇、市或州。例如在 OSM 中,用户可以编辑和覆盖地图顶层的信息,比如旅游景点或街道。通过合并各层的信息建立它们之间的关系并进行跟踪,可以生成更加动态和强大的应用程序。
表示空间数据
不管与一个或多个位置相关的信息是什么,搜索应用程序都需要通过一种高效的方式来表示这些数据。尽管可以通过几种方式来表示位置信息,但我仅关注与 Lucene 相关的方式。首先需要注意的是,许多类型的地理空间数据都可以用它们的 “原始” 格式表示,并且能够在搜索应用程序中很好地发挥作用。例如,Syracuse 表示城市 Syracuse 的完美方式,用户只要在搜索栏中输入 Syracuse 就可以找到包含 Syracuse 的所有文档,输入其他搜索关键词也将取得类似的结果。实际上,原始格式是用于表示带名称的位置,比如城市、州和 ZIP 编码的最常用方法。不过要注意,尽管我使用了术语原始表示,您仍然可以先对数据进行转换或格式化。例如,将 New York 转换成 NY 通常是一种合理的做法。
在我介绍 Lucene 能够使用的表示方式之前,您一定要理解所有表示方式都必须考虑到生成它们的空间引用。在美国,最常见的是 World Geodetic System,它通常缩写为 WGS 84。尽管在某些系统之间允许进行转换,但最好用一个系统来表示您的所有数据。本文假设使用同一个系统表示数据。
使用 Lucene 和 Solr 进行搜索时,纬度和经度(缩写为 lat/lon)等数字空间信息的表示方式是最有趣的。纬度和经度通常使用与本初子午线(位于英国的格林威治)相距的度、分和秒来表示,并且通常需要使用 double(或更高的精度)来表示。例如,对于我的例子中使用的数据 —— 美国纽约州的 Syracuse 市 —— 它的经度为东经 76.150026(如果没有指定东方,则为 -76.150026)和北纬 43.049648。
编码每个纬度和经度可能导致索引大量唯一的词汇,这取决于应用程序。这会显著减慢搜索速度,并且您将在本文的后面看到,这通常是不必要的。事实上,许多地图应用程序将搜索与特定领域关联起来,因此储存关于特定区域的适当信息会生成更少的词汇,并且不对搜索结果产生很大的负面影响。这种在精确度上采取折衷的方法通常将纬度和经度封装到层中。您可以将每个层看作是地图的特定部分的缩放级别,比如位于美国中央上方的第 2 层几乎包含了整个北美,而第 19 层可能只是某户人家的后院。尤其是,每个层都将地图分成 2层 # 的箱子或网格。然后给每个箱子分配一个号码并添加到文档索引中。我将在下一小节解释如何利用该信息加快搜索速度。
Lucene 词汇中的纬度和经度通常表示为两个不同的字段,但是这在一些应用程序中可能会影响性能。如果希望使用一个字段,那么可以使用 Geohash 编码方式将纬度/经度编码到一个 String 中。Geohash 的好处是能够通过切去散列码末尾的字符来实现任意的精度。在许多情况下,相邻的位置通常有相同的前缀。例如,在 geohash.org 中输入 Syracuse, NY 将生成散列码 dr9ughxjkrt4b,而输入 Syracuse 的郊区 Cicero, NY 生成散列码 dr9veggs4ptd3,它们的前缀都是 dr9。
到目前为止,我只是谈到几个单独的点,但是许多地理空间应用程序在图像、路线和数据中的其他关系方面都很有趣。
在搜索中将空间数据与文本合并
一旦在索引中添加了数据之后,搜索应用程序在与数据交互时至少有 5 种基本要求:
距离计算:根据给定点计算它到其他点的距离。
限定框过滤器:查找某些特定区域内所有匹配项(文档)。
排序:根据到固定点的距离对搜索结果进行排序。
相关度改进:使用距离作为记录中的增强因素,同时允许其他因素发挥作用。
查询解析:在给出位置的地址或其他一些用户规定时,创建可用于根据索引数据进行搜索的编码表示。
这 5 个因素都可以在基于位置的应用程序中扮演重要的角色,但是我在这里主要关注距离计算、限定框过滤和查询解析。排序和相关度改进仅使用距离计算,我将在本文的后面介绍它们的实际应用。
距离计算
当计算用于 GIS 应用程序的距离时,一定要知道有许多不同的实现方法,并且每种方法都有其优缺点。距离计算可以划分成 3 个组,这取决于应用程序选择以什么方式对地球进行建模。在一些情况下,完全可以采用平面地球模型,通过牺牲一些精确性来获取速度。在平面地球模型中,大部分距离计算都是勾股定理的变体。在其他情况下使用球面模型,所使用的主要距离计算为大圆弧长。大圆弧长计算球面两点之间的最短距离。当两点之间的距离相隔很远和要求更高的准确度时,需要使用球面模型。最后,可以使用椭圆的地球模型和 Vincenty 公式来获取高度精确的距离(精确到 0.5 毫米),但是在许多应用程序中用不上这种复杂的模型。
差之毫厘,失之千里
在许多本地搜索应用程序中,精度的需求由应用程序本身决定。在某些情况下,偏离一公里问题并不大,而在另一些情况下,偏离几毫米就会导致严重的问题。例如,欧几里得距离计算对于跨度很长的距离(比如跨州)通常不够精确,即使是半正矢(大圆)方法也不足以为某些场合提供所需的精度,因为将地球建模成椭圆体比建模成球体更精确。对于这些情况,使用 Vincenty 公式将得到更加满意的结果。在其他应用程序中,唯一需要注意的事情是对结果的排序,因此可以使用 Squared Euclidean Distance(实际不是距离),从而避免平方根计算。
当然,其他距离计算也是有用的,比如曼哈顿距离,它反映在由街区组成的城市中行走的距离(例如在一辆出租车中穿越纽约城的曼哈顿)。但是为了实现本文的目的,我将使用平面地球模型和大圆弧长距离来演示距离,其他方法留给读者探索。此外,本文不将海拔作为影响因素,但是一些应用程序可能需要考虑海拔。
限定框过滤器
在许多基于位置的应用程序中,可以搜索到数百万条地址信息。遍历所有这些数据来查找既包含关键字又在用户指定的距离之内的文档集将需要花费大量时间。一种合理的做法是先缩小文档集的范围然后再计算相关的子集。如果仅储存了纬度和经度信息,那么缩小文档集的首选方法是传入包含指定位置的周边区域的范围。这可以通过 图 1 来表示,其中不完全透明的方框表示包含南卡罗来纳州的查尔斯顿(Charleston)市及其周边地区的限定框:
图 1. 位于 Charleston 中央上方的限定框
如果应用程序还使用层信息或 Geohash 信息,那么可以使用这些值来更好地缩小需要搜索的文档的范围。我将在讨论使用 Lucene 和 Solr 建立索引和搜索的细节时演示这点。
查询解析
查询解析的目的是确定查询的哪个部分包含所搜索的关键字,哪个部分包含位置信息。这个过程的后半部分称为地理编码(geocoding)。尽管我在这里在查询解析的上下文中讨论地理编码,它在索引期间也非常有用。请考虑下面的用户查询例子:
1600 Pennsylvania Ave. Washington, DC
1 Washington Av. Philadelphia Pennsylvania
Mall of America, 60 East Broadway Bloomington, MN 55425
Restaurants near Mall of America
Restaurants in the Mall of America
查看前两个查询可以发现一些有趣的东西:
词汇的顺序通常很重要,但是在纯文本搜索中,顺序可能不重要。
地名表和其他空间资源,比如 GeoNames可能在将地址转换成位置时非常有用。这些资源通常包含旅游景点的列表 —— 例如,白宫等标志性建筑。
规范化缩写,比如 Ave. 和 DC,或使用同义词来包含用户输入地址信息的各种变体非常重要。
剩余的查询将展示几个微妙的地方。例如,在第三个查询中,用户指定了完整的地址;如果您要搜索每个字段以获得名称、地址、城市和 ZIP,那么就必须正确地解析这些属性。在最后两个查询中,用户选择 near 还是 in 是非常重要的。与 Mall 的距离在一定范围内的所有饭店都符合第四个查询的用户,而最后一个查询的用户仅对在 Mall 内部的饭店感兴趣。查询解析可能相差甚远,因为描述与位置的关系很复杂,更何况还存在拼写错误、语言歧义和不良数据等。
虽然地理编码很复杂,但是可以使用服务来将地址转换成位置。两种常用的服务为 Google Maps 公共 API 和 GeoNames。不幸的是,使用这些 Web 服务必须遵循使用条款(通常带有某些限制)和网络流量。对于现实的生产系统,您最好自己实现这些功能。尽管实现这些功能超出了本文的范围,但一定要记住 GeoNames 数据和其他许多空间资源是可以完全免费下载的。有了好的资源之后,最好从基础开始积累(地址、城市和州),然后再添加旅游景点和健壮的异常处理。随着时间的推移,您的查询记录将能够创建健壮的查询解析器,足以应付用户的各种输入。不管是什么搜索应用程序,良好的猜测和请求用户证实猜测结果都是好实践,如 图 2 的 Google Maps 截屏所示:
图 2. 在 Google Maps 上的良好猜测和请求用户证实猜测结果
展示使用 GeoNames 服务并具有一些其他特性的基础查询解析器,但生成版本的解析器将留给用户实现。至此,您应该具备了足够的背景知识,可以进入主题了。本文后面的内容将关注如何使用 Lucene 和 Solr 为空间信息建立索引并搜索它们。
安装样例代码
要运行样例代码,您需要安装以下软件:
最新的 Web 浏览器,比如 Firefox
您还需要本文提供的样例代码,它包含 Apache Solr 及其所依赖的软件。遵循以下步骤安装样例代码:
unzip sample.zip
cd geospatial-examples
ant install
启动 Solr: ant start-solr(以后要停止 Solr,运行 ant stop-solr)
在浏览器中访问 http://localhost:8983/solr/admin 并确认 Solr 正常运行。您应该看到一个带有查询框的基础管理员界面。
安装好 Solr 并正常运行之后,就可以在 Lucene 中开始处理空间数据了。运行安装步骤将下载一些来自 OSM 项目的样例代码,我在 http://people.apache.org/~gsingers/spatial/ 上介绍了该项目。对于本文,我包含了来自美国的 4 个位置的样例 OSM 数据(在文件中列出了到 OSM 的永久链接):
Syracuse, N.Y.
Downtown Minneapolis, Minn.
Around the Mall of America in Bloomington, Minn.
Downtown Charleston, S.C.
为了演示本文介绍的许多概念,我编写代码来在 Solr 中为 OSM 建立索引,并将一些简单的事实与特定的位置相关联(例如,查看数据目录中的 syracuse.facts 文件)。这样做的目的是展示如何合并非结构化文本和空间数据,以创建高效的搜索应用程序。此外还要注意,我使用 Solr 1.5-dev 版本(Solr 的当前开发主干),而不是最近发布的 Solr 1.4。
在 Lucene 中为空间数据建立索引
Lucene 2.9 添加了两个在空间搜索方面起到重大作用的新特性。首先,Lucene 实现了更好的数字范围查询和过滤功能,它们通常用在限定框方法中。其次,Lucene 有一个新的贡献软件(contrib)模块,它包含以前称为 Local Lucene 的独立项目。(该代码位于 Lucene 的 contrib/spatial;我已经在 样例代码 中包含了 JAR 文件)。空间贡献软件为创建笛卡儿层和 Geohash 代码提供工具,并且为创建 Lucene 查询和过滤器对象提供工具。
在查看为数据建立索引的代码之前,您需要评估如何与数据交互以及您的应用程序需要处理多少数据,这非常重要。例如,对于大多数拥有少量或中等程度文档数量(少于 1000 万)的人而言,为纬度和经度创建索引和使用简单的数字范围查询可以得到优异的性能。但是对于数据量更大的应用程序,就需要做更多的工作(比如添加笛卡尔层)来减少词汇的数量和需要过滤和记录的文档。此外,考虑使用什么格式储存信息也很重要。许多空间距离算法要求采用以弧度表示的数据,而其他算法则要求使用以度表示的数据。因此在建立索引时将纬度/经度值转换成弧度是值得的,从而避免在每次搜索都执行转换。当然,如果您需要保留两种格式的数据,则意味着需要更多的空间(磁盘,甚至内存)。最后,您是不是对位置特性进行分类、排序和记录,而不是仅将它们用于过滤?如果是这样,那么将需要交替使用不同的表示。
因为本文仅演示概念而没有考虑生产使用,所以我将用一些 Java 代码在同一个地方显示如何为 Geohash、笛卡尔层创建索引。我已经在 Solr 模式中定义了许多值(模式的位置为 geospatial-examples/solr/conf/schema.xml)来捕捉 OSM 数据。清单 1 显示了用于表示位置的主要字段:
清单 1. 样例 Solr 模式
<!-- Latitude -->
<field name="lat" type="tdouble" indexed="true" stored="true"/>
<!-- Longitude -->
<field name="lon" type="tdouble" indexed="true" stored="true"/>
<!--
lat/lon in radians
In a real system, use a copy field for these instead of sending over the wire -->
<field name="lat_rad" type="tdouble" indexed="true" stored="true"/>
<field name="lon_rad" type="tdouble" indexed="true" stored="true"/>
<!-- Hmm, what about a special field type here? -->
<field name="geohash" type="string" indexed="true" stored="true"/>
<!-- Elevation data -->
<field name="ele" type="tfloat" indexed="true" stored="true"/>
<!-- Store Cartesian tier information -->
<dynamicField name="tier_*" type="double" indexed="true" stored="true"/>
Lucene 和 Solr
尽管我使用 Solr 模式来展示需要建立索引的字段,这里的所有概念在 Lucene 中都是可用的。例如,Lucene 2.9.1 中的 tdouble 实际上就是精度为 8 的 NumericField。
我将纬度/经度值存储为 tdouble 字段。一个 tdouble 就是在内部使用 Trie 结构表示的一个 double。Lucene 可以使用它来大大减少在范围计算期间需要计算的词汇的数量,尽管实际上它向索引添加了更多词汇。我将 Geohash 储存为一个简单的 string(未分析)因为我仅需要它的精确匹配。严格而言,我进行的这些计算用不到海拔,但我将它储存为 tfloat,它是存储在 Trie 结构中的 float。最后,tier_* 动态字段允许应用程序动态地添加笛卡尔层字段,而不需要提前声明它们。至于索引过程捕捉的其他元数据字段,我将留给读者探索。
负责为数据创建索引的代码位于 sample.zip 的 source 树中。Driver 类是一个用于启动索引过程的命令行实用程序,但实际的索引过程发生在名为 OSMHandler 的实现的 SAX ContentHandler 部分。在 OSMHandler 代码内部,最关键的代码行是 startElement() 方法。我将它分成 3 个部分。第一个例子(见清单 2)以 double 的形式为纬度和经度建立索引,并将它们转换成可以索引的弧度:
清单 2. 纬度/经度的样例索引
//... current is a SolrInputDocument
double latitude = Double.parseDouble(attributes.getValue("lat"));
double longitude = Double.parseDouble(attributes.getValue("lon"));
current.addField("lat", latitude);
current.addField("lon", longitude);
current.addField("lat_rad", latitude * TO_RADS);
current.addField("lon_rad", longitude * TO_RADS);
为纬度/经度建立索引非常简单。接下来,我为纬度/经度对索引 Geohash 值,如清单 3 所示:
清单 3. 样例 Geohash 索引
//...
//See http://en.wikipedia.org/wiki/Geohash
String geoHash = GeoHashUtils.encode(latitude, longitude);
current.addField("geohash", geoHash);
在清单 3 的 Geohash 代码中,我使用随 Lucene 空间 contrib 包附带的 GeoHashUtils.encode()(有一个等效的 decode() 方法)方法将纬度/经度对转换成一个 Geohash 字符串,然后再把该字符串添加到 Solr。最后,为了添加笛卡尔层,我在 OSMHandler 代码中完成了两件事情:
我在构造器中创建 CartesianTierPlotter 类的 n 个实例,每个需要建立索引的层一个。
在 startElement() 方法中,我遍历所有 n 个描绘器,并为每个包含当前 OSM 元素的纬度和经度的每个网格元素获取标识符。该代码如清单 4 所示:
清单 4. 笛卡尔层的样例索引
//...
//Cartesian Tiers
int tier = START_TIER; //4
//Create a bunch of tiers, each deeper level has more precision
for (CartesianTierPlotter plotter : plotters)
{current.addField("tier_" + tier, plotter.getTierBoxId(latitude, longitude));
tier++;
}
一般情况下,查询一次仅需搜索一个层,因此拥有多个层通常不会造成任何问题。您应该根据搜索所需的粒度来选择层数。如果您花时间查看剩余的索引代码,将看到我添加了许多与 OSM 文件中的数据点相关的其他元数据值。我现在仅为两种 OSM 数据类型建立索引:界点(node) 和路线(way)。界点是特定的纬度和经度上的一个点,而路线是所有在某种程度上相关的界点的集合,比如街道。
什么是 CartesianTierPlotter?
CartesianTierPlotter 的工作是对地球进行投影和纬度/经度信息,将其转换成层系统所使用的网格,并且给每个网格一个唯一的号码。在搜索时,应用程序就可以通过指定网格 ID 来限制搜索范围。
您已经了解创建包含空间信息的 Solr 文档的基础知识,接下来将进行实践。Driver 类接收数据和事实文件以及运行 Solr 的 URL,并将该工作转交给 OSM2Solr 类。OSM2Solr 类将使用 Solr 的 Java 客户端 SolrJ 来接收 OSMHandler SAX 解析器创建的文档,并将它们批量发送到 Solr 服务器。您可以在命令行运行 Driver 类,或者只需运行 ant index,让 Ant 完成运行驱动程序所需的工作。完成该步骤之后,在浏览器中访问 http://localhost:8983/solr/select/?q=*:* 并确认 Solr 找到 68,945 个文档。花些时间细读返回到结果,熟悉其中包含的内容。
处理 OSM 数据的方法非常多,我在这里仅介绍了一些,不过,我们应该讨论如何在应用程序中使用这些数据了。
根据位置进行搜索
在把数据添加到索引中之后,我们将重温使用数据的各种方式。我将演示如何根据索引中的空间信息对文档进行排序、增强和过滤。
与距离相关的计算
根据距离增强文档和对文档进行排序是许多空间应用程序的常见要求。为了实现该目的,Lucene 和 Solr 包含几个用于计算距离的功能。Lucene 包含根据大圆(Haversine)公式计算距离的工具(参见 DistanceUtils 和 DistanceFieldComparatorSource),而 Solr 包含几个用于计算距离的 FunctionQuery 函数:
大圆(Haversine 和 Geohash Haversine)
Euclidean 和 Squared Euclidean
Manhattan 和其他 p-norm
使用 Solr 的距离函数根据距离增强数据是非常容易的。我将关注 Solr 的函数查询,因为它们是最容易使用的并且不需要编程。可以在 Lucene 中轻松地使用它们,或者轻松地将它们移植到 Lucene。
如前所述,我设置了几个字段来储存 OSM 数据,包括 lat/lon、lat_rad/lon_rad 和 geohash。然后,我就可以搜索和增强这些值:
hsin(大圆):http://localhost:8983/solr/select/?q=name:Minneapolis AND _val_:"recip(hsin(0.78, -1.6, lat_rad, lon_rad, 3963.205), 1, 1, 0)"^100
dist(Euclidean,Manhattan,p-norm):http://localhost:8983/solr/select/?q=name:Minneapolis AND _val_:"recip(dist(2, lat, lon, 44.794, -93.2696), 1, 1, 0)"^100
sqedist(Squared Euclidean):http://localhost:8983/solr/select/?q=name:Minneapolis AND _val_:"recip(sqedist(lat, lon, 44.794, -93.2696), 1, 1, 0)"^100
ghhdist(Geohash Haversine):http://localhost:8983/solr/select/?q=_val_:"recip (ghhsin(geohash(44.79, -93), geohash, 3963.205), 1, 1, 0)"^100
对于以上每种情况,我将一个关键字查询与一个基于距离的 FunctionQuery 结合起来,生成一个包含关键字记录和距离记录的结果集。要查看这些部分的效果,请为每个查询添加一个 &debugQuery=true 并花些时间来检查 Solr 生成的解释。这些仅是它们的用例。当然,您可以选择增强某些部分,或者根据您的需求进行更改。
至于根据距离进行排序,Solr 提供一个主要选项,这实际上是一个弥补方法,因为 Solr 没有根据函数进行排序的功能,也没有定义定制的 FieldType。不过,这种措施非常简单。如果要根据函数进行排序,需要像上面一样创建查询,但在关键字子句后面添加 0,如 q=name:Minneapolis^0 AND _val_:... 所示。这将导致关键字记录为 0(但仍然返回匹配的结果),并且函数值将是记录的唯一组成部分。从长远看,希望 Solr 添加 FieldType 来更好地支持排序,而不需清零主要查询。
完成了排序和记录之后,我们将探讨过滤。
过滤
为了使用 Solr 根据位置进行过滤,表 1 为应用程序的记录器提供了 3 种主要的机制来限制文档空间:
表 1. 过滤方法
过滤方法 |
说明 |
例子 |
范围 |
创建一个包含限定框的纬度/经度的范围过滤器。考虑到性能方面的原因,这种方法必须使用 Solr 的 TrieField (NumericField) 功能。 |
http://localhost:8983/solr/ select/?q=*:*&fq=lon:[-80 TO -78]&fq=lat:[31 TO 33] |
笛卡尔层 |
根据给出的纬度/经度和距离识别中心点周围的网格,并且仅将搜索结果限制在包含这些网格的文档上。查看 什么是 QParserPlugin? 了解更多关于源代码实现的信息。 |
http://localhost:8983/solr/ select/?q=*:*&fq={!tier x=32 y=-79 dist=50 prefix=tier_} |
距离 |
使用 Solr 的 frange(函数范围)QParserPlugin 功能和一个距离函数(见上面的 与距离相关的计算)决定点之间的距离并限制文档空间。 |
http://localhost:8983/solr/ select/?q=*:*&fq={!frange l=0 u=400}hsin(0.57, -1.3, lat_rad, lon_rad, 3963.205) |
关于密度的简要说明
特定范围的点密度在用户的搜索体验方面扮演着重要的角色。例如,为纽约的曼哈顿提供商业搜索的应用程序的点密度比为明尼苏达州的布法罗提供搜索的应用程序的点密度大。事实上,将该信息包含到过滤函数中是非常有用的,这让应用程序能够挑选一个最合适的距离,从而确保搜索结果是良好的。不过,演示如何实现该过程超出了本文的讨论范围。
哪种方法适合您呢?这取决于点的密度(参见 关于密度的简要说明),但是我们建议首先采用简单的范围方法,然后在需要的时候提升到层方法。关键因素是每次计算范围时需要计算的词汇数量,因为这个数量直接控制 Lucene 需要做多少工作来限制结果集。
一个简单的 geonames.org 查询解析器
为空间应用程序构建功能齐全的查询解析器超出了本文的范围,反之,我将构建一个简单的 QParserPlugin ,它将负责从来自 GeoNames 的位置信息获取结果。这个解析器假设应用程序能够提前将用户输入分成两部分:关键字查询和空间查询。事实上,许多本地查询应用程序都要求用户通过两个输入框输入信息。
什么是 QParserPlugin?
QParserPlugin 是 Solr 对查询解析器插件模块的称呼。和许多 Solr 部分一样,查询解析器实现也是可插拔的。对于本文,我使用 3 中不同的查询解析器插件,其中一个是随 Solr 附带的( FunctionRangeQParserPlugin ({!frange})),有两个是我自己编写的:CartesianTierQParserPlugin ({!tier}) 和 GeonamesQParserPlugin。这两个插件的源代码位于样例代码下载的 src 树中。这两个插件已经使用 solrconfig.xml 文件在 Solr 中进行了配置:在查询中通过指定 {!parserName [parameters]}[query](参数和查询可能是可选的)来调用 QParserPlugin,就像在 {!tier x=32 y=-79 dist=50 prefix=tier_} 和 {!frange l=0 u=400}hsin(0.57, -1.3, lat_rad, lon_rad, 3963.205) 中一样。
解析器可以接受以下几个参数:
topo:toponym 的缩写(参见 GeoNames 文档)。在 GeoNames 中搜索的位置。必需。
rows:从 GeoNames 获取到的行数。可选。默认值为 1。
start:从其开始的结果。默认值为 0。
lat:在 FunctionQuery 中用作 ValueSource 的纬度字段名。如果指定了它,必须也设置 lon。
lon:在 FunctionQuery 中用作 ValueSource 的经度字段名。如果指定了它,必须也设置 lat。
gh:在 FunctionQuery 中用作 ValueSource 的 Geohash 字段名。如果指定了它,就不能 设置 lat/lon。
dist:需要使用的距离函数。String。[hsin, 0-Integer.MAX_VALUE, ghhsin] 之一。如果指定了一个 geohash 字段,那么将忽略该字段。ghhsin 是自动的。2-norm (Euclidean) 的默认值为 2。
unit - KM|M:需要使用的单位,KM 表示公制,M 表示英制。默认值为 M。
boost - float:增强函数查询的量。默认值为 1。
这个例子的代码包含在样例代码下载的 GeonamesQParserPlugin.java 文件中。(下载中包含的 Solr 版本中的 Solr 服务器已经配置好)。调用它与调用上面的 CartesianTierQParserPlugin 类似。例如,要在索引中搜索明尼苏达州 Bloomington 附近的购物中心,我将使用 http://localhost:8983/solr/select/?q=text:mall AND _query_:"{!geo topo='Bloomington, MN' lat=lat_rad lon=lon_rad dist=hsin}"。
通过采用 QParserPlugin 方法,我能够关注对我而言非常重要的语法,而且在位置方面仍然允许继续使用所有基于文本的查询解析功能。
从现在开始,可以大大地扩展 GeonamesQParserPlugin,将其与邮政编码和许多其他位置规范一起使用。当然,它还需要更多的错误处理,并且很可能需要转换为使用 GeoNames 数据集,从而使其不依赖于 Web 服务。Solr 在为问题跟踪器获取更多的空间查询解析器支持方面还存在开源问题。
结束语
至此,我已经演示了 Lucene 和 Solr 根据基于点的位置模型搜索、排序和过滤文本文档的功能。接下来,将要实现一个真实的位置搜索应用程序来处理用户查询和呈现搜索结果。部分关于应用程序的伸缩性的问题可以从创建限定框过滤器时需要计算的词汇量找到答案。除了关于伸缩性的过滤器问题之外,还需要考虑其他与搜索相关的因素,比如是分发索引还是仅复制索引。
如果您对构建更加高级的 GIS 应用程序感兴趣,您将需要为路线查找、形状交叉等添加更加复杂的功能。如果您需要构建一个可靠搜索应用程序,用于合并基于点的位置的结构和非结构化文本,那么关注 Lucene 和 Solr 就足够了。
问题的提出
Internet 中每个网页除了自身包含的内容以外,还有各种各样指向外部资源的链接,这些外部链接资源所包含的内容一般是链接源页面的内容的补充、详细说明或参考资源,这些外部资源页面本身同样也会包含有指向其他资源的链接,外部链接的页面内容和链接源页面内容一起,描述某一个主题。实际上,只要 Internet 上的每个网页都有通向外部资源的链接,那么随便从哪个页面开始,遍历所有的链接,就可以访问到 Internet 的每个网页。
互联网存储了各种各样的海量资源,而搜索引擎给我们提供了访问这些资源的接口,不过这个接口现在只能提供检索服务,那么一个很简单的问题是自然而然的:搜索引擎是否还可以为我们提供存储服务?假如现在我们把 Internet 倒过来——想象一下:如果搜索引擎不仅仅提供检索各个网页信息的服务,还提供将信息存储到“Internet 的各个网页”的服务,即将各个站点、网页作为存储信息的容器、来提供信息存储服务,那么这是不是一个理想的分布式、近乎无限扩展的基础存储服务系统?
本文的目的
本文对使用搜索引擎作为存储服务的思路的实现方法做了简单的分析,并基于开源的 Apache Solr 项目构建一个基础存储服务系统,在此基础上结合一个很简单的 BLOG 网站的例子对存储服务的结构和使用进行了说明。希望本文所探讨的思路能对那些迫切需要大规模、高扩展性存储服务的应用有所启发。
基于搜索引擎的基础存储服务
为了说明基于搜索引擎的基础存储服务的构成,首先需要对搜索引擎的角色变化有所了解:
图 1. 传统的网络服务中搜索引擎扮演的角色
传统的网络服务中搜索引擎仅仅对外提供检索服务,索引服务通过自动化的 Crawler 在后台完成,一般并不对外提供索引服务接口。
图 2. 在基础存储服务中扮演的角色:
很显然,在基于搜索引擎的基础存储服务系统中,搜索引擎不再仅仅是接收检索请求返回检索结果,还可以接收存储请求(也就是索引服务)。索引服务不再隐藏在后台、仅仅通过 Crawler 来搜集信息,现在也可以接收外界存储的信息了。
存储系统的组织
存储模式
基于搜索引擎的基础存储服务并不是基于结构化的概念,而是基于声明式的概念。基础存储服务使用 URL 来组织存储资源,因为 URL 对用户来说是隐藏的,因此用户使用基础存储服务的时候不用考虑以什么样的结构来存储数据,而是直接通知基础存储服务要存储什么样的数据。当然对于不同的搜索引擎来说,其所能接受的资源的格式通常是有差别的,例如本文中所使用的开源搜索引擎 Apache Solr,其能处理的资源为 XML 格式的文档,实际使用的时候需要将存储的资源变化为 XML 格式的文档来进行存储。本文附带的例子中包含有用于进行 Java 对象到 Solr XML 文档的变换代码,详细的处理逻辑请参考例子的代码。
基础存储服务内部的资源存储方式是类似于互联网信息的存储方式:分布式、且使用 URL 来标识实体之间的关联。显然这里的实体可以类比为互联网的页面。存储服务可定制,即可以定制将实体以任何形式存储在任何场所,如果以 Internet 中页面来进行类比,即将实体存在各个网站的各个页面中。但是这个结构对外是不暴露的,用户可以不必关心 URL 的信息。
分布式
分布式对于以搜索引擎为基础的存储服务系统来说是天然的,存储系统中标识一个资源的唯一标识符是资源的 URL,但是这个 URL 不同于一般意义上的 URL(这里叫 URL 仅仅是为了和存储服务的灵感来源—— Internet 进行统一,因为 Internet 中的资源是使用 URL 来定位的),存储服务的 URL 不包含地址信息,仅包含用于标识资源的信息。虽然 URL 不包含地址信息,但是基础存储服务依然可以根据 URL 的内容决定将资源的存储节点,但是存储在哪一个节点上是根据存储系统实际的部署的情况来确定。
扩展性
扩展性对于以搜索引擎为基础的存储服务系统来说也是天然的,因为用户使用存储服务的时候不用关心资源如何存储,另外因为分布式存储是通过资源的 URL 来进行区分,而 URL 却不包含地址信息,所以就像 Internet(可能 IPv4 会有所限制,但是 IPv6 就没问题了)一样,理论上,基于搜索引擎的基础存储服务是可以无限扩展的。
事务
本文中的 BLOG 网站的例子虽然应用了事务,但是,毫无疑问,直接利用搜索引擎的索引服务自身提供的简单事务管理功能是不足以满足作为一个存储系统时的事务管理需要的。如果要进行复杂系统的开发,需要定制搜索引擎的事务管理机制。因为本文的目的仅在于对使用搜索引擎作为存储服务的可能性做简单的分析和验证,关于如何定制事务管理的问题超出了本文的范围,所以这里不再赘述。
安全
同事务一样,关于安全的问题同样超出了本文的范围,所以这里不再赘述。
使用存储系统
同组织 Internet 的资源的 URL 一样,基础存储系统最重要的方面就是资源的 URL 设计。在基础存储系统的 URL 设计思路中,主要参照了 REST 的原则。
RESTful 存储结构
什么是 REST ? REST 定义了一组体系架构原则(例如:显式地使用 HTTP 方法、无状态、公开目录结构式的 URI、传输 XML、JSON,或同时传输这两者),您可以根据这些原则设计以系统资源为中心的 Web 服务,包括使用不同语言编写的客户端如何通过 HTTP 处理和传输资源状态。
为什么需要 REST ?在基础存储服务中,REST 原则应用在资源的 URL 设计上。虽然看起来 REST 和存储服务好像没有什么关系。但是前面已经说过,基于搜索引擎的基础存储服务实际上是将 Internet 倒转过来,也就是说原来搜索引擎仅用于检索的资源现在不仅用于检索,还用于提供存储服务,那么存储系统的结构就等同于 Internet 的结构了,因此应用 REST 的原则就顺理成章了。REST 原则在组织 Internet 资源中的意义是非常重要的,在基础存储服务中,REST 原则对 URL 设计同样也是极为重要的。
URL 的结构
基于搜索引擎的存储服务的处理依赖于资源的 URL 结构。在存储和检索资源的过程中,存储系统根据资源的 URL 来对资源进行组织,例如在存储的时候,可能会将 URL 相似的对象保存到同一个存储地址空间中(当然对用户来说这个问题是不需要考虑的,决定采用什么样的策略来保存资源是存储服务系统需要考虑的问题);同样,在进行检索处理的过程中,也可能会在某一个 URL 空间内进行资源的检索,类似于使用 Google 搜索引擎检索的时候使用 site 关键字:solr site:apache.org。
在存储系统中有两种类型的资源:基础资源和附属资源,资源的类型是依照资源本身和其他关联资源的关联形式的不同加以区分(关于为何这样进行区分,参考后面的说明)。两种类型的资源的 URL 的结构是不同的:
基础资源,基础资源是指不附属于其他资源的资源。追加、更新和删除基础资源都需要以基础资源为单位来进行,追加、更新和删除其他资源的时候不会影响到基础资源的内容。基础资源的 URL 可以使用任意的形式构建。
附属资源,附属资源是指附属于其他资源的资源,附属资源的 URL 以其附属的基础资源的 URL 为基础构建。
基础资源和附属资源之所以使用不同的 URL 格式进行标识,这主要参考了面向对象方法论中对象之间关系的模式,基础存储服务使用这些模式来组织存储系统中的资源。例如:面向对象方法论中对象之间的关系主要有:has-a、contains-a 和 is-a 三种模式(其他的模式都可以划到这三种模式中),has-a 所关联的资源可以理解为基础资源,contains-a 所关联的资源可以理解为附属资源,这两种关联方式在本文的例子中都有所体现。但是 is-a 关系在本文的例子中没有体现,但是通过记录资源的类型信息可以很容易的实现 is-a 关系的存储和查询。
如何查询
毫无疑问,作为基础存储服务基础的搜索引擎所支持的查询方式,基础存储服务无疑都是支持的,例如基于 URL 的查询。除了这些基本的查询功能以外,还可以使用其他更为精确的查询方法,这依赖于具体的存储服务设计和具体的应用系统设计。
如何更新
更新过程类似于搜索引擎的索引处理。但是根据用于更新的资源和其关联资源的关联形式的不同,追加、更新和删除资源的时候其处理方式也是不同的:
追加资源,类似于关系数据库的插入处理。如果被追加的资源内部包含有其他附属资源,那么同样需要递归的追加附属资源;如果被追加的资源内部包含有其他基础资源,不做特殊处理。
更新资源,类似于关系数据的更新处理,同追加资源类似,如果被更新的资源内部包含有其他附属资源,那么同样需要递归的更新附属资源;如果被更新的资源内部包含有其他基础资源,不做特殊处理。
删除资源,类似于关系数据的删除处理,如果被删除的资源内部包含有其他附属资源,那么同样需要递归的删除附属资源;如果被更新的资源内部包含有其他基础资源,不做特殊处理。
BLOG 网站的例子
理论的说明总是不那么容易理解,下面使用一个使用基于 Apache Solr 构建的 RESTful 基础存储服务的 BLOG 网站的例子来进行更直观的说明。
BLOG 网站的功能
本文中的 BLOG 网站例子是一个最简单的 BLOG 网站,仅实现了一个 BLOG 网站应有的最基本的功能:
注册 Blogger、Blogger 登录和注销。Blogger 信息包含全部的个人可公开信息,例如姓名、电子信箱、个人主页等;
Blogger 可以发表、更新和删除 Article,删除 Article 的时候同时也会删除 Article 相关的 Review;不能直接删除 Review。
Blogger 可以针对 Article 直接使用当前登录信息(用户名和电子邮件)发表 Review,也可以提供其他的用户名和电子信箱发表 Review;
Blogger 可以基于关键字检索 Article标题或内容符合条件的 Article。
关于 Apache Solr
关于 Solrj
本文的例子程序使用 Solrj 同 Solr 服务器通信(进行了部分定制开发,详细内容请参考例子程序的代码)。Solrj 是一个访问 Solr 服务器的 Java 客户端,它提供了一个 Java 的接口用于追加、更新和查询的 Solr 的索引库。Solrj 从 Solr 1.3 开始作为 Solr 的一部分发布,因为本文的例子使用的是 Apache Solr 1.4.0,所以您不需要另外下载 Solrj,可以在 Solr 的安装目录下找到 Solrj:SOLR_HOME/dist/solrj-lib。
软件的下载和安装
要使用运行本文中的 BLOG 网站例子程序,您必须先下载或安装(解压)以下软件:
Java 1.5 或更高版本。
Web 浏览器,我使用的是 Chrome4,不过其他任何现代浏览器都没问题。
Apache Tomcat 5.5.25,假设安装目录是:TOMCAT_HOME
Apache Solr 1.4.0,假设安装目录是:SOLR_HOME
下载本文中的例子程序和 Solr 模式配置文件:solr-blog.war和 schema.xml
因为安装和配置本文的例子的时候直接使用了 Apache Solr 发布包中的例子索引库,因此不包含可以用来进行测试的 Blogger、Article 等数据。为了能够运行例子程序,请您在安装配置例子程序后,从注册一个新的 Blogger 用户开始进行测试。详细的功能请通过运行本文中的例子或直接参考例子程序的源代码。
安装和测试本文的例子程序
下载和安装上述软件以后,需要进行以下配置才能运行本文的例子程序。
首先,在 Tomcat 中配置 Apache Solr。将 Apache Solr 应用:SOLR_HOME/dist/ apache-solr-1.4.0.war解压到 TOMCAT_HOME/webapps/solr目录(如果此目录不存在需要您手动创建)。解压完成后打开 TOMCAT_HOME/webapps/solr/WEB-INF/web.xml 文件,将其中的环境变量:solr/home修改为:SOLR_HOME/example/solr(这里使用的是 Solr 安装包附带的例子索引库)。缺省状态下,环境变量 solr/home是被注释掉的,请您解除对 solr/home的注释:
清单 1. TOMCAT_HOME/webapps/solr/WEB-INF/web.xml
<web-app>
<env-entry>
<env-entry-name>solr/home</env-entry-name>
<env-entry-value>SOLR_HOME/example/solr</env-entry-value>
<env-entry-type>java.lang.String</env-entry-type>
</env-entry>
… …
<servlet>
<servlet-name>SolrServer</servlet-name>
<servlet-class>org.apache.solr.servlet.SolrServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
… …
<servlet-mapping>
<servlet-name>SolrServer</servlet-name>
<url-pattern>/select/*</url-pattern>
</servlet-mapping>
… …
</web-app>
注意:环境变量 solr/home的值中 SOLR_HOME需要替换成实际的目录信息,而不能保持原状。
然后,修改 Apache Solr 的默认的模式配置(直接使用下载的 schema.xml覆盖 SOLR_HOME/example/solr/conf/schema.xml文件亦可),主要的修改内容是(红色字体部分,其中 <field> url和 resource_type的内容是追加的,<dynamicField>和 <uniqueKey>对原来已有的内容的修改):
清单 2. SOLR_HOME/example/solr/conf/schema.xml
<?xml version="1.0" encoding="UTF-8" ?>
<schema name="example" version="1.2">
<types>
<fieldType name="string" class="solr.StrField" omitNorms="true"/>
... ...
</types>
<fields>
<! — url 字段的定义 -->
<field name="url" type="string" indexed="true" stored="true" required="false" />
<! — resource_type 字段的定义 -->
<field name="resource_type" type="string" … required="true" />
... ...
<! —缺省 dynamicField 的配置 -->
<dynamicField name="*" type="text" multiValued="true" />
</fields>
<!-- uniqueKey 的配置 -->
<uniqueKey>url</uniqueKey>
<defaultSearchField>text</defaultSearchField>
</schema>
其次,将本文附带的例子程序 solr-blog.war解压缩到 TOMCAT_HOME/webapps/blog目录,同样,如果目录不存在的话需要您手动创建。解压完成后打开 TOMCAT_HOME/webapps/blog/WEB-INF/web.xml文件,将初始化参数:solr-server-url修改为您刚刚配置的 Apache Solr 应用的 URL,例如,如果您对 Tomcat 的安装和配置没做特殊的修改的话,solr-server-url的值应该是:http://localhost:8080/solr/。其他的配置内容保持不变即可。
清单 3. TOMCAT_HOME/webapps/blog/WEB-INF/web.xml
<web-app>
<context-param>
<param-name>solr-server-url</param-name>
<param-value>http://localhost:8080/solr/</param-value>
</context-param>
<listener>
<listener-class>org.apache.solr.blogsample.InitListener</listener-class>
</listener>
<servlet>
<servlet-name>BloggerServlet</servlet-name>
<servlet-class>org.apache.solr.blogsample.BloggerServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>BloggerServlet</servlet-name>
<url-pattern>/bloggers/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>login.jsp</welcome-file>
</welcome-file-list>
</web-app>
运行 TOMCAT_HOME/bin/startup.bat(假设您的操作系统是 Windows)批处理文件,启动 Tomcat 服务器。
启动您的浏览器,打开地址:http://localhost:8080/blog,您看到的页面将会是:
图 3. 例子程序的初始页面
因为系统初始状态没有任何 Blogger,所以,接下来您需要从注册 Blogger 开始,注册完毕后登录到系统中,然后进行发表 Article、更新和删除 Article、发表 Review,以及检索 Article 的操作:
图 4. Blogger 注册页面
输入 Blogger 的基本信息就可以进行注册了,需要注意的是,Blogger 的名字必须是系统中唯一的,如果输入了重复的 Blogger 名字,程序会进行提示。
图 5.
Article 检索和一览页面
进行检索处理的时候,任何标题或内容匹配检索条件输入框中输入的内容的 Article 都会返回。点击 Blogger 名字链接会打开 Blogger 信息查看页面。点击 Title 链接会进入 Article 编辑画面。
图 6. 发表、编辑和删除 Article、以及发表 Review 页面
Article 编辑画面分为三个部分,最上面是 Article 的基本信息编辑区域,中间是已经发表的 Article 相关的 Review 显示区域,最下面是发表 Review 的编辑区域。
图 7. Blogger 信息查看页面
BLOG 网站的实现
BLOG 网站使用 Java 语言开发,在使用 Apache Solr 进行数据存储的时候直接将 Java 对象保存在存储库中,当然这个过程中需要进行 Java 对象到 XML 的变换。同样进行检索处理的时候还需要将返回的 XML 格式的检索结果变换成 Java 对象。
注:Solrj 提供了对 Java 对象到 Solr 需要 XML 格式的文档的变换处理(或反向),但是因为我们的例子需要特殊的 URL 信息,所以例子程序对这部分处理进行了定制,并没有直接使用 Solrj 的实现。
例子程序的 URL 结构设计
在本文的例子程序的实现中,基础资源的 URL结构是:resource-type/id,其中 resource-type是指要进行存储的 Java 对象的类名;id是指在 resource-type限定的范围内能唯一地标识资源的信息,例如在本文的例子中,基础资源 Blogger 对象(假设 Blogger 的名字是 bob)URL 是:
org.apache.solr.blogsample.Blogger/bob。
附属资源使用的 URL 形式是:BASE_RESOURCE_URL/attribute/id,这里的 BASE_RESOURCE_URL代表被附属资源附属的基础资源的 URL;attribute表示的是附属资源附属在基础资源中的属性名。id是指在 BASE_RESOURCE_URL/attribute限定的范围内能唯一地标识资源的信息。例如,在本文的例子中,Article 是基础资源,而 Article 的 Review 就是 Article 的附属资源,如果一个 Article 的 URL 是:
org.apache.solr.blogsample.Article/0
而第一个 Review 的 URL 是:
org.apache.solr.blogsample.Article/0/reviews/0
这里的 reviews 实际上就是 Article 对象中的、用来容纳 Review 信息的属性名。
在不同的应用系统中,基础资源和附属资源的划分根据实际应用的设计思路的不同,其划分也不尽相同。例如:在文中的例子中,Blogger 和 Article 被看作是两种基础资源,所以其 URL 分别是:
org.apache.solr.blogsample.Blogger/bob
org.apache.solr.blogsample.Article/0
而 Review 被看作是 Article 的附属资源,所以其 URL 是:
org.apache.solr.blogsample.Article/0/reviews/0
但是如果设计上考虑将 Article 定义为 Blogger 的附属资源,这个时候 Article 的 URL 就可能是:
org.apache.solr.blogsample.Blogger/bob/articles/0
而 Article 对应的 Review 的 URL 格式就变成了:
org.apache.solr.blogsample.Blogger/bob/articles/0/reviews/0
使用 Solr 进行存储的对象数据
在存储 Java 对象之前,例子程序首先需要将 Java 对象变换为 Solr 可以接受的 XML 格式的 Document。前面已经提到,因为 Solrj 无法为我们处理 URL 信息(因为它不了解例子程序是如何设计 URL 的格式的),所以例子程序对这个处理过程进行了定制,变换后的 XML 格式的 Solr 的 Document 对象数据如下:
清单 4. Blogger 对象的数据格式(Blogger 的名字是 bob)
<doc>
<field name="url">org.apache.solr.blogsample.Blogger/bob</field>
<field name="resource_type">org.apache.solr.blogsample.Blogger</field>
<field name="name">bob</field>
<field name="password">111</field>
<field name="email">bob@gmail.com</field>
<field name="gender">male</field>
<field name="phone">111-111-11</field>
<field name="homepage">http://www.app.com</field>
<field name="articles">[org.apache.solr.blogsample.Article/0]</field>
</doc>
表 1. Blogger 对象的 Document 模型
No. |
Field Name |
描述 |
1 |
url |
存储系统使用的属性,用于唯一标识一个文档的属性,url 的格式参考前面的说明, Blogger 对象的 url 表示它是一个基础对象、且它的 name=bob。 |
2 |
resource_type |
资源的类型,类型的名字在系统中是唯一的,在使用 Java 语言开发的应用中,资源类型显然设定为类名很方便。 |
3 |
name |
Blogger 的名字 |
4 |
password |
Blogger 的密码 |
5 |
|
Blogger 的电子邮件地址 |
6 |
gender |
Blogger 的性别 |
7 |
phone |
Blogger 电话号码 |
8 |
homepage |
Blogger 个人主页 |
9 |
articles |
Blogger 发表的所有 Article 的集合,articles 是一个包含其他对象的 List。同 JSON 的格式类似,List 类型的属性使用“[”和“]”标识开始和结束,List 元素之间使用“,”进行分割。 |
上述的属性中,url 是系统必须的属性,而 resource_type 属性是存储用于进行对象类型跟踪和创建的 Java 对象类型信息。其他的属性是对象本身的属性。您可能奇怪 Blogger bob 的 URL 信息中已经包含了 Java 对象的类型信息为什么这里还需要存储,这是因为只有基础资源的 URL 中包含了基础资源的类型信息,附属资源的 URL 没有包含附属资源的类型信息——它包含的是附属资源附属的基础资源的信息。
清单 5. Article 对象的数据格式(bob 发表的第一篇文章)
<doc>
<field name="url">org.apache.solr.blogsample.Article/0</field>
<field name="resource_type">org.apache.solr.blogsample.Article</field>
<field name="id">0</field>
<field name="title">title111</field>
<field name="content">content111</field>
<field name="date">2010/02/28</field>
<field name="reviews">[org.apache.solr.blogsample.Article/0/reviews/0]</field>
</doc>
表 2. Article 对象的 Document 模型
No. |
Field Name |
描述 |
1 |
url |
存储系统使用的属性,用于唯一标识一个文档的属性,url 的格式参考前面的说明。Article 对象的 url 表示它是一个基础对象、且它的 id=0。 |
2 |
resource_type |
资源的类型,类型的名字在系统中是唯一的,在使用 Java 语言开发的应用中,资源类型显然设定为类名很方便。 |
3 |
id |
Article 的 id,可以唯一的标识 Article |
4 |
title |
Article 的标题 |
5 |
content |
Article 的内容 |
6 |
date |
Article 的发表或更新日期 |
9 |
reviews |
Article 相关的所有 Review 的集合,reviews 是一个包含其他对象的 List。同 JSON 的格式类似,List 类型的属性使用“[”和“]”标识开始和结束,List 元素之间使用“,”进行分割。 |
清单 6. Review 对象的数据格式(bob 发表的第一篇文章的第一条评论)
<doc>
<field name="url">org.apache.solr.blogsample.Article/0/reviews/0</field>
<field name="resource_type">org.apache.solr.blogsample.Review</field>
<field name="id">0</field>
<field name="reviewer">mike</field>
<field name="email">mike@gmail.com</field>
<field name="date">2010/03/02</field>
<field name="content">review content111</field>
</doc>
表 3. Review 对象的 Document 模型
No. |
Field Name |
描述 |
1 |
url |
存储系统使用的属性,用于唯一标识一个文档的属性,url 的格式参考前面的说明。Review 对象的 url 表示它是 Article 对象(id=0)的附属对象。 |
2 |
resource_type |
资源的类型,类型的名字在系统中是唯一的,在使用 Java 语言开发的应用中,资源类型显然设定为类名很方便。 |
3 |
id |
Review 的 id,在其关联的 Article 对象的范围内可以唯一的标识 Review 的信息。 |
4 |
reviewer |
发表 Review 的用户名 |
5 |
|
发表 Review 的用户的电子邮件地址 |
6 |
date |
Review 的发表日期 |
9 |
content |
Review 的内容。 |
从 Solr 返回的检索结果的数据格式
从 Solr 返回的检索结果和存储时使用的数据格式很类似:
清单 7. Blogger 对象的检索结果(Blogger 的名字是 bob)
<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader"> … … </lst>
<result name="response" numFound="1" start="0">
<doc>
<str name="url">org.apache.solr.blogsample.Blogger/bob</str>
<arr name="resource_type"><str>org.apache.solr.blogsample.Blogger</str></arr>
<arr name="email"><str>bob@gmail.com</str></arr>
<arr name="gender"><str>male</str></arr>
<arr name="homepage"><str>http://bob.com</str></arr>
<str name="name">bob</str>
<arr name="password"><str>111</str></arr>
<arr name="phone"><str>1111111</str></arr>
<arr name="articles">
<str>[org.apache.solr.blogsample.Article/0]</str>
</arr>
</doc>
</result>
</response>
清单 8. Article 对象的检索结果(bob 发表的第一篇文章)
<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader"> … … </lst>
<result name="response" numFound="1" start="0">
<doc>
<str name="url">org.apache.solr.blogsample.Article/0</str>
<arr name="resource_type">org.apache.solr.blogsample.Article</str></arr>
<arr name="title"><str>Title-001</str></arr>
<arr name="content"><str>Content-001</str></arr>
<arr name="date"><str>2010/02/28 01:38:18</str></arr>
<str name="id">0</str>
<arr name="reviews">
<str>[org.apache.solr.blogsample.Article/0/reviews/0]</str>
</arr>
</doc>
</result>
</response>
清单 9. Review 对象的检索结果(bob 发表的第一篇文章的第一条评论)
<?xml version="1.0" encoding="UTF-8"?>
<response>
<lst name="responseHeader"> … … </lst>
<result name="response" numFound="1" start="0">
<doc>
<str name="url">org.apache.solr.blogsample.Article/0/reviews/0</str>
<arr name="resource_type">org.apache.solr.blogsample.Review</str></arr>
<str name="id">0</str>
<arr name="reviewer"><str>mike</str></arr>
<arr name="email"><str>mike@gmail.com</str></arr>
<arr name="content"><str>review content111</str></arr>
<arr name="date"><str>2010/02/28 01:38:18</str></arr>
</doc>
</result>
</response>
应用程序将检索到的 XML 文件格式的检索结果变换为 Java 对象后,就可以进行后续的业务处理了。
这就是所有的实现,是不是很简单?
总结
本文首先简单分析了搜索技术的应用和发展,对 Internet 的信息存储方式及这种方式对于构建基础存储系统的意义和使用搜索引擎作为存储服务的实现方法做了简单的分析,并基于开源的 Apache Solr 项目构建了一个基础存储服务系统,在此基础上实现了一个很简单的 BLOG 网站的例子。本文对于那些迫切需要能存储海量数据、分布式和高度可扩展的存储系统的应用提供了一个可行的思路。