Tutorial: Create a single-page app using the Bing Web Search API
Warning
On October 30, 2020, the Bing Search APIs moved from Azure AI services to Bing Search Services. This documentation is provided for reference only. For updated documentation, see the Bing search API documentation. For instructions on creating new Azure resources for Bing search, see Create a Bing Search resource through the Azure Marketplace.
This single-page app demonstrates how to retrieve, parse, and display search results from the Bing Web Search API. The tutorial uses boilerplate HTML and CSS, and focuses on the JavaScript code. HTML, CSS, and JS files are available on GitHub with quickstart instructions.
This sample app can:
- Call the Bing Web Search API with search options
- Display web, image, news, and video results
- Paginate results
- Manage subscription keys
- Handle errors
To use this app, an Azure AI services account with Bing Search APIs is required.
Prerequisites
Here are a few things that you'll need to run the app:
An Azure subscription - Create one for free
Once you have your Azure subscription, create a Bing Search resource in the Azure portal to get your key and endpoint. After it deploys, click Go to resource.
Node.js 8 or later
The first step is to clone the repository with the sample app's source code.
git clone https://github.com/Azure-Samples/cognitive-services-REST-api-samples.git
Then run npm install
. For this tutorial, Express.js is the only dependency.
cd <path-to-repo>/cognitive-services-REST-api-samples/Tutorials/bing-web-search
npm install
App components
The sample app we're building is made up of four parts:
bing-web-search.js
- Our Express.js app. It handles request/response logic and routing.public/index.html
- The skeleton of our app; it defines how data is presented to the user.public/css/styles.css
- Defines page styles, such as fonts, colors, text size.public/js/scripts.js
- Contains the logic to make requests to the Bing Web Search API, manage subscription keys, handle and parse responses, and display results.
This tutorial focuses on scripts.js
and the logic required to call the Bing Web Search API and handle the response.
HTML form
The index.html
includes a form that enables users to search and select search options. The onsubmit
attribute fires when the form is submitted, calling the bingWebSearch()
method defined in scripts.js
. It takes three arguments:
- Search query
- Selected options
- Subscription key
<form name="bing" onsubmit="return bingWebSearch(this.query.value,
bingSearchOptions(this), getSubscriptionKey())">
Query options
The HTML form includes options that map to query parameters in the Bing Web Search API v7. This table provides a breakdown of how users can filter search results using the sample app:
Parameter | Description |
---|---|
query |
A text field to enter a query string. |
where |
A drop-down menu to select the market (location and language). |
what |
Checkboxes to promote specific result types. Promoting images, for example, increases the ranking of images in search results. |
when |
A drop-down menu that allows the user to limit the search results to today, this week, or this month. |
safe |
A checkbox to enable Bing SafeSearch, which filters out adult content. |
count |
Hidden field. The number of search results to return on each request. Change this value to display fewer or more results per page. |
offset |
Hidden field. The offset of the first search result in the request, which is used for paging. It's reset to 0 with each new request. |
Note
The Bing Web Search API offers additional query parameters to help refine search results. This sample only uses a few. For a complete list of available parameters, see Bing Web Search API v7 reference.
The bingSearchOptions()
function converts these options to match the format required by the Bing Search 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
can be set to strict
, moderate
, or off
, with moderate
being the default setting for Bing Web Search. This form uses a checkbox, which has two states: strict
or moderate
.
If any of the Promote checkboxes are selected, the answerCount
parameter is added to the query. answerCount
is required when using the promote
parameter. In this snippet, the value is set to 9
to return all available result types.
Note
Promoting a result type doesn't guarantee that it will be included in the search results. Rather, promotion increases the ranking of those kinds of results relative to their usual ranking. To limit searches to particular kinds of results, use the responseFilter
query parameter, or call a more specific endpoint such as Bing Image Search or Bing News Search.
The textDecoration
and textFormat
query parameters are hardcoded into the script, and cause the search term to be boldfaced in the search results. These parameters aren't required.
Manage subscription keys
To avoid hardcoding the Bing Search API subscription key, this sample app uses a browser's persistent storage to store the subscription key. If no subscription key is stored, the user is prompted to enter one. If the subscription key is rejected by the API, the user is prompted to re-enter a subscription key.
The getSubscriptionKey()
function uses the storeValue
and retrieveValue
functions to store and retrieve a user's subscription key. These functions use the localStorage
object, if supported, or cookies.
// 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;
}
As we saw earlier, when the form is submitted, onsubmit
fires, calling bingWebSearch
. This function initializes and sends the request. getSubscriptionKey
is called on each submission to authenticate the request.
Call Bing Web Search
Given the query, the options string, and the subscription key, the BingWebSearch
function creates an XMLHttpRequest
object to call the Bing Web Search endpoint.
// 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;
}
Following a successful request, the load
event handler fires and calls the handleBingResponse
function. handleBingResponse
parses the result object, displays the results, and contains error logic for failed requests.
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"));
}
}
Important
A successful HTTP request doesn't mean that the search itself succeeded. If an error occurs in the search operation, the Bing Web Search API returns a non-200 HTTP status code and includes error information in the JSON response. If the request was rate-limited, the API returns an empty response.
Much of the code in both of the preceding functions is dedicated to error handling. Errors may occur at the following stages:
Stage | Potential error(s) | Handled by |
---|---|---|
Building the request object | Invalid URL | try / catch block |
Making the request | Network errors, aborted connections | error and abort event handlers |
Performing the search | Invalid request, invalid JSON, rate limits | Tests in load event handler |
Errors are handled by calling renderErrorMessage()
. If the response passes all of the error tests, renderSearchResults()
is called to display the search results.
Display search results
There are use and display requirements for results returned by the Bing Web Search API. Since a response may include various result types, it isn't enough to iterate through the top-level WebPages
collection. Instead, the sample app uses RankingResponse
to order the results to spec.
Note
If you only want a single result type, use the responseFilter
query parameter, or consider using one of the other Bing Search endpoints, such as Bing Image Search.
Each response has a RankingResponse
object that may include up to three collections: pole
, mainline
, and sidebar
. pole
, if present, is the most relevant search result and must be prominently displayed. mainline
contains most of the search results, and is displayed immediately after pole
. sidebar
includes auxiliary search results. If possible, these results should be displayed in the sidebar. If screen limits make a sidebar impractical, these results should appear after the mainline
results.
Each RankingResponse
includes a RankingItem
array that specifies how results must be ordered. Our sample app uses the answerType
and resultIndex
parameters to identify the result.
Note
There are additional ways to identify and rank results. For more information, see Using ranking to display results.
Let's take a look at the code:
// 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));
}
}
The renderResultsItems()
function iterates through the items in each RankingResponse
collection, maps each ranking result to a search result using the answerType
and resultIndex
values, and calls the appropriate rendering function to generate the HTML. If resultIndex
isn't specified for an item, renderResultsItems()
iterates through all results of that type and calls the rendering function for each item. The resulting HTML is inserted into the appropriate <div>
element in 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");
}
Review renderer functions
In our sample app, the searchItemRenderers
object includes functions that generate HTML for each type of search result.
// 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) { ... }
}
Important
The sample app has renderers for web pages, news, images, videos, and related searches. Your application will need renderers for any type of results it may receive, which could include computations, spelling suggestions, entities, time zones, and definitions.
Some of the rendering functions accept only the item
parameter. Others accept additional parameters, which can be used to render items differently based on context. A renderer that doesn't use this information doesn't need to accept these parameters.
The context arguments are:
Parameter | Description |
---|---|
section |
The results section (pole , mainline , or sidebar ) in which the item appears. |
index count |
Available when the RankingResponse item specifies that all results in a given collection are to be displayed; undefined otherwise. The index of the item within its collection and the total number of items in that collection. You can use this information to number the results, to generate different HTML for the first or last result, and so on. |
In the sample app, both the images
and relatedSearches
renderers use the context arguments to customize the generated HTML. Let's take a closer look at the images
renderer:
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...
}
The image renderer:
- Calculates the image thumbnail size (width varies, while height is fixed at 60 pixels).
- Inserts the HTML that precedes the image result based on context.
- Builds the HTML
<a>
tag that links to the page that contains the image. - Builds the HTML
<img>
tag to display the image thumbnail.
The image renderer uses the section
and index
variables to display results differently depending on where they appear. A line break (<br>
tag) is inserted between image results in the sidebar, so that the sidebar displays a column of images. In other sections, the first image result (index === 0)
is preceded by a <p>
tag.
The thumbnail size is used in both the <img>
tag and the h
and w
fields in the thumbnail's URL. The title
and alt
attributes (a textual description of the image) are constructed from the image's name and the hostname in the URL.
Here's an example of how images are displayed in the sample app:
Persist the client ID
Responses from the Bing search APIs may include a X-MSEdge-ClientID
header that should be sent back to the API with each successive request. If more than one of the Bing Search APIs is used by your app, make sure the same client ID is sent with each request across services.
Providing the X-MSEdge-ClientID
header allows the Bing APIs to associate a user's searches. First, it allows the Bing search engine to apply past context to searches to find results that better satisfy the request. 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 who have been chosen to see a feature will 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, such as Cross-Origin Resource Sharing (CORS), may prevent the sample app from accessing the X-MSEdge-ClientID
header. 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 subscription key must be included in the web page, where it's 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 a request through a CORS proxy. The response from this type of proxy has an Access-Control-Expose-Headers
header that filters response headers and makes them available to JavaScript.
It's easy to install a CORS proxy to allow our sample app to access the client ID header. Run this command:
npm install -g cors-proxy-server
Next, change the Bing Web Search endpoint in script.js
to:
http://localhost:9090/https://api.cognitive.microsoft.com/bing/v7.0/search
Start the CORS proxy with this command:
cors-proxy-server
Leave the command window open while you use the sample app; closing the window stops the proxy. In the expandable HTTP Headers section below the search results, the X-MSEdge-ClientID
header should be visible. Verify that it's the same for each request.