Solutions to IT problems

Solutions I found when learning new IT stuff

Using AJAX to mask long search times

leave a comment »


Introduction

What do you do if you have searches that can take a rather long time to complete? Long is relative in these days since often people expect google-like performance (= instantaneous display of search hits). So 10-20 seconds can already feel long for a typical user. In my case searches could take several minutes depending on the size of the database and the query. One possibility would be to just use better hardware but that can cost a lot and also has limits. The answer is simple. Humans are slow and hence if you show them the results as they are found before waiting for the search to complete, you don’t need extraordinary search speed because the user will probably take minutes to analyze the hits already on display.

Creating the search class

This is simple. Your class must be able to make search hits instantly available. I use an implementation of the java.util.concurrent.BlockingQueue interface. Pass the Queue to the search method and the Queue can be filled with the search hits. I put the unique database key into the queue from which the result can then be retrieved outside of the search class. I think that is a good approach but not mandatory.
So my search class has a method search(BlockingQueue searchHitIds).

Web Pages

You need 2 webpages: 1 for searching and 1 for displaying the search results. The Search page can be a standard html page with a form. Nothing special needed. The results page will heavily rely on JavaScript but it can be a normal html page without java code.
The results page will depend on JQuery and a JQuery plugin called “datatables“. This plug-in “beautifies” standard html tables and can load the data for display using AJAX. Your results page will need following references:

<script type="text/javascript" src="js/jquery-1.6.2.js"></script>
<script type="text/javascript" src="js/jquery.dataTables.min.js"></script>
<script type="text/javascript" src="js/datatables.fnStandingRedraw.js"></script>

General Code Flow

  1. User clicks on search button to submit search
  2. The form data is send to the initializer servlet which starts the search
  3. The initializer servlet redirects to the results page
  4. JavaScript code on results page issues AJAX request to the hit retrieval servlet
  5. The hit retrieval servlet gets the new found hits and returns them as JSON
  6. Datatables displays the new hits in a paginated table
  7. The hit retrieval servlet informs the results page when the search is completed

“Initializer Servlet”

final LinkedBlockingQueue hits =
    new LinkedBlockingQueue();
Thread searcher = new Thread() {

@Override
public void run() {

    query.search(hits);
}
};
searcher.start();
session.setAttribute("searchQueue", hits);
session.setAttribute("searchThread", searcher);
response.sendRedirect("SearchResult.html");

This servlet creates the search (not shown, depends on your implementation) and then submits the search in a new thread. The search thread and the Queue holding the results is put into the session.

Results Page

The results page contains JavaScript Code for creating the datatable and fetching the search hits for display.

    var resultsTable;
    var myTimer;

    $(document).ready(function() {
        resultsTable = $('#result').dataTable( {
            "bDeferRender": true,
            "bProcessing": true,
            "sPaginationType": "full_numbers",
            "iDisplayLength": 4,
            "aLengthMenu": [[4, -1], [4, "All"]],
            "sScrollY": "580",
            "aaSorting": [],
            "aoColumns": [
            /* column1 */   {"bSearchable": true,
                    "bSortable": true,
                    "sWidth": "50px"},
                /* column2*/{"bSearchable": false,
                    "bSortable": false,
                    "sWidth": "510px"}
            ]
        } );
        getAndAddRows();
        startTimer();
    } );

    function startTimer() {
        myTimer = window.setInterval( function() {
            getAndAddRows();
        }, 500);
    };

    function stopTimer(){
        window.clearInterval(myTimer);
    }

    function getAndAddRows(){
    $.get(
        'getSearchHits',
        function(data){
            if(data[0] == 'searchCompleted'){
                    stopTimer();
                    $('#hitsFound').text('Hits Found: '
                        + resultsTable.fnSettings().fnRecordsTotal()
                        + ' Search Completed');
                    return;
            }
            $('#result').dataTable().fnAddData(data, false);
            resultsTable.fnStandingRedraw();
            $('#hitsFound').text('Hits Found: '
                + resultsTable.fnSettings().fnRecordsTotal()
                + ' Searching...');
        },
        "json"
        );
    }

<h1 id="hitsFound">Hits Found:</h1>
<table id="result">
<thead>
<tr>
<th>column1</th>
<th>column2</th>
</tr>
</thead>
</table>

First the datatable is initialized. Important is the option to paginate and bDeferRender. The later only renders html a user actually looks at. So if you have a result with 1’000 hits and 100 pages of 10 hits only those pages the user actually looks at will be rendered. In my case this is huge because my results consist of dynamically generated images.
Then a timer is started which calls the methods that issues the Ajax calls every 200 ms. You will need the datatables plug-in function fnStandingRedraw. Without it the user would be moved to page 1 of the results paging every time a new row is added. Note that I use a “dummy message” to inform the page that the search completed.

The getSearchHits Servlet

You will need to add google Gson libary to your dependencies. The servlet takes the Queue and the search thread from the session. Each result is taken from the Queue and added to a List. Gson transforms ths list into a JSON array and a list of lists (list of rows) into a 2D JSON array.

int limit = 50; //amount of records ro return
int counter = 0;
BlockingQueue hits = (BlockingQueue) session.getAttribute("searchQueue");
Thread searcher = (Thread) session.getAttribute("searchThread");
Gson gson = new Gson();
ArrayList rows = new ArrayList();

while (hits.size() > 0 && limit > counter) {
    ArrayList row = new ArrayList();
    Integer key = hits.take();
    // get desired data and add it to current row example:
    String myData = dataccessObject.getMyData(key);

    row.add(key);
    row.add(myData);

    rows.add(row);
    counter++;
}

if (rows.size() < 1 && !searcher.isAlive()) {
    out.print("[\"searchCompleted\"]");
} else {
    String json = gson.toJson(rows); //==> json is [[1,"myData1"],[2,"myData2"]]
    out.print(json);
}

For each request the servlet return a maximum of 50 hits. The limit is there so that if retrieving the hits takes longer than new results being found, the servlet would not return until the search is completed and thus defeating its purpose. Of course this means that total search time will be considerably longer but the usability is a lot better!

Please comment in case of questions or improvements

Advertisements

Written by kienerj

November 27, 2011 at 19:26

Posted in Database, Java, Programming

Tagged with

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: