Lucene en Hibernate search

August 29th, 2012 | 9 min read | JPA, Lucene, Spring

In één van de vorige tutorials heb ik uitgelegd hoe je via Hibernate/JPA objecten uit de database kunt halen. In deze tutorial ga ik daar iets verder op indiepen door jullie kennis te laten maken met Hibernate search en Lucene.

Heads up!

In deze tutorial wordt met regelmaat verwezen naar mijn vorige Java tutorials. Het is dus handig dat je deze (zeker de eerste) eens doorneemt.

Lucene is een search engine framework dat eigenlijk toestanden mogelijk maakt zoals “Bedoelde u: appel” als je “oppel” zou typen. Het zorgt ervoor dat je objecten kan vinden op basis van geavanceerde zoekopdrachten.

Hibernate search is een onderdeel van Hibernate dat gebruik maakt van Lucene. Hibernate search zal eigenlijk de operaties die via Lucene mogelijk zijn “wrappen” zodat je ze op eenzelfde manier kan behandelen als gewone Hibernate query’s.
Hibernate search zal er eveneens voor zorgen dat objecten die gepersisteerd worden naar de database, ook aanwezig zijn in de Lucene indexes en zal beide consistent met elkaar houden.

Project opzetten

In dit  voorbeeld gaan we verder gaan op deze tutorial. Download de code, maak de database in orde en hernoem het project indien gewenst. Als je het project hernoemt dan is het ook aangeraden om in je POM file de naam aan te passen.

In diezelfde POM file moeten we ook nog een dependency toevoegen, namelijk voor Hibernate search.

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-search</artifactId>
    <version>3.4.2.Final</version>
</dependency>

Configuratie

Configuratie hebben we ditmaal zo goed als niet, omdat we in vorige tutorial al zoveel gedaan hebben. Wat we wel moeten aanpassen is

persistence.xml

.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
    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_2_0.xsd">
    <persistence-unit name="jpaHibernate"
        transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
            <property name="hibernate.search.default.directory_provider"
                value="org.hibernate.search.store.FSDirectoryProvider" />
            <property name="hibernate.search.default.indexBase" value="./index" />
            <property name="hibernate.search.default.batch.merge_factor"
                value="10" />
            <property name="hibernate.search.default.batch.max_buffered_docs"
                value="10" />
        </properties>
    </persistence-unit>
</persistence>

Aanpassingen entity

Hibernate search maakt gebruik van enkele speciale annotations die we nodig hebben om de juiste velden te indexeren. Open dus de class

UserEntity

en voorzie dus het volgende.

Boven de class zelf moet je volgende annotaties plaatsen:

@Indexed(index = "users")
@Analyzer(impl = org.apache.lucene.analysis.standard.StandardAnalyzer.class)

Boven het veld met de naam

userId

plaats je de annotatie

@DocumentId

. Boven alle andere velden plaats je de annotation

@Field(index=Index.UN_TOKENIZED, store = Store.YES)

.
Omdat een datum een speciaal veld is, moeten we deze ook nog de annotatie

@DateBridge(resolution=Resolution.DAY)

geven zodat er rekening mee wordt gehouden dat we met een datum werken.

Aanpassingen DAO

De meeste aanpassingen zullen natuurlijk gebeuren op het niveau van de DAOs.

GenericDAO interface

We gaan 4 nieuwe methodes toevoegen aan de

GenericDAO

interface. Deze methodes zijn:

   List<T> findByParameter(String field, String value);

List<T> findByWildcard(String field, String value);

List<T> findByFuzzy(String field, String value);

@Transactional(readOnly = false)
void updateIndexes();

Als je de nieuwe code download dan zal je ook merken dat ik overal

public abstract

verwijderd heb. Blijkbaar heeft Eclipse tijdens het genereren van de interface dat erbij geplakt, wat natuurlijk niet hoeft.

De methode

findByParameter

zal zoeken of er een object is waarbij een veld overeenkomt met een bepaalde waarde.
Het verschil met

findByWildcard

is dat in deze methode ook objecten terug gegeven worden die deels matchen. Als je bijvoorbeeld “App” zou ingeven, dan zou “Appel” ook als result getoond worden.
Dan is er nog

findByFuzzy

, deze methode zal foute letters toelaten, als je dus “Oppel” zou invoeren, dan kreeg je ook “Appel” als resultaat.

Ten slotte hebben we nog de methode

updateIndexes

. Zoals ik eerder al zei zal Hibernate search alle objecten die gepersisteerd worden tevens updaten in de Lucene indexes. Het probleem is echter dat objecten die van buitenaf gewijzigd worden, niet gewijzigd worden in de indexes. Om dit probleem op te lossen heb ik deze methode geschreven die ervoor zal zorgen dat de objecten terug consistent zijn in beide opslagmedia.

GenericDaoImpl

Natuurlijk zijn we niet veel met deze interface als we deze niet implementeren.

protected List<T> findAllWithLucene(final Query query) {
    return getHibernateTemplate().execute(new HibernateCallback<List<T>>() {

        @SuppressWarnings("unchecked")
        @Override
        public List<T> doInHibernate(Session session)
                throws HibernateException, SQLException {
            return Search.getFullTextSession(session)
                    .createFullTextQuery(query, getPersistentClass())
                    .list();
        }
    });
}

@Override
public List<T> findByParameter(String field, String value) {
    return findAllWithLucene(new TermQuery(new Term(field, value)));
}

@Override
public List<T> findByWildcard(String field, String value) {
    return findAllWithLucene(new WildcardQuery(new Term(field, "*" + value
            + "*")));
}

@Override
public List<T> findByFuzzy(String field, String value) {
    return findAllWithLucene(new FuzzyQuery(new Term(field, value)));
}

@Override
public void updateIndexes() {
    final List<T> entities = findAll();
    getHibernateTemplate().execute(new HibernateCallback<Object>() {

        @Override
        public Object doInHibernate(Session session) {
            FullTextSession fSess = Search.getFullTextSession(session);

            fSess.purgeAll(getPersistentClass());
            for (T e : entities) {
                fSess.index(e);
            }
            fSess.getSearchFactory().optimize();
            return null;
        }
    });
}

Zoals je kan zien lijken

updateIndexes

en

findAllWithLucene

erg op elkaar. Deze methodes zijn de werkelijke methodes die de Hibernate search API aanspreken en query’s uitvoeren via Lucene.

De methodes

findByParameter

,

findByWildcard

en

findByFuzzy

maken allemaal gebruik van de methode

findAllWithLucene

. Ze voeren enkel en alleen telkens een andere soort query uit.

DAOService

Ook de DAO service moet gewijzigd worden om ruimte te geven aan deze nieuwe methodes. Dit gaan we doen door aan deze interface één methode toe te voegen met name:

UserEntity findUserByFirstName(String firstName);

In deze methode gaat zowel de

findByWildcard

en

findByFuzzy

opgeroepen worden.

DAOServiceImpl

Net zoals bij de

GenericDAO

zijn we weinig met enkel een interface op zich. De implementatie van de methode die hierboven beschreven staat gaan we hier implementeren met de volgende code.

@Override
public UserEntity findUserByFirstName(String firstName) {
    userDao.updateIndexes();
    List<UserEntity> results = userDao.findByWildcard("firstName", firstName);
    if (results.size() > 0) {
        return results.get(0);
    }

    results = userDao.findByFuzzy("firstName", firstName);
    if (results.size() > 0) {
        return results.get(0);
    }

    return null;
}

Zoals je kan zien doet deze methode wat ik voorheen zei, eerst wordt er gecontroleerd of de naam gevonden kan worden, zoniet wordt er gebruik gemaakt van de fuzzy query om fouten in de zoekterm te filteren.

Wat je ook kan zien is dat we de methode

updateIndexes()

oproepen. Ik wil even zeggen dat dit NIET de goede aanpak is als je zou werken met grote databases.
Deze aanpak is louter illustratief en in een volgende tutorial zal ik je tonen hoe je dit beter kan aanpakken.

JSP pagina

Omdat we gaan zoeken naar een user, zullen we natuurlijk ook een extra JSP pagina moeten voorzien met een zoekveld.
De eerste stap is dat we

index.jsp

gaan hernoemen naar

user.jsp

.

Dan gaan we een nieuwe JSP pagina voorzien voor

index.jsp

met de volgende inhoud.

<html>
<body>
    <form method="post" action="index.html">
        <table>
            <tr>
                <td><label>First name:</label></td>
                <td><input type="text" name="firstName" /></td>
            </tr>
            <tr>
                <td colspan="2"><input type="submit" value="Ga" /></td>
            </tr>
        </table>
    </form>
</body>
</html>

Ook

user.jsp

gaan we een klein beetje aanpassen zodat als er geen object gevonden wordt, er “Geen resultaat” op het scherm getoond wordt.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>
<html>
<body>
    <c:choose>
        <c:when test="${not empty user}">
            <h2>${user}</h2>
            <ul>
                <li>Birthday: <fmt:formatDate value="${user.birthDay.time}"
                        type="date" dateStyle="short" /></li>
                <li>Gender: <c:choose>
                        <c:when test="${user.male}">Male</c:when>
                        <c:otherwise>Female</c:otherwise>
                    </c:choose>
                </li>
            </ul>
        </c:when>
        <c:otherwise>
            <h2>Geen resultaten gevonden</h2>
        </c:otherwise>
    </c:choose>
</body>
</html>

Controller

Ten slotte moeten we nog de controller aanpassen zodat deze het zoekveld gaat gebruiken om via de DAO service een object op te zoeken. Verwijder de methode

getIndex

uit

MainController

en maak in plaats daarvan deze methodes aan:

@RequestMapping(value = "/index.html", method = RequestMethod.GET)
public String getIndex() {
    return "index";
}

@RequestMapping(value = "/index.html", method = RequestMethod.POST)
public String postIndex(@RequestParam("firstName") String firstName, ModelMap model) {
    UserEntity entity = service.findUserByFirstName(firstName);
    if (entity != null) {
        model.addAttribute("user", mapper.map(entity, User.class));
    }
    return "user";
}

Alles hiervan zou vrij logisch moeten zijn als je mijn andere tutorials gevolgd hebt. De controle op

null

values bij het

UserEntity

object heb ik geplaatst omdat Dozer niet omweg kan met

null

values en een exception zou geven.

Hiermee zijn we dan ook aan het einde gekomen van deze tutorial, de structuur van het project zou er uit moeten zien zoals in onderstaande afbeelding.

Builden

Builden doen we zoals gewoonlijk met Maven. Eenmaal klaar dan zou je normaal gezien in je browser de index-pagina moeten zien met een form.

Probeer hier allerlei zaken uit zoals “Bill”, “Bi”, “il” en “Boll”. Al deze waardes zouden allemaal de pagina van Bill Gates moeten tonen. Vrij logisch, want “Bill”, “Bi” en “il” zijn allemaal delen die voorkomen in z’n voornaam.
“Boll” daarentegen werkt via de fuzzy query, de fout wordt gecorrigeerd en zo komen we toch nog op de juiste pagina terecht.

Als je iets totaal anders zoals “qjsdk” zou invoeren, zal je merken dat je geen resultaat terug krijgt.

Luke

We kunnen ook de Lucene indexes zelf bekijken. Download hiervoor Luke, een Lucene indexing toolbox. Eenmaal gedownload open je de applicatie en kies je als path de directory

users

in je project map.

Als je dan op OK klikt, dan zal je merken dat je al allerlei bekende zaken tegenkomt. Zo krijg je al een overzicht te zien van alle gebruikte veldnamen zoals de geboortedatum, voornaam, achternaam, … .

Als je dan het tabblad Documents opent en document ID 0 ingeeft, dan zal je de representatie van het ene User object terug vinden.

Download project

Download – hibernate-search-example.rar (12 kB)

Back to tutorialsContact me on TwitterDiscuss on Twitter

Profile picture

Dimitri "g00glen00b" Mestdagh is a consultant at Cronos and tech lead at Aquafin. Usually you can find him trying out new libraries and technologies. Loves both Java and JavaScript.