共用方式為


教學課程:使用 Bing Web 搜尋 API 建立單頁應用程式

警告

在 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 服務帳戶

先決條件

以下是您需要執行應用程式的一些事項:

第一個步驟是使用範例應用程式的原始程式碼複製存放庫。

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 可以設定為 strictmoderateoff,其 moderate 為 Bing Web 搜尋的預設設定。 此表單使用一個複選框,其中包含兩種狀態: strictmoderate

如果選取任何 [ 升階] 複選框,參數 answerCount 就會新增至查詢。 answerCount 使用 promote 參數時是必要的。 在此代碼段中,值會設定為 9 ,以傳回所有可用的結果類型。

備註

提升結果類型並不 保證 該類型會被包含在搜尋結果中。 相反地,提升會使這類結果的排名相較於其通常的排名提高。 若要將搜尋限制為特定類型的結果,請使用 responseFilter 查詢參數,或呼叫更特定的端點,例如 Bing 影像搜尋或 Bing 新聞搜尋。

textDecorationtextFormat 查詢參數會硬式編碼到腳本中,並導致搜尋字詞在搜尋結果中以粗體顯示。 不需要這些參數。

管理訂用帳戶金鑰

為了避免硬式編碼 Bing 搜尋 API 訂用帳戶金鑰,此範例應用程式會使用瀏覽器的永續性記憶體來儲存訂用帳戶密鑰。 如果未儲存任何訂用帳戶密鑰,系統會提示使用者輸入一個。 如果 API 拒絕訂用帳戶金鑰,系統會提示使用者重新輸入訂用帳戶密鑰。

getSubscriptionKey() 式會使用 storeValueretrieveValue 函式來儲存和擷取使用者的訂用帳戶密鑰。 如果支援,這些函式會使用 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 物件來呼叫 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 區塊
提出請求 網路錯誤,中止的連線 errorabort 事件處理程式
執行搜尋 無效的要求、無效的 JSON、速率限制 事件處理程式中的 load 測試

錯誤會透過呼叫renderErrorMessage()來處理。 如果回應通過所有錯誤測試, renderSearchResults() 則會呼叫 以顯示搜尋結果。

顯示搜尋結果

Bing Web 搜尋 API 所傳回的結果有 使用和顯示需求 。 由於回應可能包含各種結果類型,因此無法逐一查看最上層 WebPages 集合。 相反地,範例應用程式會使用 RankingResponse 來將結果排序為規格。

備註

如果您只想要單一結果類型,請使用 responseFilter 查詢參數,或考慮使用其他其中一個 Bing 搜尋端點,例如 Bing 影像搜尋。

每個回應都有一個 物件,最多可包含三個 RankingResponse 集合: polemainlinesidebarpole如果存在,則為最相關的搜尋結果,且必須醒目顯示。 mainline 包含大部分的搜尋結果,且會在 之後 pole立即顯示。 sidebar 包含輔助搜尋結果。 可能的話,這些結果應該會顯示在提要欄中。 如果螢幕限制使提要字段不切實際,這些結果應該會出現在 mainline 結果之後。

每個 RankingResponse 都包含一個 RankingItem 陣列,指定必須如何排序結果。 我們的範例應用程式會使用 answerTyperesultIndex 參數來識別結果。

備註

還有其他方法可以識別和排名結果。 如需詳細資訊,請參閱 使用排名來顯示結果

讓我們看看程式代碼:

// 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 顯示項目的結果區段 (polemainlinesidebar) 。
index
count
RankingResponse 專案指定要顯示指定集合中的所有結果時可用, undefined 否則為 。 項目在其集合內的索引,以及該集合中的項目總數。 您可以使用這項資訊來編號結果、為第一個或最後一個結果產生不同的 HTML 等等。

在範例應用程式中,imagesrelatedSearches 轉譯器都會使用上下文參數來自定義產生的 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> 標記以顯示影像縮圖。

影像轉譯器會使用 sectionindex 變數,根據結果出現的位置而有所不同。 換行符(<br> 標籤)會在側邊欄的影像結果之間插入,以便側邊欄顯示圖片列。 在其他區段中,第一個影像結果(index === 0)前面會加上<p>標記。

縮圖大小用於縮圖 URL 中的 <img> 標籤以及 hw 欄位。 titlealt 屬性(影像的文字描述)是從影像的名稱和URL中的主機名建構的。

以下是範例應用程式中影像顯示方式的範例:

[Bing 影像結果]

保存用戶端識別碼

來自 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 應該會顯示標頭。 確認每個要求的內容都相同。

後續步驟