Windows Live™ Search 遍访 Internet 上的内容,而且还通过 Web 服务向开发人员公开搜索功能,可以使搜索的范围仅限定为您的站点。这样,您就可以按您认为合适的方式呈现结果。
Microsoft® Office SharePoint® Server 2007 是另一个不错的选择。它是一个完整的站点构建和管理解决方案,而且还提供可靠的搜索功能。图 1 显示了集成到一个自定义 ASP.NET 页面中的 SharePoint 搜索结果。
如果您只对企业搜索感兴趣,则可以选择 Microsoft Office SharePoint Server (MOSS) 2007 的搜索版本(而不是 MOSS 的标准版或企业版)。主要区别是它们可以编入索引的最大文档数量以及将外部数据库等高级内容编入索引的能力。不过,即使采用搜索版本的标准版,您也可以将文件共享、Exchange 公共文件夹、Windows SharePoint Services (WSS) 站点和外部网站编入索引。
构建搜索基础结构
既然有那么多的搜索提供程序可供选择,那么有一点很重要,就是您的站点应采用一种不依赖于底层搜索引擎细节的抽象实现。采取这种方式,您可在以后决定是将 Live Search、SharePoint 还是其他某个提供程序用作搜索解决方案。在本文中,我们将构建一个可插入的搜索基础结构和工具集,您现在就可以将其添加到自己的网站中(代码可从《MSDN® 杂志》网站下载);作为这其中的一部分,我们还将实现 Live Search 和 SharePoint 搜索提供程序,以便您在自己的应用程序中利用其中的一个。
该实现将包括下列功能:
- 支持受查询引擎支持的含有多词和 AND/OR 运算符、完全匹配短语、词语排除以及通配符的高级查询。
- 分页显示结果,可配置每页显示的项目数。
- 支持多种语言,包括按语言过滤或分级的能力。
- 在已查得项目的摘要或说明文字中突出显示搜索到的词语。
还将包括对运行查询所用的搜索引擎进行配置(在 web.config 文件中)的能力。为了实现这些功能,我们将构建一个对象模型,实现一个简化的提供程序模型设计模式,即实际使用特定的搜索引擎实现搜索的多个提供程序类(这些类可视为数据访问层类),以及动态地实例化和使用在配置设置中指定的提供程序的 SearchEngine 类(业务逻辑层类)。
显示了高级 SearchEngine 类的代码,该类具有 ResultsPerPage、QueryText 和 Highlight 等属性,以及称为 ExecuteQuery 的无参数方法,该方法委托在类的静态构造函数中初始化的具体提供程序类的底层 ExecuteQuery 方法(因而对于应用程序的生命周期只创建一次)。
提供程序名称和其他设置位于 web.config 文件的 <appSettings> 节中,如下所示:
<add key="Search.Provider" value="LiveSearchProvider" /> <add key="Search.ResultsPerPage" value="5"/> <add key="Search.Culture" value="en-US"/> <add key="Search.HighlightEnabled" value="True"/>
通过调用 Activator.CreateInstance 返回的提供程序对象被保存到类型 ISearchProvider 的一个静态私有引用中,所有的提供程序类均实现该接口。该接口只有一个方法 ExecuteQuery,该方法将提供程序运行查询所需的全部信息作为输入。该接口的定义如下所示:
public interface ISearchProvider { SearchResults ExecuteQuery(string queryText, string culture, int resultsPerPage, int resultsPageIndex, bool hightlight); }
ExecuteQuery 方法返回 SearchResults 类的一个实例,该类包含多个静态字段,以返回结果总数 (TotalResults)、结果列表 (WebResults) 和查询文本的拼写建议列表。该类的代码如下:
public class SearchResults { public int TotalResults = 0; public List<string> Suggestions = new List<string>(); public List<ResultItem> WebResults = new List<ResultItem>(); }
Suggestions 是字符串集合,WebResults 是 ResultItem 对象集合。ResultItem 类公开一些属性,可返回结果的 URL、Title 和 Description。您可以在图 3中看到实际代码。
现在基本体系结构已准备完毕,我们可以开始实现提供程序类。
构建 Live Search 提供程序
Live Search Web 服务允许您以编程方式运行搜索查询,与在 live.com 上运行的搜索查询类型相同。该 Web 服务位于 soap.search.msn.com/webservices.asmx(用于服务的 WSDL 位于 soap.search.msn.com/webservices.asmx?wsdl),通过使用 Visual Studio® 2005 的“添加 Web 引用”选项,可以创建在自己的代码中用来执行搜索的代理类。对该服务进行实例化的方法如下:
MSNSearchService searchEngine = new MSNSearchService();
在创建了一个搜索引擎类的实例后,可按如下方式创建搜索请求:
SearchRequest searchRequest = new SearchRequest();
注意,免费服务每天限用 25,000 次查询。Live 通过跟踪唯一的“应用程序 ID”对您的查询进行计数,您必须从 search.msn.com/developer 获得该 ID 并用作 SearchRequest 的 AppID 属性值:
searchRequest.AppID = "185A8FADA7931D06D119C2FC1FF104";
SearchRequest 对象需要设置的另一重要属性是 Query,该属性包含的搜索文本和您在 live.com 上指定的搜索文本相同。您可以指定多个词语(AND 运算符是隐式的;可以显式使用 OR 来搜索任意的指定词语),通过将短语放进引号中可以搜索完全匹配的短语,可以使用括号对词语进行分组,还可以使用减号来排除词语。以下示例查找包含词语“ajax”、完全匹配短语“code example”、“Marco”或“Francesco”,但不包含词语“xml”或短语“Visual Basic”的文档:
searchRequest.Query = "ajax AND \"code example\" AND (Marco OR Francesco)" + " –(xml Visual Basic)";
此查询对整个 Web 进行搜索,就像从 live.com 上运行查询一样。如果您想将结果限定为仅搜索您站点上的页面,可以将 site:www.yoursite.com 添加到查询的末尾,如下所示:
searchRequest.Query = "ajax site:msdn.microsoft.com/msdnmag";
请参阅 help.live.com,获得搜索关键字的完整列表。不过,试验搜索字符串及组合多个过滤器和关键字的最佳方式是使用查询生成器工具,该工具位于 dev.live.com/livesearch/sdk(参见图 4)。此工具甚至生成通过查询调用 Web 服务所必需的 C# 代码,所以,它确实是您工具箱中的必备工具。
您还可以通过其他 SearchRequest 属性来设置 SafeSearch 级别(如果希望从结果中排除色情图像和文字,则使用 Moderate)、标记(比如,是否想在结果中突出显示查询词语)和当前区域性的 CultureInfo:
searchRequest.SafeSearch = SafeSearchOptions.Moderate; searchRequest.Flags = SearchFlags.MarkQueryWords; searchRequest.CultureInfo = "en-US";
现在,您必须指定想让 Live Search 返回的结果类型:网页、拼写建议(例如,当您键入“membershipa”时会建议“membership”)、地址和电话号码、新闻、内联答复或图像。对于从您站点返回页面的 Web 搜索,通常只需要包括 Web 结果和拼写建议,其实现方式是创建一个包含两个 SourceRequest 对象的数组,将其作为 SearchRequest 的属性值:
SourceRequest[] sourceRequests = new SourceRequest[2]; searchRequest.Requests = sourceRequests;
然后真正定义这两个源请求,即创建 SourceRequest 对象,指定它们的类型(Web 和 Spelling),指定要在结果中显示的字段(对于拼写建议只指定标题,对于 Web 结果指定所有的典型字段,如标题、Url 和说明)。您还可以指定想要得到的结果数量(通常,对于拼写建议来说 3 个就足够了,对于 Web 结果来说适宜的数量为 10-20 个)和第一个结果的偏移量(在总体结果中的起始索引,对分页十分重要):
sourceRequests[0] = new SourceRequest(); sourceRequests[0].Source = SourceType.Web; sourceRequests[0].ResultFields = ResultFieldMask.All | ResultFieldMask.SearchTagsArray; sourceRequests[0].Count = 20; sourceRequests[0].Offset = 0; sourceRequests[1] = new SourceRequest(); sourceRequests[1].Source = SourceType.Spelling; sourceRequests[1].ResultFields = ResultFieldMask.Title; sourceRequests[1].Count = 3; sourceRequests[1].Offset = 0;
通过调用 MSNSearchService 的 Search 方法来运行查询,并在输入中定义了 SearchRequest 对象:
SearchResponse searchResponse = searchEngine.Search(searchRequest);
调用 Search 会返回一个 SearchResponse 类型的对象。该对象的 Responses 属性是一个 SourceResponse 类型的对象数组,与以上定义的 SourceRequest 一一对应。在本例中,这就表示有一个包含两个对象的数组,一个用于 Web 结果,另一个用于拼写建议。SourceResponse 对象有一个名为 Total 的属性,表示执行查询后找到的结果总数。应该注意,Total 不是返回的结果数量(按照可分页报告的当前索引,结果通常为一个子集),而是全部结果的总数。实际结果可通过 SourceResponse 的 Results 属性进行访问,该属性是一个 Result 对象的集合。Result 对象有多个属性,其中包括 Title、Url 和 Description,根据您在相应的 SourceRequest 定义指定要返回的属性,这些属性可能已填入,也可能留空:
// Process the Web results int total = searchResponse.Responses[0].Total; foreach (Result result in searchResponse.Responses[0].Results) { // access the result.Title, result. //Url, and other values... } // Process the spelling results foreach (Result result in searchResponse.Responses[1].Results) { // print the result.Title value... }
这就是全部工作。概括来说,您使用两个 SourceRequest 对象定义了一个 SearchRequest,指定要返回 Web 结果和拼写建议,并启用了突出显示和裁决搜索;然后针对该 SearchRequest 调用了搜索引擎的 Search 方法,并对产生的 SourceResponses 的 Results 集合进行了处理。图 5 列出了 LiveSearchProvider 类的完整代码,该类实现了早先定义的 ISearchProvider 接口。此代码类似于您在本节中所看到的代码,几乎每一行都类似,当然,要求的许多信息都不是硬编码到类中,而是作为输入参数取得或从配置文件中读取。
构建 SharePoint 2007 提供程序
SharePoint Portal 2007 搜索功能非常强大。我们将仅针对我们的特定目标探讨那些最重要的功能,不过,如果可以阅读一些适用于系统管理员和开发人员的指南进行深入了解。
让我们从“共享服务”下的“搜索设置”部分中基于 SharePoint Web 的管理应用程序开始,创建一个新的搜索“内容源”。这将定义您想要编入索引的内容、方式和时间。SharePoint Portal 允许您将典型的 Internet 网站、SharePoint 网站(以特定的方式进行处理,因为它直接从数据库中获取内容)、文件共享中的文档、自定义数据库等编入索引。在本例中,您将选择第一个选项,并指定要编入索引的站点的根 URL。SharePoint 将从该 URL 开始编入索引,并通过反复跟随链接而继续索引(可以指定它跟随链接的深度,或者指定它应该对同一个域上的所有页面建立索引)。请参照图 6 查看您创建内容源所在的页面。
创建内容源时,还可以通知 SharePoint(在页面底部)在确认新的源后立即启动索引进程,并建立一个增量索引计划(例如,每天的午夜运行该进程,此时服务器上的负载较轻)。在图 7 中可以看到这些配置。
最后的配置步骤包括创建一个搜索范围,以便在运行搜索时可以限定所需的源。如果对多个网站分别建立了索引,但您想从一个站点而不是从其他站点搜索内容,这种限定就特别重要。要完成此操作,只需填写图 8 所示向导中的字段(注意,您也可以创建一个范围,包括除特定 URL 或内容源之外的所有内容)。
定义了新内容源后,单击“搜索设置”页面中的命令以更新内容源,使其包含的文档数得到重新计算。此时,从这个列出所有内容源的页面中,可查看这一有用的信息。
现在该编写一些代码来查询索引了。这在 SharePoint 中有两种实现方式,可任选其一:通过其本地对象模型(SharePoint DLL 程序集)或通过 SharePoint Web 服务。第一个方法的唯一适用情况是您的站点与 SharePoint 在同一台服务器上运行,这种情况相当少见。因此,最好使用 Web 服务,这样无论您的站点是否在同一台服务器上运行,都可以调用 Web 服务。在 Visual Studio 中,将 Web 引用添加到 YourSharePointServerName/_vti_bin/search.asmx,使其创建 QueryService 代理类。在编写代码工作中,最重要的部分是精心编制一个正确的查询,该查询结合使用一种类似 SQL 的语言和 XML。以下是一个查询的 SQL 部分的示例:
string sqlQuery = "SELECT Path, Rank, Title, HitHighlightedSummary FROM MSDNMagazine..scope() WHERE FREETEXT('ajax asynchronous') ORDER BY Rank DESC";
您可以看到,这与使用 SQL Server 全文搜索的正常 SQL 查询非常相似,即使索引存储在文件中,而不是在 SQL Server 数据库中。FROM 子句没有指定一个表名,而是指定搜索范围的名称,后面跟随“..scope()”。WHERE 筛选器使用 FREETEXT 谓词,这样便可指定多个高级条件。该查询将返回已查得结果的路径、标题、等级和说明,正如在 SELECT 中指定的那样。该 SQL 并不是原封不动地传入查询引擎,而是封装为 XML 请求的一部分,XML 请求还定义了其他信息,比如想要检索的结果数、起始偏移量(最小为 1,而不是同 Live Search 一样为 0)、区域性等等。图 9 显示了一个完整的请求(注意 SQL 查询如何插入到周围的 XML 中)。
实际运行该查询并检索结果只不过是实例化一个 QueryService 对象、将其凭据设置为适当的凭据(SharePoint 根据与这些凭据相关联的权限对结果进行筛选,但由于我们的目标是一个公共网站的搜索范围,所以不会滤除任何结果)并以 XML 请求作为输入参数调用 Query 方法:
QueryService queryService = new QueryService(); queryService.UseDefaultCredentials = true; string xml = queryService.Query(xmlQuery);
图 10显示了一个 XML 响应的一部分。
代码的其余部分只是通过一个 XmlDocument 分析 XML 文本。在图 11中可以看到 SharePointSearchProvider 的完整实现。
值得注意的是,除 Query 之外,QueryService 还公开一个 QueryEx 方法。该方法返回 DataTable 类型的结果,而不是原始的 XML。对于开发人员来说,这样就让事情变得轻松多了,因为不需要进行任何分析。然而,该方法不返回结果总数,而这是实现搜索页面分页功能所必需的(除非您想始终检索所有结果,然后在内存中完成分页,但我不推荐这种方法),所以,我们改用基本的 Query 方法。
构建自定义搜索页面
现在我们已完成了高层的接口和底层的提供程序代码,剩下的只是自定义 Search.aspx 了。在可下载的源代码中,您会找到完整的实现,因此我只是强调最重要的部分。搜索框由一个文本框和一个提交按钮组成。在页面的结果报告部分,有一个 Repeater 控件列出了拼写建议,即指向页面自身的超链接,同时建议文本通过 querystring 的“q”参数传递:
<asp:Repeater runat="server" ID="rptSuggestions"> <ItemTemplate> <asp:HyperLink runat="server" ID="lnkSuggestionUrl" NavigateUrl='<%# "~/Search.aspx?q=" + DataBinder.Eval(Container, "DataItem") %>' Text='<%# DataBinder.Eval(Container, "DataItem") %>' /> </ItemTemplate> <SeparatorTemplate>, </SeparatorTemplate> </asp:Repeater>
如果您在想为什么没有使用 LinkButton 控件代替超链接,这是因为这些控件会引起回发,而不是用更新后的 querystring 再次向页面发出请求,这样会造成用户无法将快捷方式保存到页面,以后再回到该页面时将无法得到同样的结果。
同样,该原则也适用于具有指向同一页面的超链接的各个分页栏,用同一“q”参数表示查询文本,用“p”参数表示指定要显示的结果页面的索引。实际 Web 结果通过 DataList 控件进行显示,该控件呈现每个结果的标题和指向实际找到页面的超链接,并附带某些说明文字,如下所示:
<asp:DataList ID="dlstResults" runat="server" ...styles here...> <ItemTemplate> <asp:HyperLink runat="server" ID="lnkResultUrl" NavigateUrl='<%# Eval("Url") %>' Text='<%# Eval("Title") %>' /><br /><%# Eval("Description") %> </ItemTemplate> </asp:DataList>
代码隐藏也很简单。UI 开发人员无需知道关于后台所用搜索引擎的任何事情。其实,她甚至不需要知道使用了哪个搜索引擎。这是因为所有的逻辑都已包装到 SearchEngine 类中。下列代码显示了如何使用该类,以及结果如何绑定到 UI 控件:
SearchEngine search = new SearchEngine(); search.PageIndex = this.PageIndex; search.QueryText = txtQueryText.Text; SearchResults results = search.ExecuteQuery(); rptSuggestions.DataSource = results.Suggestions; lblTotal.Text = results.TotalResults.ToString(); dlstResults.DataSource = results.WebResults; Page.DataBind();
该结果与您在图 1 中看到的结果相同。
展望未来:SearchDataSource 控件
2007 年 5 月发布的 ASP.NET Futures (asp.net/downloads/futures) 引入了一个新的名为 SearchDataSource 的数据源控件(类似于所有其他的数据源控件,如 ObjectDataSource 和 SqlDataSource),可以绑定到 Repeater、DataList 或 GridView 等控件,并自动提供要呈现的数据。当然,所提供的数据是 Web 搜索的结果,该搜索由在 web.config 文件中注册和配置的提供程序类之一运行:
<search enabled="true"> <providers> <add name="WindowsLiveSearchProvider" type="Microsoft.Web.Preview.Search.WindowsLiveSearchProvider, Microsoft.Web.Preview" appID="185A8FADA7931D06D119C2FC1FF104" siteDomainName="msdn.microsoft.com/msdnmag" /> </providers> </search>
数据源控件本身可添加到页面,这既可通过可视化的 Visual Studio 设计器实现,也可使用下列标记实现:
<asp:TextBox ID="txtQuery" runat="server" /> <asp:Button ID="btnSearch" runat="server" Text="Search" /> <asp:SearchDataSource ID="SearchDataSource1" runat="server" > <SelectParameters> <asp:ControlParameter ControlID="txtQuery" Name="query" PropertyName="Text" /> </SelectParameters> </asp:SearchDataSource>
您可以看到,有一个控件参数 query 被绑定到 txtQuery TextBox 的 Text 属性。在回发过程中,数据源控件自动重新刷新(也可以通过调用该控件的 DataBind 方法以编程方式强制执行此操作),使用该 TextBox 中包含的值运行查询,并将结果绑定到关联的数据控件(即 DataSourceID="SearchDataSource1" 的控件)。如果使用 DataList 或 Repeater 控件显示结果,则其模板定义将与我们的自定义解决方案早先显示的代码相同,因为绑定到控件的结果为 SearchResult 类型,且包含 Title、Url 和 Description 属性,这与我们的 WebResult 类一样。因此,绑定表达式将是相同的。
如果您想创建自己的提供程序,则定义一个继承 SearchProviderBase 的类,重写其 Search 方法,以提供特定的搜索引擎实现,并重写 Initialize 方法,以读取配置设置:
public class SharePointSearchProvider : SearchProviderBase { public SharePointSearchProvider() : base() { } public override SearchResult[] Search(SearchQuery searchQuery) { ... } public override void Initialize(string name, NameValueCollection config) { ... } }
SearchDataSource 控件的实现方式是非常完善的,因为它完全实现了提供程序模型设计模式(不像本文提供的简化版解决方案),同样具有吸引力的是能够用可视化的方式将此控件放到窗体上并绑定到某个数据控件,因为它不需要您编写任何代码。不过,在我写这篇文章的时候,该控件的功能还非常有限。预计 Microsoft 很快就会扩展基础提供程序类和输入 SearchQuery 类型,使其具有更多功能和成员。如果是这样,您将能够轻松地把此处展示的解决方案迁移到 SearchDataSource 模型:您需要完成的全部工作就是,将搜索实现从实现了 ISearchProvider 接口的类中转移到继承了 SearchProviderBase 的类中,从提供程序的属性或 SearchQuery 的属性中读取输入参数,而不是从 ExecuteQuery 的输入值中读取。标记代码将不作实质性更改,因为 SearchResult 和 WebResult 类型具有相同的属性。