Spring Boot + Hibernate Search example
Here we will create a Spring Boot web application example with Hibernate Search + Thymeleaf template engine, and deploy it as a WAR to Wildfly 10.1.
Technologies used:
- Spring Boot 1.5.6.RELEASE
- Java 8
- Hibernate Search 5.6.1.Final
- Embedded Tomcat, Wildfly 8.1 Final & 10.1 Final
1. Project Structure
A standard Maven project structure
1. Project Dependencies
<?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.mkyong</groupId>
<artifactId>spring-boot-web-wildfly-search</artifactId>
<version>0.0.1</version>
<packaging>war</packaging>
<name>mkyong-wildfly-spring-boot</name>
<description>Spring Boot Web Hibernate Search Example</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<logback.version>1.1.9</logback.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!--
Comment out if deploy as WAR file
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
-->
</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>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-search-engine -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-search-engine</artifactId>
<version>5.6.1.Final</version>
<exclusions>
<exclusion>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate/hibernate-search-orm -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-search-orm</artifactId>
<version>5.6.1.Final</version>
<exclusions>
<exclusion>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.3.0.Final</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>WAR</layout>
<executable>true</executable>
<mainClass>com.mkyong.WildflySpringBootApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2. Model
For this example application, we are creating a website that allows you to search for rare baseball cards. So, we make our model the baseball card and annotate what are searchable fields.
package com.mkyong.model;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Indexed;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Indexed
@Entity
public class BaseballCard {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@Field
private String name;
@Field
private String rarityLevel;
@Field
private int year;
//getters n setters
}
3. Repository
Here we have a Spring Data CrudRepository
for the BaseballCard model that will allow us to perform the create and read functions needed.
package com.mkyong.dao;
import org.springframework.data.repository.CrudRepository;
import com.mkyong.model.BaseballCard;
public interface BaseballCardRepository extends CrudRepository<BaseballCard,Long> {
}
4. Hibernate Search
package com.mkyong.service;
import com.mkyong.model.BaseballCard;
import org.apache.lucene.search.Query;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import java.util.List;
@Service
public class HibernateSearchService {
@Autowired
private final EntityManager centityManager;
@Autowired
public HibernateSearchService(EntityManager entityManager) {
super();
this.centityManager = entityManager;
}
public void initializeHibernateSearch() {
try {
FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(centityManager);
fullTextEntityManager.createIndexer().startAndWait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Transactional
public List<BaseballCard> fuzzySearch(String searchTerm) {
FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(centityManager);
QueryBuilder qb = fullTextEntityManager.getSearchFactory().buildQueryBuilder().forEntity(BaseballCard.class).get();
Query luceneQuery = qb.keyword().fuzzy().withEditDistanceUpTo(1).withPrefixLength(1).onFields("name")
.matching(searchTerm).createQuery();
javax.persistence.Query jpaQuery = fullTextEntityManager.createFullTextQuery(luceneQuery, BaseballCard.class);
// execute search
List<BaseballCard> BaseballCardList = null;
try {
BaseballCardList = jpaQuery.getResultList();
} catch (NoResultException nre) {
;// do nothing
}
return BaseballCardList;
}
}
This configures the HibernateSearchService
to be accessible.
package com.mkyong;
import javax.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.mkyong.service.HibernateSearchService;
@EnableAutoConfiguration
@Configuration
public class HibernateSearchConfiguration {
@Autowired
private EntityManager bentityManager;
@Bean
HibernateSearchService hibernateSearchService() {
HibernateSearchService hibernateSearchService = new HibernateSearchService(bentityManager);
hibernateSearchService.initializeHibernateSearch();
return hibernateSearchService;
}
}
5. Service for Model
This is a simple service that adds three cards to the repository for our example. For demonstration of SOLID programming principles, there is a separate interface for the service.
package com.mkyong.service;
public interface CardService {
void addCards();
}
package com.mkyong.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.mkyong.dao.BaseballCardRepository;
import com.mkyong.model.BaseballCard;
@Service
public class CardServiceImpl implements CardService {
@Autowired
BaseballCardRepository cardrepository;
BaseballCard TedWilliams = new BaseballCard();
BaseballCard BobGibson = new BaseballCard();
BaseballCard HonusWagner = new BaseballCard();
public void addCards() {
TedWilliams.setName("Ted Williams");
TedWilliams.setYear(1954);
TedWilliams.setRarityLevel("Very Rare");
cardrepository.save(TedWilliams);
BobGibson.setName("Bob Gibson");
BobGibson.setYear(1959);
BobGibson.setRarityLevel("Very Rare");
cardrepository.save(BobGibson);
HonusWagner.setName("Honus Wagner");
HonusWagner.setYear(1909);
HonusWagner.setRarityLevel("Rarest");
cardrepository.save(HonusWagner);
System.out.println("Cards have been added : " + cardrepository.findAll());
}
}
6. Controller
The controller is responsible for connecting the backend services to our front end Thymeleaf template.
package com.mkyong.controller;
import com.mkyong.model.BaseballCard;
import com.mkyong.service.CardService;
import com.mkyong.service.HibernateSearchService;
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.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@Controller
public class CardController {
@Autowired
private HibernateSearchService searchservice;
@Autowired
private CardService cardservice;
@RequestMapping(value = "/", method = RequestMethod.GET)
public String search(@RequestParam(value = "search", required = false) String q, Model model) {
List<BaseballCard> searchResults = null;
try {
cardservice.addCards();
searchResults = searchservice.fuzzySearch(q);
} catch (Exception ex) {
// here you should handle unexpected errors
// ...
// throw ex;
}
model.addAttribute("search", searchResults);
return "index";
}
}
7. Configuration
Now we need to configure our Thymeleaf and Hibernate Search.
#==================================
# = Thymeleaf configurations
#==================================
spring.thymeleaf.check-template-location=true
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.content-type=text/html
spring.thymeleaf.cache=false
server.contextPath=/
#===================================
#=
# Specify the Lucene Directory
spring.jpa.properties.hibernate.search.default.directory_provider = filesystem
# Using the filesystem DirectoryProvider you also have to specify the default
# base directory for all indexes
spring.jpa.properties.hibernate.search.default.indexBase = indexpath
8. Thymeleaf Template
For our Thymeleaf template, we have two purposes: allow the user to search and display the search results once a search is complete. Thankfully, Thymeleaf has conditional statements that allow us to display results if a user’s search returns items.
<!DOCTYPE html>
<html lang="en">
<head>
<!-- META SECTION -->
<title>Mkyong Wildfly Example</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<!-- END META SECTION -->
<!-- BEGIN STYLE -->
<style>
table, th, td {
border: 1px solid black;
padding: 1px;
}
</style>
<!-- END STYLE -->
</head>
<body>
<form action="#" th:action="@{/}" th:object="${search}">
<label for="search_input">Search:</label> <input name="search"
id="search">
</input>
<div th:if="${not #lists.isEmpty(search)}">
<h2>Search Results</h2>
<table>
<thead>
<tr>
<th>ID</th>
<th>name</th>
<th>rarity level</th>
<th>year</th>
</tr>
</thead>
<tbody>
<tr th:each="search : ${search}">
<td th:text="${search.id}">Text ...</td>
<td th:text="${search.name}">Text ...</td>
<td th:text="${search.rarityLevel}">Text ...</td>
<td th:text="${search.year}">Text...</td>
</tr>
</tbody>
</table>
</div>
</form>
</body>
</html>
9. SpringBootApplication
package com.mkyong;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class WildflySpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(WildflySpringBootApplication.class, args);
}
}
10. Demo
Start Spring Boot Application with the default embedded Tomcat container.
$ mvn spring-boot:run
Access http://localhost:8080/
I’ve searched “Ted” and the results are displayed in the table, http://localhost:8080/?search=ted
11. Deploy WAR file to Wildfly
The Wildfly is a JBoss open-source application server. Wildfly can be downloaded from their official website
Wildfly 10 comes with:
ActiveMQ Artemis, HA Singleton Deployments, HA Singleton Message Driven Beans (MDBs) and MDB Delivery Groups, Stateless Session Bean and Message Driven Bean Automatic Pool Sizing, Hibernate 5
Wildfly 8 and 9:
These versions do not come with the technologies built-in that Wildfly 10 has. This means we will not need to make exclusions in their configuration file.
11.1 Exclude the embedded Tomcat container.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
11.2 Extends SpringBootServletInitializer
package com.mkyong;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
@SpringBootApplication
public class WildflySpringBootApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(WildflySpringBootApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(WildflySpringBootApplication.class, args);
}
}
11.3 Maven build and copy the WAR file to JBoss Wildfly and start it.
$ mvn package
11.4 Considerations for Wildfly 10
For Wildfly 10 we need to make additional changes because of the technologies that come prepackaged with it. In our case, we are using Hibernate Search and that is packaged in Wildfly 10.
For users of Wildfly 10, you need to create a persistence.xml
file and enter:
wildfly.jpa.hibernate.search.module = none
Done.
Hi,
Anyone can tell how pagination is done here???
Then why u don’t show how to carry input data “Ted” to show like “search=Ted”.This is the most important part.
Thanks for the article.
I am getting BeanCreationException exception. can someone help me out here?
Error starting ApplicationContext. To display the conditions report re-run your application with ‘debug’ enabled.
2019-09-07 14:12:28.057 ERROR 54460 — [ main] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘hibernateSearchConfiguration’: Injection of autowired dependencies failed; nested exception is java.lang.NoSuchFieldError: session
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:380) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1411) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:592) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:843) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877) ~[spring-context-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549) ~[spring-context-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142) ~[spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) [spring-boot-2.1.5.RELEASE.jar:2.1.5.RELEASE]
at com.deals.warangaldeals.WarangaldealsApplication.main(WarangaldealsApplication.java:14) [classes/:na]
Caused by: java.lang.NoSuchFieldError: session
at org.hibernate.search.impl.FullTextSessionImpl.getSearchIntegrator(FullTextSessionImpl.java:193) ~[hibernate-search-orm-5.6.1.Final.jar:5.6.1.Final]
at org.hibernate.search.impl.FullTextSessionImpl.createMassIndexerFactory(FullTextSessionImpl.java:205) ~[hibernate-search-orm-5.6.1.Final.jar:5.6.1.Final]
at org.hibernate.search.impl.FullTextSessionImpl.createIndexer(FullTextSessionImpl.java:175) ~[hibernate-search-orm-5.6.1.Final.jar:5.6.1.Final]
at org.hibernate.search.jpa.impl.FullTextEntityManagerImpl.createIndexer(FullTextEntityManagerImpl.java:340) ~[hibernate-search-orm-5.6.1.Final.jar:5.6.1.Final]
at com.deals.warangaldeas.search.HibernateSearchService.initializeHibernateSearch(HibernateSearchService.java:34) ~[classes/:na]
at com.deals.warangaldeals.HibernateSearchConfiguration.hibernateSearchService(HibernateSearchConfiguration.java:20) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_161]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_161]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_161]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_161]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredMethodElement.inject(AutowiredAnnotationBeanPostProcessor.java:708) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:90) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:374) ~[spring-beans-5.1.7.RELEASE.jar:5.1.7.RELEASE]
… 17 common frames omitted
Great article. One question, why do you do both field and constructor injection for centityManager in HibernateSearchService?
Hi,
This page is so important to follow this tutorial : http://hibernate.org/search/releases/ (about problem compatibilty – I had many problems)
btw the pom dependency hibernate-search-engine is useless, already included in hibernate-search-orm
BTW nice article, clear and precise 🙂
Thank you
Hi Michael,
One of the well explained article. Keep up good work.
Thanks.
Thanks, great article.