 | 级别: 初级 张 军 (zhjun@cn.ibm.com), 软件工程师, IBM 田 晟 (tiansh@cn.ibm.com), 软件工程师, IBM
2009 年 6 月 08 日 搜索是人们今天使用的最多的服务之一,但是通常人们需要利用不同的搜索服务来找到所需的资料,例如使用 Google 通用搜索,Wikipedia 的搜索,Del.icio.us 的搜索和 developerWorks 的搜索等等来查找资料。在不同的情形下,人们对于搜索组合的需求也是不一样的。例如,如果人们需要去某地旅游,他可能同时要用到 Google 地图搜索,Yahoo 当地天气搜索,旅游地风景名胜搜索,以及机场机票价格搜索。本文利用了 OpenSearch 规范来描述不同的搜索服务,通过利用 IBM 的 iWidget 规范,创建了一个可以允许最终用户定制的搜索门户系统。
一个用户可定制的搜索门户系统的介绍
搜索门户的主要功能是帮助最终用户按照不同的使用场景组织他们最常用的搜索服务。用户根据自己的需要从系统的搜索服务列表中查找搜索服务并把它们添加到自己的搜索门户当中。搜索服务可以通过开发人员提供的搜索服务的 OpenSearch 规范描述文档或者搜索门户系统提供的服务发现功能添加进来。
搜索门户系统
下图是一个最终用户使用“搜索门户系统”时的截图,用户同时在“Google Search”、“Google Image Search”、“Google Map Search”、“Flickr search”、“YouTube Search”等服务中同时搜索“opensearch”。
图 1: 搜索页面分栏布局
用户也可以在“分页布局”和“分栏布局”之间进行切换。如果选择“分页布局”,则如下图所示:
图 2: 搜索页面分页布局
通过搜索服务定制页面,用户是可以定制自己的搜索门户中使用的搜索服务。这些搜索服务都是按照 OpenSearch 规范描述的。用户可以在搜索服务列表中添加需要的搜索服务到自己的门户中。用户或者开发人员也可以通过定制发布一些新的搜索服务。下一节会介绍 OpenSearch 规范和如何描述一个搜索服务。
图 3: 搜索服务定制页面
OpenSearch 规范介绍
OpenSearch 是一组用来描述搜索服务的技术集合。通过 OpenSearch 技术,网站可以把自己的搜索服务暴露出来供他人消费。现在很多网站都提供了 OpenSearch 的描述文档,当今主流的浏览器例如 Firefox3.0 现在都有 OpenSearch 描述文档的发现功能。
例如在 Firefox3 中打开 IBM 的 developerWorks 英文网站,可以看到浏览器搜索栏的图标加亮了,点击之后会会有个提示,这是因为 Firefox 识别出了 IBM developerWorks 的 OpenSearch 描述文档,用户可以把这个搜索服务添加到自己的浏览器的搜索服务列表中。
图 4: 浏览器自动发现网站 OpenSearch 服务
有了 OpenSearch 的描述文档,网站就可以把搜索服务暴露出来供用户或第三方工具使用。下面以 IBM developerWorks 中国的网站为例介绍 OpenSearch 的规范。
使用 OpenSearch 规范描述 IBM developerWorks 中文网站的搜索服务
现在 developerWorks 中文网站还没有提供 OpenSearch 的描述文档,我们可以通过自定义 OpenSearch 规范来描述 developerWorks 中国的搜索。下面是一个示例的 OpenSearch 的描述文档:
清单 1: OpenSearch 的描述
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>IBM developerWorks 中文网站 </ShortName>
<Description>IBM developerWorks 中文网站 </Description>
<InputEncoding>UTF-8</InputEncoding>
<Url type="text/html" method="GET"
template="http://www.ibm.com/developerWorks/search/searchResults.jsp?
searchType=1&searchSite=dWChina&pageLang=zh&langEncoding=UTF8&
searchScope=dW&query={searchTerms}"/>
</OpenSearchDescription>
|
在这个文档中,包括了 ShortName, Description 和 Url 等元素。其中 ShortName 和 Description 是对搜索服务的一个简单的描述。
Url 元素则是 OpenSearch 描述文档的核心。
-
Url 的属性 type, 描述了搜索服务返回的结果的 MIME 类型,这里是 text/html
-
Url 的属性 method, 描述了请求这个搜索服务时使用的 HTTP method 的类型
-
Url 的属性 template 描述了搜索服务,它定义了一个搜索服务的 url 的模板,这里使用大括号括起来的 {searchTerms} 来定义查询关键词在 Url 中的位置。
有了这个 IBM developerWorks 中国的 OpenSearch 的服务描述文档之后,搜索门户系统就可以利用这个描述文档来调用 developerWorks 中国的搜索服务。开发人员可以基于这个 OpenSearch 的服务描述文档开发自定义的搜索界面。接下来我们介绍在“搜索门户系统”中如何使用 IBM iWidget 规范定义的用户的搜索界面消费上面定义的 developerWorks 中国网站搜索服务。
IBM iWidget 规范介绍
iWidget 规范是 IBM 定义的一个基于浏览器的开放标准的组件模型规范。伴随这富互联网应用进入主流,浏览器里面越来越需要一个组件模型。基于这个组件模型开发的组件能够提供很好的可重用性。同时,Mashup 已经成为构建一个情景应用的有效手段,iWidget 规范作为一个组件模型,也是 IBM 的 Mashup 产品– Lotus Mashups 的基础。
基本概念:
-
iWidget : 一种面向浏览器的组件。为 Web 页面提供逻辑服务或者可视化部件。
-
iContext: 在 iWidget 的运行时。提供对 iWidget 页面范围内管理的上下文支持,包括页面内 iWidget 之间的交互,iWidget 和后台的交互等。
-
ItemSet: iWidget 的用户属性。用来保存该 iWidget 运行时需要的一些设置,例如一些用户的配置。这些属性可以通过 iContext 持久化下来,在下次页面刷新时重新载入。
-
Mode: iWidget 所处的模式。一个 iWidget 可以有多个模式,不同的模式对应了 iWidget 不同的 HTML 标记片段。通常情况下 iWidget 都会包括两个模式:显示模式和编辑模式。
-
iEvent: 一种 iWidget 之间页面级别相互通信的方式,iEvent 可以分为发布事件和接受事件两种。通过关联两个不同 iWidget 的 iEvent,iWidget 之间可以通过事件来互动和传递数据。
iWidget 使用 XML 来描述,下面是一个“hello world”iWidget 的示例:
清单 2:iWidget 示例
<?xml version="1.0" encoding="UTF-8"?>
<iw:iwidget name="myWidget" xmlns:iw="http://www.ibm.com/xmlns/prod/iWidget"
supportedModes="view edit" mode="view">
<iw:content mode="view">Hello World!</iw:content>
<iw:content mode="edit">Configure me.</iw:content>
</iw:iwidget>
|
下面我们看一下,如何使用 iWidget 来构建“搜索门户系统”。
搜索门户系统的实现
“搜索门户系统”由两个页面构成,分别是搜索页面和搜索服务定制页面。前者为用户日常使用主界面,后者是系统的搜索服务后台管理页面。
搜索页面的构成
搜索页面是由一个用于输入搜索关键字的用户输入部件 iWidget 以及若干个用于显示搜索结果的 OpenSearch 显示部件 iWidget 组装起来,如图 1 所示。两种 iWidget 之间通过 iEvent 关联。用户在用户输入部件 iWidget 中输入待搜索关键字并点击搜索按钮后,每个 OpenSearch 显示部件 iWidget 会通过 iEvent 接收到用户输入的关键字。再通过存储在每个 iWidget 的 ItemSet 中的 OpenSearch 描述文件地址向“搜索门户系统”服务端请求搜索结果。如图所示:
图 5: 搜索页面构成示意图
开发一个 iWidget 用来请求搜索服务并且显示搜索结果
正如前面所展示的,在“搜索门户系统”中通过 OpenSearch 显示部件 iWidget 展示每一个搜索服务结果。服务端接收到 OpenSearch 显示部件发送来的搜索关键字和 OpenSearch 描述文件地址后,通过解析 OpenSearch 文档,构建成实际的搜索请求,并且将搜索结果返回给 OpenSearch 显示部件 iWidget。根据 OpenSearch 的规范,搜索结果可以是搜索页面重定向地址,也可以是 HTML 类型,或者是 RSS 或 ATOM Feed 类型。OpenSearch 显示部件 iWidget 根据返回搜索结果的类型,对不同的类型做不同的渲染处理,再把结果呈现给用户。
下面是这个 iWidget 的定义文件
清单 3:iWidget 定义文件
<iw:iwidget name="openSearchViewlet" xmlns:iw="http://www.ibm.com/xmlns/prod/iWidget"
iScope="com.ibm.mm.openSearchViewlet"
allowInstanceContent="false" supportedModes="view edit" mode="view" lang="en">
<iw:itemSet id="attributes" >
<iw:item id="version" value="1.0.0.0.0"/>
<iw:item id="openSearchDescUrl" value="" readOnly="false"/>
</iw:itemSet>
<iw:event id="Accept Search Keyword" eventDescName="keyword" handled="true"
onEvent="onKeywordReceived" onNewWire="onKeywordReceived" />
<iw:eventDescription id="keyword" payloadType="text"
description="Text to search" lang="en">
<iw:alt description="Text to search" title="Accept Search Keyword"
lang="en"/>
</iw:eventDescription>
<iw:resource uri="openSearchViewlet.js" />
<iw:content mode="view">
<![CDATA[
<div>
<div id="_IWID_loading" style="font-size:1.4em;"><img alt="loading..."
src="/mum/js/com/ibm/mm/enabler/iw/progress-anim.gif" />Loading...</div>
<div id="_IWID_errorMessage" style="font-size:1.4em;display:none;
"><img alt="Error occurred!" src="/mum/images/error_icon.png" />
Error occured!</div>
<div id="_IWID_emptyMessage" style="font-size:1.4em;display:none">
<img alt="No result" src="/mum/images/info_icon.png" />
No result!</div>
<div id="_IWID_searchResult" style="display:none"></div>
</div>
]]>
</iw:content>
<iw:content mode="edit">
<![CDATA[
<div id="_IWID_editRoot" style="padding:20px">
<form id="_IWID_OSProviderForm" onsubmit="return false;">
<p>
<label for="_IWID_opensearchDescUrl">
OpenSearch Description URL:</label>
<input name="openSearchDesc" type="text">
</p>
<p>
<label>Content Type:</label>
<input type="radio" name="contentType" value="html"
title="HTML" checked>HTML</input>
<input type="radio" name="contentType" value="feed"
title="Feed (Atom/RSS)">Feed (Atom/RSS)</input>
</p>
<div>
<input name="save" type="button" onclick="iContext.iScope().save()"
value="Save">
<input name="cancel" type="button"
onclick="iContext.iScope().cancel()" value="Cancel">
</div>
</form>
</div>
]]>
</iw:content>
</iw:iwidget>
|
OpenSearch 显示部件 iWidget 分为 view 和 edit Mode。实际运行时,用户只看到 View Mode 的内容。这个 iWidget 的 ItemSet 中 openSearchDescUrl 用于存放 OpenSearch 描述文档地址。同时 iWidget 定义了接受搜索关键字的 iEvent。属性 onEvent 中定义了接收到 iEvent 之后的响应函数:onKeywordReceived。这个响应函数会在 iw:resource 元素指向的 Javascript 文件中定义,函数定义如下:
清单 4:Javascript 函数定义
onKeywordReceived: function(iEvent) {
this._showLoadingMessage();
var keyword = iEvent.payload;
var searchPrefix = this._searchTemplate.replace("{fileurl}", this.osDescriptionUrl);
var searchUrl = searchPrefix + keyword;
var params = [];
if (this.osContentType === "html") {
// use website iwidget
params = [{
"itemName": "url",
"itemValue": searchUrl
}];
this._createIWidget(this.resultDiv, this._widgetDefXML.htmlFragment,
params, null, null);
} else if (this.osContentType === "feed") {
// use feed reader iwidget
params = [{
"itemName": "feedURL",
"itemValue": searchUrl
}];
this._createIWidget(this.resultDiv, this._widgetDefXML.feedReader,
params, null, null);
} else {
// No content type information
dojo.xhrGet({
url: searchUrl,
handle: dojo.hitch(this, function(response, ioArgs) {
if (!response && ioArgs.xhr.getResponseHeader("
Content-Length") == 0) {
this._showEmptyMessage();
return;
}
if (ioArgs.xhr.getResponseHeader("Content-Type") === "text/html") {
this.parseHtmlContent(response);
} else if (ioArgs.xhr.getResponseHeader("Content-Type") ===
"application/atom+xml" || ioArgs.xhr.getResponseHeader("
Content-Type") ===
"application/rss+xml") {
this.parseFeedContent(response);
} else {
this._showErrorMessage();
}
}),
error: dojo.hitch(this, this.onError)
});
return;
}
this._showResult();
}
|
如果服务端结果返回搜索结果重定向 URL,则使用 WebSite Displayer iWidget 进行显示,如果返回 Feed,则使用 Feed Reader iWidget 进行显示。
搜索门户系统服务端需要定义 iWidget 和服务端交互的 REST 服务接口:
GET <host-name>/oscatalog/invoke?osdescriptionurl={osdescriptionUrl} &q={searchTerms}
例如,在 Flickr 中查找有关 Web 的图片,在搜索门户系统中关于 Flickr 的搜索服务的描述文档地址为 <host-name>/oscatalog/travel/flickr.xml。那么 iWidget 与服务端交互的请求的 url 为:<host-name>/oscatalog/invoke?osdescriptionurl=http://localhost:8080/oscatalog/travel/flickr.xml&q=web
服务端解析 OpenSearch 描述文档,构建搜索请求
服务端实现了一个 invoke 的 servlet,如下:
清单 5:serlet 实现
public class InvokeOSProviderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String osDescriptionFile = req.getParameter("osdescriptionurl");
String keywords = req.getParameter("q");
OpenSearchInvokerImpl.instance.invoke(osDescriptionFile, keywords, resp);
}
}
|
解析 OpenSearch 描述文档,构建查询请求的 url 的代码是在 OpenSearchInvokerImpl 类中实现的。该类实现了接口 OpenSearchInvoker:
清单 6: OpenSearchInvoker 接口实现
public interface OpenSearchInvoker {
public void invoke(String openSearchURL, String searchTerms,HttpServletResponse resp);
}
|
OpenSearchInvokerImpl 会解析 OpenSearch 的描述文档,构建查询请求,如果搜索服务返回的结果是“text/html”,则返回 redirect 指令,否则将作为一个代理,获取到搜索服务请求的结果。
清单 7: OpenSearchInvoker 实现
public void invoke(String openSearchURL, String searchTerms,HttpServletResponse resp) {
HttpURLConnection conn = null;
try {
Map<String,String> defaultURLTemplate =
Utils.getDefaultUrlTemplateInOSDescription(openSearchURL);
String contentType = defaultURLTemplate.get("contentType");
String template = defaultURLTemplate.get("template");
System.out.println("search Terms:"+searchTerms);
String instantiatedURL = template.replace("{searchTerms}",
URLEncoder.encode(searchTerms));
if("text/html".equalsIgnoreCase(contentType)){
System.out.println("redirect client to url:"+instantiatedURL);
resp.sendRedirect(instantiatedURL);
return;
}
//else it's feed, so retrieve it...
System.out.println("fetch result:"+instantiatedURL);
URL url = new URL(instantiatedURL);
conn = (HttpURLConnection)url.openConnection();
conn.setRequestProperty ( "User-Agent", "Mozilla/4.0 ( compatible
) " ) ;
conn.setRequestProperty ( "Accept-Language",
"en-US,en;q=0.9,de-DE;q=0.8,de;q=0.6,zh-CN;q=0.5,zh;q=0.4,en-us;
q=0.3,en;q=0.1" ) ;
resp.setContentType(contentType);
byte[] buffer = new byte[1024];
int length;
while((length=conn.getInputStream().read(buffer))!=-1){
resp.getOutputStream().write(buffer,0,length);
}
resp.getOutputStream().flush();
resp.getOutputStream().close();
} catch (IOException e) {
logger.log(Level.WARNING,e.getMessage(),e);
}
}
|

 |

|
实现一个搜索服务提供者的编目
搜索服务提供者的编目是“搜索门户系统”中一个很重要的组件。用户可以通过提供一个 OpenSearch 描述文档来发布一个新的搜索服务到编目中。下图是“搜索门户系统”中发布一个新的搜索服务提供商的界面。可以看到,在编目中关于一个搜索服务提供商的数据包括标题 (title)、标签 (tags) 和该搜索服务的 OpenSearch 描述文档的 url 等。
图 5: 添加 OpenSearch 搜索服务
服务端也为编目提供了相应的 REST 的接口,通过这个接口可以查询到与特定关键词相关的搜索服务提供者:
GET <host-name>/oscatalog/providers?searchTerms={searchTerms}
返回的结果使用 JSON 格式描述。如在编目中搜索与 Google 有关的搜索服务 - <host-name>/oscatalog/providers?searchTerms=Google,返回的结果如下:
清单 8:返回结果
{
"itemsPerPage": 10,
"totalResults": 3,
"entries": [
{
"tags": [
"Google"
],
"title": "Google Search",
"osDescriptionURL": "http:\/\/localhost:8080\/oscatalog\/samples\/google.xml",
"contentType": "text\/html"
},
{
"tags": [
"Search in Google Images"
],
"title": "Google Images Search",
"osDescriptionURL":
"http:\/\/localhost:8080\/oscatalog\/travel\/googleimage.xml",
"contentType": "text\/html"
},
{
"tags": [
"Google Map Search"
],
"title": "Google Map Search",
"osDescriptionURL":
"http:\/\/localhost:8080\/oscatalog\/travel\/googlemap.xml",
"contentType": "text\/html"
}
],
"startIndex": 1
}
|
结束语
本文介绍了 OpenSearch 开放标准,同时利用 IBM iWidget 组件化的优势,快速构建可定制的搜索门户系统页面。这个系统提供了一个门户,满足了人们在一定的情景下需要使用多个搜索服务的需求。IBM Lotus Mashups 为本系统提供了良好的 iWidget 运行时支持。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| 本文的示例代码1 | openSearchViewlet.zip | 3KB | HTTP |
|---|
| 本文的示例代码2 | searchInputBox.zip | 2KB | HTTP |
|---|
参考资料
作者简介  | |  | 张军就职于 IBM中国软件开发中心 Web2.0 开发小组,对 J2EE,RESTFul 服务和 iWidget, dojo的开发有着丰富的经验。 |
 | |  | 田晟于 2007 年加入 IBM 中国软件开发中心。曾在 IBM ETI 部门从事 Web2.0 相关工作。目前在 Lotus 部门从事 IBM Mashup Center 的相关开发工作。他加入 IBM 前毕业于中山大学,获得计算机硕士学位。 |
对本文的评价
|  |