Jiří Hradil blog

o software


Hibernate Search – fulltext nad Hibernate ORM

Pokud chceme do naší Java aplikace integrovat fulltextové vyhledávání, dříve či později skončíme u Apache Lucene, frameworku, který je pro vyhledávání v Javě v podstatě standardem a synonymem. Pokud se rozhodneme používat čistý Lucene, musíme mj. řešit:

  • mapování doménových objektů do Lucene (v Lucene jsou všechny atributy obyčejné řetězce)
  • mapování výsledků vyhledávání zpátky do doménových objektů
  • transakce
  • otevírání a zavírání indexu
  • zamykání indexu, v 1 okamžiku může do indexu zapisovat pouze 1 proces
  • škálování fulltextu přes více serverů

Jestliže se však držíme zavedených řešení a používáme Hibernate (ať už Hibernate Core nebo Hibernate jako JPA providera), můžeme využít rozšíření Hibernate Search.
Hibernate Search je projekt, který Hibernate Core doplňuje o fulltextové prohledávání persistovaných objektů a funguje jako fasáda nad Lucene.

Hibernate Search nabízí:

  • API nad fulltextem podobné JPA
  • odstínění od low-level objektů Lucene-nestaráme se vůbec o otevření/uzavření indexu, o zamykání indexu 1 procesem, atd.
  • fulltextové vyhledávání nad persistovanými objekty pomocí Lucene tříd a objektů
  • automatický převod objektů do indexu a zpět (pokud nevyhovuje/nepostačuje, lze napsat převodníky)
  • automatickou synchronizaci indexu s doménovými objekty
  • reindexaci již persistovaných objektů
  • výsledky vyhledávání jsou kompletní objekty, Hibernate Search vrátí na dotaz seznam vyhovujících ID a Hibernate pak načte z db objekty s těmito ID
  • transakční chování – zápis do fulltext indexu se provádí až nakonec po commitu změn do db
  • škálování jako master/slave pomocí JMS
  • jednoduchost, pokud známe Lucene a Hibernate, můžeme jej začít používat okamžitě.

Ukázková aplikace

Pro praktickou ukázku integrujeme fulltextové vyhledávání do jednoduché webové aplikace pro správu firem (klasická create-read-update aplikace).

Použitý stack

  • Hibernate Entity Manager 3.4.0.GA - persistence
  • Hibernate Search 3.1.0.GA – fulltext
  • Apache Maven 2.0.9 – project management
  • Apache Wicket 1.3.5 – prezentační vrstva
  • Spring 2.5.5 – lepidlo
  • PostgreSQL 8.2-relační databáze

Poznámka k následujícím zdrojovým kódům: uvádím pouze soubory, které se bezprostředně týkají Hibernate Search, kompletní zdrojáky aplikace jsou na konci příspěvku.

Závislosti

pom.xml

<?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/maven-v4_0_0.xsd">

 <modelVersion>4.0.0</modelVersion>
 <groupId>org.hradil</groupId>
 <artifactId>HibernateSearchSimpleApplication</artifactId>
<packaging>war</packaging>
 <version>1.0</version>
 <name>Hibernate Search Simple Application</name>

 <dependencies>

 <!--  wicket -->
 <dependency>
 <groupId>org.apache.wicket</groupId>
 <artifactId>wicket</artifactId>
 <version>1.3.5</version>
 </dependency>

 <!-- wicket+spring -->
 <dependency>
 <groupId>org.apache.wicket</groupId>
 <artifactId>wicket-spring-annot</artifactId>
 <version>1.3.5</version>
 </dependency>

 <dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-entitymanager</artifactId>
 <version>3.4.0.GA</version>
 </dependency>

 <dependency>
 <artifactId>hibernate-annotations</artifactId>
 <groupId>org.hibernate</groupId>
 <version>3.4.0.GA</version>
 </dependency>

 <!-- Hibernate Search -->
 <dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-search</artifactId>
 <version>3.1.0.GA</version>
 </dependency>

 <dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring</artifactId>
 <version>2.5.5</version>
 <exclusions>
 <exclusion>
 <groupId>commons-logging</groupId>
 <artifactId>commons-logging</artifactId>
 </exclusion>
 </exclusions>
 </dependency>

 <!-- LOGGING DEPENDENCIES - LOG4J -->
 <dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-api</artifactId>
 <version>1.5.2</version>
 </dependency>

 <dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-log4j12</artifactId>
 <version>1.5.2</version>
 </dependency>

 <!-- work around for jetty commons logging issue -->
 <dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>jcl-over-slf4j</artifactId>
 <version>1.5.2</version>
 </dependency>

 <dependency>
 <groupId>c3p0</groupId>
 <artifactId>c3p0</artifactId>
 <version>0.9.1.2</version>
 </dependency>

 <dependency>
 <groupId>postgresql</groupId>
 <artifactId>postgresql</artifactId>
 <version>8.2-507.jdbc3</version>
 <scope>compile</scope>
 </dependency>

 <!--  JUNIT DEPENDENCY FOR TESTING -->
 <dependency>
 <groupId>junit</groupId>
 <artifactId>junit</artifactId>
 <version>3.8.2</version>
 <scope>test</scope>
 </dependency>

 <!--  JETTY DEPENDENCIES FOR TESTING  -->

 <dependency>
 <groupId>org.mortbay.jetty</groupId>
 <artifactId>jetty</artifactId>
 <version>6.1.4</version>
 <scope>provided</scope>
 </dependency>

 </dependencies>

 <build>
 <resources>
 <resource>
 <directory>src/main/resources</directory>
 </resource>
 <resource>
 <directory>src/main/java</directory>
 <includes>
 <include>**</include>
 </includes>
 <excludes>
 <exclude>**/*.java</exclude>
 </excludes>
 </resource>
 </resources>
 <testResources>
 <testResource>
 <directory>src/test/java</directory>
 <includes>
 <include>**</include>
 </includes>
 <excludes>
 <exclude>**/*.java</exclude>
 </excludes>
 </testResource>
 </testResources>
<plugins>
<plugin>
 <groupId>org.mortbay.jetty</groupId>
 <artifactId>maven-jetty-plugin</artifactId>

 <configuration>
 <scanIntervalSeconds>5</scanIntervalSeconds>
 <contextPath>/</contextPath>
 </configuration>

 </plugin>
<plugin>
 <artifactId>maven-compiler-plugin</artifactId>
 <version>2.0.2</version>
 <configuration>
 <source>1.6</source>
 <target>1.6</target>
 </configuration>
 </plugin>

 </plugins>
 </build>

</project>

Konfigurace

applicationContext-jpa.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 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-2.5.xsd
 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"
 >

 <description>
 Konfigurace kontextu pro JPA, vcetne datovych zdroju a transakcnich manazeru.
 </description>

 <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
 <description>
 Datovy zdroj pro persistentni vrstvu. Obsahuje udaje o pripojeni k databazi.
 </description>
<property name="driverClass" value="org.postgresql.Driver"/>
<property name="jdbcUrl" value="jdbc:postgresql://localhost/HibernateSearchSimple"/>
<property name="user" value="postgres"/>
<property name="password" value=""/>
 </bean>

 <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
 <description>
 Tovarna pro manazer entit. Je pouzita trida LocalContainerEntityManagerFactoryBean,
 ktera je doporucena pro produkcni nasazeni JPA.
 Viz. http://static.springframework.org/spring/docs/2.5.x/reference/orm.html
 </description>
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
 <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="databasePlatform" value="org.hibernate.dialect.PostgreSQLDialect" />
<property name="generateDdl" value="false"/>
 </bean>
 </property>

 <!-- nastaveni JPA a Hibernate Search -->
<property name="jpaProperties">

 <value>

 # konfigurace JPA
 hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
 hibernate.hbm2ddl.auto=validate

 # debugging / logging
 hibernate.show_sql=true
 hibernate.format_sql=true
 hibernate.use_sql_comments=true

 # konfigurace Hibernate Search
 # kde bude ulozen Lucene index
 hibernate.search.default.indexBase=/tmp/index

 </value>

 </property>
 </bean>

 <tx:annotation-driven transaction-manager="transactionManager"/>

 <!-- transakcni manazer -->
 <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
 </bean>

</beans>

Entita

Company.java

package org.hradil.search.entity;

import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import org.hibernate.search.annotations.DocumentId;
import org.hibernate.search.annotations.Field;
import org.hibernate.search.annotations.Indexed;

/**
 * Entita reprezentuje firmu.
 * Firmu lze ve fulltextu vyhledat podle id, name a regNo.
 *
 * @author jirka@hradil.org
 */
@Entity
@Table(name = "company")
@Indexed //tridu budeme chtit indexovat ve fulltextu
public class Company implements Serializable {

 private static final long serialVersionUID = 1216348069826762176L;

 @Id
 @Column(name = "id")
 @SequenceGenerator(name = "company_id_seq", sequenceName = "company_id_seq")
 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "company_id_seq")
 @DocumentId //primarni klic objektu ve fulltextu, zaroven podle nej muzeme vyhledavat
 private int id;

 @Column
 @Field //ve fulltextu chceme hledat firmu podle nazvu
 private String name;

 @Column
 @Field //ve fulltextu chceme hledat firmu podle IC
 private String regNo;

 /**
 * Vytvori novou firmu.
 */
 public Company() {
 }

 /**
 * Vytvori novou firmu a predvyplni atributy.
 * @param name nazev firmy
 * @param regNo IC
 */
 public Company(final String name, final String regNo) {
 this.name = name;
 this.regNo = regNo;
 }

 /**
 * Vrati id firmy
 * @return id
 */
 public int getId() {
 return id;
 }

 /**
 * Nastavi id firmy
 * @param id id
 */
 public void setId(int id) {
 this.id = id;
 }

 /**
 * Vrati nazev firmy.
 * @return nazev
 */
 public String getName() {
 return name;
 }

 /**
 * Nastavi nazev firmy.
 * @param name nazev
 */
 public void setName(String name) {
 this.name = name;
 }

 /**
 * Vrati IC firmy.
 * @return IC
 */
 public String getRegNo() {
 return regNo;
 }

 /**
 * Nastavi IC firmy.
 * @param regNo IC
 */
 public void setRegNo(String regNo) {
 this.regNo = regNo;
 }
}

Servisní vrstva - uložení, úprava, vyhledání

CompanyServiceImpl.java

package org.hradil.search.service;

import org.hradil.search.entity.Company;

import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.queryParser.MultiFieldQueryParser;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.MatchAllDocsQuery;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.hibernate.search.jpa.FullTextQuery;
import org.hibernate.search.jpa.Search;
import org.hibernate.search.jpa.FullTextEntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * Implementace sluzby pro firmu.
 *
 * @author jirka@hradil.org
 */
@Transactional(propagation = Propagation.REQUIRED)
@Service
public class CompanyServiceImpl implements CompanyService {

 @PersistenceContext
 private EntityManager em;

 /**
 * {@inheritDoc}
 */
 @Override
 public void addCompany(Company newCompany) {
 em.persist(newCompany);
 }

 /**
 * {@inheritDoc}
 */
 @Override
 @SuppressWarnings("unchecked")
 @Transactional(readOnly = true)
 public List<Company> findCompanyBy(final String fulltextQuery) {

 Assert.notNull(fulltextQuery, "retezec pro vyhledavani nesmi byt null!");

 //vezmeme instanci manazeru fulltextu
 FullTextEntityManager ftEm = Search.getFullTextEntityManager(em);

 //vytvorime parser pro prohledavane atributy firmy, pouzijeme standardni analyzer
 QueryParser parser = new MultiFieldQueryParser(new String[]{"id", "name", "regNo"}, new StandardAnalyzer());

 //vytvorime dotaz do Lucene
 org.apache.lucene.search.Query luceneQuery;

 //pokud po zruseni vsech bilych znaku a hvezdicek zustane jen prazdny retezec, pak vracime vsechny zaznamy
 //napr. dotaz "**** * **  *" bude vyhodnocen tak, ze chceme prohledavat vsechny firmy
 if (StringUtils.trimAllWhitespace(StringUtils.deleteAny(fulltextQuery, "*")).isEmpty()) {
 luceneQuery = new MatchAllDocsQuery();
 } else { //byl zadan retezec, vyhledavame
 try {
 luceneQuery = parser.parse(fulltextQuery); //zparsujeme predany dotaz pomoci parseru Lucene
 } catch (ParseException e) { //neplatny dotaz, prekonverujeme na runtime vyjimku, nemusime zachytavat
 throw new RuntimeException("Neplatny dotaz do fulltextu: " + fulltextQuery, e);
 }
 }

 //vytvorime normalni JPA dotaz, ale pres rozhrani fulltextu
 FullTextQuery query = ftEm.createFullTextQuery(
 luceneQuery,
 Company.class);

 //tridime dle relevance DESC, pote dle id DESC
 SortField[] sortFields = new SortField[2];
 sortFields[0] = SortField.FIELD_SCORE; //relevance, default DESC
 sortFields[1] = new SortField("id", SortField.INT, true); //id, je to intener, DESC=true
 Sort sort = new Sort(sortFields);

 //pridame trideni do dotazu
 query.setSort(sort);

 //a vratime rovnou vyhovujici seznam firem z ORM
 return query.getResultList();

 }

 /**
 * {@inheritDoc}
 */
 @Override
 @SuppressWarnings("unchecked")
 @Transactional(readOnly = true)
 public Company findCompanyBy(int id) {
 return em.find(Company.class, id);
 }

 @Override
 @Transactional(readOnly = false)
 public void updateCompany(Company company) {
 em.merge(company);
 }
}

Zdrojové kódy aplikace

hibernatesearchsimple

Konfiguraci a rozjetí v clusteru si ukážeme v některém dalším příspěvku.

Publikoval Jiří Hradil • 10.05.2009 v 22:05 • pod kategorií hibernatesearch1 komentář