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.

Přidat komentář

Security Code: