警告
在 2020 年 10 月 30 日,Bing 搜尋 API 從 Azure AI 服務移至 Bing 搜尋服務。 本文件僅供參考之用。 如需更新的文件,請參閱 Bing 搜尋 API 文件。 如需針對 Bing 搜尋建立新 Azure 資源的指示,請參閱透過 Azure Marketplace 建立 Bing 搜尋資源。
Bing 影像搜尋 API 可讓您搜尋 Web 以取得高品質的相關影像。 使用本教學課程來建置單頁 Web 應用程式,以將搜尋查詢傳送至 API,並在網頁內顯示結果。 本教學課程類似於 Bing Web 搜尋的 對應教學 課程。
本教學課程應用程式說明如何:
- 在 JavaScript 中執行 Bing 影像搜尋 API 呼叫
- 使用搜尋選項改善搜尋結果
- 顯示搜尋結果並逐頁瀏覽
- 要求和處理 API 訂用帳戶金鑰,以及 Bing 用戶端識別碼。
先決條件
- 最新版的 Node.js。
- Node.js的 Express.js 架構。 GitHub 範例自述檔提供原始程式碼的安裝指示。
管理及儲存使用者訂用帳戶金鑰
此應用程式會使用網頁瀏覽器的永續性記憶體來儲存 API 訂用帳戶密鑰。 如果未儲存金鑰,網頁會提示使用者輸入金鑰,並儲存以供稍後使用。 如果 API 稍後拒絕金鑰,應用程式會將其從記憶體中移除。 此範例會使用全域端點。 您也可以使用 Azure 入口網站中針對您的資源顯示的 自訂子域 端點。
定義 storeValue 和 retrieveValue 函式以使用 localStorage 物件(如果瀏覽器支援它),或 Cookie。
// Cookie names for data being stored
API_KEY_COOKIE = "bing-search-api-key";
CLIENT_ID_COOKIE = "bing-search-client-id";
// The Bing Image Search API endpoint
BING_ENDPOINT = "https://api.cognitive.microsoft.com/bing/v7.0/images/search";
try { //Try to use localStorage first
localStorage.getItem;
window.retrieveValue = function (name) {
return localStorage.getItem(name) || "";
}
window.storeValue = function(name, value) {
localStorage.setItem(name, value);
}
} catch (e) {
//If the browser doesn't support localStorage, try a cookie
window.retrieveValue = function (name) {
var cookies = document.cookie.split(";");
for (var i = 0; i < cookies.length; i++) {
var keyvalue = cookies[i].split("=");
if (keyvalue[0].trim() === name) return keyvalue[1];
}
return "";
}
window.storeValue = function (name, value) {
var expiry = new Date();
expiry.setFullYear(expiry.getFullYear() + 1);
document.cookie = name + "=" + value.trim() + "; expires=" + expiry.toUTCString();
}
}
函 getSubscriptionKey() 式會嘗試使用 retrieveValue擷取先前儲存的金鑰。 如果找不到金鑰,則會提示使用者輸入金鑰,並使用 來儲存金鑰 storeValue。
// Get the stored API subscription key, or prompt if it's not 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;
}
HTML <form> 標籤 onsubmit 呼叫 bingWebSearch 函式以傳回搜尋結果。
bingWebSearch 會使用 getSubscriptionKey 來驗證每個查詢。 如上一個定義所示,如果尚未輸入密鑰,getSubscriptionKey 提示使用者輸入密鑰。 接著會儲存金鑰以供應用程式繼續使用。
<form name="bing" onsubmit="this.offset.value = 0; return bingWebSearch(this.query.value,
bingSearchOptions(this), getSubscriptionKey())">
傳送搜尋要求
此應用程式會使用 HTML <form> 來一開始傳送使用者搜尋要求,並使用 onsubmit 屬性呼叫 newBingImageSearch()。
<form name="bing" onsubmit="return newBingImageSearch(this)">
根據預設,onsubmit 處理程式會傳回 false,讓表單無法提交。
選取搜尋選項
Bing 影像搜尋 API 提供數個 篩選查詢參數 來縮小和篩選搜尋結果。 這個應用程式中的 HTML 窗體會使用 並顯示下列參數選項:
| 選項 | 說明 |
|---|---|
where |
用於選取用於搜尋之市場(位置和語言)的下拉功能表。 |
query |
要在其中輸入搜尋字詞的文字欄位。 |
aspect |
用於選擇所找到影像比例的單選按鈕:大致為正方形、寬或高。 |
color |
|
when |
下拉功能表,選擇性地將搜尋限制為最近一天、一周或月份。 |
safe |
複選框,指出是否要使用 Bing 的 SafeSearch 功能來篩選出「成人」結果。 |
count |
隱藏欄位。 每個要求要回傳的搜尋結果數目。 變更以顯示每個頁面的較少或更多結果。 |
offset |
隱藏欄位。 請求中第一個搜尋結果的偏移量,用於分頁。 它會在每次新請求時重設為 0。 |
nextoffset |
隱藏欄位。 收到搜尋結果時,此欄位會設定為回應中的 值 nextOffset 。 使用此欄位可避免後續頁面上的重疊結果。 |
stack |
隱藏欄位。 搜尋結果前幾頁位移的 JSON 編碼清單,用於返回到前幾頁進行巡覽。 |
函式會將 bingSearchOptions() 這些選項格式化為部分查詢字串,以在應用程式的 API 要求中使用。
// Build query options from the HTML form
function bingSearchOptions(form) {
var options = [];
options.push("mkt=" + form.where.value);
options.push("SafeSearch=" + (form.safe.checked ? "strict" : "off"));
if (form.when.value.length) options.push("freshness=" + form.when.value);
var aspect = "all";
for (var i = 0; i < form.aspect.length; i++) {
if (form.aspect[i].checked) {
aspect = form.aspect[i].value;
break;
}
}
options.push("aspect=" + aspect);
if (form.color.value) options.push("color=" + form.color.value);
options.push("count=" + form.count.value);
options.push("offset=" + form.offset.value);
return options.join("&");
}
執行要求
使用搜尋查詢、選項字串和 API 金鑰,函 BingImageSearch() 式會使用 XMLHttpRequest 物件向 Bing 影像搜尋端點提出要求。
// perform a search given query, options string, and API key
function bingImageSearch(query, options, key) {
// scroll to top of window
window.scrollTo(0, 0);
if (!query.trim().length) return false; // empty query, do nothing
showDiv("noresults", "Working. Please wait.");
hideDivs("results", "related", "_json", "_http", "paging1", "paging2", "error");
var request = new XMLHttpRequest();
var queryurl = BING_ENDPOINT + "?q=" + encodeURIComponent(query) + "&" + options;
// open 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 erorrs
request.addEventListener("error", function() {
renderErrorMessage("Error completing request");
});
// event handler for aborted request
request.addEventListener("abort", function() {
renderErrorMessage("Request aborted");
});
// send the request
request.send();
return false;
}
HTTP 要求成功完成時,JavaScript 會呼叫 「load」 事件處理程式 handleBingResponse() 來處理成功的 HTTP GET 要求。
// handle Bing search request results
function handleBingResponse() {
hideDivs("noresults");
var json = this.responseText.trim();
var jsobj = {};
// try to parse JSON results
try {
if (json.length) jsobj = JSON.parse(json);
} catch(e) {
renderErrorMessage("Invalid JSON response");
}
// show raw JSON and 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 HTTP response is 200 OK, try to render search 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 === "Images") {
if (jsobj.nextOffset) document.forms.bing.nextoffset.value = jsobj.nextOffset;
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 an error
else {
// 401 is unauthorized; force re-prompt for API key for next request
if (this.status === 401) invalidateSubscriptionKey();
// some error responses don't have a top-level errors object, so gin one up
var errors = jsobj.errors || [jsobj];
var errmsg = [];
// display 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]);
}
// also display Bing Trace ID if it isn't blocked by CORS
var traceid = this.getResponseHeader("BingAPIs-TraceId");
if (traceid) errmsg.push("\nTrace ID " + traceid);
// and display the error message
renderErrorMessage(errmsg.join("\n"));
}
}
這很重要
成功的 HTTP 要求可能包含失敗的搜尋資訊。 如果在搜尋作業期間發生錯誤,Bing 影像搜尋 API 會在 JSON 回應中傳回非 200 HTTP 狀態代碼和錯誤資訊。 此外,如果要求受到速率限制,API 會傳回空的回應。
顯示搜尋結果
此函式會顯示 renderSearchResults() 搜尋結果,它會接受 Bing 影像搜尋服務所傳回的 JSON,並在任何傳回的影像和相關搜尋上呼叫適當的轉譯器函式。
function renderSearchResults(results) {
// add Prev / Next links with result count
var pagingLinks = renderPagingLinks(results);
showDiv("paging1", pagingLinks);
showDiv("paging2", pagingLinks);
showDiv("results", renderImageResults(results.value));
if (results.relatedSearches)
showDiv("sidebar", renderRelatedItems(results.relatedSearches));
}
影像搜尋結果會包含在 JSON 回應的最上層 value 物件中。 這些會傳遞至 renderImageResults(),它會逐一查看結果,並將每個項目轉換成 HTML。
function renderImageResults(items) {
var len = items.length;
var html = [];
if (!len) {
showDiv("noresults", "No results.");
hideDivs("paging1", "paging2");
return "";
}
for (var i = 0; i < len; i++) {
html.push(searchItemRenderers.images(items[i], i, len));
}
return html.join("\n\n");
}
Bing 影像搜尋 API 可以傳回四種類型的搜尋建議,以協助引導使用者的搜尋體驗,每個類型都是在其本身的最上層物件中:
| 建議 | 說明 |
|---|---|
pivotSuggestions |
在原始搜尋中以不同詞取代樞紐字的查詢。 例如,如果您搜尋「紅色花」,樞紐字可能是「紅色」,而樞紐建議可能是「黃色花」。 |
queryExpansions |
藉由新增更多字詞來縮小原始搜尋範圍的查詢。 例如,如果您搜尋「Microsoft Surface」,查詢擴充可能是「Microsoft Surface Pro」。 |
relatedSearches |
其他使用者在輸入原始搜尋後也輸入的查詢。 例如,如果您搜尋「Mount Rainier」,相關搜尋可能是「Mt」。 聖海倫斯。 |
similarTerms |
與原始搜尋類似之查詢。 例如,如果您搜尋「小貓」,類似的詞彙可能是「可愛」。 |
此應用程式只會轉譯 relatedItems 建議,並將產生的連結放在頁面的提要欄中。
轉譯搜尋結果
在此應用程式中, searchItemRenderers 物件包含為每種搜尋結果產生 HTML 的轉譯器函式。
searchItemRenderers = {
images: function(item, index, count) { ... },
relatedSearches: function(item) { ... }
}
這些轉譯器函式接受下列參數:
| 參數 | 說明 |
|---|---|
item |
包含項目屬性的 JavaScript 物件,例如其 URL 和其描述。 |
index |
其集合中結果項目的索引。 |
count |
搜尋結果專案集合中的項目數。 |
index和 count 參數可用來編號結果、產生集合的 HTML,以及組織內容。 具體來說,它:
- 計算影像縮圖大小(寬度會有所不同,最小為 120 像素,而高度固定為 90 像素)。
- 建置 HTML
<img>標記以顯示影像縮圖。 - 建置 HTML
<a>標籤,連結至影像和包含影像的頁面。 - 建立描述,以顯示圖片及其所在網站的相關信息。
images: function (item, index, count) {
var height = 120;
var width = Math.max(Math.round(height * item.thumbnail.width / item.thumbnail.height), 120);
var html = [];
if (index === 0) html.push("<p class='images'>");
var title = escape(item.name) + "\n" + getHost(item.hostPageDisplayUrl);
html.push("<p class='images' style='max-width: " + width + "px'>");
html.push("<img src='"+ item.thumbnailUrl + "&h=" + height + "&w=" + width +
"' height=" + height + " width=" + width + "'>");
html.push("<br>");
html.push("<nobr><a href='" + item.contentUrl + "'>Image</a> - ");
html.push("<a href='" + item.hostPageUrl + "'>Page</a></nobr><br>");
html.push(title.replace("\n", " (").replace(/([a-z0-9])\.([a-z0-9])/g, "$1.<wbr>$2") + ")</p>");
return html.join("");
}, // relatedSearches renderer omitted
縮圖影像的 height 和 width 會同時用於 <img> 標籤和縮圖 URL 中的 h 和 w 欄位。 這可讓 Bing 傳回大小完全相同的 縮圖 。
持續保存用戶端識別碼
來自 Bing 搜尋 API 的回應可能包含 X-MSEdge-ClientID 標頭,應該在後續請求中傳回給 API。 如果使用多個 Bing 搜尋 API,則應該盡可能使用相同的用戶端識別碼來搭配所有 API。
X-MSEdge-ClientID提供標頭可讓 Bing API 關聯所有使用者的搜尋,這在 中很有用
首先,它可讓 Bing 搜尋引擎套用過去的內容來搜尋,以尋找更符合用戶的結果。 例如,如果使用者先前已搜尋與帆船相關的字詞,則稍後搜尋「結」可能會優先傳回航行中所用結的相關信息。
其次,Bing 可能會隨機選取用戶來體驗新功能,然後再廣泛提供這些功能。 為每個要求提供相同的用戶端標識碼,可確保已選擇查看功能的使用者一律會看到此功能。 若沒有用戶端標識碼,使用者可能會在其搜尋結果中看到功能出現並消失,看似隨機。
瀏覽器安全策略 (CORS) 可能會防止 X-MSEdge-ClientID 標頭可供 JavaScript 使用。 當搜尋回應與要求該回應的頁面不同來源時,就會發生此限制。 在生產環境中,您應該裝載伺服器端腳本,在與網頁相同的網域上執行 API 呼叫,藉此解決此原則。 由於腳本的原點與網頁相同,因此 X-MSEdge-ClientID 標頭接著可供 JavaScript 使用。
備註
在生產環境的 Web 應用程式中,您應該在伺服器端執行此請求。 否則,您的 Bing 搜尋 API 金鑰必須包含在網頁中,供任何檢視來源的人員使用。 您的 API 訂閱金鑰下的所有使用量,甚至是未經授權使用者提出的要求,因此請務必保護您的密鑰不被公開。
基於開發目的,您可以透過 CORS Proxy 提出 Bing Web 搜尋 API 要求。 來自這類 Proxy 的回應具有 Access-Control-Expose-Headers 標頭,可允許響應標頭,並將其提供給 JavaScript 使用。
很容易安裝 CORS 代理伺服器,以允許我們的教學應用程式存取客戶端 ID 標頭。 首先,如果您還沒有它,安裝 Node.js。 然後在指令視窗中發出下列命令:
npm install -g cors-proxy-server
接下來,將 HTML 檔案中的 Bing Web 搜尋端點變更為:
http://localhost:9090/https://api.cognitive.microsoft.com/bing/v7.0/search
最後,使用下列命令啟動 CORS Proxy:
cors-proxy-server
當您使用教學課程應用程式時,請保持命令視窗開啟。關閉視窗會停止代理伺服器。 在搜尋結果下方的可展開 HTTP 標頭區段中,您現在可以看到 X-MSEdge-ClientID 標頭(等等),並確認每個要求都相同。