Tutorial: Criar uma aplicação de página única com a API de Pesquisa de Imagens do Bing

Aviso

A 30 de outubro de 2020, as APIs de Pesquisa do Bing passaram dos serviços de IA do Azure para os Serviços Pesquisa do Bing. Esta documentação é fornecida apenas para referência. Para obter documentação atualizada, veja a documentação da API de pesquisa do Bing. Para obter instruções sobre como criar novos recursos do Azure para a pesquisa do Bing, veja Criar um recurso de Pesquisa do Bing através do Azure Marketplace.

A API de Pesquisa de Imagens do Bing permite-lhe procurar na Web imagens relevantes e de alta qualidade. Utilize este tutorial para criar uma aplicação Web de página única que possa enviar consultas de pesquisa para a API e apresentar os resultados na página Web. Este tutorial é semelhante ao tutorial correspondente para Pesquisa na Web do Bing.

A aplicação de tutorial ilustra como:

  • Fazer uma chamada à API de Pesquisa de Imagens do Bing em JavaScript
  • Melhorar os resultados da pesquisa com opções de pesquisa
  • Apresentar e navegar pelos resultados da pesquisa
  • Pedir e processar uma chave de subscrição da API e o ID de cliente do Bing.

Pré-requisitos

  • A versão mais recente do Node.js.
  • A arquitetura Express.js para Node.js. As instruções de instalação para o código fonte estão disponíveis no ficheiro de readme de exemplo do GitHub.

Gerir e armazenar chaves de subscrição de utilizador

Esta aplicação utiliza o armazenamento persistente dos browsers para armazenar chaves de subscrição da API. Se não for armazenada nenhuma chave, a página Web irá pedir ao utilizador a respetiva chave e irá armazená-la para utilização posterior. Se a chave for rejeitada pela API mais tarde, a aplicação irá removê-la do armazenamento. Este exemplo utiliza o ponto final global. Também pode utilizar o ponto final de subdomínio personalizado apresentado no portal do Azure do recurso.

Defina as funções storeValue e retrieveValue para utilizar o objeto localStorage (se for suportado pelo browser) ou um 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();
    }
}

A função getSubscriptionKey() tenta obter uma chave anteriormente armazenada com o método retrieveValue. Se não for encontrada nenhuma chave, será pedido ao utilizador a sua chave, que será armazenada com o método 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;
}

A tag <form> de HTML onsubmit chama a função bingWebSearch, para devolver os resultados da pesquisa. bingWebSearch utiliza getSubscriptionKey para autenticar cada consulta. Conforme mostrado na definição anterior, getSubscriptionKey pede a chave ao utilizador caso a mesma não tenha sido introduzida. Depois, a chave é armazenada e utilizada continuamente pela aplicação.

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

Enviar pedidos de pesquisa

Esta aplicação utiliza uma tag <form> de HTML para enviar inicialmente pedidos de pesquisa de utilizador, ao utilizar o atributo onsubmit para chamar newBingImageSearch().

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

Por predefinição, o processador onsubmit devolve false, que impede o formulário de ser submetido.

Selecionar as opções de pesquisa

[Formulário de Pesquisa de Imagens do Bing]

A API de Pesquisa de Imagens do Bing oferece vários parâmetros de consulta de filtro para restringir e filtrar os resultados da pesquisa. O formulário HTML nesta aplicação utiliza e apresenta as seguintes opções de parâmetros:

Opção Descrição
where Um menu pendente para selecionar o mercado (localização e idioma) utilizado para a pesquisa.
query O campo de texto no qual introduzir os termos da pesquisa.
aspect Botões de opção para escolher as proporções das imagens encontradas: aproximadamente quadrada, larga ou alta.
color
when Menu pendente para limitar, opcionalmente, a pesquisa ao dia, semana ou mês mais recente.
safe Uma caixa de verificação que indica se deve ser utilizada a funcionalidade SafeSearch do Bing para filtrar resultados de conteúdos para adultos.
count Campo oculto. O número de resultados de pesquisa a devolver em cada pedido. Altere-o para mostrar menos ou mais resultados por página.
offset Campo oculto. O desfasamento do primeiro resultado da pesquisa no pedido, utilizado para paginação. É reposto para 0 nos pedidos novos.
nextoffset Campo oculto. Ao receber um resultado de pesquisa, este campo é definido para o valor de nextOffset na resposta. Com este campo evita resultados sobrepostos em páginas sucessivas.
stack Campo oculto. Uma lista com codificação JSON dos desvios das páginas anteriores de resultados de pesquisa, para navegar novamente para as páginas anteriores.

A função bingSearchOptions() formata estas opções numa cadeia de consulta parcial, que pode ser utilizada nos pedidos da API da aplicação.

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

Fazer o pedido

Com a consulta de pesquisa, a cadeia de opções e a chave de API, a função BingImageSearch() utiliza um objeto XMLHttpRequest para fazer o pedido ao ponto final da Pesquisa de Imagens do 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;
}

Após a conclusão do pedido HTTP com êxito, o JavaScript chama o processador de eventos de "carga" handleBingResponse(), para processar um pedido HTTP GET com êxito.

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

Importante

Os pedidos HTTP bem-sucedidos podem conter informações de pesquisa com falhas. Se ocorrer um erro durante a operação de pesquisa, a API de Pesquisa de Imagens do Bing devolve um código de estado HTTP que não 200 e informações do erro na resposta JSON. Além disso, se o pedido tiver limites de velocidade, a API irá devolver uma resposta vazia.

Apresentar os resultados da pesquisa

Os resultados da pesquisa são apresentados pela função renderSearchResults(), que utiliza o JSON devolvido pelo serviço de Pesquisa de Imagens do Bing e chama uma função de compositor apropriada em quaisquer imagens e pesquisas relacionadas.

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));
}

Os resultados da pesquisa de imagens estão incluídos no objeto value de nível superior na resposta JSON. Estes são transmitidos a renderImageResults(), que itera através dos resultados e converte cada item em 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");
}

A API de Pesquisa de Imagens do Bing pode devolver quatro tipos de sugestões de pesquisa para ajudar a orientar as experiências de pesquisa dos utilizadores, cada um no seu próprio objeto de nível superior:

Sugestão Description
pivotSuggestions Consultas que substituem uma palavra “pivô” na pesquisa original por outra diferente. Por exemplo, se procurar "flores vermelhas", uma palavra pivô poderá ser "vermelhas" e uma sugestão pivô "flores amarelas".
queryExpansions Consultas que reduzem a consulta original mediante a adição de mais termos. Por exemplo, se procurar "Microsoft Surface", uma expansão da consulta poderá ser "Microsoft Surface Pro".
relatedSearches Consultas que também foram introduzidas por outros utilizadores que introduziram a pesquisa original. Por exemplo, se procurar "Monte Rainier", uma consulta relacionada poderá ser "Monte de Santa Helena".
similarTerms Consultas cujo significado é semelhante ao da pesquisa original. Por exemplo, se procurar "gatinhos", um termo semelhante poderá ser "fofos".

Esta aplicação apenas apresenta as sugestões de relatedItems e coloca as ligações resultantes na barra lateral da página.

Composição de resultados da pesquisa

Nesta aplicação, o objeto searchItemRenderers contém funções de compositor que geram o HTML de cada tipo de resultado da pesquisa.

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

Estas funções de compositor podem aceitar os seguintes parâmetros:

Parâmetro Description
item O objeto JavaScript que contém as propriedades do item, como o URL e a descrição.
index O índice do item do resultado dentro da respetiva coleção.
count O número de itens na coleção do item do resultado da pesquisa.

Os parâmetros index e count são utilizados para numerar os resultados, gerar HTML para coleções e organizar o conteúdo. Mais concretamente:

  • Calcula o tamanho da miniatura da imagem (a largura varia, com um mínimo de 120 píxeis e a altura está fixa em 90 píxeis).
  • Cria a etiqueta HTML <img> para apresentar a miniatura da imagem.
  • Cria as tags <a> de HTML que ligam à imagem e à página que a contém.
  • Cria a descrição que apresenta as informações sobre a imagem e o site no qual a imagem se encontra.
    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

A height e width da imagem da miniatura são utilizadas na tag <img> e nos campos h e w no respetivo URL. Isto permite que o Bing devolva uma miniatura exatamente desse tamanho.

ID de cliente persistente

As respostas das APIs de Pesquisa do Bing podem incluir um cabeçalho X-MSEdge-ClientID, o qual deve ser reenviado à API com os sucessivos pedidos. Se estiverem a ser utilizadas várias APIs de Pesquisa do Bing, deve ser utilizado o mesmo ID de cliente em todas as APIs, se possível.

Fornecer o cabeçalho X-MSEdge-ClientID permite que as APIs do Bing associem todas as pesquisas de um utilizador, o que é útil porque:

Em primeiro lugar, permite que o motor de busca do Bing aplique um contexto passado às pesquisas para encontrar resultados que deixem o utilizador mais satisfeito. Se um utilizador tiver procurado termos relacionados com vela, por exemplo, ao procurar posteriormente a palavra "nós" poderá devolver, de preferência, informações sobre os nós utilizados em vela.

Em segundo lugar, o Bing pode selecionar utilizadores aleatoriamente para experimentarem funcionalidades novas antes de serem disponibilizadas ao grande público. Fornecer o mesmo ID de cliente em todos os pedidos garante que os utilizadores que foram escolhidos para ver uma funcionalidade a verão sempre. Sem o ID de cliente, os utilizadores poderão ver a funcionalidade aparecer e desaparecer, de forma aparentemente aleatória, nos resultados da pesquisa.

As políticas de segurança do browser (CORS) podem impedir que o cabeçalho X-MSEdge-ClientID esteja disponível para o JavaScript. Esta limitação ocorre quando a origem da resposta da pesquisa é diferente da página que a pediu. Num ambiente de produção, deve abordar esta política ao alojar um script do lado do servidor que faça a chamada à API no mesmo domínio da página Web. Uma vez que a origem do script é a mesma da página Web, o cabeçalho X-MSEdge-ClientID ficará disponível para o JavaScript.

Nota

Numa aplicação Web de produção, deve fazer o pedido no lado do servidor mesmo assim. Caso contrário, a chave da API de Pesquisa do Bing tem de ser incluída na página Web, onde ficará disponível para qualquer pessoa que veja a origem. São-lhe cobradas todas as utilizações feitas com a sua chave de subscrição da API, mesmo os pedidos feitos por partes não autorizadas, pelo que é importante que não revele a sua chave.

Para fins de programação, pode fazer o pedido da API de Pesquisa na Web do Bing através de um proxy do CORS. A resposta de um proxy deste tipo tem um Access-Control-Expose-Headers cabeçalho que permite cabeçalhos de resposta e os disponibiliza para JavaScript.

É fácil instalar um proxy do CORS para permitir que a nossa aplicação de tutorial aceda ao cabeçalho do ID de cliente. Em primeiro lugar, se ainda não o tiver, instale Node.js. Em seguida, emita o comando seguinte numa janela de comando:

npm install -g cors-proxy-server

Depois, altere o ponto final da Pesquisa na Web do Bing no ficheiro HTML para:
http://localhost:9090/https://api.cognitive.microsoft.com/bing/v7.0/search

Por fim, inicie o proxy do CORS com o comando seguinte:

cors-proxy-server

Deixe a janela de comando aberta enquanto utiliza a aplicação de tutorial. Se a janela for fechada, o proxy é interrompido. Na secção Cabeçalhos HTTP expansíveis, abaixo dos resultados da pesquisa, pode agora ver o cabeçalho X-MSEdge-ClientID (entre outros) e confirmar se é o mesmo em todos os pedidos.

Passos seguintes

Ver também