教程:使用必应图像搜索 API 创建单页应用

警告

2020 年 10 月 30 日,必应搜索 API 从 Azure AI 服务迁移到必应搜索服务。 本文档仅供参考。 有关更新的文档,请参阅必应搜索 API 文档。 关于为必应搜索创建新的 Azure 资源的说明,请参阅通过 Azure 市场创建必应搜索资源

必应图像搜索 API 使你能够搜索 Web 以获取高质量的相关图像。 使用本教程生成一个单页 Web 应用程序,该应用程序可将搜索查询发送到 API,并在网页中显示结果。 本教程类似于必应网页搜索的相应教程。

本教程应用演示了如何:

  • 在 JavaScript 中执行必应图像搜索 API 调用
  • 使用搜索选项改进搜索结果
  • 显示搜索结果并分页浏览
  • 请求和处理 API 订阅密钥和 Bing 客户端 ID。

先决条件

管理和存储用户订阅密钥

此应用程序使用 Web 浏览器的持久性存储来存储 API 订阅密钥。 如果未存储任何密钥,网页将提示用户输入其密钥,并存储该密钥供以后使用。 如果密钥稍后被 API 拒绝,应用将从存储中删除该密钥。 此示例使用全局终结点。 还可以使用 Azure 门户中为资源显示的 自定义子域 终结点。

定义 storeValueretrieveValue 函数以使用 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,使表单不被提交。

选择搜索选项

[必应图像搜索表单]

必应图像搜索 API 提供了多个 筛选器查询参数 来缩小和筛选搜索结果。 此应用程序中的 HTML 窗体使用以下参数选项:

选项 说明
where 用于选择用于搜索的市场(位置和语言)的下拉菜单。
query 要在其中输入搜索词的文本字段。
aspect 用于选择所找到图像比例的单选按钮:比例大致为正方形、宽或高。
color
when 用于选择性地将搜索限制为最近一天、星期或月份的下拉菜单。
safe 一个复选框,指示是否使用必应的安全搜索功能筛选出“成人”结果。
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 对象向必应图像搜索终结点发出请求。

// 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 请求可能包含失败的搜索信息。 如果在搜索作期间发生错误,必应图像搜索 API 将在 JSON 响应中返回非 200 HTTP 状态代码和错误信息。 此外,如果请求速率受限,API 将返回空响应。

显示搜索结果

搜索结果由 renderSearchResults() 函数显示,该函数采用必应图像搜索服务返回的 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");
}

必应图像搜索 API 可以返回四种类型的搜索建议,以帮助指导用户的搜索体验,每个建议在其自己的顶级对象中:

建议 说明
pivotSuggestions 在原始搜索中用不同的词替换关键字的查询。 例如,如果搜索“红色花朵”,那么“红色”可能是一个关键词,而建议的可选项可能是“黄色花朵”。
queryExpansions 通过添加更多字词来缩小原始搜索范围的查询。 例如,如果搜索“Microsoft Surface”,查询扩展可能是“Microsoft Surface Pro”。
relatedSearches 其他用户在进行相同原始搜索时也输入的查询。 例如,如果搜索“雷尼尔山”,则相关搜索可能是“雷尼尔”。 圣海伦斯。
similarTerms 与原始搜索在含义上类似的查询。 例如,如果搜索“小猫”,类似的术语可能是“可爱”。

此应用程序仅呈现 relatedItems 建议,并将生成的链接放置在页面的边栏中。

呈现搜索结果

在此应用程序中,对象 searchItemRenderers 包含为每种搜索结果生成 HTML 的呈现器函数。

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

这些呈现器函数接受以下参数:

参数 说明
item 包含项属性的 JavaScript 对象,例如其 URL 及其说明。
index 其集合中结果项的索引。
count 搜索结果项集合中的项数。

参数indexcount用于对结果进行编号、为集合生成 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

缩略图图像的heightwidth用于<img>标签以及缩略图 URL 中的hw字段。 这使得必应能够返回一个大小完全相同的缩略图

持续保存客户端 ID

来自必应搜索 API 的响应可能包括一个 X-MSEdge-ClientID 标头,该标头应随后续请求发送回 API。 如果使用多个必应搜索 API,则应将相同的客户端 ID 与所有这些 API 一起使用(如果可能)。

提供X-MSEdge-ClientID标头可以使必应 API 关联所有用户的搜索,这在以下方面非常有用:

首先,它允许必应搜索引擎将过去的上下文应用于搜索,以查找更能满足用户的结果。 例如,如果用户之前搜索了与航行相关的术语,则以后搜索“结”可能会优先返回有关帆船中使用的结的信息。

其次,必应可能会随机挑选用户来体验新功能,这发生在它们被广泛使用之前。 为每个请求提供相同的客户端 ID 可确保选择查看功能的用户始终看到该功能。 如果没有客户端 ID,用户可能会在搜索结果中看到一个功能出现并消失(看似随机)。

浏览器安全策略(CORS)可能会阻止 X-MSEdge-ClientID 标头可用于 JavaScript。 当搜索响应的来源与请求该响应的页面不同时,就会出现此限制。 在生产环境中,应通过托管在与网页相同的域上执行 API 调用的服务器端脚本来解决此策略。 由于脚本的源与网页相同,X-MSEdge-ClientID 标头随后可供 JavaScript 使用。

注释

在生产 Web 应用程序中,在任何情况下都应执行服务器端请求。 否则,您的必应搜索 API 密钥必须放在网页中,这样,查看源代码的任何人都可以使用。 API 订阅密钥下的所有使用情况(甚至未经授权的各方发出的请求)都会向你计费,因此请务必不公开密钥。

出于开发目的,可以通过 CORS 代理发起微软必应 Web 搜索 API 请求。 来自此类代理的响应具有一个 Access-Control-Expose-Headers 标头,该标头允许响应标头并将其提供给 JavaScript。

可以轻松安装 CORS 代理,以允许我们的教程应用访问客户端 ID 标头。 首先,如果尚未安装,安装 Node.js。 然后在命令窗口中发出以下命令:

npm install -g cors-proxy-server

接下来,将 HTML 文件中的必应 Web 搜索终结点更改为:
http://localhost:9090/https://api.cognitive.microsoft.com/bing/v7.0/search

最后,使用以下命令启动 CORS 代理:

cors-proxy-server

使用教程应用时,使命令窗口保持打开状态;关闭窗口会停止代理。 在搜索结果下方的可展开 HTTP 标头部分中,现在可以看到 X-MSEdge-ClientID 标头(等等),并验证每个请求是否相同。

后续步骤

另请参阅