使用Hibernate 6进行PostgreSQL全文搜索

Hibernate

Hibernate本身并不具备全文搜索功能,需要依赖数据库引擎的支持或第三方解决方案。

有一种扩展名为Hibernate Search,它与Apache LuceneElasticsearch(还有与OpenSearch的集成)相结合。

Postgres

自版本7.3起,Postgres便内置了全文搜索功能。尽管它无法与Elasticsearch或Lucene等搜索引擎相抗衡,但仍提供了一个灵活且稳健的解决方案,足以满足应用用户的需求——包括词干提取、排名和索引等功能。

我们将简要介绍如何在Postgres中进行全文搜索。更多详情,请参阅Postgres文档。至于基本的文本匹配,最关键的是数学运算符@@

如果文档(类型为tsvector的对象)与查询(类型为tsquery的对象)匹配,该运算符将返回true

运算符的顺序并不重要。因此,无论是将文档放在运算符的左侧,查询放在右侧,还是以其他顺序排列,都不影响结果。

为了更好地演示,我们使用一个名为tweet的数据库表。

SQL

 

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

数据如下:

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

现在让我们看看每条记录的short_content列对应的tsvector对象是什么样的。

SQL

 

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

输出结果:

输出展示了to_tsvector如何将文本列转换为适用于’english‘文本搜索配置的tsvector对象。

文本搜索配置

在上例中,to_tsvector函数传入的第一个参数是文本搜索配置的名称,即”english“。根据Postgres文档,文本搜索配置定义如下:

…全文搜索功能还包括许多其他能力:跳过索引某些单词(停用词)、处理同义词以及使用复杂的解析方法,例如,不仅仅基于空格进行解析。这些功能由文本搜索配置控制。

因此,配置是整个流程中至关重要的一环,对全文搜索结果至关重要。不同的配置会导致Postgres引擎返回不同的结果。这不一定适用于不同语言的词典。例如,您可以为同一种语言设置两种配置,但其中一种会忽略包含数字的名称(例如,某些序列号)。如果我们输入必须查找的特定序列号作为查询条件,那么在忽略数字的配置下,我们将无法找到任何记录。即便数据库中存在这样的记录,请查阅配置文档以获取更多信息。

文本查询

文本查询支持诸如&(AND)、|(OR)、!(NOT)以及<->(紧随其后)等操作符。前三个操作符无需过多解释。<->操作符检查单词是否存在及其特定的顺序。例如,对于查询”rat <-> cat“,我们期望“cat”这个词存在,并且紧跟在“rat”之后。

示例

  • 包含ratcat的内容:
SQL

 

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

  • 包含数据库市场,市场是数据库后的第三个词:
SQL

 

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

  • 包含数据库但不包含Postgres的内容:
SQL

 

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

  • 内容涉及PostgresOracle:
SQL

 

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

包装函数

本文已提及一种创建文本查询的包装函数,即to_tsquery。还有其他类似函数如:

  • plainto_tsquery
  • phraseto_tsquery
  • websearch_to_tsquery

plainto_tsquery

plainto_tsquery将所有传入的单词转换为查询,其中所有单词通过&(AND)运算符组合。例如,plainto_tsquery('english', 'Rat cat')等同于to_tsquery('english', 'Rat & cat')

对于以下用法:

SQL

 

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

我们得到如下结果:

phraseto_tsquery

phraseto_tsquery将所有传入的单词转换为查询,其中所有单词通过<->(FOLLOW BY)运算符组合。例如,phraseto_tsquery('english', 'cat rule')等同于to_tsquery('english', 'cat <-> rule')

对于以下用法:

SQL

 

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

我们得到如下结果:

websearch_to_tsquery使用替代语法来创建有效的文本查询。

  • 未引用文本:以与plainto_tsquery相同的方式转换部分语法
  • 引用文本:以与phraseto_tsquery相同的方式转换部分语法
  • OR:转换为”|“(OR)运算符
  • -“:与”!“(NOT)运算符相同

例如,websearch_to_tsquery('english', '"cat rule" or database -Postgres')的等效表达是to_tsquery('english', 'cat <-> rule | database & !Postgres')

对于以下用法:

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

我们得到如下结果:

Postgres与Hibernate原生支持

如文章所述,Hibernate本身不具备全文搜索支持,需依赖数据库引擎。这意味着我们可以执行原生SQL查询,如下例所示:

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

使用posjsonhelper库的Hibernate

posjsonhelper库是一个开源项目,为Hibernate查询增加了对PostgreSQL JSON函数及全文搜索的支持。

对于Maven项目,我们需要添加以下依赖项

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>

为了使用存在于posjsonhelper库中的组件,我们需要在Hibernate上下文中注册它们。

这意味着必须有一个指定的org.hibernate.boot.model.FunctionContributor实现。该库中有一个此接口的实现,即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.

还有一种方式可以注册posjsonhelper的组件,这可以通过编程实现。要了解如何操作,请查看此链接

现在,我们可以在Hibernate查询中使用全文搜索操作符。

PlainToTSQueryFunction

这是一个封装了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();
    }

对于配置值为'english'的情况,代码将生成以下语句:

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

该组件封装了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();
        }

当配置值为'english'时,代码将生成以下语句:

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

此组件封装了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();
    }

当配置值为'english'时,代码将生成以下语句:

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查询

上述所有组件均可用于HQL查询。如需了解具体使用方法,请点击链接

为何在可以使用Hibernate原生方法时还要使用posjsonhelper库?

虽然动态拼接被视为HQL或SQL查询的字符串可能很简单,但实现谓词会是更好的实践,尤其是在需要根据API中的动态属性处理搜索条件时。

结论

如前文所述,在某些情况下,Postgres的全文搜索支持可以作为Elasticsearch或Lucene等大型搜索引擎的良好替代方案。这可以避免我们在技术栈中添加第三方解决方案,从而减少额外的复杂性和成本。

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