Volledige tekstzoekopdracht in Postgres met Hibernate 6

Hibernate

Hibernate zelf biedt geen ondersteuning voor full-text zoeken. Het moet vertrouwen op ondersteuning van de database-engine of derdenoplossingen.

Een uitbreiding genaamd Hibernate Search integreert met Apache Lucene of Elasticsearch (er is ook integratie met OpenSearch).

Postgres

Postgres heeft sinds versie 7.3 functionaliteit voor full-text zoeken. Hoewel het niet kan concurreren met zoekmachines zoals Elasticsearch of Lucene, biedt het nog steeds een flexibele en robuuste oplossing die misschien wel voldoende is om de verwachtingen van applicatiegebruikers te vervullen—functies zoals afstemming, rangschikking en indexering.

We zullen kort uitleggen hoe we full-text zoeken in Postgres kunnen doen. Voor meer, bezoek de documentatie van Postgres. Wat betreft essentiële tekstmatching, is het belangrijkste deel de wiskundige operator @@.

Het geeft true terug als het document (object van het type tsvector) overeenkomt met de query (object van het type tsquery).

De volgorde is niet cruciaal voor de operator. Dus het maakt niet uit of we het document aan de linkerkant van de operator plaatsen en de query aan de rechterkant of in een andere volgorde.

Voor een betere demonstratie gebruiken we een database tabel genaamd tweet.

SQL

 

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

Met zulke gegevens:

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

Laten we nu eens kijken hoe het tsvector object eruit ziet voor de short_content kolom voor elk van de records.

SQL

 

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

Uitvoer:

De uitvoer laat zien hoe to_tsvector de tekstkolom converteert naar een tsvector object voor de ‘english‘ tekstzoekconfiguratie.

Tekst Zoek Configuratie

De eerste parameter voor de to_tsvector functie die in bovenstaand voorbeeld werd doorgegeven, was de naam van de tekstzoekconfiguratie. In dat geval was het de “english“. Volgens de Postgres documentatie is de tekstzoekconfiguratie als volgt:

… de volledige tekstzoekfunctionaliteit omvat de mogelijkheid om nog veel meer dingen te doen: bepaalde woorden overslaan bij indexeren (stopwoorden), synoniemen verwerken en geavanceerde parseren gebruiken, bijvoorbeeld parseren op basis van meer dan alleen witruimte. Deze functionaliteit wordt beheerd door tekstzoekconfiguraties.

Dus, configuratie is een cruciale onderdeel van het proces en essentieel voor onze volledig-tekst zoekresultaten. Voor verschillende configuraties kan de Postgres-engine verschillende resultaten retourneren. Dit hoeft niet het geval te zijn tussen woordenboeken voor verschillende talen. Je kunt bijvoorbeeld twee configuraties hebben voor dezelfde taal, maar één negeert namen die cijfers bevatten (bijvoorbeeld sommige serienummers). Als we onze query de specifieke serienummer doorgeven dat we zoeken, wat verplicht is, vinden we geen record voor configuratie die woorden met nummers negeert. Zelfs als we dergelijke records in de database hebben, raadpleeg de configuratiedocumentatie voor meer informatie.

Tekst Query

Tekst query ondersteunt zulke operatoren als & (EN), | (OF), ! (NIET), en <-> (GEVOLgd DOOR). De eerste drie operatoren vereisen geen diepere uitleg. De <-> operator controleert of woorden bestaan en of ze in een specifieke volgorde staan. Dus, bijvoorbeeld, voor de query “rat <-> cat“, verwachten we dat het woord “cat” zal bestaan, gevolgd door de “rat.”

Voorbeelden

  • Content die de rat en cat:bevat:
SQL

 

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

  • Content die database en market, bevat, en de market is het derde woord na database:
SQL

 

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

  • Content die database bevat maar niet Postgres:
SQL

 

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

  • Inhoud die Postgres of Oracle:
SQL

 

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

Wrapperfuncties

Een van de wrapperfuncties die tekstquery’s creëert, is al genoemd in dit artikel, namelijk de to_tsquery. Er zijn meer van dergelijke functies, zoals:

  • plainto_tsquery
  • phraseto_tsquery
  • websearch_to_tsquery

plainto_tsquery

De plainto_tsquery converteert alle doorgegeven woorden naar een query waarbij alle woorden worden gecombineerd met de & (EN) operator. Bijvoorbeeld, het equivalent van de plainto_tsquery('english', 'Rat cat') is to_tsquery('english', 'Rat & cat').

Voor de volgende gebruik:

SQL

 

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

Krijgen we het resultaat hieronder:

phraseto_tsquery

De phraseto_tsquery converteert alle doorgegeven woorden naar een query waarbij alle woorden worden gecombineerd met <-> (VOLGEDOOR) operator. Bijvoorbeeld, het equivalent van de phraseto_tsquery('english', 'cat rule') is to_tsquery('english', 'cat <-> rule').

Voor de volgende gebruik:

SQL

 

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

Krijgen we het resultaat hieronder:

websearch_to_tsquery

De websearch_to_tsquery gebruikt alternatieve syntaxis om een geldige tekstquery te creëren.

  • Onuitgevonden tekst: Zet een deel van de syntaxis om op dezelfde manier als plainto_tsquery
  • Uitgevonden tekst: Zet een deel van de syntaxis om op dezelfde manier als phraseto_tsquery
  • OF: Zet om in “|” (OF) operator
  • -“: Gelijk aan “!” (NIET) operator

Bijvoorbeeld, het equivalent van de websearch_to_tsquery('english', '"cat rule" or database -Postgres') is to_tsquery('english', 'cat <-> rule | database & !Postgres').

Voor de volgende gebruik:

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

We krijgen het resultaat hieronder:

Postgres en Hibernate Native Support

Zoals in het artikel genoemd, heeft Hibernate op zichzelf geen full-text zoekondersteuning. Het moet vertrouwen op ondersteuning van de database-engine. Dit betekent dat we toegestaan zijn om native SQL-query’s uit te voeren zoals weergegeven in de onderstaande voorbeelden:

  • 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 Met posjsonhelper Library

De posjsonhelper bibliotheek is een open source project dat ondersteuning toevoegt voor Hibernate-query’s voor PostgreSQL JSON functies en full-text zoeken.

Voor het Maven project moeten we de volgende afhankelijkheden toevoegen:

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>

Om componenten te gebruiken die bestaan in de posjsonhelper bibliotheek, moeten we deze registreren in de Hibernate context.

Dit betekent dat er een specifieke org.hibernate.boot.model.FunctionContributor implementatie moet zijn. De bibliotheek heeft een implementatie van deze interface, namelijk 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.

Er is nog een manier om de component van posjsonhelper te registreren, wat kan via programmabiliteit. Om te zien hoe dat moet, bekijk deze link.

Nu kunnen we volledig tekst zoekoperators gebruiken in Hibernate queries.

PlainToTSQueryFunction

Dit is een component die de plainto_tsquery functie omsluit.

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

Voor een configuratie met de waarde 'english' zal de code de onderstaande statement genereren:

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

Deze component wikkelt de phraseto_tsquery functie.

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

Voor configuratie met de waarde 'english' zal de code de onderstaande statement genereren:

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 

Deze component wikkelt de websearch_to_tsquery functie.

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

Voor configuratie met de waarde 'english' zal de code de onderstaande statement genereren:

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 Queries

Alle genoemde componenten kunnen worden gebruikt in HQL queries. Om te zien hoe dat kan, klik op deze link.

Waarom de posjsonhelper Library gebruiken als we de native aanpak met Hibernate kunnen gebruiken?

Hoewel het dynamisch samenvoegen van een string die bedoeld is als een HQL of SQL query misschien eenvoudig is, is het implementeren van predicaten beter, vooral wanneer je zoekcriteria moet afhandelen op basis van dynamische attributen van je API.

Conclusie

Zoals vermeld in het vorige artikel, kan de ondersteuning voor Postgres full-text zoeken een goede alternatief zijn voor omvangrijke zoekmachines zoals Elasticsearch of Lucene, in sommige gevallen. Dit kan ons ontslaan van de keuze om derdenoplossingen aan onze technologiestapel toe te voegen, wat ook meer complexiteit en extra kosten kan opleveren.

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