Autocomplete search box with jQuery and Spring MVC

(P) Codever is an open source bookmarks and snippets manager for developers & co. See our How To guides to help you get started. Public bookmarks repos on Github ⭐🙏
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 :
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:
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.
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="https://code.jquery.com/jquery-1.9.1.min.js"></script>
<script type="text/javascript" src="https://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: "https://" + 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 performeddelay = 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, overAJAX
, will getJSON
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 handlerselect
– 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.
Source code for this post is available on Github - podcastpedia.org is an open source project.
References
- jQuery UI autocomplete
- jQuery.getJSON – jQuery API Documentation
- jQuery Core – $(document).ready()
- jQuery.map()
- AJAX
- Ajax Simplifications in Spring 3.0
Adrian’s favorite Spring and jQuery books