1 Introduction Hibernate Search enhances the ORM model by introducing search extensions, and among the 7 main modules of Hibernate Suite.This module has an inbuilt integration with Apache Lucene for execution of free-text search support, thus bringing power of full text search engines to a persistence domain model. Hibernate Search has direct dependencies on Hibernate Core and Apache Lucene, and optional dependencies with Hibernate Annotations (if domain model is annotated) and Hibernate EntityManager (if JPA is used) and Apache Solr, for analyzer definitions This tutorial discusses a simple book-reviewer search capability using Hibernate Search with JPA support, on a MySQL database and TestNG framework for testing search features. The definitions and usage reasoning is explained accordingly as the tutorial unravels. 2 Pre-requistes: The reader is assumed to know basics of various entity level Hibernate Annotations and basics on JPA oriented EntityManager. At the end of the tutorial, the reader could understand various features exposed by the Hibernate Search along with basic annotation constructs to be used on entity for index-enabling the fields. Following are the other requirements: 1) Access to MySQL database 2) Maven2 and JDK 5 installed. 3) IDE such as Eclipse or Netbeans.
3 About the example This example explores searching options among a list of books, along with reviews. The book can have multiple review comments, along with the review names. All the entity's attributes should be search-enabled and the entities should be index-enabled.
You may download the example code here.
4 Steps of execution 4.1 Creating a maven project Module Assumptions Ensure maven2 is installed and is of latest build. Also ensure the MySQL database is created and is accessible. Module Expectations At the end of this step, the project is setup using Maven and one can start plumbing the entitymanager framewire and implementing the domain model, with test cases development.
Steps 1) Create a simple quickstart maven project, removing the default generated sources.
mvn archetype:create -DgroupId=<your.group.id> -DartifactId=<your.artifact.id> -DarchetypeArtifactId=maven-archetype-quickstart
2) Modify the pom.xml contained in the project's root folder, hetherito refered as ${basedir} a) Add following dependencies
<!-- Hibernate-Search with its main/optional dependencies --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate</artifactId> <version>3.2.6.ga</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-search</artifactId> <version>3.1.0.GA</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>3.3.2.GA</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-annotations</artifactId> <version>3.3.1.GA</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.5.8</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.5.8</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.14</version> </dependency>
<!-- Required for some utility class references for toString/hashcode --> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.4</version> </dependency>
<!-- To resolve database drivers currently under use--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency>
b) Add following plugin definitions under build/plugins
<!-- Compiler Plugin (support for JDK.5)--> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.5</source> <target>1.5</target> </configuration> </plugin>
This completes the maven setup required for the project. Lets us now start adding the actual crux of the logic and build a simple framework for getting-to-know-how a simple Hibernate Search application.
4.2 Deriving a data-model As given in section 3, there are 2 main entities to be derived: Book and Review. Book will have a title, its published date and number of pages, along with list of Review entities. A Review representing a book review has a reviewing person and a comment. Below contains the code snippets of the both POJOs (methods have been omitted for simplicity sake)
@Entity @Table(name="Books") @Indexed(index="indexes/book") public class Book implements Serializable { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Integer bookId;
@Column(nullable=false) @Field private String title; @Column(name="page_count") @Field(index=Index.UN_TOKENIZED) private Integer numberOfPages; @Column(name="publish_date") @Field (index=Index.UN_TOKENIZED) @DateBridge(resolution=Resolution.MONTH) private Date publishedDate;
@OneToMany(mappedBy="book") private Set reviews = new HashSet(0); }
@Entity @Table(name = "reviews") @Indexed(index="indexes/review") public class Review implements Serializable { @ManyToOne @JoinColumn(name = "bookId") @IndexedEmbedded private Book book; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer reviewId; @Column(nullable = false) private String comment; @Column(nullable = false) @Field private String reviewer; }
Leaving out the Hibernate core annotations, let us try and understand some of the Hibernate Search annotations
* Indexed - marks a entity as a Indexable, and gives an index name. * Field - Marks a column as searchable. One can customize to tokenize the result by the analyzer and excluding common terms. * IndexEmbedded - Refers to a parent reference key from which the link reference is derived. * DateBridge - Refers to the search pattern to use on Date. By default Lucene search is on string literal values. Other bridges include EnumBridge, FieldBridge etc.
There is one more annotation DocumentId (omitted post Hibernate Search 3.1.0+) which can be used to mark a field that index is based. The default field is the @Id column (primary-key).
4.3) Creating a simple JPA framework with DAO layer In this step, we will identify abstractions of persistence store interaction with regard to search results retreival and indexing. As this example also stores few books along with reviews, we need an abstraction which would model the starting transaction, commiting and rolling back a transaction. Below is such an abstraction snippet
public interface EntityTransaction { void init(String persitenceUnit); public void openTransaction(); public void commitTransaction(); public void rollbackTransaction(); public void close(); }
We would create one abstraction for search results retreival
import org.hibernate.search.jpa.FullTextEntityManager; public interface EntitySearch { public FullTextEntityManager getTextEntityManager(); }
Having defined the persistence layer abstractions, lets define the abstractions for the persistence of Books. For the search functionality this abstraction is optional as coupling with Hibernate Search is not functional entity driven.
import domain.Book;
public interface BookManager extends EntityTransaction, EntitySearch { public void saveBook(Book book); }
4.4 Defining persistence meta-configuration. As we plan to integrate JPA with Hibernate-Search, we need to define ${basedir}/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0"> <persistence-unit name="hibersearchdb" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <jar-file>file:target/classes</jar-file> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/> <property name="hibernate.hbm2ddl.auto" value="update"/> <property name="hibernate.show_sql" value="false"/> <property name="hibernate.format_sql" value="false"/> <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" /> <property name="hibernate.connection.url" value="jdbc:mysql://myhost:3306/mydb?autoReconnect=true" /> <property name="hibernate.connection.username" value="myuser" /> <property name="hibernate.connection.password" value="myu53r" /> <property name="hibernate.search.default.directory_provider" value="org.hibernate.search.store.FSDirectoryProvider" /> <property name="hibernate.search.default.indexBase" value="target"/> </properties> </persistence-unit> </persistence>
Few notable points here are: 1) persistence-unit name should correspond to the parameter name that you pass in the EntityTransaction's init(); 2) A property 'hibernate.search.default.directory_provider' specifies the Index Directory Provider provided by Hibernate Search. There are 2 sets of directory providers from Hibernate Search, which correspond to filesystem and in-memory. Lucene stores indexes based on the provider implementation. 3) Another property 'hibernate.search.default.indexBase' specifies the directory where indices are to be stored for search results.
4.5 Defining a test bed Defining test cases using TestNG is simple as test case execution can be tangled in defined life-cycle events driven by annotations on the methods. Following are the dependencies to be included in the ${basedir}/pom.xml for test dependencies.
<dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>5.7</version> <scope>test</scope> <classifier>jdk15</classifier> </dependency> <!-- Required for assertions --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.4</version> <scope>test</scope> </dependency>
Also include the following plugin for TestNG maven configuration under build/plugins.
<!-- Surefire plugin for TestNG configuration --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <suiteXmlFiles> <suiteXmlFile>src/test/resources/testng.xml</suiteXmlFile> </suiteXmlFiles> <parallel>true</parallel> <threadCount>10</threadCount> </configuration> </plugin>
Below is a wireframe of how a test-bed could be implemented
public class BookSearchTest { Logger log = Logger.getLogger(BookSearchTest.class.getName()); private BookManager bookManager;
@BeforeSuite public void init() { bookManager = new BookManagerImpl(); bookManager.init("hibersearchdb"); }
@BeforeTest public void createSomeBooks() { Random random = new Random(); Object[] titles = EnumSet.allOf(BookTitle.class).toArray(); Object[] reviewers = EnumSet.allOf(Reviewers.class).toArray(); bookManager.openTransaction(); for (int t = 0; t < 20; ++t) { Book book = new Book(); book.setNumberOfPages(random.nextInt(400)); book.setTitle("Mastering " + titles[random.nextInt(titles.length)]); book.setPublishedDate(new Date(System.currentTimeMillis()));
for (int x = 0; x < 4; ++x) { Review review = new Review(); review.setComment("blah blah blah..." + Math.random()); review.setReviewer(String.valueOf(reviewers[random.nextInt(reviewers.length)])); book.addReview(review); } bookManager.saveBook(book); } bookManager.commitTransaction(); }
@AfterSuite public void closeSearchActivity() { bookManager.close(); }
@SuppressWarnings("unchecked") @Test public void testBookByTitleOrReviewByReviewerSearch() { FullTextEntityManager textEntityManager = bookManager.getTextEntityManager(); QueryParser parser = new QueryParser("title", new StandardAnalyzer()); Query lucenceQuery = null; try { lucenceQuery = parser.parse("title:" + BookTitle.HIBERNATE + " or reviewer:" + Reviewers.RMOTWANI); } catch (ParseException e) { throw new RuntimeException("Cannot search with query string", e); } FullTextQuery query = textEntityManager.createFullTextQuery(lucenceQuery); List records = query.getResultList(); log.info("Found "+records.size()+" records with books of title "+BookTitle.HIBERNATE+" and reviewers by name: "+Reviewers.RMOTWANI); for (Object record : records) { assertTrue((record instanceof Book) || (record instanceof Review)); } } }
Few notable points here are * init() with @BeforeSuite initializes the BookManager abstraction with the datasource. * createSomeBooks() @BeforeTest creates some books along with reviews for testing search functionality. * closeSearchActivity() @AfterSuite finalizes the persistence layer resources. * testBookByTitleOrReviewByReviewerSearch() tests a search condition on OR of two separate conditions. The testing configuration for TestNG is given below
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="hibernateSearch" parallel="tests"> <test name="bookSearch"> <classes> <class name="com.ts.hibersearch.test.BookSearchTest" /> </classes> </test> </suite>
Having all this setup, the user can try testing the hibernate search for book review function from ${basedir}
mvn clean test
The user can test search for different conditions, and enhancements to the functionality provided. 5 Conclusions Hibernate search provides extensive function point for search integration for Hibernate including analyzers and indexers. The back end integration can be JMS apart from Lucene and supports both synchronous/asynchronous indexing strategies out of the box.
|