警告
在 2020 年 10 月 30 日,Bing 搜尋 API 從 Azure AI 服務移至 Bing 搜尋服務。 本檔僅供參考。 如需更新的檔案,請參閱 Bing 搜尋 API 檔案。 如需建立 Bing 搜尋新 Azure 資源的指示,請參閱透過 Azure Marketplace 建立 Bing 搜尋資源。
這個單頁應用程式示範如何從 Bing Web 搜尋 API 擷取、剖析及顯示搜尋結果。 本教學課程使用重複使用的 HTML 和 CSS,並著重於 JavaScript 程式代碼。 HTML、CSS 和 JS 檔案可在 GitHub 上使用快速入門指示。
這個範例應用程式可以:
- 使用搜尋選項呼叫 Bing Web 搜尋 API
- 顯示 Web、影像、新聞和視訊結果
- 分頁結果
- 管理訂用帳戶金鑰
- 處理錯誤
若要使用此應用程式,需要具有 Bing 搜尋 API 的 Azure AI 服務帳戶 。
先決條件
以下是您需要執行應用程式的一些事項:
Azure 訂用帳戶 - 建立免費帳戶
擁有 Azure 訂用帳戶之後,在 Azure 入口網站中建立 Bing 搜尋資源 ,以取得您的密鑰和端點。 部署之後,按一下 [移至資源]。
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- 包含對 Bing Web 搜尋 API 提出要求、管理訂用帳戶密鑰、處理和剖析回應,以及顯示結果的邏輯。
本教學課程著重於 scripts.js 呼叫 Bing Web 搜尋 API 並處理回應所需的邏輯。
HTML 表單
包含 index.html 可讓使用者搜尋及選取搜尋選項的表單。 當提交表單時,屬性 onsubmit 會被觸發,從而調用在 bingWebSearch() 中定義的 scripts.js 方法。 它接收三個參數:
- 搜尋字詞
- 選取的選項
- 訂用帳戶金鑰
<form name="bing" onsubmit="return bingWebSearch(this.query.value,
bingSearchOptions(this), getSubscriptionKey())">
查詢選項
HTML 表單包含對應至 Bing Web 搜尋 API v7 中查詢參數的選項。 下表提供如何使用範例應用程式篩選搜尋結果的細目:
| 參數 | 說明 |
|---|---|
query |
文字欄位,用於輸入查詢字串。 |
where |
用於選擇市場(位置和語言)的下拉選單。 |
what |
用來推廣特定結果類型的複選框。 例如,推廣影像可以提高搜尋結果中的影像排名。 |
when |
下拉功能表,可讓使用者將搜尋結果限製為今天、本周或本月。 |
safe |
您可以選擇啟用 Bing SafeSearch 的複選框,以過濾掉成人內容。 |
count |
隱藏欄位。 每個要求要回傳的搜尋結果數目。 變更此值以顯示每個頁面的較少或更多結果。 |
offset |
隱藏欄位。 請求中第一個搜尋結果的偏移量,用於分頁。 它會隨著每個新要求重設為 0。 |
備註
Bing Web 搜尋 API 提供其他查詢參數,以協助精簡搜尋結果。 此範例只使用一些。 如需可用參數的完整清單,請參閱 Bing Web 搜尋 API v7 參考。
函 bingSearchOptions() 式會轉換這些選項,以符合 Bing 搜尋 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 為 Bing Web 搜尋的預設設定。 此表單使用一個複選框,其中包含兩種狀態: strict 或 moderate。
如果選取任何 [ 升階] 複選框,參數 answerCount 就會新增至查詢。
answerCount 使用 promote 參數時是必要的。 在此代碼段中,值會設定為 9 ,以傳回所有可用的結果類型。
備註
提升結果類型並不 保證 該類型會被包含在搜尋結果中。 相反地,提升會使這類結果的排名相較於其通常的排名提高。 若要將搜尋限制為特定類型的結果,請使用 responseFilter 查詢參數,或呼叫更特定的端點,例如 Bing 影像搜尋或 Bing 新聞搜尋。
和 textDecorationtextFormat 查詢參數會硬式編碼到腳本中,並導致搜尋字詞在搜尋結果中以粗體顯示。 不需要這些參數。
管理訂用帳戶金鑰
為了避免硬式編碼 Bing 搜尋 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 會在每次提交時被呼叫以認證請求。
呼叫 Bing 網頁搜尋
給定查詢、選項字串和訂閱金鑰,BingWebSearch 函式會建立 XMLHttpRequest 物件來呼叫 Bing 網頁搜尋端點。
// 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 要求 並不 表示搜尋本身成功。 如果在搜尋作業中發生錯誤,Bing Web 搜尋 API 會傳回非 200 HTTP 狀態代碼,並在 JSON 回應中包含錯誤資訊。 如果要求受到速率限制,API 會傳回空的回應。
上述兩個函式中的大部分程式代碼都專用於錯誤處理。 錯誤可能會在下列階段發生:
| 舞臺 | 潛在錯誤 | 處理者 |
|---|---|---|
| 建置要求物件 | URL 無效 |
try
/
catch 區塊 |
| 提出請求 | 網路錯誤,中止的連線 |
error 和 abort 事件處理程式 |
| 執行搜尋 | 無效的要求、無效的 JSON、速率限制 | 事件處理程式中的 load 測試 |
錯誤會透過呼叫renderErrorMessage()來處理。 如果回應通過所有錯誤測試, renderSearchResults() 則會呼叫 以顯示搜尋結果。
顯示搜尋結果
Bing Web 搜尋 API 所傳回的結果有 使用和顯示需求 。 由於回應可能包含各種結果類型,因此無法逐一查看最上層 WebPages 集合。 相反地,範例應用程式會使用 RankingResponse 來將結果排序為規格。
備註
如果您只想要單一結果類型,請使用 responseFilter 查詢參數,或考慮使用其他其中一個 Bing 搜尋端點,例如 Bing 影像搜尋。
每個回應都有一個 物件,最多可包含三個 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排名結果映射到搜尋結果,並呼叫適當的渲染函數來生成 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) 。 |
indexcount |
當 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。
-
<a>建置 HTML 標籤,連結至包含影像的頁面。 - 建置 HTML
<img>標記以顯示影像縮圖。
影像轉譯器會使用 section 和 index 變數,根據結果出現的位置而有所不同。 換行符(<br> 標籤)會在側邊欄的影像結果之間插入,以便側邊欄顯示圖片列。 在其他區段中,第一個影像結果(index === 0)前面會加上<p>標記。
縮圖大小用於縮圖 URL 中的 <img> 標籤以及 h 和 w 欄位。
title和 alt 屬性(影像的文字描述)是從影像的名稱和URL中的主機名建構的。
以下是範例應用程式中影像顯示方式的範例:
保存用戶端識別碼
來自 Bing 搜尋 API 的回應可能包含 X-MSEdge-ClientID 標頭,該標頭應該隨著每個後續要求傳回 API。 如果您的應用程式使用多個 Bing 搜尋 API,請確定每個服務都傳送相同的用戶端識別碼。
提供X-MSEdge-ClientID標頭可讓 Bing API 將使用者的搜尋關聯起來。 首先,它可讓 Bing 搜尋引擎套用過去的內容來搜尋,以尋找更符合要求的結果。 例如,如果使用者先前已搜尋與帆船相關的字詞,則稍後搜尋「結」可能會優先傳回航行中所用結的相關信息。 其次,Bing 可能會隨機選取用戶來體驗新功能,然後再廣泛提供這些功能。 為每個要求提供相同的用戶端標識碼,可確保已選擇查看功能的使用者一律會看到此功能。 若沒有用戶端標識碼,使用者可能會在其搜尋結果中看到功能出現並消失,看似隨機。
瀏覽器安全策略,例如跨原始來源資源分享 (CORS),可能會防止範例應用程式存取 X-MSEdge-ClientID 標頭。 當搜尋回應與要求該回應的頁面不同來源時,就會發生此限制。 在生產環境中,您應該裝載伺服器端腳本,在與網頁相同的網域上執行 API 呼叫,藉此解決此原則。 由於腳本的原點與網頁相同,因此 X-MSEdge-ClientID 標頭接著可供 JavaScript 使用。
備註
在生產階段的 Web 應用程式中,您應該確保在伺服器端執行該請求。 否則,您的 Bing 搜尋 API 訂用帳戶金鑰必須包含在網頁中,供任何檢視來源的人員使用。 您的 API 訂閱金鑰下的所有使用量,甚至是未經授權使用者提出的要求,因此請務必保護您的密鑰不被公開。
為了開發目的,您可以透過 CORS Proxy 提出要求。 此類型 Proxy 的回應包含一個標頭,該標頭可以篩選響應標頭,並使其供 JavaScript 使用。
很容易安裝 CORS Proxy,以允許我們的範例應用程式存取用戶端標識碼標頭。 執行此指令:
npm install -g cors-proxy-server
接下來,將 中的 script.js Bing Web 搜尋端點變更為:
http://localhost:9090/https://api.cognitive.microsoft.com/bing/v7.0/search
使用此指令啟動 CORS Proxy:
cors-proxy-server
當您使用範例應用程式時,保持命令視窗開啟,關閉視窗會停止代理。 在搜尋結果下方的可展開 HTTP 標頭區段中, X-MSEdge-ClientID 應該會顯示標頭。 確認每個要求的內容都相同。