Einführung in die Ruhezustandssuche

1. Überblick

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

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 integrates Apache Lucene, a high-performance and extensible full-text search-engine library written in Java. Dies kombiniert die Kraft von Lucene mit der Einfachheit von Hibernate und JPA.

Einfach ausgedrückt, wir müssen nur einige zusätzliche Anmerkungen zu unseren Domänenklassen undthe tool will take care of the things like database/index synchronization. hinzufügen

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 erforderlichendependencies zu unserenpom.xml hinzufügen:


    org.hibernate
    hibernate-search-orm
    5.8.2.Final

Der Einfachheit halber verwenden wirH2 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 Eigenschafthibernate.search.default.directory_provider erfolgen.

Wir wählenfilesystem, was für unseren Anwendungsfall die einfachste Option ist. Weitere Optionen sind inofficial documentationaufgeführt. Filesystem-master/filesystem-slave and infinispan are noteworthy for clustered applications, where the index has to be synchronized between nodes.

Wir müssen auch ein Standard-Basisverzeichnis definieren, in dem die 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.

On top of the JPA annotations @Entity and @Table, we have to add an @Indexed annotation. Hiermit wird Hibernate Search mitgeteilt, dass die EntitätProduct indiziert werden soll.

After that, we have to define the required attributes as searchable by adding a @Field annotation:

@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 AttributtermVector = TermVector.YES wird später für die Abfrage "More Like This" benötigt.

5. Aufbau des Lucene-Index

Vor dem Starten der eigentlichen Abfragen werdenwe have to trigger Lucene to build the index initially:

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

After this initial build, Hibernate Search will take care of keeping the index up to date. I. e. Wir können Entitäten wie gewohnt überEntityManager erstellen, bearbeiten und löschen.

Hinweis:we have to make sure that entities are fully committed to the database before they can be discovered and indexed by Lucene (dies ist übrigens auch der Grund, warum der anfängliche Testdatenimport inexample code test cases in einem dedizierten JUnit-Testfall erfolgt, der mit@Commit versehen ist).

6. Abfragen erstellen und ausführen

Jetzt können wir unsere erste Abfrage erstellen.

Im folgenden Abschnitt werdenwe’ll show the general workflow for preparing and executing a query.

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

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

Preparing and executing a query in general consists of four steps:

In Schritt 1 müssen wir einen JPAFullTextEntityManager und daraus einenQueryBuilder 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 DSL-Abfrage im Ruhezustand:

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

In Schritt 3 wird die Lucene-Abfrage in eine Hibernate-Abfrage eingeschlossen:

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();

Note: 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, d.h. e. So erstellen Sie verschiedene Arten von Abfragen.

6.2. Keyword-Abfragen

Der grundlegendste Anwendungsfall istsearching for a specific word.

Das haben wir eigentlich schon im vorigen Abschnitt gemacht:

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

Hier gibtkeyword() an, dass wir nach einem bestimmten Wort suchen,onField() sagt Lucene, wo gesucht werden soll, undmatching(), wonach gesucht werden soll.

6.3. Fuzzy-Abfragen

Fuzzy-Abfragen funktionieren wie Keyword-Abfragen, mit der Ausnahme, dasswe can define a limit of “fuzziness”, über dem Lucene die beiden Begriffe als übereinstimmend akzeptiert.

UmwithEditDistanceUpTo(),we can define how much a term may deviate from the other. Es kann auf 0, 1 und 2 gesetzt werden, wobei der Standardwert 2 ist (note: Diese Einschränkung ergibt sich aus der Implementierung von Lucene).

MitwithPrefixLength() können wir die Länge des Präfixes definieren, die von der Unschärfe ignoriert werden soll:

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

6.4. Platzhalterabfragen

Die Suche im Ruhezustand ermöglicht es uns auch, Platzhalterabfragen auszuführen, d.h. e. Abfragen, für die ein Teil eines Wortes unbekannt ist.

Dazu können wir "?” für ein einzelnes Zeichen und"*” für jede Zeichenfolge verwenden:

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

6.5. Phrasenabfragen

Wenn wir nach mehr als einem Wort suchen möchten, können wir Phrasenabfragen verwenden. Bei Bedarf können wir entwederfor exact or for approximate sentences mitphrase() undwithSlop() suchen. Der Slop-Faktor definiert die Anzahl der anderen im Satz zulässigen Wörter:

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

6.6. Einfache Abfragezeichenfolgenabfragen

Bei den vorherigen Abfragetypen mussten wir den Abfragetyp explizit angeben.

Wenn wir dem Benutzer mehr Leistung geben möchten, können wir einfache Abfragezeichenfolgenabfragen verwenden:by that, he can define his own queries at runtime.

Die folgenden Abfragetypen werden unterstützt:

  • Boolean (UND mit "+" ODER mit "|", NICHT mit "-")

  • Präfix (Präfix *)

  • Phrase ("irgendeine Phrase")

  • Vorrang (unter Verwendung von Klammern)

  • unscharf (unscharf ~ 2)

  • Near-Operator für Phrasenabfragen ("einige Phrasen" ~ 3)

Das folgende Beispiel kombiniert Fuzzy-, Phrasen- und Boolesche Abfragen:

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

6.7. Bereichsabfragen

Range queries search for avalue in between given boundaries. Dies kann auf Zahlen, Datumsangaben, Zeitstempel und Zeichenfolgen angewendet werden:

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

6.8. Eher wie diese Abfragen

Unser letzter Abfragetyp ist die Abfrage "More Like This". Zu diesem Zweck geben wir eine Entität undHibernate Search returns a list with similar entities mit jeweils einer Ähnlichkeitsbewertung an.

Wie bereits erwähnt, ist für diesen Fall das AttributtermVector = TermVector.YES in unserer Modellklasse erforderlich: Es weist Lucene an, die Häufigkeit für jeden Term während der Indizierung zu speichern.

Basierend darauf wird die Ähnlichkeit zur Ausführungszeit der Abfrage berechnet:

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. Mehr als ein Feld suchen

Bisher haben wir nur Abfragen zum Suchen eines Attributs mitonField() erstellt.

Je nach Anwendungsfallwe can also search two or more attributes:

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

Darüber hinaus sindwe can specify each attribute to be searched separately, z. g. Wenn wir einen Boost für ein Attribut definieren wollen:

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

6.10. Abfragen kombinieren

Schließlich unterstützt die Ruhezustand-Suche auch das Kombinieren von Abfragen mit verschiedenen Strategien:

  • SHOULD: Die Abfrage sollte die übereinstimmenden Elemente der Unterabfrage enthalten

  • MUST: Die Abfrage muss die übereinstimmenden Elemente der Unterabfrage enthalten

  • MUST NOT: Die Abfrage darf nicht die übereinstimmenden Elemente der Unterabfrage enthalten

Die Aggregationen sindsimilar to the boolean ones AND, OR and NOT. Die Namen unterscheiden sich jedoch, um hervorzuheben, dass sie sich auch auf die Relevanz auswirken.

Beispielsweise ähnelt einSHOULD zwischen zwei Abfragen dem booleschenOR:. 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 Ruhezustandsuche erläutert und gezeigt, wie die wichtigsten Abfragetypen implementiert werden. Weiterführende Themen finden Sie inofficial documentation.

Wie immer ist der vollständige Quellcode der Beispieleover on GitHub verfügbar.