警告
2020 年 10 月 30 日,必应搜索 API 从 Azure AI 服务迁移到必应搜索服务。 本文档仅供参考。 有关更新的文档,请参阅必应搜索 API 文档。 关于为必应搜索创建新的 Azure 资源的说明,请参阅通过 Azure 市场创建必应搜索资源。
此单页应用演示如何从必应 Web 搜索 API 检索、分析和显示搜索结果。 本教程使用样本 HTML 和 CSS,重点介绍 JavaScript 代码。 GITHub 上提供了 HTML、CSS 和 JS 文件,并提供快速入门说明。
此示例应用可以:
- 使用搜索选项调用必应“Web”搜索 API
- 显示 Web、图像、新闻和视频结果
- 分页结果
- 管理订阅密钥
- 处理错误
若要使用此应用,需要使用具有必应搜索 API 的 Azure AI 服务帐户 。
先决条件
以下是运行该应用所需的一些事项:
Azure 订阅 - 免费创建订阅
获得 Azure 订阅后,在 Azure 门户中创建必应搜索资源 以获取密钥和终结点。 部署后,单击 转到资源。
Node.js 8 或更高版本
第一步是使用示例应用的源代码克隆存储库。
git clone https://github.com/Azure-Samples/cognitive-services-REST-api-samples.git
然后运行 npm install
。 对于本教程,Express.js 是唯一的依赖项。
cd <path-to-repo>/cognitive-services-REST-api-samples/Tutorials/bing-web-search
npm install
应用组件
我们构建的示例应用由四个部分组成:
-
bing-web-search.js
- 我们的 Express.js 应用。 它处理请求/响应逻辑和路由。 -
public/index.html
- 应用框架;它定义如何将数据呈现给用户。 -
public/css/styles.css
- 定义页面样式,如字体、颜色、文本大小。 -
public/js/scripts.js
- 包含向必应 Web 搜索 API 发出请求、管理订阅密钥、处理和分析响应以及显示结果的逻辑。
本教程强调scripts.js
和调用必应 Web 搜索 API 以及处理响应所需的逻辑。
HTML 表单
其中包括 index.html
一个表单,使用户能够搜索和选择搜索选项。 当表单提交时,属性 onsubmit
将被触发,调用在 bingWebSearch()
中定义的 scripts.js
方法。 它需要三个参数:
- 搜索请求
- 所选选项
- 订阅密钥
<form name="bing" onsubmit="return bingWebSearch(this.query.value,
bingSearchOptions(this), getSubscriptionKey())">
查询选项
HTML 窗体中的选项映射到 必应 Web 搜索 API v7 的查询参数。 下表详细介绍了如何使用示例应用筛选搜索结果:
参数 | 说明 |
---|---|
query |
用于输入查询字符串的文本字段。 |
where |
用于选择市场(位置和语言)的下拉菜单。 |
what |
用于提升特定结果类型的复选框。 例如,提升图像会增加搜索结果中图像的排名。 |
when |
一个下拉菜单,允许用户将搜索结果限制为今天、本周或本月。 |
safe |
启用必应安全搜索的复选框,用于筛除成人内容。 |
count |
隐藏字段。 要在每个请求中返回的搜索结果数。 更改此值以显示每页的更少或更多结果。 |
offset |
隐藏字段。 请求中第一个搜索结果的偏移量,用于分页显示。 每次有新请求时,都会重置为 0 。 |
注释
必应 Web 搜索 API 提供其他查询参数来帮助优化搜索结果。 此示例只使用几个。 有关可用参数的完整列表,请参阅 必应 Web 搜索 API v7 参考。
该 bingSearchOptions()
函数转换这些选项以匹配必应搜索 API 所需的格式。
// Build query options from selections in the HTML form.
function bingSearchOptions(form) {
var options = [];
// Where option.
options.push("mkt=" + form.where.value);
// SafeSearch option.
options.push("SafeSearch=" + (form.safe.checked ? "strict" : "moderate"));
// Freshness option.
if (form.when.value.length) options.push("freshness=" + form.when.value);
var what = [];
for (var i = 0; i < form.what.length; i++)
if (form.what[i].checked) what.push(form.what[i].value);
// Promote option.
if (what.length) {
options.push("promote=" + what.join(","));
options.push("answerCount=9");
}
// Count option.
options.push("count=" + form.count.value);
// Offset option.
options.push("offset=" + form.offset.value);
// Hardcoded text decoration option.
options.push("textDecorations=true");
// Hardcoded text format option.
options.push("textFormat=HTML");
return options.join("&");
}
SafeSearch
可以设置为 strict
、moderate
或 off
,其中 moderate
是必应 Web 搜索的默认设置。 此窗体使用一个复选框,其中包含两种状态: strict
或 moderate
。
如果选择了任何 “升级 ”复选框,则参数 answerCount
将添加到查询中。
answerCount
使用 promote
参数时是必需的。 在此代码片段中,该值设置为 9
返回所有可用的结果类型。
注释
提升结果类型不能保证它会被包含在搜索结果中。 相反,提升排名会使这类结果的排名相对其平时的排名提高。 若要将搜索限制为特定类型的结果,请使用 responseFilter
查询参数,或调用更具体的终结点,例如必应图像搜索或必应新闻搜索。
查询参数textDecoration
和textFormat
被硬编码到脚本中,这会导致搜索词在搜索结果中以加粗形式显示。 不需要这些参数。
管理订阅密钥
为了避免对必应搜索 API 订阅密钥进行硬编码,此示例应用使用浏览器的持久存储来存储订阅密钥。 如果未存储任何订阅密钥,系统会提示用户输入一个。 如果 API 拒绝订阅密钥,系统会提示用户重新输入订阅密钥。
该 getSubscriptionKey()
函数使用 storeValue
和 retrieveValue
函数来存储和检索用户的订阅密钥。 这些函数使用 localStorage
对象(如果受支持)或 Cookie。
// Cookie names for stored data.
API_KEY_COOKIE = "bing-search-api-key";
CLIENT_ID_COOKIE = "bing-search-client-id";
BING_ENDPOINT = "https://api.cognitive.microsoft.com/bing/v7.0/search";
// See source code for storeValue and retrieveValue definitions.
// Get stored subscription key, or prompt if it isn't found.
function getSubscriptionKey() {
var key = retrieveValue(API_KEY_COOKIE);
while (key.length !== 32) {
key = prompt("Enter Bing Search API subscription key:", "").trim();
}
// Always set the cookie in order to update the expiration date.
storeValue(API_KEY_COOKIE, key);
return key;
}
正如我们之前看到的,当表单提交时, onsubmit
触发,呼叫 bingWebSearch
。 此函数初始化并发送请求。
getSubscriptionKey
在每个提交中调用 ,以对请求进行身份验证。
调用必应网页搜索
给定查询、选项字符串和订阅密钥,该 BingWebSearch
函数将创建一个 XMLHttpRequest
对象来调用必应 Web 搜索终结点。
// Perform a search constructed from the query, options, and subscription key.
function bingWebSearch(query, options, key) {
window.scrollTo(0, 0);
if (!query.trim().length) return false;
showDiv("noresults", "Working. Please wait.");
hideDivs("pole", "mainline", "sidebar", "_json", "_http", "paging1", "paging2", "error");
var request = new XMLHttpRequest();
var queryurl = BING_ENDPOINT + "?q=" + encodeURIComponent(query) + "&" + options;
// Initialize the request.
try {
request.open("GET", queryurl);
}
catch (e) {
renderErrorMessage("Bad request (invalid URL)\n" + queryurl);
return false;
}
// Add request headers.
request.setRequestHeader("Ocp-Apim-Subscription-Key", key);
request.setRequestHeader("Accept", "application/json");
var clientid = retrieveValue(CLIENT_ID_COOKIE);
if (clientid) request.setRequestHeader("X-MSEdge-ClientID", clientid);
// Event handler for successful response.
request.addEventListener("load", handleBingResponse);
// Event handler for errors.
request.addEventListener("error", function() {
renderErrorMessage("Error completing request");
});
// Event handler for an aborted request.
request.addEventListener("abort", function() {
renderErrorMessage("Request aborted");
});
// Send the request.
request.send();
return false;
}
成功请求后, load
事件处理程序将触发并调用该 handleBingResponse
函数。
handleBingResponse
分析结果对象,显示结果,并包含失败请求的错误逻辑。
function handleBingResponse() {
hideDivs("noresults");
var json = this.responseText.trim();
var jsobj = {};
// Try to parse results object.
try {
if (json.length) jsobj = JSON.parse(json);
} catch(e) {
renderErrorMessage("Invalid JSON response");
return;
}
// Show raw JSON and the HTTP request.
showDiv("json", preFormat(JSON.stringify(jsobj, null, 2)));
showDiv("http", preFormat("GET " + this.responseURL + "\n\nStatus: " + this.status + " " +
this.statusText + "\n" + this.getAllResponseHeaders()));
// If the HTTP response is 200 OK, try to render the results.
if (this.status === 200) {
var clientid = this.getResponseHeader("X-MSEdge-ClientID");
if (clientid) retrieveValue(CLIENT_ID_COOKIE, clientid);
if (json.length) {
if (jsobj._type === "SearchResponse" && "rankingResponse" in jsobj) {
renderSearchResults(jsobj);
} else {
renderErrorMessage("No search results in JSON response");
}
} else {
renderErrorMessage("Empty response (are you sending too many requests too quickly?)");
}
}
// Any other HTTP response is considered an error.
else {
// 401 is unauthorized; force a re-prompt for the user's subscription
// key on the next request.
if (this.status === 401) invalidateSubscriptionKey();
// Some error responses don't have a top-level errors object, if absent
// create one.
var errors = jsobj.errors || [jsobj];
var errmsg = [];
// Display the HTTP status code.
errmsg.push("HTTP Status " + this.status + " " + this.statusText + "\n");
// Add all fields from all error responses.
for (var i = 0; i < errors.length; i++) {
if (i) errmsg.push("\n");
for (var k in errors[i]) errmsg.push(k + ": " + errors[i][k]);
}
// Display Bing Trace ID if it isn't blocked by CORS.
var traceid = this.getResponseHeader("BingAPIs-TraceId");
if (traceid) errmsg.push("\nTrace ID " + traceid);
// Display the error message.
renderErrorMessage(errmsg.join("\n"));
}
}
重要
成功的 HTTP 请求 并不意味着 搜索本身成功。 如果在搜索作中出错,必应 Web 搜索 API 将返回非 200 HTTP 状态代码,并在 JSON 响应中包含错误信息。 如果请求速率有限,API 将返回空响应。
上述两个函数中的大部分代码都专用于错误处理。 错误可能在以下阶段发生:
阶段 | 可能的错误 | 由...处理 |
---|---|---|
生成请求对象 | URL 无效 |
try
/
catch 块 |
发出请求 | 网络错误、已中止的连接 |
error 和 abort 事件处理程序 |
进行搜索 | 请求无效、JSON 无效、速率限制 | 事件处理程序中的 load 测试 |
通过调用 renderErrorMessage()
来处理错误。 如果响应通过所有错误测试, renderSearchResults()
则调用它以显示搜索结果。
显示搜索结果
必应 Web 搜索 API 返回的结果 有使用和显示要求 。 由于响应可能包含各种结果类型,因此它不足以循环访问顶级 WebPages
集合。 相反,示例应用使用 RankingResponse
来按规范对结果进行排序。
注释
如果只需要单个结果类型,请使用 responseFilter
查询参数,或考虑使用其他必应搜索终结点之一,例如必应图像搜索。
每个响应都有一个对象,该对象最多包含三个 RankingResponse
集合: pole
、 mainline
和 sidebar
。
pole
(如果存在)是最相关的搜索结果,必须突出显示。
mainline
包含大部分搜索结果,并紧接着 pole
显示。
sidebar
包括辅助搜索结果。 如果可能,应在边栏中显示这些结果。 如果屏幕限制使边栏不切实际,这些结果应显示在 mainline
结果之后。
每个 RankingResponse
数组都包含一个 RankingItem
数组,用于指定结果的排序方式。 我们的示例应用使用 answerType
和 resultIndex
参数来标识结果。
注释
可通过其他方法来识别和排名结果。 有关详细信息,请参阅 “使用排名显示结果”。
让我们看看代码:
// Render the search results from the JSON response.
function renderSearchResults(results) {
// If spelling was corrected, update the search field.
if (results.queryContext.alteredQuery)
document.forms.bing.query.value = results.queryContext.alteredQuery;
// Add Prev / Next links with result count.
var pagingLinks = renderPagingLinks(results);
showDiv("paging1", pagingLinks);
showDiv("paging2", pagingLinks);
// Render the results for each section.
for (section in {pole: 0, mainline: 0, sidebar: 0}) {
if (results.rankingResponse[section])
showDiv(section, renderResultsItems(section, results));
}
}
该 renderResultsItems()
函数循环访问每个 RankingResponse
集合中的项,使用 answerType
和 resultIndex
值将每个排名结果映射到搜索结果,并调用相应的呈现函数来生成 HTML。 如果未为某项指定 resultIndex
,则 renderResultsItems()
会遍历该类型的所有结果,并为每个项目调用呈现函数。 生成的 HTML 被插入到 <div>
中的相应 index.html
元素。
// Render search results from the RankingResponse object per rank response and
// use and display requirements.
function renderResultsItems(section, results) {
var items = results.rankingResponse[section].items;
var html = [];
for (var i = 0; i < items.length; i++) {
var item = items[i];
// Collection name has lowercase first letter while answerType has uppercase
// e.g. `WebPages` RankingResult type is in the `webPages` top-level collection.
var type = item.answerType[0].toLowerCase() + item.answerType.slice(1);
if (type in results && type in searchItemRenderers) {
var render = searchItemRenderers[type];
// This ranking item refers to ONE result of the specified type.
if ("resultIndex" in item) {
html.push(render(results[type].value[item.resultIndex], section));
// This ranking item refers to ALL results of the specified type.
} else {
var len = results[type].value.length;
for (var j = 0; j < len; j++) {
html.push(render(results[type].value[j], section, j, len));
}
}
}
}
return html.join("\n\n");
}
查看呈现器函数
在我们的示例应用中,该 searchItemRenderers
对象包括为每种搜索结果类型生成 HTML 的函数。
// Render functions for each result type.
searchItemRenderers = {
webPages: function(item) { ... },
news: function(item) { ... },
images: function(item, section, index, count) { ... },
videos: function(item, section, index, count) { ... },
relatedSearches: function(item, section, index, count) { ... }
}
重要
示例应用包含网页、新闻、图像、视频和相关搜索的呈现器。 应用程序将需要呈现器来获取它可能收到的任何类型的结果,其中包括计算、拼写建议、实体、时区和定义。
某些呈现函数仅接受 item
参数。 其他人接受其他参数,这些参数可用于根据上下文以不同的方式呈现项。 不使用此信息的呈现器不需要接受这些参数。
上下文参数为:
参数 | 说明 |
---|---|
section |
项目出现的结果部分(pole 、mainline 或sidebar )。 |
index count |
当 RankingResponse 项指定显示给定集合中的所有结果时可用;否则为 undefined 。 项目在其集合中的索引和该集合中的项目总数。 可以使用此信息对结果进行编号,为第一个或最后一个结果生成不同的 HTML,等等。 |
在示例应用中,images
和 relatedSearches
两个呈现器都使用上下文参数来自定义生成的 HTML。 让我们仔细看看 images
渲染器:
searchItemRenderers = {
// Render image result with thumbnail.
images: function(item, section, index, count) {
var height = 60;
var width = Math.round(height * item.thumbnail.width / item.thumbnail.height);
var html = [];
if (section === "sidebar") {
if (index) html.push("<br>");
} else {
if (!index) html.push("<p class='images'>");
}
html.push("<a href='" + item.hostPageUrl + "'>");
var title = escape(item.name) + "\n" + getHost(item.hostPageDisplayUrl);
html.push("<img src='"+ item.thumbnailUrl + "&h=" + height + "&w=" + width +
"' height=" + height + " width=" + width + " title='" + title + "' alt='" + title + "'>");
html.push("</a>");
return html.join("");
},
// Other renderers are omitted from this sample...
}
图像呈现器:
- 计算图像缩略图大小(宽度变化,而高度固定为 60 像素)。
- 根据上下文插入图像结果之前的HTML代码。
- 生成链接到包含图像的页面的 HTML
<a>
标记。 - 生成 HTML
<img>
标记以显示图像缩略图。
图像呈现器使用 section
和 index
变量根据结果的显示位置而有所不同。 在边栏的图像结果之间插入换行符(<br>
标记),以便边栏显示为一列图像。 在其他部分中,标记 (index === 0)
出现在第一个图像结果 <p>
之前。
缩略图大小用于 <img>
标签以及缩略图 URL 中的 h
和 w
字段。 属性title
alt
(图像的文本说明)是从图像的名称和 URL 中的主机名构造的。
下面是如何在示例应用中显示图像的示例:
保留客户端 ID
来自必应搜索 API 的响应可能包括一个 X-MSEdge-ClientID
标头,该标头应随每个后续请求一起发送回 API。 如果应用使用了多个必应搜索 API,请确保在服务之间为每个请求发送相同的客户端 ID。
提供X-MSEdge-ClientID
标头可以使必应 API 关联到用户的搜索记录。 首先,它允许必应搜索引擎将过去的上下文应用于搜索,以查找更好地满足请求的结果。 例如,如果用户之前搜索了与航行相关的术语,则以后搜索“结”可能会优先返回有关帆船中使用的结的信息。 其次,必应可能会随机挑选用户来体验新功能,这发生在它们被广泛使用之前。 为每个请求提供相同的客户端 ID 可确保选择查看功能的用户将始终看到该功能。 如果没有客户端 ID,用户可能会在搜索结果中看到一个功能出现并消失(看似随机)。
浏览器安全策略(如跨域资源共享(CORS)可能会阻止示例应用访问 X-MSEdge-ClientID
标头。 当搜索响应的来源与请求该响应的页面不同时,就会出现此限制。 在生产环境中,应通过托管在与网页相同的域上执行 API 调用的服务器端脚本来解决此策略。 由于脚本的源与网页相同,X-MSEdge-ClientID
标头随后可供 JavaScript 使用。
注释
在生产 Web 应用程序中,在任何情况下都应执行服务器端请求。 否则,必应搜索 API 的订阅密钥必须包含在网页中,任何查看源代码的用户都可以看到。 API 订阅密钥下的所有使用情况(甚至未经授权的各方发出的请求)都会向你计费,因此请务必不公开密钥。
出于开发目的,可以通过 CORS 代理发出请求。 此类型的代理的响应具有一个 Access-Control-Expose-Headers
标头,用于筛选响应标头并使其可用于 JavaScript。
可以轻松安装 CORS 代理,以允许示例应用访问客户端 ID 标头。 运行以下命令:
npm install -g cors-proxy-server
接下来,将必应网页搜索的端点 script.js
更改为:
http://localhost:9090/https://api.cognitive.microsoft.com/bing/v7.0/search
使用以下命令启动 CORS 代理:
cors-proxy-server
使用示例应用时将命令窗口保持打开状态;关闭窗口会停止代理。 在搜索结果下方的可展开 HTTP 标头部分中, X-MSEdge-ClientID
标头应可见。 验证每个请求是否相同。