当前位置:Java -> 使用Hibernate 6进行Postgres全文搜索

使用Hibernate 6进行Postgres全文搜索

Hibernate

Hibernate本身不具备全文搜索支持。 它必须依赖数据库引擎支持或第三方解决方案。

一个名为Hibernate Search的扩展集成了Apache LuceneElasticsearch(还可以集成OpenSearch)。

Postgres

Postgres自7.3版本以来就具有全文搜索功能。尽管它无法与Elasticsearch或Lucene等搜索引擎竞争,但它仍然提供了一个灵活而强大的解决方案,可能足以满足应用用户的期望 - 包括词干处理、排名和索引等功能。

我们将简要解释一下如何在Postgres中进行全文搜索。更多内容,请参阅Postgres文档。对于基本文本匹配,最关键的部分是数学运算符@@

如果文档(类型为tsvector的对象)与查询(类型为tsquery的对象)匹配,则返回true

运算符的顺序对于操作数并不重要。所以,无论我们将文档放在运算符的左侧,查询放在右侧,还是以不同的顺序,都无关紧要。

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

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


使用此类数据:

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对象是什么样子。

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


输出:

Output: to_tsvector

输出显示to_tsvcector如何将文本列转换为“english”文本搜索配置的tsvector对象。

文本搜索配置

在上面的例子中,to_tsvector函数的第一个参数是文本搜索配置的名称。 在这种情况下,它是“english”。 根据Postgres文档,文本搜索配置如下:

…全文搜索功能包括做很多其他事情的能力:跳过索引化某些单词(停用词)、处理同义词,并使用复杂的解析,例如,根据不止空格进行解析。 这个功能由文本搜索配置控制。

因此,配置是过程的重要部分,对我们的全文搜索结果至关重要。 对于不同的配置,Postgres引擎可以返回不同的结果。 这在不同语言的字典之间并不一定是必要的。 例如,您可以为同一种语言有两个配置,但其中一个会忽略包含数字的名称(例如,一些序列号)。 如果我们在查询中传递了要查找的特定序列号,这是强制性的,我们将无法在忽略带数字单词的配置中找到任何记录。即使我们的数据库中有这样的记录,请查看配置文档以获取更多信息。

文本查询

文本查询支持诸如&(AND)、|(OR)、!(NOT)和<->(FOLLOWED BY)等运算符。 前三个操作符不需要更深入的解释。 <->操作符检查单词是否存在,并且它们是否按特定顺序排列。 因此,例如,对于查询“rat <-> cat”,我们期望“cat”单词存在,并且紧接着是“rat”。

示例

  • 包含ratcat的内容:
SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ to_tsquery('english', 'Rat & cat');


Output: Rats and Cats hate each other

  • 包含databasemarket的内容,并且在database之后是第三个字的market:
SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ to_tsquery('english', 'database <3> market');


输出: Postgres 是市场上广泛使用的数据库之一

  • 包含数据库 但不包含Postgres:
SELECT t.id, t.short_content FROM tweet t WHERE to_tsvector('english', t.short_content) @@ to_tsquery('english', 'database & !Postgres');


输出: 在市场上有很多数据库具有类似的功能,比如 Oracle

  • 包含Postgres  Oracle:
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')

对于以下用法:

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


我们得到以下结果:

plainto_tsquery 的输出

phraseto_tsquery

phraseto_tsquery将所有传递的单词转换为查询,其中所有单词都使用<->(FOLLOW BY)运算符组合。例如,phraseto_tsquery('english', 'cat rule')的等效内容是to_tsquery('english', 'cat <-> rule')

对于以下用法:

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


我们得到以下结果:

输出: 猫统治着世界

websearch_to_tsquery

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

对于以下用法:

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


我们得到以下结果:

Output: short_content, character varying

Postgres 和 Hibernate 本地支持

如文章中所述,Hibernate 单独并没有全文搜索支持。它必须依赖于数据库引擎的支持。这意味着我们可以执行原生 SQL 查询,如下面的示例所示:

  • plainto_tsquery
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
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 与 posjsonhelper 库

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

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

<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

为了使用这个实现,需要一个名为 "org.hibernate.boot.model.FunctionContributor" 的文件,存放在 "resources/META-INF/services" 目录下。

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

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

PlainToTSQueryFunction

这是一个包装了 plainto_tsquery 函数的组件。

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',代码将生成下面的语句:

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 函数。

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',代码将生成下面的语句:

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 函数。

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',代码将生成下面的语句:

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这样的大型搜索引擎的一个很好的替代方案。这可以避免我们决定向技术堆栈中添加第三方解决方案,这样也可以避免增加更多复杂性和额外成本。

推荐阅读: 03. 数组中重复的数字

本文链接: 使用Hibernate 6进行Postgres全文搜索