Einführung in die Suche im Ruhezustand

1. Übersicht

In diesem Artikel werden die Grundlagen der Suche im Ruhezustand sowie deren Konfiguration erläutert und einige einfache Abfragen implementiert.

2. Grundlagen der Suche im Ruhezustand

Wann immer wir Volltextsuchfunktionen implementieren müssen, ist die Verwendung von Tools, mit denen wir bereits vertraut sind, immer ein Plus.

Falls wir Hibernate und JPA bereits für ORM verwenden, sind wir nur einen Schritt von der Hibernate-Suche entfernt.

Hibernate Search integriert Apache Lucene, eine leistungsstarke und erweiterbare Volltextsuchmaschinenbibliothek, die in Java geschrieben wurde . Dies kombiniert die Kraft von Lucene mit der Einfachheit von Hibernate und JPA.

Einfach ausgedrückt, wir müssen unseren Domänenklassen nur einige zusätzliche Anmerkungen hinzufügen, und das Tool kümmert sich um Dinge wie die Datenbank- / Indexsynchronisation.

Hibernate Search bietet auch eine Elasticsearch-Integration. Da es sich jedoch noch in einem experimentellen Stadium befindet, konzentrieren wir uns hier auf Lucene.

3. Konfigurationen

3.1. Maven-Abhängigkeiten

Bevor wir beginnen, müssen wir zuerst die erforderlichen Abhängigkeiten zu unserer pom.xml hinzufügen :

 org.hibernate hibernate-search-orm 5.8.2.Final 

Der Einfachheit halber verwenden wir H2 als Datenbank:

 com.h2database h2 1.4.196 

3.2. Konfigurationen

Wir müssen auch angeben, wo Lucene den Index speichern soll.

Dies kann über die Eigenschaft hibernate.search.default.directory_provider erfolgen .

Wir wählen das Dateisystem , das für unseren Anwendungsfall die einfachste Option ist. Weitere Optionen sind in der offiziellen Dokumentation aufgeführt. Dateisystem-Master / Dateisystem-Slave und Infinispan sind bemerkenswert für Cluster-Anwendungen, bei denen der Index zwischen Knoten synchronisiert werden muss.

Wir müssen auch ein Standardbasisverzeichnis definieren, in dem Indizes gespeichert werden:

hibernate.search.default.directory_provider = filesystem hibernate.search.default.indexBase = /data/index/default

4. Die Modellklassen

Nach der Konfiguration können wir nun unser Modell angeben.

Zusätzlich zu den JPA-Annotationen @Entity und @Table müssen wir eine @ Index- Annotation hinzufügen . Es sagt Hibernate Search , dass das Unternehmen Produkt indiziert werden soll.

Danach müssen wir die erforderlichen Attribute als durchsuchbar definieren, indem wir eine @ Field- Annotation hinzufügen :

@Entity @Indexed @Table(name = "product") public class Product { @Id private int id; @Field(termVector = TermVector.YES) private String productName; @Field(termVector = TermVector.YES) private String description; @Field private int memory; // getters, setters, and constructors }

Das Attribut termVector = TermVector.YES wird später für die Abfrage "More Like This" benötigt.

5. Erstellen des Lucene-Index

Bevor wir mit den eigentlichen Abfragen beginnen, müssen wir Lucene auslösen, um den Index zunächst zu erstellen :

FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager); fullTextEntityManager.createIndexer().startAndWait();

Nach dieser ersten Erstellung sorgt Hibernate Search dafür, dass der Index auf dem neuesten Stand gehalten wird . I. e. Wir können Entitäten wie gewohnt über den EntityManager erstellen, bearbeiten und löschen .

Hinweis: Wir müssen sicherstellen, dass Entitäten vollständig für die Datenbank festgeschrieben sind, bevor sie von Lucene erkannt und indiziert werden können. Dies ist übrigens auch der Grund, warum der erste Import von Testdaten in unseren Beispielcode-Testfällen in einer dedizierten JUnit erfolgt Testfall, kommentiert mit @Commit ).

6. Abfragen erstellen und ausführen

Jetzt können wir unsere erste Abfrage erstellen.

Im folgenden Abschnitt zeigen wir den allgemeinen Workflow zum Vorbereiten und Ausführen einer Abfrage.

Danach erstellen wir einige Beispielabfragen für die wichtigsten Abfragetypen.

6.1. Allgemeiner Workflow zum Erstellen und Ausführen einer Abfrage

Das Vorbereiten und Ausführen einer Abfrage besteht im Allgemeinen aus vier Schritten :

In Schritt 1 müssen wir einen JPA FullTextEntityManager und daraus einen QueryBuilder erhalten :

FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager); QueryBuilder queryBuilder = fullTextEntityManager.getSearchFactory() .buildQueryBuilder() .forEntity(Product.class) .get();

In Schritt 2 erstellen wir eine Lucene-Abfrage über die Hibernate-Abfrage DSL:

org.apache.lucene.search.Query query = queryBuilder .keyword() .onField("productName") .matching("iphone") .createQuery();

In Schritt 3 verpacken wir die Lucene-Abfrage in eine Hibernate-Abfrage:

org.hibernate.search.jpa.FullTextQuery jpaQuery = fullTextEntityManager.createFullTextQuery(query, Product.class);

Schließlich führen wir in Schritt 4 die Abfrage aus:

List results = jpaQuery.getResultList();

Hinweis : Standardmäßig sortiert Lucene die Ergebnisse nach Relevanz.

Die Schritte 1, 3 und 4 sind für alle Abfragetypen gleich.

Im Folgenden konzentrieren wir uns auf Schritt 2, dh wie verschiedene Arten von Abfragen erstellt werden.

6.2. Keyword-Abfragen

Der grundlegendste Anwendungsfall ist die Suche nach einem bestimmten Wort .

Dies haben wir bereits im vorherigen Abschnitt getan:

Query keywordQuery = queryBuilder .keyword() .onField("productName") .matching("iphone") .createQuery();

Here, keyword() specifies that we are looking for one specific word, onField() tells Lucene where to look and matching() what to look for.

6.3. Fuzzy Queries

Fuzzy queries are working like keyword queries, except that we can define a limit of “fuzziness”, above which Lucene shall accept the two terms as matching.

By withEditDistanceUpTo(), we can define how much a term may deviate from the other. It can be set to 0, 1, and 2, whereby the default value is 2 (note: this limitation is coming from the Lucene's implementation).

By withPrefixLength(), we can define the length of the prefix which shall be ignored by the fuzziness:

Query fuzzyQuery = queryBuilder .keyword() .fuzzy() .withEditDistanceUpTo(2) .withPrefixLength(0) .onField("productName") .matching("iPhaen") .createQuery();

6.4. Wildcard Queries

Hibernate Search also enables us to execute wildcard queries, i. e. queries for which a part of a word is unknown.

For this, we can use “?” for a single character, and “*” for any character sequence:

Query wildcardQuery = queryBuilder .keyword() .wildcard() .onField("productName") .matching("Z*") .createQuery();

6.5. Phrase Queries

If we want to search for more than one word, we can use phrase queries. We can either look for exact or for approximate sentences, using phrase() and withSlop(), if necessary. The slop factor defines the number of other words permitted in the sentence:

Query phraseQuery = queryBuilder .phrase() .withSlop(1) .onField("description") .sentence("with wireless charging") .createQuery();

6.6. Simple Query String Queries

With the previous query types, we had to specify the query type explicitly.

If we want to give some more power to the user, we can use simple query string queries: by that, he can define his own queries at runtime.

The following query types are supported:

  • boolean (AND using “+”, OR using “|”, NOT using “-“)
  • prefix (prefix*)
  • phrase (“some phrase”)
  • precedence (using parentheses)
  • fuzzy (fuzy~2)
  • near operator for phrase queries (“some phrase”~3)

The following example would combine fuzzy, phrase and boolean queries:

Query simpleQueryStringQuery = queryBuilder .simpleQueryString() .onFields("productName", "description") .matching("Aple~2 + \"iPhone X\" + (256 | 128)") .createQuery();

6.7. Range Queries

Range queries search for avalue in between given boundaries. This can be applied to numbers, dates, timestamps, and strings:

Query rangeQuery = queryBuilder .range() .onField("memory") .from(64).to(256) .createQuery();

6.8. More Like This Queries

Our last query type is the “More Like This” – query. For this, we provide an entity, and Hibernate Search returns a list with similar entities, each with a similarity score.

As mentioned before, the termVector = TermVector.YES attribute in our model class is required for this case: it tells Lucene to store the frequency for each term during indexing.

Based on this, the similarity will be calculated at query execution time:

Query moreLikeThisQuery = queryBuilder .moreLikeThis() .comparingField("productName").boostedTo(10f) .andField("description").boostedTo(1f) .toEntity(entity) .createQuery(); List results = (List) fullTextEntityManager .createFullTextQuery(moreLikeThisQuery, Product.class) .setProjection(ProjectionConstants.THIS, ProjectionConstants.SCORE) .getResultList();

6.9. Searching More Than One Field

Until now, we only created queries for searching one attribute, using onField().

Depending on the use case, we can also search two or more attributes:

Query luceneQuery = queryBuilder .keyword() .onFields("productName", "description") .matching(text) .createQuery();

Moreover, we can specify each attribute to be searched separately, e. g. if we want to define a boost for one attribute:

Query moreLikeThisQuery = queryBuilder .moreLikeThis() .comparingField("productName").boostedTo(10f) .andField("description").boostedTo(1f) .toEntity(entity) .createQuery();

6.10. Combining Queries

Finally, Hibernate Search also supports combining queries using various strategies:

  • SHOULD: the query should contain the matching elements of the subquery
  • MUST: the query must contain the matching elements of the subquery
  • MUST NOT: the query must not contain the matching elements of the subquery

The aggregations are similar to the boolean ones AND, OR and NOT. However, the names are different to emphasize that they also have an impact on the relevance.

Ein SOLLTE zwischen zwei Abfragen ähnelt beispielsweise dem booleschen ODER: Wenn eine der beiden Abfragen eine Übereinstimmung aufweist, wird diese Übereinstimmung zurückgegeben.

Wenn jedoch beide Abfragen übereinstimmen, hat die Übereinstimmung eine höhere Relevanz als wenn nur eine Abfrage übereinstimmt:

Query combinedQuery = queryBuilder .bool() .must(queryBuilder.keyword() .onField("productName").matching("apple") .createQuery()) .must(queryBuilder.range() .onField("memory").from(64).to(256) .createQuery()) .should(queryBuilder.phrase() .onField("description").sentence("face id") .createQuery()) .must(queryBuilder.keyword() .onField("productName").matching("samsung") .createQuery()) .not() .createQuery();

7. Fazit

In diesem Artikel haben wir die Grundlagen der Ruhezustandssuche erläutert und gezeigt, wie die wichtigsten Abfragetypen implementiert werden. Weiterführende Themen finden Sie in der offiziellen Dokumentation.

Wie immer ist der vollständige Quellcode der Beispiele auf GitHub verfügbar.