Autocomplete search box with jQuery and Spring MVC


Octocat **Promotion** - Efficiently manage your coding bookmarks, aka #codingmarks, on www.codingmarks.org and share your hidden gems with the world. They will be published weekly on Github. Please help us build THE programming-resources location - Star


Functionality

Each podcast on Podcastpedia.org has one or several associated keywords. When you go to a specific podcast you will see all the associated keywords :

Podcast keywords sample

Podcast keywords sample

There is also a special entry in the main menu – Keywords – that displays all the keywords associated to podcasts, ordered by the number of podcasts associated with. But the really cool part of the page is the autocomplete box – you can easily find podcasts related to a topic of your interest by typing in the first characters of the topic’s name. Let’s say you would like to see if there any podcasts related to Java, you would type “Ja”, the autocomplete functionality will display the keywords that start with “ja” and you can see “Java” exist and select it:

Autocomplete jQuery

Autocomplete keywords box

As shown in the picture above, you can select several keywords on the same page. But enough talking… let’s see how the magic happens behind the scenes, because this is actually the topic of this post.

Octocat Source code for this post is available on Github - podcastpedia.org is an open source project.

The Model

In the model we need a Keyword/Tag class, that has an Id, a Name and a NumberOfPodcasts property associated with the it.

package org.podcastpedia.domain;

import java.io.Serializable;
import net.sf.ehcache.pool.sizeof.annotations.IgnoreSizeOf;

@IgnoreSizeOf
public class Tag implements Serializable{

    private static final long serialVersionUID = -2370292880165225805L;

    /** id of the tag - BIGINT in MySQL DB */
    private long tagId;

    /** name of the tag */
    private String name;

    /** number of podcasts the tag is associated with */
    private Integer nrOfPodcasts;

    public Integer getNrOfPodcasts() {
        return nrOfPodcasts;
    }

    public void setNrOfPodcasts(Integer nrOfPodcasts) {
        this.nrOfPodcasts = nrOfPodcasts;
    }

    public long getTagId() {
        return tagId;
    }

    public void setTagId(long tagId) {
        this.tagId = tagId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

The IgnoreSizeOf annotation is set to ignore the Tag as a referenced object when calculating the size of the object graph in Ehcache. This was a measure taken to fix an EhCache net.sf.ehcache.pool.sizeof.ObjectGraphWalker warning :

WARN 2013-02-04 10:09:14,632 net.sf.ehcache.pool.sizeof.ObjectGraphWalker: The configured limit of 1,000 object references was reached while attempting to calculate the size of the object graph. Severe performance degradation could occur if the sizing operation continues. This can be avoided by setting the
CacheManger or Cache  elements maxDepthExceededBehavior to "abort" or adding stop points with
@IgnoreSizeOf annotations. If performance degradation is NOT an issue at the configured limit, raise the limit value using the CacheManager or
Cache  elements maxDepth attribute. For more information, see the Ehcache configuration documentation.

But more about caching with Ehcache in a future post.

Also at the model layer, we need a user interaction service, which returns a list of Tags, given the first characters of the search term:

public class UserInteractionServiceImpl implements UserInteractionService{

    @Autowired
    private UserInteractionDao userInteractionDao;
    ....
	public List getTagList(String query) {
		return userInteractionDao.getTagList(query + "%");
	}
}

Behind the UserInteractionDao, there is a MyBatis mapping that uses the following SQL statement:

SELECT
   t.tag_id,
   t.name,
   count(pt.podcast_id) as number_of_tags
FROM
   tags t,
   podcasts_tags pt,
   podcasts p
WHERE
   t.name like #{value}
   AND
   t.tag_id=pt.tag_id
   AND
   p.podcast_id = pt.podcast_id
   AND
   p.availability = 200
GROUP BY t.tag_id
ORDER BY
   t.name ASC
LIMIT 0,21

The statement will return the first 21 keywords starting with input value (line 10) ordered in alphabetial – we don’t want too many displayed at once.

I won’t insist on the MyBatis mapping, because it is sort of irrelevant for this post and this will be explained in detail in a future MyBatis post.

The View

JSP

The html/jsp code behind the input box is quite simple – it is a regular input tag inside of a div tag, that has an associated class of type ui-widget. Make sure to also include the jQuery and jQuery-ui libraries:

<script type="text/javascript" src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script type="text/javascript" src="http://code.jquery.com/ui/1.10.3/jquery-ui.js"></script><
<div id="find_keyword">
<div class="ui-widget"><input id="tagQuery" type="text" name="tagQuery" value="<spring:message code=" />" onFocus="inputFocus(this)" onBlur="inputBlur(this)"></div>
</div>

The inputFocus and onBlur JavaScript-functions will just change the coloring of the input text when onFocus and onBlur events occur on the input field:

<script type="text/javascript">// <![CDATA[
	function inputFocus(i){
		if(i.value==i.defaultValue){ i.value=""; i.style.color="#000"; }
	}
	function inputBlur(i){
		if(i.value==""){ i.value=i.defaultValue; i.style.color="#848484"; }
	}
// ]]></script>

jQuery code

$(document).ready(function() {
    //attach autocomplete
    $("#tagQuery").autocomplete({
        minLength: 1,
        delay: 500,
        //define callback to format results
        source: function (request, response) {
            $.getJSON("/tags/get_tag_list", request, function(result) {
                response($.map(result, function(item) {
                    return {
                        // following property gets displayed in drop down
                        label: item.name + "(" + item.nrOfPodcasts + ")",
                        // following property gets entered in the textbox
                        value: item.name,
                        // following property is added for our own use
                        tag_url: "http://" + window.location.host + "/tags/" + item.tagId + "/" + item.name
                    }
                }));
            });
        },

        //define select handler
        select : function(event, ui) {
            if (ui.item) {
                event.preventDefault();
                $("#selected_tags span").append('<a href=" + ui.item.tag_url + " target="_blank">'+ ui.item.label +'</a>');
                //$("#tagQuery").value = $("#tagQuery").defaultValue
                var defValue = $("#tagQuery").prop('defaultValue');
                $("#tagQuery").val(defValue);
                $("#tagQuery").blur();
                return false;
            }
        }
    });
});

The real magic happens in the jQuery UI autocomplete function – once the document is ready, the autocomplete function is associated to the tagQuery input field (line 3). After that a couple of parameters specific to the function need to be set:

  • minLength = 1 – the minimum number of characters a user must type before a search is performed
  • delay = 500 – the delay in milliseconds between when a keystroke occurs and when a search is performed (you want to give the database some time to rest 🙂 if you have many records)
  • source – must be specified and defines the data to use. In our case the data is provided by a callback function that, over AJAX, will get JSON data from the Server (the following step will present how this is implemented in Spring ). The result from the server is, as specified in the model, a list of Tag objects, which, with the help of jQuery.map(), gets translated to jQuery UI elements that are understood by autocomplete ui select handler
  • select – this is triggered when a keyword is selected from the proposed list. The action in this case will be to append the selected keyword to the existing ones (line 29), and restore the input text field value to the default one(lines 31 -33).

The Controller

Starting with version 3.0 Spring has significantly improved its support for AJAX calls. In our case there is a single method in the TagController, that will make use of an UserInteractionService defined in the model to return a list of Tag objects:

package org.podcastpedia.controllers;
...
/**
 * Annotation-driven controller that handles requests to display tags categories in different forms.
 * @author Ama
 */
@Controller
@RequestMapping("/tags")
public class TagController {
...
    @RequestMapping(value = "/get_tag_list",  method = RequestMethod.GET)
    public @ResponseBody List getTagList(@RequestParam("term") String query) {
        List tagList = userInteractionService.getTagList(query);
        return tagList;
    }
}

The @ResponseBody annotation instructs Spring MVC to serialize the list of Tags to the client and bind it to the web response body. Spring MVC automatically serializes to JSON because the client accepts that content type.

Underneath the covers, Spring MVC delegates to a HttpMessageConverter to perform the serialization. In this case, Spring MVC invokes a MappingJacksonHttpMessageConverter built on the Jackson JSON processor. This happens automatically but you need to use the mvc:annotation-driven configuration element and have with Jackson present in your classpath. In the project, the Jackson libraries are loaded with maven using the following dependencies in the pom.xml file:


<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-mapper-asl</artifactId>
    <version>1.9.12</version>
</dependency>
<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-jaxrs</artifactId>
    <version>1.9.12</version>
</dependency>
<dependency>
    <groupId>org.codehaus.jackson</groupId>
    <artifactId>jackson-core-asl</artifactId>
    <version>1.9.12</version>
</dependency>

The controller method is called asynchronously every time the user tips a new character, no sooner than every 500ms as set in the View.

Well, this is how the magic happens behind the scenes. If you notice any room for optimization please contact us or leave a message. Many thanks to the jQuery and Spring creators and contributers, to the open source communities, to Google, Stackoverflow and to all the great people out there.

Octocat Source code for this post is available on Github - podcastpedia.org is an open source project.

References

  1. jQuery UI autocomplete
  2. jQuery.getJSON – jQuery API Documentation
  3. jQuery Core – $(document).ready()
  4. jQuery.map()
  5. AJAX
  6. Ajax Simplifications in Spring 3.0
Podcastpedia image

Adrian Matei

Creator of Podcastpedia.org and Codingpedia.org, computer science engineer, husband, father, curious and passionate about science, computers, software, education, economics, social equity, philosophy - but these are just outside labels and not that important, deep inside we are all just consciousness, right?

Adrian’s favorite Spring and jQuery books

Parallel calls with async-await in javascript - I promise you all performance and simplicity

I was blown away about the simplicity and performance gain of making parallel calls with the new async-await feature in javascript. See the blog post to understand why. Continue reading