Tutorial: Single-page web app
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.
The Bing Entity Search API lets you search the Web for information about entities and places. You may request either kind of result, or both, in a given query. The definitions of places and entities are provided below.
Result | Description |
---|---|
Entities | Well-known people, places, and things that you find by name |
Places | Restaurants, hotels, and other local businesses that you find by name or by type (Italian restaurants) |
In this tutorial, we build a single-page Web application that uses the Bing Entity Search API to display search results right in the page. The application includes HTML, CSS, and JavaScript components.
The API lets you prioritize results by location. In a mobile app, you can ask the device for its own location. In a Web app, you can use the getPosition()
function. But this call works only in secure contexts, and it may not provide a precise location. Also, the user may want to search for entities near a location other than their own.
Our app therefore calls upon the Bing Maps service to obtain latitude and longitude from a user-entered location. The user can then enter the name of a landmark ("Space Needle") or a full or partial address ("New York City"), and the Bing Maps API provides the coordinates.
Note
The JSON and HTTP headings at the bottom of the page reveal the JSON response and HTTP request information when clicked. These details are useful when exploring the service.
The tutorial app illustrates how to:
- Perform a Bing Entity Search API call in JavaScript
- Perform a Bing Maps
locationQuery
API call in JavaScript - Pass search options to the API calls
- Display search results
- Handle the Bing client ID and API subscription keys
- Deal with any errors that might occur
The tutorial page is entirely self-contained; it does not use any external frameworks, style sheets, or even image files. It uses only widely supported JavaScript language features and works with current versions of all major Web browsers.
In this tutorial, we discuss only selected portions of the source code. The full source code is available on a separate page. Copy and paste this code into a text editor and save it as bing.html
.
Note
This tutorial is substantially similar to the single-page Bing Web Search app tutorial, but deals only with entity search results.
Prerequisites
To follow along with the tutorial, you need subscription keys for the Bing Search API, and Bing Maps API.
- 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.
- Create a Bing Maps resource in the Azure portal to get your key and endpoint. After it deploys, click Go to resource.
App components
Like any single-page Web app, the tutorial application includes three parts:
- HTML - Defines the structure and content of the page
- CSS - Defines the appearance of the page
- JavaScript - Defines the behavior of the page
This tutorial doesn't cover most of the HTML or CSS in detail, as they are straightforward.
The HTML contains the search form in which the user enters a query and chooses search options. The form is connected to the JavaScript that actually performs the search by the <form>
tag's onsubmit
attribute:
<form name="bing" onsubmit="return newBingEntitySearch(this)">
The onsubmit
handler returns false
, which keeps the form from being submitted to a server. The JavaScript code actually does the work of collecting the necessary information from the form and performing the search.
The search is done in two phases. First, if the user has entered a location restriction, a Bing Maps query is done to convert it into coordinates. The callback for this query then kicks off the Bing Entity Search query.
The HTML also contains the divisions (HTML <div>
tags) where the search results appear.
Managing subscription keys
Note
This app requires subscription keys for both the Bing Search API and the Bing Maps API.
To avoid having to include the Bing Search and Bing Maps API subscription keys in the code, we use the browser's persistent storage to store them. If either key has not been stored, we prompt for it and store it for later use. If the key is later rejected by the API, we invalidate the stored key so the user is asked for it upon their next search.
We define storeValue
and retrieveValue
functions that use either the localStorage
object (if the browser supports it) or a cookie. Our getSubscriptionKey()
function uses these functions to store and retrieve the user's key. You can use the global endpoint below, or the custom subdomain endpoint displayed in the Azure portal for your resource.
// cookie names for data we store
SEARCH_API_KEY_COOKIE = "bing-search-api-key";
MAPS_API_KEY_COOKIE = "bing-maps-api-key";
CLIENT_ID_COOKIE = "bing-search-client-id";
// API endpoints
SEARCH_ENDPOINT = "https://api.cognitive.microsoft.com/bing/v7.0/entities";
MAPS_ENDPOINT = "https://dev.virtualearth.net/REST/v1/Locations";
// ... omitted definitions of storeValue() and retrieveValue()
// get stored API subscription key, or prompt if it's not found
function getSubscriptionKey(cookie_name, key_length, api_name) {
var key = retrieveValue(cookie_name);
while (key.length !== key_length) {
key = prompt("Enter " + api_name + " API subscription key:", "").trim();
}
// always set the cookie in order to update the expiration date
storeValue(cookie_name, key);
return key;
}
function getMapsSubscriptionKey() {
return getSubscriptionKey(MAPS_API_KEY_COOKIE, 64, "Bing Maps");
}
function getSearchSubscriptionKey() {
return getSubscriptionKey(SEARCH_API_KEY_COOKIE, 32, "Bing Search");
}
The HTML <body>
tag includes an onload
attribute that calls getSearchSubscriptionKey()
and getMapsSubscriptionKey()
when the page has finished loading. These calls serve to immediately prompt the user for their keys if they haven't yet entered them.
<body onload="document.forms.bing.query.focus(); getSearchSubscriptionKey(); getMapsSubscriptionKey();">
Selecting search options
The HTML form includes the following controls:
Control | 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. |
safe |
A checkbox indicating whether SafeSearch is turned on (restricts "adult" results) |
what |
A menu for choosing to search for entities, places, or both. |
mapquery |
The text field in which the user may enter a full or partial address, a landmark, etc. to help Bing Entity Search return more relevant results. |
Note
Places results are currently available only in the United States. The where
and what
menus have code to enforce this restriction. If you choose a non-US market while Places is selected in the what
menu, what
changes to Anything. If you choose Places while a non-US market is selected in the where
menu, where
changes to the US.
Our JavaScript function bingSearchOptions()
converts these fields to a partial query string for the Bing Search 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.what.selectedIndex) options.push("responseFilter=" + form.what.value);
return options.join("&");
}
For example, the SafeSearch feature can be strict
, moderate
, or off
, with moderate
being the default. But our form uses a checkbox, which has only two states. The JavaScript code converts this setting to either strict
or off
(we don't use moderate
).
The mapquery
field isn't handled in bingSearchOptions()
because it is used for the Bing Maps location query, not for Bing Entity Search.
Obtaining a location
The Bing Maps API offers a locationQuery
method, which we use to find the latitude and longitude of the location the user enters. These coordinates are then passed to the Bing Entity Search API with the user's request. The search results prioritize entities and places that are close to the specified location.
We can't access the Bing Maps API using an ordinary XMLHttpRequest
query in a Web app because the service does not support cross-origin queries. Fortunately, it supports JSONP (the "P" is for "padded"). A JSONP response is an ordinary JSON response wrapped in a function call. The request is made by inserting using a <script>
tag into the document. (Loading scripts is not subject to browser security policies.)
The bingMapsLocate()
function creates and inserts the <script>
tag for the query. The jsonp=bingMapsCallback
segment of the query string specifies the name of the function to be called with the response.
function bingMapsLocate(where) {
where = where.trim();
var url = MAPS_ENDPOINT + "?q=" + encodeURIComponent(where) +
"&jsonp=bingMapsCallback&maxResults=1&key=" + getMapsSubscriptionKey();
var script = document.getElementById("bingMapsResult")
if (script) script.parentElement.removeChild(script);
// global variable holds reference to timer that will complete the search if the maps query fails
timer = setTimeout(function() {
timer = null;
var form = document.forms.bing;
bingEntitySearch(form.query.value, "", bingSearchOptions(form), getSearchSubscriptionKey());
}, 5000);
script = document.createElement("script");
script.setAttribute("type", "text/javascript");
script.setAttribute("id", "bingMapsResult");
script.setAttribute("src", url);
script.setAttribute("onerror", "BingMapsCallback(null)");
document.body.appendChild(script);
return false;
}
Note
If the Bing Maps API does not respond, the bingMapsCallBack()
function is never called. Ordinarily, that would mean that bingEntitySearch()
isn't called, and the entity search results do not appear. To avoid this scenario, bingMapsLocate()
also sets a timer to call bingEntitySearch()
after five seconds. There is logic in the callback function to avoid performing the entity search twice.
When the query completes, the bingMapsCallback()
function is called, as requested.
function bingMapsCallback(response) {
if (timer) { // we beat the timer; stop it from firing
clearTimeout(timer);
timer = null;
} else { // the timer beat us; don't do anything
return;
}
var location = "";
var name = "";
var radius = 1000;
if (response) {
try {
if (response.statusCode === 401) {
invalidateMapsKey();
} else if (response.statusCode === 200) {
var resource = response.resourceSets[0].resources[0];
var coords = resource.point.coordinates;
name = resource.name;
// the radius is the largest of the distances between the location and the corners
// of its bounding box (in case it's not in the center) with a minimum of 1 km
try {
var bbox = resource.bbox;
radius = Math.max(haversineDistance(bbox[0], bbox[1], coords[0], coords[1]),
haversineDistance(coords[0], coords[1], bbox[2], bbox[1]),
haversineDistance(bbox[0], bbox[3], coords[0], coords[1]),
haversineDistance(coords[0], coords[1], bbox[2], bbox[3]), 1000);
} catch(e) { }
var location = "lat:" + coords[0] + ";long:" + coords[1] + ";re:" + Math.round(radius);
}
}
catch (e) { } // response is unexpected. this isn't fatal, so just don't provide location
}
var form = document.forms.bing;
if (name) form.mapquery.value = name;
bingEntitySearch(form.query.value, location, bingSearchOptions(form), getSearchSubscriptionKey());
}
Along with latitude and longitude, the Bing Entity Search query requires a radius that indicates the precision of the location information. We calculate the radius using the bounding box provided in the Bing Maps response. The bounding box is a rectangle that surrounds the entire location. For example, if the user enters NYC
, the result contains roughly central coordinates of New York City and a bounding box that encompasses the city.
We first calculate the distances from the primary coordinates to each of the four corners of the bounding box using the function haversineDistance()
(not shown). We use the largest of these four distances as the radius. The minimum radius is a kilometer. This value is also used as a default if no bounding box is provided in the response.
Having obtained the coordinates and the radius, we then call bingEntitySearch()
to perform the actual search.
Performing the search
Given the query, a location, an options string, and the API key, the BingEntitySearch()
function makes the Bing Entity Search request.
// perform a search given query, location, options string, and API keys
function bingEntitySearch(query, latlong, 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("pole", "mainline", "sidebar", "_json", "_http", "error");
var request = new XMLHttpRequest();
var queryurl = SEARCH_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);
if (latlong) request.setRequestHeader("X-Search-Location", latlong);
// 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 our load
event handler, the handleBingResponse()
function, to handle a successful HTTP GET request to the API.
// 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 === "SearchResponse") {
renderSearchResults(jsobj);
} else {
renderErrorMessage("No search results in JSON response");
}
} else {
renderErrorMessage("Empty response (are you sending too many requests too quickly?)");
}
if (divHidden("pole") && divHidden("mainline") && divHidden("sidebar"))
showDiv("noresults", "No results.<p><small>Looking for restaurants or other local businesses? Those currently areen't supported outside the US.</small>");
}
// Any other HTTP status is an error
else {
// 401 is unauthorized; force re-prompt for API key for next request
if (this.status === 401) invalidateSearchKey();
// 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
A successful HTTP request does not necessarily mean that the search itself succeeded. If an error occurs in the search operation, the Bing Entity Search API returns a non-200 HTTP status code and includes error information in the JSON response. Additionally, 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 JavaScript 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()
with any details known about the error. If the response passes the full gauntlet of error tests, we call renderSearchResults()
to display the search results in the page.
Displaying search results
The Bing Entity Search API requires you to display results in a specified order. Since the API may return two different kinds of responses, it is not enough to iterate through the top level Entities
or Places
collection in the JSON response and display those results. (If you want only one type of result, use the responseFilter
query parameter.)
Instead, we use the rankingResponse
collection in the search results to order the results for display. This object refers to items in the Entitiess
and/or Places
collections.
rankingResponse
may contain up to three collections of search results, designated pole
, mainline
, and sidebar
.
pole
, if present, is the most relevant search result and should be displayed prominently. mainline
refers to the bulk of the search results. Mainline results should be displayed immediately after pole
(or first, if pole
is not present).
Finally. sidebar
refers to auxiliary search results. They may be displayed in an actual sidebar or simply after the mainline results. We have chosen the latter for our tutorial app.
Each item in a rankingResponse
collection refers to the actual search result items in two different, but equivalent, ways.
Item | Description |
---|---|
id |
The id looks like a URL, but should not be used for links. The id type of a ranking result matches the id of either a search result item in an answer collection, or an entire answer collection (such as Entities ). |
answerType resultIndex |
The answerType refers to the top-level answer collection that contains the result (for example, Entities ). The resultIndex refers to the result's index within that collection. If resultIndex is omitted, the ranking result refers to the entire collection. |
Note
For more information on this part of the search response, see Rank Results.
You may use whichever method of locating the referenced search result item is most convenient for your application. In our tutorial code, we use the answerType
and resultIndex
to locate each search result.
Finally, it's time to look at our function renderSearchResults()
. This function iterates over the three rankingResponse
collections that represent the three sections of the search results. For each section, we call renderResultsItems()
to render the results for that section.
// render the search results given the parsed JSON response
function renderSearchResults(results) {
// if spelling was corrected, update search field
if (results.queryContext.alteredQuery)
document.forms.bing.query.value = results.queryContext.alteredQuery;
// for each possible section, render the results from that section
for (section in {pole: 0, mainline: 0, sidebar: 0}) {
if (results.rankingResponse[section])
showDiv(section, renderResultsItems(section, results));
}
}
Rendering result items
In our JavaScript code is an object, searchItemRenderers
, that contains renderers: functions that generate HTML for each kind of search result.
searchItemRenderers = {
entities: function(item) { ... },
places: function(item) { ... }
}
A renderer function may 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 can be used to number results, to generate special HTML for the beginning or end of a collection, to insert line breaks after a certain number of items, and so on. If a renderer does not need this functionality, it does not need to accept these two parameters. In fact, we do not use them in the renderers for our tutorial app.
Let's take a closer look at the entities
renderer:
entities: function(item) {
var html = [];
html.push("<p class='entity'>");
if (item.image) {
var img = item.image;
if (img.hostPageUrl) html.push("<a href='" + img.hostPageUrl + "'>");
html.push("<img src='" + img.thumbnailUrl + "' title='" + img.name + "' height=" + img.height + " width= " + img.width + ">");
if (img.hostPageUrl) html.push("</a>");
if (img.provider) {
var provider = img.provider[0];
html.push("<small>Image from ");
if (provider.url) html.push("<a href='" + provider.url + "'>");
html.push(provider.name ? provider.name : getHost(provider.url));
if (provider.url) html.push("</a>");
html.push("</small>");
}
}
html.push("<p>");
if (item.entityPresentationInfo) {
var pi = item.entityPresentationInfo;
if (pi.entityTypeHints || pi.entityTypeDisplayHint) {
html.push("<i>");
if (pi.entityTypeDisplayHint) html.push(pi.entityTypeDisplayHint);
else if (pi.entityTypeHints) html.push(pi.entityTypeHints.join("/"));
html.push("</i> - ");
}
}
html.push(item.description);
if (item.webSearchUrl) html.push(" <a href='" + item.webSearchUrl + "'>More</a>")
if (item.contractualRules) {
html.push("<p><small>");
var rules = [];
for (var i = 0; i < item.contractualRules.length; i++) {
var rule = item.contractualRules[i];
var link = [];
if (rule.license) rule = rule.license;
if (rule.url) link.push("<a href='" + rule.url + "'>");
link.push(rule.name || rule.text || rule.targetPropertyName + " source");
if (rule.url) link.push("</a>");
rules.push(link.join(""));
}
html.push("License: " + rules.join(" - "));
html.push("</small>");
}
return html.join("");
}, // places renderer omitted
Our entity renderer function:
- Builds the HTML
<img>
tag to display the image thumbnail, if any. - Builds the HTML
<a>
tag that links to the page that contains the image. - Builds the description that displays information about the image and the site it's on.
- Incorporates the entity's classification using the display hints, if any.
- Includes a link to a Bing search to get more information about the entity.
- Displays any licensing or attribution information required by data sources.
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 have two important benefits.
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 "docks" might preferentially return information about places to dock a sailboat.
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 allow lists 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.cognitive.microsoft.com/bing/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.