Tutorial: Create a single-page app using the Bing Image Search API

The Bing Image Search API enables you to search the web for high-quality, relevant images. 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. This tutorial is similar to the corresponding tutorial for Bing Web Search.

The tutorial app illustrates how to:

  • Perform a Bing Image Search API call in JavaScript
  • Improve search results using search options
  • Display and page through search results
  • Request and handle an API subscription key, and Bing client ID.

The full source code for this tutorial is available on GitHub.

Prerequisites

  • The latest version of Node.js.
  • The Express.js framework for Node.js. Installation instructions for the source code are available in the GitHub sample readme file.

Create an Azure resource

Start using the Bing Image Search API by creating one of the following Azure resources.

Bing Search v7 resource

  • Available through the Azure portal until you delete the resource.
  • Use the free pricing tier to try the service, and upgrade later to a paid tier for production.

Multi-service resource

  • Available through the Azure portal until you delete the resource.
  • Use the same key and endpoint for your applications, across multiple Cognitive Services.

Manage and store user subscription keys

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. If the key is later rejected by the API, The app will remove it from storage.

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.bing.microsoft.com/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();
    }
}

The getSubscriptionKey() function attempts to retrieve a previously stored key using retrieveValue. 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;
}

The HTML <form> tag onsubmit calls the bingWebSearch function to return search results. bingWebSearch uses getSubscriptionKey to authenticate each query. 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

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)">

By default, the onsubmit handler returns false, keeping the form from being submitted.

Select search options

[Bing Image Search form]

The Bing Image Search API offers several filter query parameters to narrow and filter search results. The HTML form in this application uses and displays the following parameter options:

Option Description
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 Drop-down menu for optionally specifying the color of the found images.
when Drop-down menu for optionally limiting the search to the most recent day, week, or month.
safe 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. It's reset to 0 on a new request.
nextoffset Hidden field. 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. A JSON-encoded list of the offsets of preceding pages of search results, for navigating back to previous pages.

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

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

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

Important

Successful HTTP requests may contain failed search information. 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. Additionally, if the request was rate-limited, the API will return an empty response.

Display the search results

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

Image search results are contained in the top-level value object within the JSON response. 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");
}

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. 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."

This application only renders the relatedItems suggestions, and places the resulting links in the page's sidebar.

Rendering search results

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 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.

The index and count parameters are used to number results, generate HTML for collections, and organize the content. Specifically, it:

  • Calculates the image thumbnail size (width varies, with a minimum of 120 pixels, while height is fixed at 90 pixels).
  • Builds the HTML <img> tag to display the image thumbnail.
  • 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

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. This enables Bing to return a thumbnail of exactly that size.

Persisting client ID

Responses from the Bing search APIs may include a X-MSEdge-ClientID header that should be sent back to the API with successive requests. If multiple Bing Search APIs are being used, the same client ID should be used with all of them, if possible.

Providing the X-MSEdge-ClientID header allows the Bing APIs to associate all of a user's searches, which is useful in:

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.

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.

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. 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. Since the script has the same origin as the Web page, the X-MSEdge-ClientID header is then available to JavaScript.

Note

In a production Web application, you should perform the request server-side anyway. Otherwise, your Bing Search API key must be included in the Web page, where it is available to anyone who views source. 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.

For development purposes, you can make the Bing Web Search API request through a CORS proxy. The response from such a proxy has an Access-Control-Expose-Headers header that allows response headers and makes them available to JavaScript.

It's easy to install a CORS proxy to allow our tutorial app to access the client ID header. 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

Next, change the Bing Web Search endpoint in the HTML file to:
http://localhost:9090/https://api.bing.microsoft.com/v7.0/search

Finally, start the CORS proxy with the following command:

cors-proxy-server

Leave the command window open while you use the tutorial app; closing the window stops the proxy. 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