Alle Beiträge, Technologien & Entwicklung

Hibernate beyond OR-Mapping: Hibernate Search

Hibernate ist als OR-Mapping-Framework seit Jahren fest in der Java-Welt (und darüber hinaus, siehe NHibernate ) etabliert. Weniger bekannt sind allerdings die zusätzlich enthaltenen Module, mit denen Hibernate dem Entwickler auch über die OR-Mapping-Thematik hinaus das Leben einfacher machen kann.

Eines dieser Module ist Hibernate Search, das im jStage Backendsystem “Stagemanager” bereits für eine Vielzahl an Suchen verwendet wird. Es setzt auf das objekt-relationale Modell von Hibernate auf und erweitert es um Funktionen zur Implementierung von Volltextsuchen. Unter der Haube steckt dabei die mächtige Such-Bibliothek Lucene. Die Verwendung von Hibernate Search setzt zwar ein gewisses Grundverständnis der Funktionsweise von Lucene voraus, entbindet den Entwickler  aber größtenteils von der Low-Level-Programmierung auf Lucene-Ebene. Sowohl bei der Entwicklung der Indexierung als auch der eigentlichen Suche ergeben sich so signifikante Produktivitätsvorteile gegenüber einer rein auf Lucene basierenden Implementierung.

Konfiguration

Sehen wir uns zunächst die Definition eines Suchindex in Hibernate Search an. Zunächst müssen wir unser Hibernate Properties File, bzw. unter JPA unsere persistence.xml, um die folgenden Angaben ergänzen.

In der ersten Property definieren wir, dass wir unsere Indizes in einem Dateisystem ablegen wollen. Die zweite Property legt den Pfad zum Indexverzeichnis fest. Damit haben wir die technische Basis definiert und können zur Strukturierung der Suchindizes übergehen.

Indexierung

Um einen Suchindex aufzubauen versehen wir die zu suchenden Entity Beans mit zusätzlichen Annotationen, wie in folgendem Beispiel zu sehen ist.

Indexierte Entities werden auf Klassenebene mit der Annotation @Indexed gekennzeichnet. Jede so gekennzeichnete Entität wird auf ein Indexverzeichnis im Dateisystem abgebildet. Das Id-Feld wird mit @DocumentId annotiert. Die Annotation @Field kennzeichnet die zu indexierenden Properties. Standardmäßig wird jede so gekennzeichnete Property auf ein gleichnamiges Indexfeld gemappt. Es ist aber auch möglich, mehrere Properties in einem Indexfeld zusammenzuführen oder den Inhalt einer Property auf mehrere Indexfelder aufzuteilen.

Wichtig ist auch die Annotation @Analyzer. Der Analyzer ist eine Lucene-Klasse, die dafür zuständig ist, den Text der indexierten Felder in die einzelnen Worte zu zerlegen und weitere Transformationsprozesse auszuführen, welche den Text für die Suche optimieren. Dazu gehört z. B. die einheitliche Kleinschreibung und das Entfernen von Satzzeichen. Lucene liefert Analyzer für unterschiedliche Bedürfnisse mit. Es ist auch möglich, eigene Analyzer zu implementieren und einzubinden.

Die Annotation @IndexedEmbedded ermöglicht es, Felder assoziierter Entity Beans mit in den Suchindex der referenzierenden Entität aufzunehmen. Dazu müssen wir auch in der referenzierten Entität die entsprechenden Felder per @Field kennzeichnen. Auf @Indexed und @DocumentId können wir hier verzichten, da wir für ItemType keinen eigenen Index aufbauen wollen, sondern lediglich die Property name in den Index von Item aufnehmen wollen.

Der Index für Item könnte dann mit Testdaten wie folgt aussehen.

id itemNumber name ItemType.name
1 0001 iphone 6 smartphone
2 0002 blade mach 25 fpv quadcopter
3 0003 blade runner dvd

Alternativ wäre es auch möglich, für ItemType einen eigenen Index zu definieren und bei der Suche beide Indizes abzufragen. Man muss anhand der Anforderungen an die Suche von Fall zu Fall abwägen, welcher Weg der passendere ist.

Sobald alle Indexierungsparameter gesetzt sind, kümmert sich Hibernate Search automatisch um die Aktualisierung des Index. Alle über Hibernate erfolgen Insert- und Update-Operationen werden innerhalb der gleichen Transaktion auch auf dem Index ausgeführt, der so immer aktuell gehalten wird. Falls diese transparente Indexierung nicht gewünscht wird, kann sie auch deaktiviert werden. Die Indexaktualisierung muss dann selbst mithilfe der angebotenen API-Methoden implementiert werden. Diese manuelle Indexierung ist auch nötig, um einen bestehenden Datenbestand erstmalig zu indexieren.

Die Index-Konfiguration über Annotations ist zwar bequem, kann aber in manchen Fällen Probleme machen. Denkbar wäre z. B. eine Konstellation, bei der die Software an verschiedene Kunden ausgeliefert wird, die aber unterschiedliche Suchkonfigurationen bevorzugen. Daher bietet Hibernate Search auch eine programmatische API zum Mapping der indexierten Entities an.

Suche

Kommen wir nun zu der Stelle, wo für den Endanwender “die Musik spielt”, nämlich zur eigentlichen Suche. Während wir bei der transparenten Indexierung nicht direkt mit dem Index zu interagieren brauchen, benötigen wir hier eine Schnittstelle um den Index abzufragen.

Hibernate Search bietet gleich zwei solcher Interfaces: FullTextSession und FullTextEntityManager. FullTextSession ist von org.hibernate.Session abgeleitet, FullTextEntityManager von javax.persistence.EntityManager. Wenn man JPA-konform bleiben will, wird man den FullTextEntityManager verwenden, was auch in den folgenden Beispielen passiert. Die dort gezeigten Grundprinzipien gelten aber genauso für das Arbeiten mit der FullTextSession.

Sehen wir uns zunächst an, wie ein FullTextEntityManager erzeugt wird. Wir benutzen dazu eine Factory-Methode der Klasse org.hibernate.search.jpa.Search und übergeben ihr einen bereits initialisierten EntityManager.

Der zurückgelieferte FullTextEntityManager bietet Methoden, um den Index zu manipulieren und abzufragen. Die Abfragen werden für gewöhnlich mit booleschen Verknüpfungen formuliert, sofern sie über mehrere Felder gehen. Die Klasse QueryParser ist dann dafür zuständig aus dem Abfragetext ein Lucene-Query-Objekt zu erzeugen.

Man muss darauf achten, dem QueryParser die gleiche Analyzer-Klasse zu übergeben, die man auch für die Indexierung verwendet hat, da auf den Suchtext die gleichen Transformationsprozesse angewendet werden müssen, um ein möglichst gutes Ergebnis zu erhalten.

Nachdem wir das Lucene-Query-Objekt erzeugt haben, müssen wir es noch an Hibernate Search übergeben. Dazu verwerden wir den FullTextEntityManager, um eine Hibernate-Search-Query zu erzeugen.

Die Create-Methode bekommt als zweiten Paramter den Namen der Zielklasse übergeben. Somit ist für Hibernate Search automatisch klar, auf welchem Index die Suche ausgeführt werden muss, denn wie wir bereits gesehen haben, gilt: Jede mit @Indexed annotierte Entität wird auf einen eigenen Index abgebildet.

Wie oben zu sehen ist, implementiert die erzeugte Hibernate-Search-Query das Interface javax.persistence.Query. Somit stehen für Hibernate-Search-Abfragen auch Methoden zur Verfügung, die man auch schon von der Arbeit mit EJB-QL kennt, z. B. setMaxResults() und setFirstResult() zum “Scrollen” durch eine Ergebnisliste.

Somit kann nun mit query.getResultList() die Ergebnisliste geladen werden. Hibernate Search kümmert sich im Hintergrund darum, die Ergebnisse aus dem Index auf die entsprechenden Entity Beans zu mappen.

Es ist aber auch möglich, die Entitäten mit den Daten aus dem Index befüllen zu lassen, oder z. B. nur die Ids auszulesen.

Ausblick

Dieser Beitrag gibt einen kurzen Einblick in Hibernate Search. Natürlich ist es in einem so knappen Umfang nicht möglich, eine Komplettübersicht zu liefern. Die Abfragemöglichkeiten gehen z. B. weit über die hier gezeigte Volltextsuche hinaus. So ist es  auch möglich, nach Datumszeiträumen und Geodaten zu suchen, rechtschreibfehlertolarante Suchen durchzuführen und vieles mehr. Zudem kann jeder Aspekt von der Indexierung über die Textanalyse bis zur Suche den eigenen Bedürfnissen entsprechend modifiziert werden.

Zum Selbststudium kann das hervorragende Buch Hibernate Search in Action von Emmanuel Bernard und John Griffin empfohlen werden,  das im Manning Verlag erschienen ist. Online ist die offizielle Dokumentation auf der Hibernate-Homepage verfügbar.

Ein weiteres interessantes Hibernate-Modul ist “Envers”, zur Versionierung von Datensätzen. Dieses setzen wir bereits erfolgreich zur Darstellung einer Historie im “Stagemanager” ein. Envers wird das Thema eines Folgeeintrags auf unserem Blog sein.