Pesquisa de Texto Completo no Postgres com Hibernate 6

Hibernate

O Hibernate por si só não possui suporte a pesquisa de texto completo. Ele precisa contar com o suporte do mecanismo de banco de dados ou soluções de terceiros.

Uma extensão chamada Hibernate Search se integra com Apache Lucene ou Elasticsearch (também há integração com o OpenSearch).

Postgres

O Postgres possui funcionalidade de pesquisa de texto completo desde a versão 7.3. Embora não possa competir com mecanismos de pesquisa como o Elasticsearch ou Lucene, ainda oferece uma solução flexível e robusta que pode ser suficiente para atender às expectativas dos usuários de aplicações—recursos como stemming, classificação e indexação.

Vamos explicar brevemente como podemos fazer uma pesquisa de texto completo no Postgres. Para mais informações, visite a documentação do Postgres. Quanto à correspondência de texto essencial, a parte mais crucial é o operador matemático @@.

Ele retorna true se o documento (objeto do tipo tsvector) corresponder à consulta (objeto do tipo tsquery).

A ordem não é crucial para o operador. Portanto, não importa se colocamos o documento à esquerda do operador e a consulta à direita ou em uma ordem diferente.

Para melhor demonstração, utilizamos uma tabela de banco de dados chamada tweet.

SQL

 

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

Com tais dados:

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

Agora, vejamos como se parece o objeto tsvector para a coluna short_content de cada registro.

SQL

 

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

Saída:

A saída mostra como to_tsvector converte a coluna de texto em um objeto tsvector para a configuração de pesquisa de texto ‘english‘.

Configuração de Pesquisa de Texto

O primeiro parâmetro para a função to_tsvector passado no exemplo acima foi o nome da configuração de pesquisa de texto. Nesse caso, foi o “english“. De acordo com a documentação do Postgres, a configuração de pesquisa de texto é a seguinte:

… a funcionalidade de pesquisa de texto completa inclui a capacidade de fazer muito mais: pular a indexação de certas palavras (stop words), processar sinônimos e usar análise sofisticada, por exemplo, analisar com base em mais do que apenas espaços em branco. Essa funcionalidade é controlada por configurações de pesquisa de texto.

Então, a configuração é uma parte crucial do processo e vital para os resultados de pesquisa de texto completo. Para configurações diferentes, o motor Postgres pode retornar resultados diferentes. Isso não precisa ser o caso entre dicionários para diferentes idiomas. Por exemplo, você pode ter duas configurações para a mesma língua, mas uma ignora nomes contendo dígitos (por exemplo, alguns números de série). Se passarmos na nossa consulta o número de série específico que estamos procurando, que é obrigatório, não encontraremos nenhum registro para a configuração que ignora palavras com números. Mesmo que tenhamos tais registros no banco de dados, consulte a documentação de configuração para mais informações.

Consulta de Texto

A consulta de texto suporta tais operadores como & (E), | (OU), ! (NÃO) e <-> (SEGUINDO). Os três primeiros operadores não requerem uma explicação mais profunda. O operador <-> verifica se as palavras existem e se estão em uma ordem específica. Então, por exemplo, para a consulta “rat <-> cat“, esperamos que a palavra “cat” exista, seguida pela “rat”.

Exemplos

  • Conteúdo que contém o rat e cat:
SQL

 

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

  • Conteúdo que contém banco de dados e mercado, e o mercado é a terceira palavra após banco de dados:
SQL

 

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

  • Conteúdo que contém banco de dados mas não Postgres:
SQL

 

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

  • Conteúdo que contém Postgres ou Oracle:
SQL

 

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

Funções de Wrapper

Uma das funções de wrapper que cria consultas de texto já foi mencionada neste artigo, que é a to_tsquery. Existem mais funções como:

  • plainto_tsquery
  • phraseto_tsquery
  • websearch_to_tsquery

plainto_tsquery

A plainto_tsquery converte todas as palavras passadas em uma consulta onde todas as palavras são combinadas com o operador & (AND). Por exemplo, o equivalente de plainto_tsquery('english', 'Rat cat') é to_tsquery('english', 'Rat & cat').

Para o seguinte uso:

SQL

 

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

Temos o resultado abaixo:

phraseto_tsquery

A phraseto_tsquery converte todas as palavras passadas em uma consulta onde todas as palavras são combinadas com o operador <-> (FOLLOW BY). Por exemplo, o equivalente de phraseto_tsquery('english', 'cat rule') é to_tsquery('english', 'cat <-> rule').

Para o seguinte uso:

SQL

 

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

Temos o resultado abaixo:

websearch_to_tsquery

A websearch_to_tsquery utiliza uma sintaxe alternativa para criar uma consulta de texto válida.

  • Texto sem aspas: Converte parte da sintaxe da mesma maneira que plainto_tsquery
  • Texto com aspas: Converte parte da sintaxe da mesma maneira que phraseto_tsquery
  • OU: Converte para “|” (operador OR)
  • -“: Igual a “!” (operador NOT)

Por exemplo, o equivalente ao websearch_to_tsquery('english', '"cat rule" or database -Postgres') é to_tsquery('english', 'cat <-> rule | database & !Postgres').

Para o seguinte uso:

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

Temos o resultado abaixo:

Suporte Nativo de Postgres e Hibernate

Como mencionado no artigo, Hibernate sozinho não possui suporte à pesquisa de texto completo. Ele precisa contar com o suporte do mecanismo do banco de dados. Isso significa que somos autorizados a executar consultas SQL nativas como mostrado nos exemplos abaixo:

  • 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 Com a Biblioteca posjsonhelper

A biblioteca posjsonhelper é um projeto de código aberto que adiciona suporte para consultas Hibernate para funções JSON do PostgreSQL e pesquisa de texto completo.

Para o projeto Maven, precisamos adicionar as dependências abaixo:

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>

Para utilizar componentes que existem na biblioteca posjsonhelper, precisamos registrá-los no contexto do Hibernate.

Isso significa que deve haver uma implementação especificada do org.hibernate.boot.model.FunctionContributor. A biblioteca possui uma implementação desta interface, que é 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.

Há outra maneira de registrar o componente de posjsonhelper, que pode ser feito através da programação. Para ver como fazer isso, confira este link.

Agora, podemos usar operadores de pesquisa de texto completo em consultas do Hibernate.

PlainToTSQueryFunction

Este é um componente que envolve a função plainto_tsquery.

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

Para uma configuração com o valor 'english', o código vai gerar a declaração abaixo:

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

Este componente envolve a função phraseto_tsquery.

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

Para configuração com o valor 'english', o código irá gerar a declaração abaixo:

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 

Este componente envolve a função websearch_to_tsquery.

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

Para configuração com o valor 'english', o código irá gerar a declaração abaixo:

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', ?)

Consultas HQL

Todos os componentes mencionados podem ser usados em consultas HQL. Para verificar como isso pode ser feito, por favor clique neste link.

Por que usar a biblioteca posjsonhelper quando podemos usar a abordagem nativa com Hibernate?

Embora concatenação dinâmica de uma string que deveria ser uma consulta HQL ou SQL possa ser fácil, implementar predicados seria uma prática melhor, especialmente quando você tem que lidar com critérios de pesquisa baseados em atributos dinâmicos de sua API.

Conclusão

Como mencionado no artigo anterior, o suporte de pesquisa de texto completo do Postgres pode ser uma boa alternativa para motores de pesquisa substanciais como Elasticsearch ou Lucene, em alguns casos. Isso poderia nos poupar da decisão de adicionar soluções de terceiros ao nosso conjunto de tecnologias, o que também poderia adicionar mais complexidade e custos adicionais.

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