Volltextsuche in Postgres mit Hibernate 6

Hibernate

Hibernate allein bietet keine Volltextsuche. Es muss auf die Unterstützung des Datenbank-Engines oder Drittanbieterlösungen zurückgreifen.

Eine Erweiterung namens Hibernate Search integriert sich mit Apache Lucene oder Elasticsearch (es gibt auch eine Integration mit OpenSearch).

Postgres

Postgres verfügt seit Version 7.3 über Volltextsuche-Funktionalität. Obwohl es nicht mit Suchmaschinen wie Elasticsearch oder Lucene konkurrieren kann, bietet es dennoch eine flexible und robuste Lösung, die möglicherweise ausreicht, um die Erwartungen der Anwendungsbenutzer zu erfüllen – Funktionen wie Wortstammmerkmale, Rangfolge und Indizierung.

Wir werden kurz erklären, wie wir eine Volltextsuche in Postgres durchführen können. Weitere Informationen finden Sie in der Postgres-Dokumentation. Für den grundlegenden Textabgleich ist der wichtigste Teil der mathematische Operator @@.

Er gibt true zurück, wenn das Dokument (Objekt vom Typ tsvector) die Abfrage (Objekt vom Typ tsquery) entspricht.

Die Reihenfolge ist für den Operator nicht entscheidend. Daher spielt es keine Rolle, ob wir das Dokument auf der linken Seite des Operators und die Abfrage auf der rechten Seite oder in einer anderen Reihenfolge platzieren.

Für eine bessere Demonstration verwenden wir eine Datenbanktabelle namens tweet.

SQL

 

create table tweet (
        id bigint not null,
        short_content varchar(255),
        title varchar(255),
        primary key (id)
    )

Mit solchen Daten:

SQL

 

INSERT INTO tweet (id, title, short_content) VALUES (1, 'Cats', 'Cats rules the world');
INSERT INTO tweet (id, title, short_content) VALUES (2, 'Rats', 'Rats rules in the sewers');
INSERT INTO tweet (id, title, short_content) VALUES (3, 'Rats vs Cats', 'Rats and Cats hates each other');

INSERT INTO tweet (id, title, short_content) VALUES (4, 'Feature', 'This project is design to wrap already existed functions of Postgres');
INSERT INTO tweet (id, title, short_content) VALUES (5, 'Postgres database', 'Postgres is one of the widly used database on the market');
INSERT INTO tweet (id, title, short_content) VALUES (6, 'Database', 'On the market there is a lot of database that have similar features like Oracle');

Schauen wir uns nun an, wie das tsvector Objekt für die short_content Spalte für jede der Einträge aussieht.

SQL

 

SELECT id, to_tsvector('english', short_content) FROM tweet;

Ausgabe:

Die Ausgabe zeigt, wie to_tsvcector die Textspalte in ein tsvector Objekt für die ‚english‚ Textsuche konfiguriert.

Textsuche Konfiguration

Der erste Parameter für die to_tsvector Funktion in dem obigen Beispiel war der Name der Textsuche Konfiguration. In diesem Fall war es „english„. Laut Postgres Dokumentation ist die Textsuche Konfiguration wie folgt:

… die Volltextsuche Funktionalität beinhaltet die Fähigkeit, weitere Dinge zu tun: bestimmte Wörter (Stoppwörter) beim Indizieren zu überspringen, Synonyme zu verarbeiten und anspruchsvolle Analysen durchzuführen, z.B. Analysen basierend auf mehr als nur Leerzeichen. Diese Funktionalität wird von Textsuche Konfigurationen gesteuert.

Also ist die Konfiguration ein entscheidender Teil des Prozesses und von entscheidender Bedeutung für unsere Volltextsuchergebnisse. Bei verschiedenen Konfigurationen kann der Postgres-Engine unterschiedliche Ergebnisse zurückgeben. Dies muss jedoch nicht zwischen Wörterbüchern für verschiedene Sprachen der Fall sein. Sie können beispielsweise zwei Konfigurationen für dieselbe Sprache haben, wobei eine Namen, die Ziffern enthalten (z. B. einige Seriennummern), ignoriert. Wenn wir unserer Abfrage die spezifische Seriennummer übergeben, die wir suchen müssen, finden wir keine Aufzeichnung für die Konfiguration, die Wörter mit Zahlen ignoriert. Auch wenn wir solche Datensätze in der Datenbank haben, lesen Sie bitte die Konfigurationsdokumentation für weitere Informationen.

Textabfrage

Die Textabfrage unterstützt solche Operatoren wie & (UND), | (ODER), ! (NICHT) und <-> (NACHFOLGEND). Die ersten drei Operatoren erfordern keine tiefere Erklärung. Der <-> Operator überprüft, ob Wörter existieren und ob sie in einer bestimmten Reihenfolge angeordnet sind. So erwarten wir beispielsweise für die Abfrage „rat <-> cat„, dass das Wort „cat“ existiert und von „rat“ gefolgt wird.

Beispiele

  • Inhalt, der das rat und das cat: enthält
SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ to_tsquery('english', 'Rat & cat');

  • Inhalt, der database und market, und das market ist das dritte Wort nach database enthält:
SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ to_tsquery('english', 'database <3> market');

  • Inhalt, der database enthält, aber nicht Postgres:
SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ to_tsquery('english', 'database & !Postgres');

  • Inhalt, der Postgres oder Oracle:
SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ to_tsquery('english', 'Postgres | Oracle');

Wrapper-Funktionen

Eine der Wrapper-Funktionen, die Textabfragen erstellt, wurde bereits in diesem Artikel erwähnt, nämlich die to_tsquery. Es gibt weitere solcher Funktionen wie:

  • plainto_tsquery
  • phraseto_tsquery
  • websearch_to_tsquery

plainto_tsquery

Die plainto_tsquery wandelt alle übergebenen Wörter in eine Abfrage um, bei der alle Wörter mit dem & (UND) Operator kombiniert werden. Zum Beispiel ist das Äquivalent zu plainto_tsquery('english', 'Rat cat') to_tsquery('english', 'Rat & cat').

Bei folgendem Gebrauch:

SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ plainto_tsquery('english', 'Rat cat');

erhalten wir das Ergebnis unten:

phraseto_tsquery

Die phraseto_tsquery wandelt alle übergebenen Wörter in eine Abfrage um, bei der alle Wörter mit dem <-> (FOLLOW BY) Operator kombiniert werden. Zum Beispiel ist das Äquivalent zu phraseto_tsquery('english', 'cat rule') to_tsquery('english', 'cat <-> rule').

Bei folgendem Gebrauch:

SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ phraseto_tsquery('english', 'cat rule');

erhalten wir das Ergebnis unten:

websearch_to_tsquery

Die websearch_to_tsquery verwendet eine alternative Syntax zur Erstellung einer gültigen Textabfrage.

  • Unzitierter Text: Wandelt einen Teil der Syntax auf die gleiche Weise wie plainto_tsquery
  • Zitierter Text: Wandelt einen Teil der Syntax auf die gleiche Weise wie phraseto_tsquery
  • ODER:Wandelt in „|“ (ODER)-Operator
  • -„: Gleichwertig mit „!“ (NICHT)-Operator

Zum Beispiel ist das Äquivalent zu websearch_to_tsquery('english', '"cat rule" or database -Postgres') to_tsquery('english', 'cat <-> rule | database & !Postgres').

Für die folgende Verwendung:

SQL

 

SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ websearch_to_tsquery('english', '"cat rule" or database -Postgres');

Erhalten wir das Ergebnis unten:

Postgres und Hibernate Native Support

Wie im Artikel erwähnt, verfügt Hibernate alleine nicht über Volltextsuche. Es muss sich auf die Unterstützung des Datenbank-Engines verlassen. Das bedeutet, dass wir native SQL-Abfragen wie in den folgenden Beispielen ausführen dürfen:

  • plainto_tsquery
Java

 

public List<Tweet> findBySinglePlainQueryInDescriptionForConfigurationWithNativeSQL(String textQuery, String configuration) {
        return entityManager.createNativeQuery(String.format("select * from tweet t1_0 where to_tsvector('%1$s', t1_0.short_content) @@ plainto_tsquery('%1$s', :textQuery)", configuration), Tweet.class).setParameter("textQuery", textQuery).getResultList();
    }

  • websearch_to_tsquery
Java

 

public List<Tweet> findCorrectTweetsByWebSearchToTSQueryInDescriptionWithNativeSQL(String textQuery, String configuration) {
        return entityManager.createNativeQuery(String.format("select * from tweet t1_0 where to_tsvector('%1$s', t1_0.short_content) @@ websearch_to_tsquery('%1$s', :textQuery)", configuration), Tweet.class).setParameter("textQuery", textQuery).getResultList();
    }

Hibernate mit posjsonhelper-Bibliothek

Die posjsonhelper-Bibliothek ist ein Open-Source-Projekt, das die Unterstützung für Hibernate-Abfragen für PostgreSQL JSON-Funktionen und Volltextsuche hinzufügt.

Für das Maven Projekt müssen wir die folgenden Abhängigkeiten hinzufügen:

XML

 

<dependency>
    <groupId>com.github.starnowski.posjsonhelper.text</groupId>
    <artifactId>hibernate6-text</artifactId>
    <version>0.3.0</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.4.0.Final</version>
</dependency>

Um Komponenten aus der posjsonhelper Bibliothek zu verwenden, müssen diese im Hibernate-Kontext registriert werden.

Dies bedeutet, dass eine spezifische Implementierung von org.hibernate.boot.model.FunctionContributor erforderlich ist. Die Bibliothek enthält eine Implementierung dieses Interfaces, nämlich com.github.starnowski.posjsonhelper.hibernate6.PosjsonhelperFunctionContributor.

A file with the name „org.hibernate.boot.model.FunctionContributor“ under the „resources/META-INF/services“ directory is required to use this implementation.

Es gibt eine weitere Möglichkeit, Komponenten von posjsonhelper zu registrieren, die programmierbar erfolgen kann. Um zu sehen, wie das geht, schaut euch diesen Link an.

Ab sofort können wir Volltext-Suchoperatoren in Hibernate-Abfragen verwenden.

PlainToTSQueryFunction

Dies ist eine Komponente, die die plainto_tsquery Funktion einwickelt.

Java

 

public List<Tweet> findBySinglePlainQueryInDescriptionForConfiguration(String textQuery, String configuration) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Tweet> query = cb.createQuery(Tweet.class);
        Root<Tweet> root = query.from(Tweet.class);
        query.select(root);
        query.where(new TextOperatorFunction((NodeBuilder) cb, new TSVectorFunction(root.get("shortContent"), configuration, (NodeBuilder) cb), new PlainToTSQueryFunction((NodeBuilder) cb, configuration, textQuery), hibernateContext));
        return entityManager.createQuery(query).getResultList();
    }

Für eine Konfiguration mit dem Wert 'english' wird der folgende Code die untenstehende Anweisung generieren:

Java

 

select
        t1_0.id,
        t1_0.short_content,
        t1_0.title 
    from
        tweet t1_0 
    where
        to_tsvector('english', t1_0.short_content) @@ plainto_tsquery('english', ?);

PhraseToTSQueryFunction

Dieser Baustein wickelt die Funktion phraseto_tsquery ein.

Java

 

public List<Tweet> findBySinglePhraseInDescriptionForConfiguration(String textQuery, String configuration) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Tweet> query = cb.createQuery(Tweet.class);
        Root<Tweet> root = query.from(Tweet.class);
        query.select(root);
        query.where(new TextOperatorFunction((NodeBuilder) cb, new TSVectorFunction(root.get("shortContent"), configuration, (NodeBuilder) cb), new PhraseToTSQueryFunction((NodeBuilder) cb, configuration, textQuery), hibernateContext));
        return entityManager.createQuery(query).getResultList();
        }

Bei der Konfiguration mit dem Wert 'english' wird der Code die folgende Anweisung generieren:

SQL

 

select
        t1_0.id,
        t1_0.short_content,
        t1_0.title 
    from
        tweet t1_0 
    where
        to_tsvector('english', t1_0.short_content) @@ phraseto_tsquery('english', ?)

WebsearchToTSQueryFunction 

Dieser Baustein wickelt die Funktion websearch_to_tsquery ein.

Java

 

public List<Tweet> findCorrectTweetsByWebSearchToTSQueryInDescription(String phrase, String configuration) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Tweet> query = cb.createQuery(Tweet.class);
        Root<Tweet> root = query.from(Tweet.class);
        query.select(root);
        query.where(new TextOperatorFunction((NodeBuilder) cb, new TSVectorFunction(root.get("shortContent"), configuration, (NodeBuilder) cb), new WebsearchToTSQueryFunction((NodeBuilder) cb, configuration, phrase), hibernateContext));
        return entityManager.createQuery(query).getResultList();
    }

Bei der Konfiguration mit dem Wert 'english' wird der Code die folgende Anweisung generieren:

SQL

 

select
        t1_0.id,
        t1_0.short_content,
        t1_0.title 
    from
        tweet t1_0 
    where
        to_tsvector('english', t1_0.short_content) @@ websearch_to_tsquery('english', ?)

HQL-Abfragen

Alle erwähnten Bausteine können in HQL-Abfragen verwendet werden. Um zu sehen, wie dies funktioniert, klicken Sie bitte auf diesen Link.

Warum die posjsonhelper-Bibliothek verwenden, wenn wir die native Herangehensweise mit Hibernate nutzen können?

Obwohl das dynamische Verketten eines Strings, der als HQL- oder SQL-Abfrage vorgesehen ist, möglicherweise einfach ist, ist die Implementierung von Prädikaten eine bessere Praxis, insbesondere wenn man Suchkriterien basierend auf dynamischen Attributen aus Ihrer API handhaben muss.

Schlussfolgerung

Wie im vorherigen Artikel erwähnt, kann die Unterstützung für Volltextsuche in Postgres in einigen Fällen eine gute Alternative für umfangreiche Suchmaschinen wie Elasticsearch oder Lucene sein. Dies könnte uns davor bewahren, Drittanbietersysteme zu unserer Technologiestapel hinzuzufügen, was auch mehr Komplexität und zusätzliche Kosten mit sich bringen könnte.

Source:
https://dzone.com/articles/postgres-full-text-search-with-hibernate-6