使用模糊搜尋功能尋找近似相符的項目

本頁面說明如何在全文搜尋中使用模糊搜尋。

除了使用 SEARCHSEARCH_SUBSTRING 函式執行精確符記搜尋之外,Spanner 也支援近似 (或模糊) 搜尋。模糊搜尋可在查詢和文件之間存在細微差異的情況下,找出相符的文件。

Spanner 支援下列模糊搜尋類型:

  • 以 N-gram 為基礎的近似搜尋
  • 使用 Soundex 進行拼音搜尋

以 N-gram 為基礎的模糊搜尋功能會使用與子字串搜尋相同的子字串符號化功能。分析器的設定相當重要,因為它會影響搜尋品質和效能。以下範例說明如何建立含有拼錯或不同拼法的字詞的查詢,以便在搜尋索引中找出近似的字詞。

結構定義

GoogleSQL

CREATE TABLE Albums (
  AlbumId STRING(MAX) NOT NULL,
  AlbumTitle STRING(MAX),
  AlbumTitle_Tokens TOKENLIST AS (
    TOKENIZE_SUBSTRING(AlbumTitle, ngram_size_min=>2, ngram_size_max=>3,
                      relative_search_types=>["word_prefix", "word_suffix"])) HIDDEN
) PRIMARY KEY(AlbumId);

CREATE SEARCH INDEX AlbumsIndex
ON Albums(AlbumTitle_Tokens)
STORING (AlbumTitle);

PostgreSQL

本範例使用 spanner.tokenize_substring

CREATE TABLE albums (
  albumid character varying NOT NULL,
  albumtitle character varying,
  albumtitle_tokens spanner.tokenlist GENERATED ALWAYS AS (
    spanner.tokenize_substring(albumtitle, ngram_size_min=>2, ngram_size_max=>3,
                      relative_search_types=>'{word_prefix, word_suffix}'::text[])) VIRTUAL HIDDEN,
PRIMARY KEY(albumid));

CREATE SEARCH INDEX albumsindex
ON albums(albumtitle_tokens)
INCLUDE (albumtitle);

查詢

以下查詢會找出標題最接近「Hatel Kaliphorn」的專輯,例如「Hotel California」。

GoogleSQL

SELECT AlbumId
FROM Albums
WHERE SEARCH_NGRAMS(AlbumTitle_Tokens, "Hatel Kaliphorn")
ORDER BY SCORE_NGRAMS(AlbumTitle_Tokens, "Hatel Kaliphorn") DESC
LIMIT 10

PostgreSQL

本範例使用 spanner.score_ngramsspanner.search_ngrams

SELECT albumid
FROM albums
WHERE spanner.search_ngrams(albumtitle_tokens, 'Hatel Kaliphorn')
ORDER BY spanner.score_ngrams(albumtitle_tokens, 'Hatel Kaliphorn') DESC
LIMIT 10

針對以 n-gram 為基礎的近似搜尋,最佳化效能和回傳

上一節中的範例查詢會分兩個階段進行搜尋,並使用兩個不同的函式:

  1. SEARCH_NGRAMS 會找出與搜尋查詢共用 n-gram 的所有候選專輯。舉例來說,「California」的三字 n-gram 包括 [cal, ali, lif, ifo, for, orn, rni, nia],而「Kaliphorn」的三字 n-gram 則包括 [kal, ali, lip, iph, pho, hor, orn]。這些資料集中的共用 n-gram 為 [ali, orn]。根據預設,SEARCH_NGRAMS 會比對所有至少有兩個共用 n-gram 的文件,因此「Kaliphorn」會與「California」相符。
  2. SCORE_NGRAMS 會依相似度排序相符項目。兩個字串的相似度定義為不重複的共用 n-gram 與不重複的非共用 n-gram 的比率:
$$ \frac{shared\_ngrams}{total\_ngrams_{index} + total\_ngrams_{query} - shared\_ngrams} $$

通常 SEARCH_NGRAMSSCORE_NGRAMS 函式都會使用相同的搜尋查詢。建議您使用帶有查詢參數的引數,而非字串文字常值,並在 SEARCH_NGRAMSSCORE_NGRAMS 函式中指定相同的查詢參數。

Spanner 有三個可與 SEARCH_NGRAMS 搭配使用的設定引數:

  • 使用 TOKENIZE_SUBSTRING(/spanner/docs/reference/standard-sql/search_functions#tokenize_substring) 或 TOKENIZE_NGRAMS 函式,即可指定 n-gram 的最小和最大大小。我們不建議使用單一字元的 n-gram,因為這類字元可能會與大量文件相符。另一方面,長 n-gram 會導致 SEARCH_NGRAMS 遺漏短的錯字。
  • SEARCH_NGRAMS 必須比對的 n-gram 最小數量 (使用 SEARCH_NGRAMS 中的 min_ngramsmin_ngrams_percent 引數設定)。數量越高,查詢速度通常越快,但喚回率會降低。

為了在效能和召回率之間取得良好平衡,您可以根據特定查詢和工作負載設定這些引數。

我們也建議您加入內部 LIMIT,以免在遇到熱門 n-gram 組合時,產生非常昂貴的查詢。

GoogleSQL

SELECT AlbumId
FROM (
  SELECT AlbumId,
        SCORE_NGRAMS(AlbumTitle_Tokens, @p) AS score
  FROM Albums
  WHERE SEARCH_NGRAMS(AlbumTitle_Tokens, @p)
  LIMIT 10000  # inner limit
)
ORDER BY score DESC
LIMIT 10  # outer limit

PostgreSQL

本範例使用查詢參數 $1,該參數已繫結至「Hatel Kaliphorn」。

SELECT albumid
FROM
  (
    SELECT albumid, spanner.score_ngrams(albumtitle_tokens, $1) AS score
    FROM albums
    WHERE spanner.search_ngrams(albumtitle_tokens, $1)
    LIMIT 10000
  ) AS inner_query
ORDER BY inner_query.score DESC
LIMIT 10

以 N-gram 為基礎的模糊搜尋與強化查詢模式

除了以 n-gram 為基礎的模糊搜尋功能外,強化查詢模式也能處理部分拼錯的字詞。因此,這兩項功能有些重疊。下表總結了兩者的差異:

以 n-gram 為基礎的模糊搜尋 強化查詢模式
費用 需要以 n-gram 為基礎,進行較耗費的子字串符號化 需要較便宜的全文符記化
搜尋查詢類型 適用於只有幾個字的短文件,例如人名、城市名稱或產品名稱 無論文件大小或搜尋查詢大小為何,都能順利執行
部分字詞搜尋 執行子字串搜尋,允許輸入錯別字 僅支援搜尋整個字詞 (SEARCH_SUBSTRING 不支援 enhance_query 引數)
錯字 支援索引或查詢中的拼字錯誤字詞 僅支援查詢中的錯字
更正 找出任何拼錯的字詞比對結果,即使比對結果不是實際字詞也一樣 修正常見字詞的拼寫錯誤

使用 Soundex 進行音譯搜尋

Spanner 提供 SOUNDEX 函式,用於找出拼法不同但發音相同的字詞。例如 SOUNDEX("steven")SOUNDEX("stephen")SOUNDEX("stefan") 都是「s315」,而 SOUNDEX("stella") 則是「s340」。SOUNDEX 會區分大小寫,且只適用於以拉丁字母為基礎的字母。

您可以使用產生的資料欄和搜尋索引,實作含有 SOUNDEX 的語音搜尋功能,如以下範例所示:

GoogleSQL

CREATE TABLE Singers (
  SingerId INT64,
  AlbumTitle STRING(MAX),
  AlbumTitle_Tokens TOKENLIST AS (TOKENIZE_FULLTEXT(AlbumTitle)) HIDDEN,
  Name STRING(MAX),
  NameSoundex STRING(MAX) AS (LOWER(SOUNDEX(Name))),
  NameSoundex_Tokens TOKENLIST AS (TOKEN(NameSoundex)) HIDDEN
) PRIMARY KEY(SingerId);

CREATE SEARCH INDEX SingersPhoneticIndex ON Singers(AlbumTitle_Tokens, NameSoundex_Tokens);

PostgreSQL

本範例使用 spanner.soundex

CREATE TABLE singers (
  singerid bigint,
  albumtitle character varying,
  albumtitle_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.tokenize_fulltext(albumtitle)) VIRTUAL HIDDEN,
  name character varying,
  namesoundex character varying GENERATED ALWAYS AS (lower(spanner.soundex(name))) VIRTUAL,
  namesoundex_tokens spanner.tokenlist GENERATED ALWAYS AS (spanner.token(lower(spanner.soundex(name))) VIRTUAL HIDDEN,
PRIMARY KEY(singerid));

CREATE SEARCH INDEX singersphoneticindex ON singers(albumtitle_tokens, namesoundex_tokens);

以下查詢會將「stefan」與 SOUNDEX 上的「Steven」比對,以及包含「cat」的 AlbumTitle

GoogleSQL

SELECT SingerId
FROM Singers
WHERE NameSoundex = LOWER(SOUNDEX("stefan")) AND SEARCH(AlbumTitle_Tokens, "cat")

PostgreSQL

SELECT singerid
FROM singers
WHERE namesoundex = lower(spanner.soundex('stefan')) AND spanner.search(albumtitle_tokens, 'cat')

後續步驟