How to Create a Customer Ranked Auto Suggest with Bing Maps and Azure Mobile Services

From time to time I come across developers who want to have an auto suggest search box to use with their map. Your first thought might be to simply use the Bing Maps geocoding services to do this, however this often ends up generating a large number of transactions. If you are using Bing Maps under the free terms of use this can result in your application quickly exceeding the free usage limits. If you have an enterprise license for Bing Maps these additional transactions could potential increase the cost of your license significantly.

Now let’s take a closer look at this method for a minute. The Bing Maps geocoder returns about 5 locations on average per request. When doing auto suggest using the Bing Maps geocoder it can sometimes take a lot of characters before the option your user is looking for is returned. This means that for each character the user has to add to their query there are more suggestions that aren’t relevant to the user. This is a bit of a waste and not the ideal user experience.

Your application is unique, shouldn’t the suggestion also be unique as well? The Bing Maps terms of use state that you can store the geocode results for later use in your application. We can use this to our advantage to create a database of past search results that your user actually selected. This will greatly increase the accuracy of the auto suggest feature. Overtime this will result in nearly every single user being able to find the result they are looking for using auto suggest faster than using the previously described method. This will result in a lot less requests made against the Bing Maps geocoder which means you can do a lot more within the free usage limits or potentially reduce your overall usage enough to lower your licensing costs of Bing Maps.

In this blog post we are going to create a customer ranked auto suggest service. Initially no suggestions will be provided to the user as the database will be empty. Many of the searches will need to be passed to the Bing Maps geocoder and the results presented to the user so they can select the best option. From there the selection can be added to the auto suggest database for future use. Once there are some suggestions in the database the user will see the auto suggestions appear. If they select one of the suggestions the rank value of that suggestion will increase so that it shows up higher in the results of future suggestions.

There are a number of different ways we could develop this. We could create a custom web service that connects to a database. By using a web service we can reuse this auto suggest feature not only in web apps but in mobile apps as well. If we are building an app we could embed a SQLite database and store the suggestions locally for the individual user. In this blog post we are going to use the Azure Mobile Services. By using the Azure Mobile Services we can leverage a web service that is connected to a database in a fraction of the time it would take to create a custom web service. The client app that shows how to use the mobile service will be a simple web page to keep things simple.

Full source code for the client app can be downloaded from the MSDN Code Gallery here.

Setting up the Azure Mobile Service

The Windows Azure Mobile Services you to quickly create connected apps that can make use of cloud based storage, authenticate users, and send push notifications. With SDKs for Windows, Android, iOS, and HTML as well as a powerful and flexible REST API. One of the great benefits of it being a cloud based service is that it has on-demand scaling allowing you to easily support more users as your app grows with just a few clicks of a button. The Windows Azure Mobile services has a free tier  that allows you to create up to 10 services running across 500 devices and 500,000 API calls per month. As your application grows in popularity you can upgraded to increase these limits.

To get started go to http://azure.com and sign-in or create a free trial account. This site also has some great tutorials that you might want to refer to if you want to extend this application. Once you are logged into your account you will see a number of different services listed on the left side panel, select Mobile Services, and then select the option to create a new mobile service.

A form will appear to configure the mobile service. You will need to provide a uniqueURL name for the service. We are going to need a database, select the free 20MB SQL database option. You can select a region for where your service will be hosted. Choose the region that is closest to you for best performance. For the backend we will use JavaScript. This will make things very easy. When you are done click the arrow button to go to the next step.

The next step is to configure the database. Create a new SQL database and give it a name. Also specify a login name and password. Select the same region as before. This will ensure that your database and service will be able to communicate with the best performance. When you are done click the check button.

You will now see your new mobile service listed in a table. While you are here copy the URL for later use. Also click on the Manage Keys button at the bottom of the page. This will open up a dialog that has an application and a master key. Copy the application key for later use.

Now click on the name of your new mobile service. This will bring you into the dashboard for the service. On the first page you will see an option to choose a platform. This will provide you with different options for getting started. Ignore this for now and Data tab at the top of the page. On the page that opens select the option to create a new table. Call the table SearchRankings and set the permissions as I have in the screenshot below. Click on the check mark button when you are done.

Now back on the Data tab you should see your table listed, click on the tables name to open it up. Click on the Columns tab and use the add column button at the bottom of the page to add the following columns.

Name

Type

Description

name

string

We will store the suggestion location name in this field.

latitude

number

The latitude coordinate for the location.

longitude

number

The longitude coordinate for the location.

rank

number

A number used to rank the suggestion. Each time a suggestion is reused its rank will be increased by 1. Suggestions will be sorted by rank.

zoom

number

A zoom level to use to display the suggestion on the map. We could store best map view bounding box for the location but that would requiring a lot more information. A simple zoom level will suffice. Countries will be more zoomed out than cities, cities will be more zoomed out than addresses.

The next step is to customize the server side scripts for the table. When a new suggestion is added to the table we will want to make sure that valid values are being passed in for the suggestion. We will also check that the name of property of the suggestion is unique and will set its rank to 1. To do this click on the Script tab and select the insert operation. Update the JavaScript with the following.

function insert(item, user, request) {
    //Ensure that the item has suitable values
    if (item.name != null && item.name.length > 0 &&
        item.latitude >= -90 && item.latitude <= 90 &&
        item.longitude >= -180 && item.longitude <= 180) {
        //Get a reference to the table
        var searchRankingTable = tables.getTable('SearchRankings');

        //Search the table for existing items with the same name.
        searchRankingTable.where({
            name: item.name
        }).read({
            success: insertUniqueItem
        });
    }
    else {
        request.respond(statusCodes.BAD_REQUEST, 'Item has an invalid parameter.');
    }

    //Callback function for inserting unique locations to the table.
    function insertUniqueItem(existingItems) {
        if (existingItems.length == 0) {
            //Set the rank of the new item to 1.
            item.rank = 1;

            //Insert the item as normal. 
            request.execute();
        }
    }
}

We will want to lock down the update operation such that only the rank value can be incremented by one when the operation is called. Select the update operation and update the JavaScript with the following.

function update(item, user, request) {
    //Check to see if the rank should be incremented.
    if (request.parameters.incrementRank === 'true') {
        //Get a reference to the table
        var searchRankingTable = tables.getTable('SearchRankings');

        //Search the table for existing items with the same name.
        searchRankingTable.where({
            id: item.id
        }).read({
            success: incrementItemRank
        });
    }

    //Callback function for incrementing the rank of an item
    function incrementItemRank(existingItems) {
        if (existingItems.length > 0) {
            item.rank = existingItems[0].rank + 1;

            //Update the item. 
            request.execute();
        }
    }
}

We will leave the delete and read operations as they are. At this point our mobile service is as complete as we need to do our development. However, if you decide to use this service from a web application that is hosted online you will need to add the host name of the web app to the mobile service configuration. You can do this by clicking on the Configure tab from the main dashboard for your mobile service. Add the host name to the Cross-Origin Resource Sharing section of the configuration.

Creating a Client Web App

On the main dashboard of our mobile service you can choose the platform you can to develop the client app for and download a sample application to get started. We are not going to use these in this section but they may be useful if you want to create a client app for a different platform. To get started open up Visual Studios and create a new ASP.NET Web Application project called CustomerRankedAutoSuggest.

Next select the Empty template. We will be creating a basic web app to keep things simple.

Next create a new HTML page called page.html by right clicking on the project and selecting Add → New Item. Create two folders called js and css. In the css folder create a style sheet called page.css. We will use this file to store all our styles for laying out the web app. In the js folder create two JavaScript files, one called page.js and the other called AutoSuggest.js. At this point your project should look like this.

In the page.html file we will need to add references to the Bing Maps JavaScript SDK and the Azure Mobile Services JavaScript SDK. We will make use of jQuery and jQuery UI to make things a bit easier. We will also need to add references to our JavaScript and CSS files. The body of the page will have a search bar that consists of a textbox where the user will enter a search query and a search button. The search button will likely only be used when the auto suggestion doesn’t provide an answer, this button will use Bing Maps to geocode the users query. Below the search bar we will display a map. When the Bing Maps geocoder is used we will also display those search results in a modal dialog above the map. Open the page.html file and update it with the following code.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Auto Suggest Sample</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <!-- Bing Maps reference --> 
    <script type="text/javascript" src="http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0"></script>
 1:  
 2:  
 3:     <!-- jquery and jquery UI references -->
 4:     <script type="text/javascript" src='http://code.jquery.com/jquery-1.11.0.min.js'>
 1: </script>
 2:    
 3:     <script src="http://code.jquery.com/ui/1.10.4/jquery-ui.min.js" type="text/javascript">
 1: </script>
 2:     <link href="http://code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css" />
 3:  
 4:     <!-- Azure Mobile Services reference -->
 5:     <script type="text/javascript" src='http://ajax.aspnetcdn.com/ajax/mobileservices/MobileServices.Web-1.1.0.min.js'>
 1: </script>
 2:     
 3:     <!-- Our JavaScript and CSS references -->
 4:     <script type="text/javascript" src='js/AutoSuggest.js'>
 1: </script>
 2:     <script type="text/javascript" src='js/page.js'>
</script>
    <link rel="stylesheet" type="text/css" href="css/page.css"/>
</head>
<body>
    <div class="container">
        <div class="searchBar">
            <div class="ui-widget">
                <label for="searchBox">Search:</label>
                <input id="searchBox" />
                <button id="searchBtn"></button>
            </div>
            <div id="searchResult" class="ui-widget"></div>
        </div>

        <div id="myMap"></div>

        <div id="resultsDialog">
            <ul id="resultsList"></ul>
        </div>
    </div>
</body>
</html>

Next open the page.cssfile and update it with the following styles.

.container {
    width: 800px;
    margin: auto;
}

#myMap {
    position: absolute;
    width: 800px;
    height: 600px;
}

.searchBar {
    margin: 10px 0;
}
        
#searchBtn {
    height: 30px;
    margin-top: -5px;
}
        
#searchBox, #searchResult {
    width: 300px;
}

Open the AutoSuggest.js file. In here we will add code that wraps our mobile service and exposes some functions for easily retrieving, inserting and incrementing the rank of suggestions. Update this file with the following code. Note that you will need to update it with the URL to your mobile service and the application key needed to access it.

var AutoSuggest = new function () {
    var client = new WindowsAzure.MobileServiceClient('YOUR_MOBILE_SERVICE_URL', 'YOUR_APPLICATION_KEY');
    var searchTable = client.getTable('searchRankings');

    function handleError(error) {
        alert(error + (error.request ? ' - ' + error.request.status : ''));
    }

    this.search = function (val, maxResults, callback) {
        var searchQuery = searchTable.orderByDescending('rank');

        if (maxResults && maxResults > 0) {
            searchQuery.take(maxResults);
        }

        if (val && val != '') {
            searchQuery.where(function (v) {
                //Make query case insensitive and 
                return this.name.toLowerCase().indexOf(v.toLowerCase()) > -1;
            }, val);
        }

        searchQuery.read().then(callback, handleError);
    };

    this.insert = function (item) {
        searchTable.insert(item).then(null, handleError);
    };
    
    this.incrementRank = function (id) {
        searchTable.update({ id: id }, { incrementRank: true }).then(null, handleError);
    };
};

We will put all our application logic in the page.js file. In here we will need to load the map and the Bing Maps Search module. We then need to add the autocomplete widget from jQuery UI to our search textbox and set it up so that it pulls the suggestions from our mobile service. When a selection is made the autocomplete widget will increment the rank value of the selected suggestion in our database and zoom the map into the selected location. Next we will add a keypress listener to the search textbox so that when the user presses the enter button it geocodes the users query. We will also add a click event to the search button. We will create a function that uses the Bing Maps Search module to geocode the users query. The results will be displayed in a dialog box above the map. When the user selects one of the results it will be added to the suggestion database and the map will zoom into that location. To do all this open the page.js file and update it with the following code. Note, you will need to provide your Bing Maps key for this code to work correctly.

$(document).ready(function () {
    //Load the map
    var map = new Microsoft.Maps.Map($("#myMap")[0], {
        credentials: "YOUR_BING_MAPS_KEY"
    });

    //Load the Bing Maps Search Manager
    var searchManager = null;

    Microsoft.Maps.loadModule('Microsoft.Maps.Search', {
        callback: function () {
            searchManager = new Microsoft.Maps.Search.SearchManager(map);
        }
    });

    //Wire up auto complete functionality
    $("#searchBox").autocomplete({
        source: function (request, response) {
            AutoSuggest.search(request.term, null, function (results) {
                if (results) {
                    response($.map(results, function (item) {
                        return {
                            data: item,
                            label: item.name,
                            value: item.name
                        }
                    }));
                }
            });
        },
        select: function (event, ui) {  //Suggestion selected
            var item = ui.item.data;

            //Increment the rank of the selected value.
            AutoSuggest.incrementRank(item.id);

            //Zoom into the selected item.
            map.setView({ center: new Microsoft.Maps.Location(item.latitude, item.longitude), zoom: item.zoom });
        },
        minLength: 1    //Minimium number of characters before auto suggest is triggered
    });

    //Handle users enter key press
    $("#searchBox").keypress(function (event) {
        if (event.which == 13) {
            event.preventDefault();
            GeocodeQuery();
        }
    });

    //Create the search button
    $('#searchBtn').button({
        icons: {
            primary: "ui-icon-search"
        },
        text: false
    }).click(GeocodeQuery);

    //Functionality for geocoding users query against Bing Maps
    var geocodeResults = [];

    $('#resultsDialog').dialog({
        modal: true,
        autoOpen: false,
        title: 'Geocode Results'
    });

    function GeocodeQuery() {
        var query = $("#searchBox").val();

        if (query != '') {
            searchManager.geocode({
                where: query,
                callback: function (r) {
                    if (r && r.results && r.results.length > 0) {
                        geocodeResults = [];

                        var result, zoom;

                        for (var i = 0; i < r.results.length; i++) {
                            result = r.results[i];

                            //Calculate a preferred Zoom level based on bestView data
                            zoom = Math.floor(Math.log(180.0 / 256.0 * 600 / result.bestView.height) / Math.log(2));

                            geocodeResults.push({
                                name: result.name,
                                //Round off coordinates to 5 decimal places
                                latitude : Math.round(result.location.latitude * 100000) / 100000,
                                longitude: Math.round(result.location.longitude * 100000) / 100000,
                                zoom : zoom
                            });

                            $('#resultsList').append('<li rel="'+ i +'">'+ result.name + '</li>');
                        }                        

                        $('#resultsList li').click(function () {
                            var idx = $(this).attr('rel');
                            var r = geocodeResults[idx];

                            $("#searchBox").val(r.name);

                            //Add the new selected result to the database
                            AutoSuggest.insert(r); 

                            //Set the map view over the location
                            map.setView({ center: new Microsoft.Maps.Location(r.latitude, r.longitude), zoom: r.zoom });

                            $('#resultsList').html('');

                            $('#resultsDialog').dialog('close');
                        });
                    } else {
                        $('#resultsList').html('No results found.');
                    }

                    $('#resultsDialog').dialog('open');
                }
            });
        }
    }
});

At this point the application is complete. Right click on the page.html file and select Set As Start Page. Press F5 or the debug button to test. Initially when you do a query you will not see any suggestions and when you press the search button a list of results from the geocoder will be displayed like so.

After you have some locations in your database your application will start to provide suggestions as the user types in the search box like the screenshot below.

If over time more users select Seattle as a suggestion its rank will increase until eventually it will be listed as the first suggestion like this.

You can download the full source code for the web app in this blog from the MSDN Code Samples here.

- Ricky Brundritt, EMEA Bing Maps TSP