Text Search With Hibernate Search And Spring Boot

Hibernate Search is a quick and easy way to add full text search to a java application that uses Spring. Spring Boot can be used very simply to demonstrate how quick it can be to get a text search up and running in a java application.

Lets create a web application running in Spring Boot to demonstrate this. Of course the text search part of this can be split out if required, this is here to make it easy to visualise.

Technologies

Heres what we are going to use

Project

Heres our basic setup for our eclipse project

Maven Pom

<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.colwil.tutorials</groupId>
	<artifactId>springboothsearch1</artifactId>
	<version>0.0.1</version>

 	<properties>
		<junit-version>4.12</junit-version>
		<hamcrest-version>1.3</hamcrest-version>
		<h2-version>1.4.200</h2-version>
		<hibernate-search-version>5.11.3.Final</hibernate-search-version>
		<hsqldb-version>2.5.0</hsqldb-version>
		<lombok-version>1.18.10</lombok-version>
        <java.version>1.8</java.version>
	</properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.0.RELEASE</version>
        <relativePath/> 
    </parent>	

	<dependencies>
	
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>${junit-version}</version>
		</dependency>

		<dependency>
			<groupId>org.hamcrest</groupId>
			<artifactId>hamcrest-all</artifactId>
			<version>${hamcrest-version}</version>
		</dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-engine</artifactId>
            <version>${hibernate-search-version}</version>
        </dependency>

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-search-orm</artifactId>
            <version>${hibernate-search-version}</version>
        </dependency>

        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
	        <version>${hsqldb-version}</version>
        </dependency>
        
		<dependency>
		    <groupId>org.projectlombok</groupId>
		    <artifactId>lombok</artifactId>
		    <version>${lombok-version}</version>
		    <scope>provided</scope>
		</dependency>
        
	  </dependencies>
</project>

As usual I run the maven build in eclipse to grab all the relevant dependencies and setup the project using the goal

eclipse:clean eclipse:eclipse -DdownloadSources -DdownloadJavadocs

Model

Lets define our domain model.

package com.colwil.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Indexed;

import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@Indexed
@NoArgsConstructor
public class Employee {

	private @Id @GeneratedValue Long id;
	@Field
	private String firstName;
	@Field
	private String lastName;

	public Employee(String firstName, String lastName) {
		super();
		this.firstName = firstName;
		this.lastName = lastName;
	}
}

By using Lombok we literally have to just mark this with @Data, Lombok will then automatically add the required getters and setters, toString() method, hashCode(), equals(), and the default no argument constructor… nice!

Also for Hibernate Search we need to tell it to index data in the class by marking it as @Indexed, and tell it which fields to index by marking them as @Field. There are additional options available within Hibernate Search for more control of its config for indexing the data, but we are using the defaults here for now.

Repository

By extending the Spring Data interfaces we get access to data access methods without writing our own implementation. By specifying interfaces in here we are able to use Spring Datas implementations automatically.

If we want to add additional methods for anything beyond those provided by Spring Data, we can create those in another class and extend that

package com.colwil.repository;

import org.springframework.data.repository.Repository;

import com.colwil.model.Employee;

public interface EmployeeRepository extends Repository<Employee, Long>, EmployeeRepositoryExt {

	Employee save(final Employee tweet);

}

So here we are using Spring Datas save implementation, but we want an additional method that we will specify in EmployeeRepositoryExt,

package com.colwil.repository;

import java.util.List;

import com.colwil.model.Employee;

public interface EmployeeRepositoryExt {

	List<Employee> search(final String keywords);

}

and we implement that in EmployeeRepositoryImpl.

Implementing The Hibernate Search

package com.colwil.repository;

import java.util.List;

import javax.persistence.EntityManager;

import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.query.dsl.BooleanJunction;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.springframework.transaction.annotation.Transactional;

import com.colwil.model.Employee;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class EmployeeRepositoryImpl implements EmployeeRepositoryExt {

	private final EntityManager entityManager;

	@Override
	@Transactional(readOnly = true)
	public List<Employee> search(final String keywords) {

		final FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);

		// Search query builder
		final QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory().buildQueryBuilder()
				.forEntity(Employee.class).get();

		// Use a boolean junction and then add queries to it
		final BooleanJunction<BooleanJunction> outer = queryBuilder.bool();
		outer.must(queryBuilder.keyword().onFields("firstName", "lastName").matching(keywords).createQuery());

		@SuppressWarnings("unchecked")
		List<Employee> resultList = fullTextEntityManager.createFullTextQuery(outer.createQuery(), Employee.class)
				.getResultList();
		return resultList;
	}
}

So here we are going to search on the keywords in the firstName and lastName fields for an exact match in either field

Initialising the Hibernate Search

We are also using a Spring Application Listener so that we can run the Hibernate Search indexing process once the application has started.

package com.colwil.service;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.Search;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@Transactional
public class BuildSearchIndex implements ApplicationListener<ApplicationReadyEvent> {

	@PersistenceContext
	private EntityManager entityManager;

	@Override
	public void onApplicationEvent(ApplicationReadyEvent event) {
		initializeHibernateSearch();
	}

	public void initializeHibernateSearch() {

		try {
			FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager);
			fullTextEntityManager.createIndexer().startAndWait();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

}

The Hibernate Search indexer will index the fields that we annotated in the Employee domain model class.

Spring MVC Controller

Lets create a controller to process the view form, trigger the search and return the reponse back to the form.

package com.colwil.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.colwil.model.Employee;
import com.colwil.repository.EmployeeRepository;

@Controller
public class SearchController {

	@Autowired
	private EmployeeRepository repository;

	@RequestMapping(value = "/")
	public String search(@RequestParam(value = "search", required = false) String q, Model model) {
		List<Employee> searchResults = null;
		try {
			searchResults = repository.search(q);

		} catch (Exception ex) {
			// Nothing
		}
		model.addAttribute("search", searchResults);
		return "index";

	}

}

Thymeleaf UI

We will use a Thymeleaf template to implement the UI, to allow the search using a form and display the results.

<!DOCTYPE html>
<html lang="en">
<head>
<title>Hibernate Search With Spring Boot</title>

<link href="/index.css?x11591" rel="stylesheet">
</head>
<body>

	<h1>Search</h1>
	<div>
		<form action="#" th:action="@{/}" th:object="${search}">
			<input class="formStyle" name="search" id="search"
				placeholder="Keywords" />
			<div th:if="${not #lists.isEmpty(search)}">
				<table>
					<thead>
						<tr>
							<th>id</th>
							<th>First Name</th>
							<th>Surname</th>
						</tr>
					</thead>
					<tbody>
						<tr th:each="search : ${search}">
							<td th:text="${search.id}">Text ...</td>
							<td th:text="${search.firstName}">Text ...</td>
							<td th:text="${search.lastName}">Text ...</td>
						</tr>
					</tbody>
				</table>
			</div>
		</form>
	</div>
</body>
</html>

and we will add some css to style that a little

body {
  background-color: #2ecc71;
  font-family: source-sans-pro, sans-serif;
}

h1 {
  margin-top: 50px;
  font-weight: 100;
  font-size: 2.8em;
  color: #ffffff;
}

div {
  width: 500px;
}

.formStyle { 
  background-color: #2ecc71;
  padding: 20px; 
  width: 400px; 
  margin-bottom: 20px; 
  border-bottom-width: 1px; 
  border-bottom-style: solid; 
  border-bottom-color: #ecf0f1; 
  border-top-style: none; 
  border-right-style: none; 
  border-left-style: none; 
  font-size: 1em;
  font-weight: 100;
  color: #ffffff;
}

.formButton {
  float: right;
	background-color:#ffffff;
	display:inline-block;
	color:#2ecc71;
	font-size:28px;
	font-weight: 500;
	padding:6px 24px;
  margin-top: 15px;
  margin-right: 60px;
	text-decoration:none;
}

.formButton:hover {
	background-color: #27ae60;
  color:#ffffff;
}

.formButton:active {
	position:relative;
	top:3px;
}

input:focus {
  outline: none;
}

/* To format the placeholder text color */
::-webkit-input-placeholder {
   color: #ecf0f1;
}

:-moz-placeholder { /* Firefox 18- */
   color: #ecf0f1;  
}

::-moz-placeholder {  /* Firefox 19+ */
   color: #ecf0f1;  
}

:-ms-input-placeholder {  
   color: #ecf0f1;  
}

CommandLineRunner

Lets implement a Spring CommandLineRunner to ensure we can populate some data in the embedded HSQL database (Spring Boot will have automatically configured the embedded database and started it, as it found the drivers on the classpath based on our POM.

package com.colwil.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import com.colwil.model.Employee;
import com.colwil.repository.EmployeeRepository;

@Component
public class DatabaseLoader implements CommandLineRunner {

	private final EmployeeRepository repository;

	@Autowired
	public DatabaseLoader(EmployeeRepository repository) {
		this.repository = repository;
	}

	@Override
	public void run(String... strings) throws Exception {
		this.repository.save(new Employee("Joe", "Bloggs"));
	}
}

SpringApplication

Lets implement a SpringApplication to startup our app.

package com.colwil;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EnableJpaRepositories("com.colwil.repository")
@ComponentScan(basePackages = { "com.colwil.configuration", "com.colwil.controller", "com.colwil.service" })
public class SearchApplication {

	public static void main(String[] args) {
		SpringApplication.run(SearchApplication.class, args);
	}

}

We specify additional package locations for Spring, as we arent using the default options of having all our code in the same package as the SpringApplication.

If we run our SearchApplication now, it will start Spring Boot and then we should be able to go to http://localhost:8080/ and see our application.

if we enter some some search terms and search we should get results if they match. Search for Joe

We get a result as it matched. If we now search for Jo

We get nothing as it doesnt match.

Conclusion

Hope this helped to show you how easy it is to get started with Spring Boot and Hibernate Search. Of course there is a lot more to both, but its nice to get something up and running that you can then tweak, play with and improve on.

Leave a Comment

Your email address will not be published. Required fields are marked *