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 (há também 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 aplicativos—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 o objeto tsvector se parece 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 o to_tsvcector 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 completo inclui a capacidade de fazer muito mais: pular a indexação de certas palavras (palavras irrelevantes), 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 fundamental para os resultados de pesquisa de texto completo. Para configurações diferentes, o motor do Postgres pode retornar resultados diferentes. Isso não precisa ser o caso entre dicionários de 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 em 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 obter mais informações.

Consulta de Texto

A consulta de texto suporta operadores como & (AND), | (OR), ! (NOT) e <-> (SEGUIDO POR). Os três primeiros operadores não exigem 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 em 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 à 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 à 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
  • OR: 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 para Postgres e Hibernate

Como mencionado no artigo, Hibernate sozinho não possui suporte a pesquisa de texto completo. Ele precisa contar com o suporte do mecanismo do banco de dados. Isso significa que podemos 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 usar 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 deste 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.

Existe outra maneira de registrar o componente de posjsonhelper, que pode ser feito através da programação. Para ver como fazer isso, verifique 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 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 gerará a seguinte declaração:

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 gerará a seguinte declaração:

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 ver como isso pode ser feito, clique neste link.

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

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

Conclusão

Como mencionado no artigo anterior, o suporte ao full-text search do Postgres pode ser uma boa alternativa para motores de pesquisa substanciais como o Elasticsearch ou Lucene, em alguns casos. Isso poderia nos poupar da decisão de adicionar soluções de terceiros à nossa pilha tecnológica, o que também poderia adicionar mais complexidade e custos adicionais.

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