教學課程:使用 Bing 影像搜尋 API 來建立單頁應用程式Tutorial: Create a single-page app using the Bing Image Search API

「Bing 影像搜尋 API」可讓您搜尋 Web 來尋找高品質的相關影像。The Bing Image Search API enables you to search the web for high-quality, relevant images. 您可以使用此教學課程來建置可將搜尋查詢傳送給該 API 並在網頁內顯示結果的單頁 Web 應用程式。Use this tutorial to build a single-page web application that can send search queries to the API, and display the results within the webpage. 此教學課程與「Bing Web 搜尋」的相對應教學課程類似。This tutorial is similar to the corresponding tutorial for Bing Web Search.

本教學課程應用程式說明如何:The tutorial app illustrates how to:

  • 在 JavaScript 中執行 Bing 影像搜尋 API 呼叫Perform a Bing Image Search API call in JavaScript
  • 使用搜尋選項來改善搜尋結果Improve search results using search options
  • 顯示搜尋結果並逐頁檢視Display and page through search results
  • 要求及處理 API 訂用帳戶金鑰和 Bing 用戶端識別碼。Request and handle an API subscription key, and Bing client ID.

GitHub 上有此教學課程的完整原始程式碼。The full source code for this tutorial is available on GitHub.

必要條件Prerequisites

  • 最新版的 Node.jsThe latest version of Node.js.
  • 適用於 Node.js 的 Express.js 架構。The Express.js framework for Node.js. GitHub 範例讀我檔案中會提供原始程式碼的安裝指示。Installation instructions for the source code are available in the GitHub sample readme file.

您必須有具備 Bing 搜尋 API 存取權的認知服務 API 帳戶You must have a Cognitive Services API account with access to the Bing Search APIs. 如果您沒有 Azure 訂用帳戶,可以建立免費帳戶If you don't have an Azure subscription, you can create an account for free. 繼續之前,您需要有啟動免費試用版後所提供的存取金鑰,或是從 Azure 儀表板中取得的付費訂用帳戶金鑰。Before continuing, You will need the access key provided after activating your free trial, or a paid subscription key from your Azure dashboard.

管理及儲存使用者訂用帳戶金鑰Manage and store user subscription keys

此應用程式使用網頁瀏覽器的永續性儲存體來儲存 API 訂用帳戶金鑰。This application uses web browsers' persistent storage to store API subscription keys. 如果未儲存任何金鑰,網頁將會提示使用者提供金鑰,並儲存該金鑰以供稍後使用。If no key is stored, the webpage will prompt the user for their key and store it for later use. 如果該金鑰稍後被 API 拒絕,應用程式就會從儲存體中移除它。If the key is later rejected by the API, The app will remove it from storage.

請定義 storeValueretrieveValue 函式以使用 localStorage 物件 (如果瀏覽器支援此物件) 或 Cookie。Define storeValue and retrieveValue functions to use either the localStorage object (if the browser supports it) or a 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 來儲存該金鑰。If one isn't found, it will prompt the user for their key, and store it using 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 函式來傳回搜尋結果。The HTML <form> tag onsubmit calls the bingWebSearch function to return search results. bingWebSearch 使用 getSubscriptionKey 來驗證每個查詢。bingWebSearch uses getSubscriptionKey to authenticate each query. 如先前的定義所示,若尚未輸入金鑰.getSubscriptionKey 就會提示使用者輸入金鑰。As shown in the previous definition, getSubscriptionKey prompts the user for the key if the key hasn't been entered. 其後應儲存金鑰,以供應用程式繼續使用。The key is then stored for continuing use by the application.

<form name="bing" onsubmit="this.offset.value = 0; return bingWebSearch(this.query.value,
bingSearchOptions(this), getSubscriptionKey())">

傳送搜尋要求Send search requests

此應用程式會使用 HTML <form> 來一開始傳送使用者搜尋要求,其中會使用 onsubmit 屬性來呼叫 newBingImageSearch()This application uses an HTML <form> to initially send user search requests, using the onsubmit attribute to call newBingImageSearch().

<form name="bing" onsubmit="return newBingImageSearch(this)">

onsubmit 處理常式預設會傳回 false,以防止提交表單。By default, the onsubmit handler returns false, keeping the form from being submitted.

選取搜尋選項Select search options

[Bing 影像搜尋表單]

「Bing 影像搜尋 API」提供數個篩選查詢參數,可針對搜尋結果縮小範圍及進行篩選。The Bing Image Search API offers several filter query parameters to narrow and filter search results. 此應用程式中的 HTML 表單會使用及顯示下列參數選項:The HTML form in this application uses and displays the following parameter options:

where 可供選取市場 (位置和語言) 以用於搜尋的下拉式功能表。A drop-down menu for selecting the market (location and language) used for the search.
query 要在其中輸入搜尋字詞的文字欄位。The text field in which to enter the search terms.
aspect 選項按鈕用於選擇找到的映像比例:大致是方形、寬或高。Radio buttons for choosing the proportions of the found images: roughly square, wide, or tall.
color
when 下拉式功能表可選擇性地將搜尋限制在最近一天、一週或一個月。Drop-down menu for optionally limiting the search to the most recent day, week, or month.
safe 指出是否要使用 Bing 的安全搜尋功能來篩選掉「成人」結果的核取方塊。A checkbox indicating whether to use Bing's SafeSearch feature to filter out "adult" results.
count 隱藏的欄位。Hidden field. 毎個要求要傳回的搜尋結果數目。The number of search results to return on each request. 變更為每頁顯示更多或更少結果。Change to display fewer or more results per page.
offset 隱藏的欄位。Hidden field. 要求中第一個搜尋結果的位移;用於分頁。The offset of the first search result in the request; used for paging. 它會在新要求執行時重設為 0It's reset to 0 on a new request.
nextoffset 隱藏的欄位。Hidden field. 收到搜尋結果後,此欄位會設為回應中的 nextOffset 值。Upon receiving a search result, this field is set to the value of the nextOffset in the response. 使用此欄位可避免連續頁面上的重複結果。Using this field avoids overlapping results on successive pages.
stack 隱藏的欄位。Hidden field. JSON 編碼之上一頁搜尋結果的位移清單,用於導覽回上一個頁面。A JSON-encoded list of the offsets of preceding pages of search results, for navigating back to previous pages.

bingSearchOptions() 函式會將這些選項的格式製作成部分查詢字串,以供在應用程式的 API 要求中使用。The bingSearchOptions() function formats these options into a partial query string, which can be used in the app's API requests.

// 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("&");
}

執行要求Performing the request

在使用搜尋查詢、選項字串及 API 金鑰的情況下,BingImageSearch() 函式會使用 XMLHttpRequest 物件向「Bing 影像搜尋」端點提出要求。Using the search query, options string, and API key, the BingImageSearch() function uses an XMLHttpRequest object to make the request to the Bing Image Search endpoint.

// 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 會呼叫「載入」事件處理常式 handleBingResponse() 來處理成功的 HTTP GET 要求。Upon successful completion of the HTTP request, JavaScript calls the "load" event handler handleBingResponse() to handle a successful HTTP GET request.

// 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 要求可能會包含失敗的搜尋資訊。Successful HTTP requests may contain failed search information. 如果在進行搜尋作業時發生錯誤,「Bing 影像搜尋 API」將會傳回非 200 HTTP 狀態碼,並在 JSON 回應中包含錯誤資訊。If an error occurs during the search operation, the Bing Image Search API will return a non-200 HTTP status code and error information in the JSON response. 此外,若要求的速率受到限制,API 將會傳回空白回應。Additionally, if the request was rate-limited, the API will return an empty response.

顯示搜尋結果Display the search results

renderSearchResults() 函式會顯示搜尋結果,此函式會接受「Bing 影像搜尋」服務所傳回的 JSON,並針對所傳回的任何影像和相關搜尋,呼叫適當的轉譯器函式。Search results are displayed by the renderSearchResults() function, which takes the JSON returned by the Bing Image Search service and calls an appropriate renderer function on any returned images and related searches.

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 物件中。Image search results are contained in the top-level value object within the JSON response. 這些會傳遞給 renderImageResults(),它會逐一查看結果並將每個項目轉換成 HTML。These are passed to renderImageResults(), which iterates through the results and converts each item into 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」可以傳回四種類型的搜尋建議來協助引導使用者的搜尋體驗,其中每種建議都在自己的最上層物件中:The Bing Image Search API can return four types of search suggestions to help guide users' search experiences, each in its own top-level object:

建議Suggestion 說明Description
pivotSuggestions 將原始搜尋中的樞紐字組取代為不同樞紐字組的查詢。Queries that replace a pivot word in original search with a different one. 比方說,若您搜尋「紅色花卉」,樞紐字組可能是「紅色」,而樞紐建議可能是「黃色花卉」。For example, if you search for "red flowers," a pivot word might be "red," and a pivot suggestion might be "yellow flowers."
queryExpansions 藉由新增多個字詞以縮小原始搜尋範圍的查詢。Queries that narrow the original search by adding more terms. 比方說,若您搜尋 "Microsoft Surface",可能是查詢擴充可能是 "Microsoft Surface Pro"。For example, if you search for "Microsoft Surface," a query expansion might be "Microsoft Surface Pro."
relatedSearches 也已由輸入原始搜尋的其他使用者輸入的查詢。Queries that have also been entered by other users who entered the original search. 比方說,若您搜尋「雷尼爾山」,相關搜尋可能是「For example, if you search for "Mount Rainier," a related search might be "Mt. 聖海倫山」。Saint Helens."
similarTerms 與原始搜尋的意義類似的查詢。Queries that are similar in meaning to the original search. 例如,如果您搜尋「小貓」,類似的字詞可能是「可愛」。For example, if you search for "kittens," a similar term might be "cute."

此應用程式只會轉譯 relatedItems 建議,並將產生的連結置於頁面的資訊看板中。This application only renders the relatedItems suggestions, and places the resulting links in the page's sidebar.

轉譯搜尋結果Rendering search results

在此應用程式中,searchItemRenderers 物件會包含可為每一種搜尋結果產生 HTML 的轉譯器函式。In this application, the searchItemRenderers object contains renderer functions that generate HTML for each kind of search result.

searchItemRenderers = {
    images: function(item, index, count) { ... },
    relatedSearches: function(item) { ... }
}

這些轉譯器函式可接受下列參數:These renderer functions accept the following parameters:

參數Parameter 說明Description
item JavaScript 物件,其中包含項目的屬性,例如其 URL 及其描述。The JavaScript object containing the item's properties, such as its URL and its description.
index 集合內結果項目的索引。The index of the result item within its collection.
count 搜尋結果項目集合中的項目數目。The number of items in the search result item's collection.

indexcount 參數可用來將結果編號、為集合產生 HTML,以及組織內容。The index and count parameters are used to number results, generate HTML for collections, and organize the content. 具體而言,它會:Specifically, it:

  • 計算影像縮圖大小 (寬度變動,最小值為 120 像素,而高度固定為 90 像素)。Calculates the image thumbnail size (width varies, with a minimum of 120 pixels, while height is fixed at 90 pixels).
  • 建置 HTML <img> 標籤,以顯示影像縮圖。Builds the HTML <img> tag to display the image thumbnail.
  • 建置 HTML <a> 標籤,以連結至影像和內含影像的頁面。Builds the HTML <a> tags that link to the image and the page that contains it.
  • 建置描述,以顯示影像及執行所在網站的相關資訊。Builds the description that displays information about the image and the site it's on.
    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

縮圖影像的 heightwidth 會同時用於 <img> 標記及縮圖 URL 中的 hw 欄位。The thumbnail image's height and width are used in both the <img> tag and the h and w fields in the thumbnail's URL. 這讓 Bing 能夠傳回與該大小完全相同的縮圖This enables Bing to return a thumbnail of exactly that size.

保存用戶端識別碼Persisting client ID

來自 Bing 搜尋 API 的回應可能會包含應在後續要求中傳回至 API 的 X-MSEdge-ClientID 標頭。Responses from the Bing search APIs may include a X-MSEdge-ClientID header that should be sent back to the API with successive requests. 若使用多個 Bing 搜尋 API,請盡可能對所有 API 使用相同的用戶端識別碼。If multiple Bing Search APIs are being used, the same client ID should be used with all of them, if possible.

提供 X-MSEdge-ClientID 標頭可讓 Bing API 將使用者的所有搜尋建立關聯,這在下列情況下相當有用:Providing the X-MSEdge-ClientID header allows the Bing APIs to associate all of a user's searches, which is useful in

首先,它可讓 Bing 搜尋引擎將過去內容套用至搜尋結果,以尋找更符合使用者的結果。First, it allows the Bing search engine to apply past context to searches to find results that better satisfy the user. 例如,若使用者之前搜尋與航行相關的字詞,稍後搜尋「節」可能會優先傳回航行中所使用節數的相關資訊。If a user has previously searched for terms related to sailing, for example, a later search for "knots" might preferentially return information about knots used in sailing.

其次,Bing 可能會隨機選取使用者來體驗新功能,再廣泛提供這些功能。Second, Bing may randomly select users to experience new features before they are made widely available. 在每個要求中提供相同的用戶端識別碼,可確保已選擇看到功能的使用者一律會看到功能。Providing the same client ID with each request ensures that users that have been chosen to see a feature always see it. 若沒有用戶端識別碼,使用者可能會在其搜尋結果中隨機看到功能出現並消失。Without the client ID, the user might see a feature appear and disappear, seemingly at random, in their search results.

瀏覽器安全性原則 (CORS) 可防止將 X-MSEdge-ClientID 標頭提供給 JavaScript 使用。Browser security policies (CORS) may prevent the X-MSEdge-ClientID header from being available to JavaScript. 當搜尋回應的來源與要求回應的頁面不同時,就會發生這項限制。This limitation occurs when the search response has a different origin from the page that requested it. 在生產環境中,您應該裝載伺服器端指令碼,在與網頁相同的網域上執行 API 呼叫,以處理此原則。In a production environment, you should address this policy by hosting a server-side script that does the API call on the same domain as the Web page. 由於指令碼的來源與網頁相同,因此 X-MSEdge-ClientID 標頭會接著提供給 JavaScript 使用。Since the script has the same origin as the Web page, the X-MSEdge-ClientID header is then available to JavaScript.

注意

在生產 Web 應用程式中,無論如何都應該執行要求伺服器端。In a production Web application, you should perform the request server-side anyway. 否則,您的 Bing 搜尋 API 金鑰必須包含在網頁中,以提供給檢視來源的任何人。Otherwise, your Bing Search API key must be included in the Web page, where it is available to anyone who views source. 您會根據 API 訂用帳戶金鑰的所有使用量付費,即使是未經授權的合作對象所提出的要求,因此請務必不要公開您的金鑰。You are billed for all usage under your API subscription key, even requests made by unauthorized parties, so it is important not to expose your key.

若要進行開發,您可以透過 CORS Proxy 提出 Bing Web 搜尋 API 要求。For development purposes, you can make the Bing Web Search API request through a CORS proxy. 來自這類 Proxy 的回應包含 Access-Control-Expose-Headers 標頭,可讓回應標頭列入允許清單並提供給 JavaScript 使用。The response from such a proxy has an Access-Control-Expose-Headers header that allows response headers and makes them available to JavaScript.

您可以輕鬆安裝 CORS Proxy,讓我們的教學課程應用程式存取用戶端識別碼標頭。It's easy to install a CORS proxy to allow our tutorial app to access the client ID header. 首先,請安裝 Node.js (若尚未安裝)。First, if you don't already have it, install Node.js. 然後在命令視窗中發出下列命令:Then issue the following command in a command window:

npm install -g cors-proxy-server

接下來,將 HTML 檔案中的 Bing Web 搜尋端點變更為:Next, change the Bing Web Search endpoint in the HTML file to:

http://localhost:9090/https://api.cognitive.microsoft.com/bing/v7.0/search

最後,使用下列命令啟動 CORS Proxy:Finally, start the CORS proxy with the following command:

cors-proxy-server

當您使用教學課程應用程式時,請保持開啟命令視窗;關閉視窗會停止 Proxy。Leave the command window open while you use the tutorial app; closing the window stops the proxy. 在可展開的 [HTTP 標頭] 區段搜尋結果下,您現在可以看到 X-MSEdge-ClientID 標頭 (及其他標頭),並確認每個要求的此標頭都相同。In the expandable HTTP Headers section below the search results, you can now see the X-MSEdge-ClientID header (among others) and verify that it is the same for each request.

後續步驟Next steps

另請參閱See also