Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / Spring

Integrating Full Text Search to Spring MVC with Hibernate

4.79/5 (9 votes)
17 Oct 2014MPL16 min read 86.8K   2.8K  
In this article, I will show you how to integrate Hibernate Search into a simple yet feature complete web application with the Spring MVC and Hibernate enabled.

Introduction

When I was working on my first ever blog engine (sorry about this shameful self-promotion) recently, at the very late stage, I discovered a problem -- there is no search capability in this blog engine. I released it out on Digital Ocean as my personal photography site. But I regretted the decision of not having the search function in the application.

For my own benefit, I decided to research for a solution to add full text search to the web app. It turned out that there are several approaches available. The obvious two for me are:

  • Add a customized SQL dialect for the Hibernate and take advantage of MySQL's (or any other SQL DB) full text search functionality.
  • Use Hibernate Search and integrate with existing Spring MVC app.

I highly recommended not trying the first approach because it is not portable. Here is the thing, if you follow this approach, you are making a tight couple of your application to a specific relational database. In the event of changing to a different database, you must implement a new dialect.

Based on this reasoning, I chose the second approach which was quite a popular approach based on the search results I have received through Google. Regardless of the approaches, I still find it hard to get some good documentations on the type of integration I worked on. This article is intended to track all the information and give the audience a good working copy of the work I have done.

Background on Technologies Used

Before I get into the details of how the implementation goes, I would like to discuss in brief the technologies I have used for this. The project is a Spring MVC based web application. I used Spring 3 components. The build is done using maven 2. The persistence layer is using Hibernate 4. The back end data store is MySQL. Everything is integrated through Spring.

The attached zip file will have all you need to run the web app. What you need to do are:

  • Install Java 1.6
  • Install Apache Maven 2 or 3
  • Install MySQL and install the DB and tables.
  • Set the environment variables for JAVA_HOME and M2_HOME, and the PATH variable for both.
  • Run mvn clean install. Once the war file has been created, deploy to your machine and run.

Alternatively, you can use Maven to generate the Eclipse project files and import the project into Eclipse for editing. I even run the server in Eclipse for trouble shooting. The main focus of this article is to describe how the implementation of full text search on SQL based DB works, so I will not spend time explaining how to setup the dev environment for this. There are plenty of resources to help you with this problem.

Understanding the Code

I will discuss the project architecture and code structure in detail. First, let me describe what this application does. It is a simple web based application. It allows the user to enter the details of a book: title, description, and author. And it allows the user to search the entered information by 1 simple keyword. As you can see, it is so simple and it does not do much. This is intended. All I want to show you is how to setup the whole thing. How you like to implement full text search functionality using Hibernate Search is up to you to do. Let's go through the steps on setting up this Java project.

Step 1: Create DB User and Tables

In the zip file, there is a folder called DB, which contains 2 scripts. The first script creates a DB user. The content looks like this:

SQL
CREATE DATABASE fulltextsearch;

CREATE USER 'ftuser1'@'localhost' IDENTIFIED BY '123test321';

GRANT ALL PRIVILEGES ON fulltextsearch.* TO 'ftuser1'@'localhost';

FLUSH PRIVILEGES;

Run the above script on MySQL with root user, you will create a new user and new database for this project.

Another SQL script is included which will create a new table in the new DB. The content looks like the following:

SQL
use fulltextsearch;

DROP TABLE IF EXISTS book;

CREATE TABLE book (
   id VARCHAR(37) NOT NULL PRIMARY KEY,
   title VARCHAR(128) NOT NULL,
   description VARCHAR(256) NOT NULL,
   author VARCHAR(64) NOT NULL,
   createdate DATETIME NOT NULL,
   updatedate DATETIME NOT NULL
);

Again, I won't explain how you will run these two scripts. There should be plenty of resources online to help you out.

Step 2: Meet the POM File

Finally, I have switched to using Maven for building a Java application. That was 3 years ago. During then, I published 1 article here using just Eclipse. It wasn't quite well received (only got 5000 views). The problem I suspected was that I can't include all the dependent jars in the source code for people to download, which made the previous article somewhat hard to follow.

Anyways, for this article, packaging all the dependent jars for people to download is still a terrible idea. With maven, this problem will not be a problem. As long as you setup your dev environment correctly, you should be able to download the dependent jars, compile, package war files all in 1 automated process.

For this project, as it turned out, I need to include a number of jars, making this POM file looks quite scary. So let me post it out and scare off any rookie programmers, and then for whoever stayed, I will explain the key elements of this POM file. Here it goes:

XML
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <groupId>org.hanbo.spring.app</groupId>
   <artifactId>SampleSpringApp</artifactId>
   <packaging>war</packaging>
   <version>1.0-SNAPSHOT</version>
   <name>Sample Spring REST App</name>
   <url>http://maven.apache.org</url>

   <properties>
      <spring.version>3.2.6.RELEASE</spring.version>
   </properties>

   <dependencies>
    <dependency>
       <groupId>antlr</groupId>
       <artifactId>antlr</artifactId>
       <version>2.7.7</version>
    </dependency>
    <dependency>
       <groupId>aopalliance</groupId>
       <artifactId>aopalliance</artifactId>
       <version>1.0</version>
    </dependency>
    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-aop</artifactId>
       <version>3.2.6.RELEASE</version>
    </dependency>
    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-beans</artifactId>
       <version>3.2.6.RELEASE</version>
    </dependency>
    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-context</artifactId>
       <version>3.2.6.RELEASE</version>
    </dependency>
    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-core</artifactId>
       <version>3.2.6.RELEASE</version>
    </dependency>
    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-expression</artifactId>
       <version>3.2.6.RELEASE</version>
    </dependency>
    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-jdbc</artifactId>
       <version>3.2.6.RELEASE</version>
    </dependency>
    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-orm</artifactId>
       <version>3.2.6.RELEASE</version>
    </dependency>
    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-tx</artifactId>
       <version>3.2.6.RELEASE</version>
    </dependency>
    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-web</artifactId>
       <version>3.2.6.RELEASE</version>
    </dependency>
    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-webmvc</artifactId>
       <version>3.2.6.RELEASE</version>
    </dependency>
      <dependency>
         <groupId>commons-dbcp</groupId>
         <artifactId>commons-dbcp</artifactId>
         <version>1.4</version>
      </dependency>
      <dependency>
         <groupId>org.hibernate</groupId>
         <artifactId>hibernate-core</artifactId>
         <version>4.2.15.Final</version>
      </dependency>
      <dependency>
     <groupId>org.hibernate</groupId>
     <artifactId>hibernate-search-engine</artifactId>
     <version>4.3.0.Final</version>
      </dependency>
      <dependency>
     <groupId>org.hibernate</groupId>
     <artifactId>hibernate-search-orm</artifactId>
     <version>4.3.0.Final</version>
      </dependency>
      <dependency>
     <groupId>org.hibernate</groupId>
     <artifactId>hibernate-search-orm</artifactId>
     <version>4.3.0.Final</version>
      </dependency>
      <dependency>
         <groupId>org.hibernate.javax.persistence</groupId>
         <artifactId>hibernate-jpa-2.0-api</artifactId>
         <version>1.0.1.Final</version>
      </dependency>
      <dependency>
         <groupId>javassist</groupId>
         <artifactId>javassist</artifactId>
         <version>3.12.1.GA</version>
      </dependency>
      <dependency>
         <groupId>org.jboss.logging</groupId>
         <artifactId>jboss-logging</artifactId>
         <version>3.1.4.GA</version>
      </dependency>
      <dependency>
         <groupId>javax.transaction</groupId>
         <artifactId>jta</artifactId>
         <version>1.1</version>
      </dependency>
      <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
         <version>5.1.29</version>
      </dependency>
      <dependency>
         <groupId>javax.persistence</groupId>
         <artifactId>persistence-api</artifactId>
         <version>1.0.2</version>
      </dependency>
   </dependencies>
   <build>
      <finalName>SpringMVC</finalName>
      <plugins>
         <plugin>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            <configuration>
               <source>1.6</source>
               <target>1.6</target>
            </configuration>
         </plugin>
      </plugins>
   </build>
</project>

Please ignore any uncleanness of this file. I used a previous project and added a few more dependencies. To be honest, this file is very easy to be read. There are two major sections, the first section is the dependencies. For each dependency you have groupId, artifactId, and version. At the top of this section, there is a bunch of dependencies on Spring, Spring Data and Spring MVC. All of them have version of 3.2.6.RELEASE.

The next part of this section is the hibernate dependencies. There are a couple gotchas here that you must be aware:

  • I can only get the whole thing to work with hibernate-core of version 4.2.15.Final. I have tried with later version of hibernate-core. There was a dependency class which was removed from these versions of hibernate-core. And I got an exception at initialization, which I cannot resolve. There is no solution on the net. Which got me to speculate that no one else is looking at this issue, and possibly no one is using hibernate search or something. Or I have missed something simple that just resolves this issue.
  • I kept trying to find the right hibernate-search.jar because all these jars with different versions contain no classes. Apparently, you don't need this jar. Instead just include hibernate-search-engine and hibernate-search-orm. And you are all set. I choose the 4.3.0.Final. It was an early stable version which seemed a lot of people where using.
  • There is another gotcha. I will tell you later.

The last part is the misc. jars that needed, like: JSTL, persistence-api, etc. One interesting you should be aware is that somehow, hibernate had a dependency on jboss-logging which I have not researched on how to get rid of. Instead of fighting it, I just include it in the dependency list.

The other section is the compiler plugin used for compile the project and generate the final war file. I set the compiler version to 1.6 so that I don't have to deal with Java 1.7.

Step 3: The Project Directory Structure

This is the directory structure:

SampleSpringFullTextSearch1
|
 ---- DB
      |
       ---- tables.sql
      |
       ---- user.sql
|
 ---- Index (used for storing the indexes for full tgext search)
|
 ---- src
      |
       ---- main
            |
         ---- java
              |
           ---- org / hanbo / mvc / controller (MVC controller classes)
          |
           ---- org / hanbo / mvc / models (MVC data models used for display)
          |
           ---- org / hanbo / mvc / entities (data persistence classes) 
           
        |
         ---- resources
        |
         ---- webapp
              |
           ---- META-INF
            |
             ---- MANIFEST.MF
          |
           ---- WEB-INF
                |
             ---- lib
            |
             ---- pages
                  |
                   ---- A number of JSP pages, used as MVC views
            |
             ---- mvc-dispatcher-servlet.xml
            |
             ---- web.xml

I don't want to delve into detail too much, the above project directory structure is the basic structure for creating a Spring MVC project, and using Maven to create a deployable war file. This is based on the assumption that the configurations are properly done, which will be explained next.

Step 4: Web.xml

web.xml is the deployment descriptor. Something that the J2EE container has to look at and figure out how to properly wire the web application for user to interact with. For this app, the web.xml looks just like the web.xml for a typical spring web application. The content of this file looks like:

XML
<web-app id="WebApp_ID" version="2.4"
   xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
   http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
   
   <display-name>Spring Web MVC Application</display-name>
   <servlet>
      <servlet-name>mvc-dispatcher</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <load-on-startup>1</load-on-startup>
   </servlet>
   
   <servlet-mapping>
      <servlet-name>mvc-dispatcher</servlet-name>
      <url-pattern>/</url-pattern>
   </servlet-mapping>
   
   <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>
   </context-param>
   
   <listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
   </listener>
</web-app>

As I have described, there is nothing special about this file. The first section describes what the servlet name is. And it uses Spring Web's DispatchServlet to handle incoming request. Note the name of servlet is called mvc-dispatcher. This name is important. Spring Web when initialized, will look for the application context with this naming convention: <servlet-name>-servlet.xml. So in the third section, you see the contextConfiguration is set to /WEB-INF/mvc-dispatcher-servlet.xml. And it has to be mvc-dispatcher-servlet.xml. If you name the application context configuration file to something else, when the servlet initializes, it will fail with an exception.

The second section basically indicates that any request to "/" will be handled by Spring Web's DispatcherServlet. The fourth and last section is basically registering an event listener to context loading that the event listener when notified about the context loading event would load the specified application context configuration. There is not much to it. For more information about Spring configuration, please search online. Next, we will check out the application context config file. This is the heart of this app.

Step 5: Application Context Config File

Compare to a typical web app that uses Hibernate for persistence, the application context config file of this app is almost the same, except with a few modifications. First let's see the full content of this file, named mvc-dispatcher-servlet.xml:

XML
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans     
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
        http://www.springframework.org/schema/tx 
        http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-3.0.xsd">

   <context:component-scan base-package="org.hanbo.mvc.controller" />
   <context:component-scan base-package="org.hanbo.mvc.entities" />
    
   <mvc:resources mapping="/assets/**" location="/assets/" />
    
   <mvc:annotation-driven />
    
   <bean
      class="org.springframework.web.servlet.view.InternalResourceViewResolver">
      <property name="prefix">
         <value>/WEB-INF/pages/</value>
      </property>
      <property name="suffix">
         <value>.jsp</value>
      </property>
   </bean>
   
    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
       <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
       <property name="url" value="jdbc:mysql://127.0.0.1:3306/fulltextsearch?autoReconnect=true"/>
       <property name="username" value="ftuser1"/>
       <property name="password" value="123test321"/>
       <property name="maxActive" value="8"/>
       <property name="maxIdle" value="4"/>
       <property name="maxWait" value="900000"/>
       <property name="validationQuery" value="SELECT 1" />
       <property name="testOnBorrow" value="true" />
    </bean>
    
    <bean id="mySessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
      <property name="dataSource" ref="myDataSource"/>
      <property name="packagesToScan">
        <array>
          <value>org.hanbo.mvc.entities</value>
        </array>
      </property>
      <property name="hibernateProperties">
        <props>
          <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
          <prop key="hibernate.cache.provider_class">org.hibernate.cache.NoCacheProvider</prop>
          <prop key="hibernate.search.default.directory_provider">
          org.hibernate.search.store.impl.FSDirectoryProvider</prop>
          <prop key="hibernate.search.default.indexBase">
          C:/Users/hanbo/workspace-ee/SampleSpringFullTextSearch/indexes</prop>
        </props>
      </property>
    </bean>

    <bean id="transactionManager" 
    class="org.springframework.orm.hibernate4.HibernateTransactionManager">
      <property name="sessionFactory" ref="mySessionFactory"/>
    </bean>
    
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

Now, let's break apart and discuss each part in detail. The first part is to set the component scan packages. The idea is to allow Spring to scan the designated packages for classes that are annotated as beans (either for autowire or allow Spring components to know what they are and how they can be used):

XML
<context:component-scan base-package="org.hanbo.mvc.controller" />
<context:component-scan base-package="org.hanbo.mvc.entities" />
... ...
<mvc:annotation-driven />

The next section is configured for Spring MVC to find the view JSP files. As showN in the following code snippet, the code sets the folder within the war file /WEB-INF/pages as the container of all the view pages, and it tells Spring MVC that the view pages has suffix .jsp.

XML
<bean
   class="org.springframework.web.servlet.view.InternalResourceViewResolver">
   <property name="prefix">
      <value>/WEB-INF/pages/</value>
   </property>
   <property name="suffix">
      <value>.jsp</value>
   </property>
</bean>

The next section is the data source configuration of this app. Nothing special here. If you want to try this app out, make sure the configuration works for you. If it does not work for you, you might have to tweak the values. To this point, nothing about Hibernate search has been configured.

XML
    <bean id="myDataSource" 
class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
       <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
       <property name="url" value="jdbc:mysql://127.0.0.1:3306/fulltextsearch?autoReconnect=true"/>
       <property name="username" value="ftuser1"/>
       <property name="password" value="123test321"/>
       <property name="maxActive" value="8"/>
       <property name="maxIdle" value="4"/>
       <property name="maxWait" value="900000"/>
       <property name="validationQuery" value="SELECT 1" />
       <property name="testOnBorrow" value="true" />
    </bean>

The next section is getting interesting. So let me show you what is needed to for Hibernate persistence configuration:

XML
    <bean id="mySessionFactory" 
class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
      <property name="dataSource" ref="myDataSource"/>
      <property name="packagesToScan">
        <array>
          <value>org.hanbo.mvc.entities</value>
        </array>
      </property>
      <property name="hibernateProperties">
        <props>
          <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
          <prop key="hibernate.cache.provider_class">org.hibernate.cache.NoCacheProvider</prop>
      ... ...
        </props>
      </property>
    </bean>

As you can see, all the code that are not "... ..." are just Hibernate persistence specific configuration. Now to fill the gap with the Hibernate specific configuration, it was quite simple, you use the following:

XML
<prop key="hibernate.search.default.directory_provider">
org.hibernate.search.store.impl.FSDirectoryProvider</prop>
<prop key="hibernate.search.default.indexBase">
C:/Users/hsun/workspace-ee/SampleSpringFullTextSearch/indexes</prop>

As I have mentioned before, there is one gotcha. And it is right here. There are many pages in Google results showing the value of "hibernate.search.default.directory_provider" should be "org.hibernate.search.store.FSDirectoryProvider". This is no longer valid. The class has been moved to a different package in new versions. It took me some digging to find this FSDirectoryProvider. As you can guess, it worked.

This is the end of the configurations. Next, the fun stuff - Java classes.

Java Classes

Before we dig in to the Java class, I need to explain how this application works. This is a simple web application that allows user to enter a new book. And it allows the user to search books by taking one keyword and match by the book's title, author and description (one keyword matching three different fields). There is one additional functionality that allows the app to re-index all the records

Below is the screenshot showing the add book page, the URL is http://localhost:8080/SampleSpringFullTextSearch/addBook:

Image 1

Here is the screenshot showing the search book page, the URL is http://localhost:8080/SampleSpringFullTextSearch/search:

Image 2

As you can see, the demo app is quite simple, you can input some book info into the DB, then you use some keyword search to see if the full text feature is working or not. To get this app going, first we must define 3 different actions, all within the same controller:

  • An action that demos the ability to reindex all the table records;
  • An action that allows Book info to be inserted into the back end DB;
  • An action that performs the simple full text search and lists the result out.

Before we start with these fun stuff with the controller and the three actions, let's first check it out the entity class (this is fun).

The Entity Class

Remember that DB table that we have to create in the beginning of this article? In order to use Hibernate, we need a corresponding entity class. This is where the Hibernate Search comes into play.

You will find the entity class "Book.java" in org\hanbo\mvc\entities directory. In the same directory, there is the repository class "BookRepository.java".

Java
package org.hanbo.mvc.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.Store;

import java.util.Date;

@Entity
@Indexed
@Table(name = "book")
public class Book
{
   @Id
   @Column(name = "id")
   private String id;
   
   @Column(name = "title", nullable= false, length = 128)
   @Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
   private String title;
   
   @Column(name = "description", nullable= false, length = 256)
   @Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
   private String description;
   
   @Column(name = "author", nullable= false, length = 64)
   @Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
   private String author;
   
   @Column(name = "createdate", nullable= false)
   private Date createDate;
   
   @Column(name = "updatedate", nullable= false)
   private Date updateDate;

   public String getId()
   {
      return id;
   }

   public void setId(String id)
   {
      this.id = id;
   }

   public String getTitle()
   {
      return title;
   }

   public void setTitle(String title)
   {
      this.title = title;
   }

   public String getDescription()
   {
      return description;
   }

   public void setDescription(String description)
   {
      this.description = description;
   }

   public String getAuthor()
   {
      return author;
   }

   public void setAuthor(String author)
   {
      this.author = author;
   }

   public Date getCreateDate()
   {
      return createDate;
   }

   public void setCreateDate(Date createDate)
   {
      this.createDate = createDate;
   }

   public Date getUpdateDate()
   {
      return updateDate;
   }

   public void setUpdateDate(Date updateDate)
   {
      this.updateDate = updateDate;
   }
}

My entity class uses the JPA annotations to associate the class to the table, and instance members of the class to the table column. These are pretty standard stuff with Hibernate. What is cool Hibernate Search is that it provided a couple annotation so that you can tag the instance members of the class, a.k.a. the table column, as indexable fields. They are:

Java
import org.hibernate.search.annotations.Indexed;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Index;
import org.hibernate.search.annotations.Analyze;
import org.hibernate.search.annotations.Store;

To make the table column indexable with Hibernate Search, here is the code:

Java
@Column(name = "title", nullable= false, length = 128)
@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String title;

@Column(name = "description", nullable= false, length = 256)
@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String description;

@Column(name = "author", nullable= false, length = 64)
@Field(index=Index.YES, analyze=Analyze.YES, store=Store.NO)
private String author;

The annotation @Field and its parameters basically making the table columns "title", "description", and "author" indexable and during the indexing operation, the content of the column will be analyzed. According to the Hibernate Search tutorial, "analyzing means chunking a sentence into individual words, lowercase them and potentially excluding common words like 'a' or 'the'." Anyways, the 3 @Field annotations use the default setting. The last parameter store is set to No, meaning that I do not want the content to be stored in the full text search index store. I do this to save the space (not sure I can justify this).

The Repository Class

So, we have a quarter of the way to a final product. The next thing I like show is the CRUD operations of BookRepository. Here is the code:

Java
package org.hanbo.mvc.entities;

import java.util.Date;
import java.util.List;
import java.util.UUID;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
@SuppressWarnings("unchecked")
public class BookRepository
{
   @Autowired
   private SessionFactory mySessionFactory;

   @Transactional
   public void indexBooks() throws Exception
   {
      try
      {
         Session session = mySessionFactory.getCurrentSession();
      
         FullTextSession fullTextSession = Search.getFullTextSession(session);
         fullTextSession.createIndexer().startAndWait();
      }
      catch(Exception e)
      {
         throw e;
      }
   }
   
   @Transactional
   public void addBookToDB(String bookTitle, String bookDescription, String bookAuthor)
   {
      Session session = mySessionFactory.getCurrentSession();
      
      Book book = new Book();
 
      UUID x = UUID.randomUUID();
      
      Date dateNow = new Date();
      book.setId(x.toString());
      book.setAuthor(bookAuthor);
      book.setDescription(bookDescription);
      book.setTitle(bookTitle);
      book.setCreateDate(dateNow);
      book.setUpdateDate(dateNow);
      
      session.saveOrUpdate(book);
   }
   
   @Transactional
   public List<Book> searchForBook(String searchText) throws Exception
   {
      try
      {
         Session session = mySessionFactory.getCurrentSession();
         
         FullTextSession fullTextSession = Search.getFullTextSession(session);

         QueryBuilder qb = fullTextSession.getSearchFactory()
           .buildQueryBuilder().forEntity(Book.class).get();
         org.apache.lucene.search.Query query = qb
           .keyword().onFields("description", "title", "author")
           .matching(searchText)
           .createQuery();

         org.hibernate.Query hibQuery = 
            fullTextSession.createFullTextQuery(query, Book.class);

         List<Book> results = hibQuery.list();
         return results;
      }
      catch(Exception e)
      {
         throw e;
      }
   }
}

This class is really where the fun really is, the first thing I like to show is the record insertion operation, which is this part:

Java
@Transactional
public void addBookToDB(String bookTitle, String bookDescription, String bookAuthor)
{
   Session session = mySessionFactory.getCurrentSession();

   Book book = new Book();

   UUID x = UUID.randomUUID();

   Date dateNow = new Date();
   book.setId(x.toString());
   book.setAuthor(bookAuthor);
   book.setDescription(bookDescription);
   book.setTitle(bookTitle);
   book.setCreateDate(dateNow);
   book.setUpdateDate(dateNow);

   session.saveOrUpdate(book);
}

As you can see from this code, it is a pretty standard way for Hibernate to persist an entity into the database. What is not revealed here is that behind the scenes, Hibernate Search would also index the new entity on its properties title, author and description. That is very cool!

You might ask, what if I have an existing DB and I retrofit my front end to have full text search, how do I add existing records into the search index? This is do-able; and it is quite easy to do. There is a gotcha, however, according to the documentation, this is an expensive little operation which has worse performance if there are a lot of tables and each has lots of data in it. Basically what happens is that the entire index is wiped away and every record in every table will be re-processed into the new index. If you have a small set of data, it will finish quickly. If you have a large set of data, this will take a lot of time to complete. So you best bet is that:

  1. You don't have to re-index, just create empty tables and index the records when the records are inserted.
  2. You do re-index once, and once only during maintenance. Anyway, in the above class, here is the code that does the re-indexing:
Java
@Transactional
public void indexBooks() throws Exception
{
   try
   {
      Session session = mySessionFactory.getCurrentSession();

      FullTextSession fullTextSession = Search.getFullTextSession(session);
      fullTextSession.createIndexer().startAndWait();
   }
   catch(Exception e)
   {
      throw e;
   }
}

Finally, let's check the logic that does the full text search:

Java
@Transactional
public List<Book> searchForBook(String searchText) throws Exception
{
   try
   {
      Session session = mySessionFactory.getCurrentSession();

      FullTextSession fullTextSession = Search.getFullTextSession(session);

      QueryBuilder qb = fullTextSession.getSearchFactory()
        .buildQueryBuilder().forEntity(Book.class).get();
      org.apache.lucene.search.Query query = qb
        .keyword().onFields("description", "title", "author")
        .matching(searchText)
        .createQuery();

      org.hibernate.Query hibQuery =
         fullTextSession.createFullTextQuery(query, Book.class);

      List<Book> results = hibQuery.list();
      return results;
   }
   catch(Exception e)
   {
      throw e;
   }
}

The above code does the following:

  • Get current Hibernate session.
  • Get the full text search session.
  • Create a query against Book entity, and search on fields "title", "description", and "author".
  • Do the search and return a list of results.

Note that this search feature is very limited. You enter one word, as long as any entity contains that word, it will show up on the result list. This just demonstrates the fact that the app is setup for full text search.

The Controller Class

The controller class is the last piece of the puzzle. The controller class was lazily implemented. It uses the repository object directly instead of using a service layer object. The actions this controller exposed are:

  • Url: /welcome - This action will force re-index all the full text indexes. In reality this should never be exposed to end user, but in this example, you can use this action to see the re-index in action.
  • Url: /addBookToDB - This action will receive post data from the book input page, and insert the book detail into Book table in DB.
  • Url: /doSearch - This action will perform the full text search and list all relevant results.

In addition to these actions, there are two more that would display the initial page:

  • Url: /addBook - This action will display the page that allows user to enter the detail of a book.
  • Url: /search - This action will display the page that allows the user to search for books.

Before we get into these actions, let's check out the code:

Java
package org.hanbo.mvc.controller;

import java.util.ArrayList;
import java.util.List;

import org.hanbo.mvc.entities.Book;
import org.hanbo.mvc.entities.BookRepository;
import org.hanbo.mvc.models.BookModel;
import org.jboss.logging.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class HelloWorld
{
   private static Logger _logger = Logger.getLogger(HelloWorld.class);
   
   @Autowired
   private BookRepository _repo;
   
   @RequestMapping(value = "/welcome", method = RequestMethod.GET)
   public ModelAndView welcome() throws Exception
   {
      _repo.indexBooks();

      ModelAndView mav = new ModelAndView("hello");
      mav.addObject("message", "Hello World!");
      return mav;
   }

   @RequestMapping(value = "/addBook", method = RequestMethod.GET)
   public ModelAndView addBookPage()
   {
      ModelAndView mav = new ModelAndView("addBook", "command", new BookModel());
      return mav;
   }
   
   @RequestMapping(value = "/addBookToDB", method = RequestMethod.POST)
   public ModelAndView addBookToDB(
      @ModelAttribute("BookModel")
      BookModel bookInfo
   ) throws Exception
   {
      _logger.info(bookInfo.getBookTitle());
      _logger.info(bookInfo.getBookDescription());
      _logger.info(bookInfo.getBookAuthor());
      
      _repo.addBookToDB(
         bookInfo.getBookTitle(),
         bookInfo.getBookDescription(),
         bookInfo.getBookAuthor()
      );
      
      ModelAndView mav = new ModelAndView("done");
      mav.addObject("message", "Add book to DB successfully");
      return mav;
   }
   
   @RequestMapping(value = "/search", method = RequestMethod.GET)
   public ModelAndView searchPage()
   {
      ModelAndView mav = new ModelAndView("search");
      return mav;
   }

   @RequestMapping(value = "/doSearch", method = RequestMethod.POST)
   public ModelAndView search(
      @RequestParam("searchText")
      String searchText
   ) throws Exception
   {
      List<Book> allFound = _repo.searchForBook(searchText);
      List<BookModel> bookModels = new ArrayList<BookModel>();
      
      for (Book b : allFound)
      {
         BookModel bm = new BookModel();
         bm.setBookAuthor(b.getAuthor());
         bm.setBookDescription(b.getDescription());
         bm.setBookTitle(b.getTitle());
         
         bookModels.add(bm);
      }
      
      ModelAndView mav = new ModelAndView("foundBooks");
      mav.addObject("foundBooks", bookModels);
      return mav;
   }
}

Let's go through this one by one. First with the most uninteresting pieces, the page display actions. To display the add book page, this is the code that does it:

Java
@RequestMapping(value = "/addBook", method = RequestMethod.GET)
public ModelAndView addBookPage()
{
   ModelAndView mav = new ModelAndView("addBook", "command", new BookModel());
   return mav;
}

To display the search book page, this is the code that does it:

Java
@RequestMapping(value = "/addBook", method = RequestMethod.GET)
public ModelAndView addBookPage()
{
   ModelAndView mav = new ModelAndView("addBook", "command", new BookModel());
   return mav;
}

Now the more interesting stuff, to add a new book, this piece of code does it:

Java
@RequestMapping(value = "/addBookToDB", method = RequestMethod.POST)
public ModelAndView addBookToDB(
   @ModelAttribute("BookModel")
   BookModel bookInfo
) throws Exception
{
   _logger.info(bookInfo.getBookTitle());
   _logger.info(bookInfo.getBookDescription());
   _logger.info(bookInfo.getBookAuthor());

   _repo.addBookToDB(
      bookInfo.getBookTitle(),
      bookInfo.getBookDescription(),
      bookInfo.getBookAuthor()
   );

   ModelAndView mav = new ModelAndView("done");
   mav.addObject("message", "Add book to DB successfully");
   return mav;
}

What I am doing right now, is just use the _repo object and save the newly created book object into DB. The data is from the BookModel object. The BookModel is the display related data model, contains the values for book title, description and author. You can find it org.hanbo.mvc.models.BookModel.

Finally, the code that does the book search using Hibernate Search:

Java
@RequestMapping(value = "/doSearch", method = RequestMethod.POST)
public ModelAndView search(
   @RequestParam("searchText")
   String searchText
) throws Exception
{
   List<Book> allFound = _repo.searchForBook(searchText);
   List<BookModel> bookModels = new ArrayList<BookModel>();

   for (Book b : allFound)
   {
      BookModel bm = new BookModel();
      bm.setBookAuthor(b.getAuthor());
      bm.setBookDescription(b.getDescription());
      bm.setBookTitle(b.getTitle());

      bookModels.add(bm);
   }

   ModelAndView mav = new ModelAndView("foundBooks");
   mav.addObject("foundBooks", bookModels);
   return mav;
}

This piece of code is again uninteresting because the actual search is performed in the repository object. This piece of code will get a list of return results, then transform into BookModel objects, put in a list and return the page for display.

There are a bunch of JSP pages in webapp/WEB-INF/pages. You can check them out. The way Spring MVC controller does is that, for the action methods, they return either a string of the page name (without extension), and Spring MVC uses the InternalResourceViewResolver object to get to the JSP files in webapp/WEB-INF/pages.

This is basically it. Have fun playing with the demo app. Don't forget to check out my blogger page.

History

  • 10/16/2014 - Complete initial draft

License

This article, along with any associated source code and files, is licensed under The Mozilla Public License 1.1 (MPL 1.1)