本页面介绍了如何将模糊搜索用作全文搜索的一部分。
除了使用 SEARCH
和 SEARCH_SUBSTRING
函数执行精确 token 搜索之外,Spanner 还支持近似(或模糊)搜索。模糊搜索可查找匹配的文档,即使查询与文档之间存在细微差异。
Spanner 支持以下类型的模糊搜索:
- 基于 N 元语法的近似搜索
- 使用 Soundex 进行音标搜索
使用基于 N 元语法的近似搜索
基于 N 元语法的模糊搜索依赖于子字符串搜索所需的相同子字符串词元化。词元化器的配置非常重要,因为它会影响搜索质量和性能。以下示例展示了如何创建包含拼写错误或拼写不同的字词的查询,以便在搜索索引中查找近似匹配项。
架构
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_ngrams
和 spanner.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 元语法的近似搜索的性能和召回率
上一部分中的示例查询分两个阶段进行搜索,使用两个不同的函数:
SEARCH_NGRAMS
会查找与搜索查询共享 N 元语法的所有候选专辑。例如,“California”的三字母 N 元语法包括[cal, ali, lif, ifo, for, orn, rni, nia]
,“Kaliphorn”的三字母 N 元语法包括[kal, ali, lip, iph, pho, hor, orn]
。这些数据集中的共享 N 元语法为[ali, orn]
。默认情况下,SEARCH_NGRAMS
会与至少有两个共享 N 元语法的文档匹配,因此“Kaliphorn”与“California”匹配。SCORE_NGRAMS
按相似度对匹配项进行排名。两个字符串的相似度定义为不同共享 N 元语法与不同非共享 N 元语法的比率:
通常,SEARCH_NGRAMS
和 SCORE_NGRAMS
函数中的搜索查询是相同的。建议将该参数与查询参数(而非字符串字面量)一起使用,并在 SEARCH_NGRAMS
和 SCORE_NGRAMS
函数中指定相同的查询参数。
Spanner 有三个配置参数可与 SEARCH_NGRAMS
搭配使用:
- 使用
TOKENIZE_SUBSTRING
(/spanner/docs/reference/standard-sql/search_functions#tokenize_substring) 或TOKENIZE_NGRAMS
函数指定 N 元语法的最小和最大大小。我们不建议使用单个字符 N 元语法,因为它们可能会与大量文档匹配。另一方面,长 N 元语法会导致SEARCH_NGRAMS
忽略拼写错误的短单词。 SEARCH_NGRAMS
必须匹配的最小 N 元语法数量(使用SEARCH_NGRAMS
中的min_ngrams
和min_ngrams_percent
参数进行设置)。数字越大,查询速度通常越快,但召回率越低。
为了在性能和召回率之间取得良好的平衡,您可以配置这些参数,以适合特定的查询和工作负载。
我们还建议添加一个内部 LIMIT
,以避免在遇到热门 N 元语法组合时创建费用很高的查询。
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
此示例使用绑定到“Hatel Kaliphorn”的查询参数 $1
。
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 元语法的模糊搜索与增强型查询模式
除了基于 N 元语法的模糊搜索之外,增强型查询模式还可以处理一些拼写错误的字词。因此,这两项功能之间存在一些重叠。下表总结了一些区别:
基于 N 元语法的模糊搜索 | 增强型查询模式 | |
成本 | 需要执行基于 N 元语法的费用较高的子字符串词元化操作 | 需要执行费用较低的全文本词元化操作 |
搜索查询类型 | 适用于包含少字词的短文档,例如包含人名、城市名称或产品名称的文档 | 同样适用于任何大小的文档和任何大小的搜索查询 |
部分字词搜索 | 执行子字符串搜索,该搜索允许存在拼写错误 | 仅支持搜索整个字词(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);
以下查询会在 SOUNDEX
上将“stefan”与“Steven”进行匹配,并且 AlbumTitle
包含“cat”:
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')
后续步骤
- 了解词元化和 Spanner 词元化器。
- 了解搜索索引。
- 了解全文搜索查询。