使用Hibernate 6進行Postgres全文搜索

Hibernate

Hibernate本身並不具備全文搜索功能,需依賴數據庫引擎或第三方解決方案。

有一個擴展名為Hibernate Search,它整合了Apache LuceneElasticsearch(亦可與OpenSearch整合)。

Postgres

Postgres自版本7.3起便內建全文搜索功能。雖然它無法與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”。

示例

  • 包含rat cat:的內容:
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

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庫是一個開源項目,它為PostgreSQL JSON 函數和全文搜索添加了對 Hibernate 查詢的支援。

對於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 查詢。如需了解如何操作,請點擊此 連結

為何使用 posjsonhelper 庫而非直接使用 Hibernate 的原生方法?

雖然動態拼接一個 HQL 或 SQL 查詢字符串可能很簡單,但實現謂詞會是更好的做法,特別是當你需要根據 API 中的動態屬性處理搜索條件時。

結論

如前文所述,Postgres 的全文搜索支持在某些情況下可以是 Elasticsearch 或 Lucene 等大型搜索引擎的良好替代方案。這可以避免我們在技術堆棧中添加第三方解決方案,從而可能增加更多複雜性和額外成本。

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