想象你管理着一座藏书十万册的图书馆。某天读者来问:“有没有讲数据库性能的书?”新手管理员怎么办呢?他只能走到每一排书架前,一本一本地翻开目录页,翻完十万本,才敢告诉你答案——这就像数据库里的 LIKE '%关键词%' 全表扫描,慢得让人绝望。可一位老管理员会直接走向大厅角落的检索目录柜:柜子里每个关键词都对应一张卡片,卡片上清清楚楚地写着哪些书、在第几页提到过它。一分钟不到,答案就出来了。PostgreSQL 全文搜索要做的,正是给数据库这座图书馆装上这样一座“检索目录柜”。 这个柜子由四件套组成:tsvector 是编目员写下的目录卡,tsquery 是读者填写的查询单,GIN 索引是存放卡片的大柜子,中文分词则是专门处理中文书籍的编目规则。把这四件套装配起来,你不需要搬来 Elasticsearch 那套重型设备,也能在数据库里完成中等规模的搜索。这篇文章就用“图书馆”这个比喻,带你从原理到实践,完整走一遍 PostgreSQL 全文搜索的搭建过程。

岗位关联

对后端开发者而言,掌握 PG 全文搜索意味着多一套“轻量搜索”的武器库:处理百万级数据的中小型项目不必引入 ES;同时能更清醒地判断何时该升级到专用搜索引擎,避免架构过度设计。

目录
  1. 可视化演示
  2. 搜索原理:从翻书到查目录
  3. tsvector 词向量:编目员写下的目录卡
  4. tsquery 查询:读者填写的查询单
  5. GIN 索引:装卡片的大柜子
  6. 中文分词:给中文书籍编目
  7. 排序调优:按推荐度排书架
  8. 实践应用:何时换大设备
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示PostgreSQL全文搜索的核心机制,通过输入/处理/存储/输出四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

PostgreSQL全文搜索:Post PostgreSQL全文搜索核 tsvector文档表示与ts 核心机制可视化 PostgreSQL全文搜索整体架构与关键组件

搜索原理:从翻书到查目录

老管理员找书,靠的不是把每本书翻一遍,而是大厅角落那排抽屉柜。柜子里插满卡片,每张卡片顶端写着一个关键词,下面密密麻麻排着书号。想找“数据库”相关的书,抽出“数据库”这张卡片,书号直接出现在眼前。这套机制的学名叫倒排索引——正常索引是“书 → 关键词”的正向关系,而倒排索引把方向反过来,建成“关键词 → 书籍列表”,正好匹配“按内容找书”的检索场景。

PostgreSQL 的全文搜索正是围绕倒排索引设计的。当你用 LIKE '%数据库%' 搜索时,数据库必须逐行扫描、逐行做字符串匹配,就像新手管理员翻书;而全文搜索先把每篇文档拆解成词条(这个过程叫词项归一化:转小写、去停用词、还原词根),再将这些词条连同文档编号一起存入倒排索引,查询时只需查找索引。

为了让结果更聪明,搜索引擎还引入了相关度打分:一篇文档里关键词出现得越多、这个词在全库越罕见,这篇文档就越“相关”。这就是 TF-IDF 的核心思想:

目录柜的想法很朴素,但正是这套“关键词 → 书号”的反向映射,让全文搜索比翻书快上几个数量级。接下来我们看 PostgreSQL 如何把一本书变成一张目录卡——这就是 tsvector。

weight(t,d)=tf(t,d)×ln⁡(Ndf(t))\displaystyle \text{weight}(t,d) = \text{tf}(t,d) \times \ln\left(\frac{N}{\text{df}(t)}\right)
这个公式衡量“关键词 t 在文档 d 中有多重要”:出现得多加分,到处都出现的常见词则降权。
  1. 词频 tf(t,d)统计词条 t 在文档 d 里出现的次数。一篇文章里“数据库”出现十次,比出现一次更可能是在讲数据库。
    tf(t,d)=count(t∈d)\displaystyle \text{tf}(t,d) = \text{count}(t \in d)
  2. 逆文档频率 idf(t)用文档总数 N 除以包含词 t 的文档数再取对数。“数据库”只出现在 100 篇里,比出现在 90000 篇里的“的”更能区分主题。
    idf(t)=ln⁡(Ndf(t))\displaystyle \text{idf}(t) = \ln\left(\frac{N}{\text{df}(t)}\right)
  3. 综合权重两者相乘,得到词条在文档中的最终权重——既看局部出现密度,也看全局稀有程度。
    weight(t,d)=tf(t,d)×idf(t)\displaystyle \text{weight}(t,d) = \text{tf}(t,d) \times \text{idf}(t)

tsvector 词向量:编目员写下的目录卡

老管理员从不整本抄书。他拿过一本书,翻一翻,提炼出几十个关键词,每个词后面标注它出现在第几页,再按重要程度贴上不同颜色的标签。PostgreSQL 里的 tsvector 就是这张目录卡:把一整篇文本压缩成“词素 + 位置 + 权重”的集合,体积小、检索快。

创建目录卡的工具是 to_tsvector 函数。它会先调用分词器把文本拆成词,再做归一化(英文转小写、去词尾变化),最后记录每个词在原文里的位置。看一个例子:

注意 jumps 变成了 jump,lazy 变成了 lazi,这就是词根还原。每个词后面的数字是位置编号,而权重(A/B/C/D)可以后续手动指定,比如标题里的词标为 A,正文里的词标为 D。数学上,tsvector 就是一组三元组的集合:

目录卡建好了,读者要怎样描述需求呢?这就轮到 tsquery 登场。

tsvector(d)={(l,p,w)∣l∈lexemes(d), p∈N+, w∈{A,B,C,D}}\displaystyle \text{tsvector}(d) = \{ (l, p, w) \mid l \in \text{lexemes}(d),\ p \in \mathbb{N}^+,\ w \in \{A,B,C,D\} \}
tsvector 不是简单存词,而是存“词素、位置、权重”三条信息,为短语匹配和排序打分做准备。
  1. 词素 l词经过归一化后的标准形态,不同写法在这里汇成同一个词条。录卡时“running“和”ran“会统一成”run”。
    l=normalize(word)\displaystyle l = \text{normalize}(\text{word})
  2. 位置 p记录词在文档中第几个出现,短语匹配和距离运算都靠它知道“高性能”和“数据库”是否紧挨着。
    pi=position of li in doc\displaystyle p_i = \text{position of } l_i \text{ in doc}
  3. 权重 wA/B/C/D 四个等级,A 最重、D 最轻。你可以在tsvector构造时给不同字段赋不同权重,影响排序。
    w∈{A,B,C,D}\displaystyle w \in \{A, B, C, D\}
sql2 行
SELECT to_tsvector('english', 'The quick brown fox jumps over the lazy dog');-- 结果: 'fox':4  'jump':5  'lazi':8  'quick':2

tsquery 查询:读者填写的查询单

读者在查询单上写:“我要数据库 和 性能相关的书,但 不要 讲缓存的。”管理员一看就能明白:取“数据库”和“性能”两张卡片的交集,再排除“缓存”卡片上的书。tsquery 就是这张结构化查询单,它支持 &(同时满足,即 AND)、|(任一满足,即 OR)、!(排除,即 NOT)三种逻辑运算。

构造查询用 to_tsquery,它和 to_tsvector 使用同一个分词器,保证查询词和文档词用同一套规则归一化:

to_tsquery 还支持两种进阶写法:用 <-> 表示“两个词必须相邻出现”(短语匹配),如 高性能 <-> 数据库;用 :* 表示前缀匹配,如 数据:* 能命中“数据”“数据库”“数据结构”。查询的匹配条件本质上是一个布尔表达式:

有了查询单,图书馆里的目录柜得够快才行。GIN 索引就是这个“快”的关键。

Match(d,Q) where Q=(l1∧l2)∨¬l3\displaystyle \text{Match}(d, Q) \ \text{where} \ Q = (l_1 \wedge l_2) \vee \neg l_3
查询单必须写成结构化表达式,数据库才能把“找书需求”翻译成对倒排索引的一次快速查找。
  1. 查询词素 l_i查询词先经过与文档相同的分词归一化,保证“查询单语言”和“目录卡语言”一致,否则两边对不上。
    li=normalize(query wordi)\displaystyle l_i = \text{normalize}(\text{query word}_i)
  2. 与运算 ∧要求文档同时包含两个词,对应 & 符号。需求越限越严,结果越精准。
    l1∧l2\displaystyle l_1 \wedge l_2
  3. 或运算 ∨包含任意一个词即可,对应 | 符号,扩大召回范围。
    l1∨l2\displaystyle l_1 \vee l_2
  4. 非运算 ¬排除包含某词的文档,对应 ! 符号,用于过滤干扰项。
    ¬l3\displaystyle \neg l_3
sql2 行
SELECT to_tsquery('english', 'postgres & (performance | index)');-- 结果: 'postgres' & ( 'perform' | 'index' )

GIN 索引:装卡片的大柜子

目录卡片如果随意堆在桌上,找起来一样慢。PostgreSQL 用 GIN(Generalized Inverted Index) 索引来存放 tsvector 中的词条。它的内部结构是:每个词条对应一个倒排表,表中记录哪些文档包含这个词。查询时只需要两步:先找到词条,再取出这个文档列表。

GIN 基于 B-tree 组织词条,所以定位词条的复杂度是 O(log n);随后取文档列表的耗时正比于命中文档数。于是整体查询代价是:

创建 GIN 索引很简单,但要注意:通常先把文本转成独立的 tsvector 列,再对该列建索引,避免每次查询都现场转换:

GIN 的代价是“写放大”:每插入一篇文章,要更新多个词条的倒排表,写入比普通 B-tree 索引更慢。查询快、写入慢,这就是目录柜的保养费。中文书籍进馆时还有一个特殊问题——中文没有空格,编目员怎么切词?

Tsearch(q)=O(log⁡∣L∣+k)\displaystyle T_{\text{search}}(q) = O(\log |L| + k)
|L| 是所有不同词条的数量,k 是命中结果数。GIN 的厉害之处在于:无论全库有 10 万还是 100 万篇文档,定位词条都是对数级时间。
  1. 倒排表 PL(l)词条 l 对应的文档编号集合,建索引时由 tsvector 自动生成。
    PL(l)={d∣l∈lexemes(d)}\displaystyle \text{PL}(l) = \{ d \mid l \in \text{lexemes}(d) \}
  2. 词条定位 O(log|L|)GIN 内部用 B-tree 组织所有不重复词条,查找一个词只需走树高,而非扫全表。
    Tlookup=O(log⁡∣L∣)\displaystyle T_{\text{lookup}} = O(\log |L|)
  3. 结果回表 O(k)拿到文档编号列表后,回原表取出整行数据,耗时与命中数 k 线性相关。
    Tfetch=O(k)\displaystyle T_{\text{fetch}} = O(k)
sql3 行
ALTER TABLE articles ADD COLUMN body_tsv tsvector;UPDATE articles SET body_tsv = to_tsvector('english', body);CREATE INDEX idx_gin_body ON articles USING GIN (body_tsv);

中文分词:给中文书籍编目

英文图书的词汇天然被空格分开,编目员照抄即可。但中文的一句“研究生命科学”可以切出“研究/生命/科学”,也可能被切出“研究生/命/科学”,含义天差地别。PostgreSQL 默认的分词器只认识空格和标点,面对连续汉字会不知所措,所以中文搜索必须引入专门的分词插件。

主流的两个方案是 zhparser(基于 SCWS 分词引擎)和 pg_jieba(结巴分词的 PG 移植)。它们的原理类似:在预置词典的基础上,用统计语言模型挑选概率最大的切分方式。给定句子 S,分词器在所有可能的切分中寻找累计概率最高的一种:

实际配置时,装好扩展后需要创建名为 zhparser 或 jieba 的全文搜索配置,并在每次建 tsvector 时指定它。整套配置只在会话内生效,重启数据库后仍需设置 search_path——这也是后面误区部分要提醒的重点。切好词、建好目录卡,最后一个问题浮出水面:命中的十本书里,哪本最值得读?

S∗=arg⁡max⁡S∏i=1mP(wi)\displaystyle S^* = \arg\max_{S} \prod_{i=1}^{m} P(w_i)
分词不是“找唯一正确答案”,而是“在所有合法切法里选最像人话的”——出现频率高的词组合胜出。
  1. 候选切分 S一句话往往有多个合法切分,如“数据库/性能”或“数据/库/性能”都是候选。
    S={w1,w2,…,wm}\displaystyle S = \{w_1, w_2, \dots, w_m\}
  2. 词概率 P(w_i)每个词在大量语料中出现的频率,常见词如“数据”概率远高于生僻组合。
    P(wi)=count(wi)total words\displaystyle P(w_i) = \frac{\text{count}(w_i)}{\text{total words}}
  3. 最优切分 S\*遍历所有候选切分,把词概率连乘,乘积最大的就是最自然的分法。
    S∗=arg⁡max⁡S∏i=1mP(wi)\displaystyle S^* = \arg\max_{S} \prod_{i=1}^{m} P(w_i)

排序调优:按推荐度排书架

当“数据库 + 性能”命中三十本书时,管理员不能把这三十本一股脑堆给读者,他得按推荐度排个序:整本专讲数据库性能的书排最前,只在第一章提到一句的排最后。PostgreSQL 用 ts_rank 函数给每篇命中文档打分,分高者排在前面。

打分的依据有两个:一是词条在文档里的权重等级(A 比 D 贡献大),二是词条在文档中的出现频次。setweight 可以给已构造的 tsvector 批量赋权,比如标题字段设为 A、正文字段设为 D:

一个常见技巧是用 COALESCE 合并多个向量:setweight(to_tsvector(title),'A') || setweight(to_tsvector(body),'D'),这样标题命中的文档自然排在正文命中的前面。简化后的排序模型是:

排序规则明确后,全文搜索的整套工具链就齐了。但还有一个问题藏在幕后:这套方案到底适合什么规模?什么时候该把书搬进更大的图书馆?

R(d,q)=∑t∈qweight(t)×occ(t,d)\displaystyle R(d,q) = \sum_{t \in q} \text{weight}(t) \times \text{occ}(t,d)
ts_rank 对所有命中词条加权求和:权重等级决定“哪一处的命中更值钱”,出现频次决定“命中的密度有多高”。
  1. 命中词条 t ∈ q只有查询中出现、且文档里也存在的词条参与计分,两者缺一不计。
    t∈(q∩lexemes(d))\displaystyle t \in (q \cap \text{lexemes}(d))
  2. 权重 weight(t)A/B/C/D 等级的数值化体现,A 级命中贡献远大于 D 级,是调优的主要旋钮。
    weight(t)∈[0.1,1.0]\displaystyle \text{weight}(t) \in [0.1, 1.0]
  3. 频次 occ(t,d)词条在文档中出现的次数,出现越多说明词条与文档主题关联越紧密。
    occ(t,d)=∣{p:(t,p,w)∈tsvector(d)}∣\displaystyle \text{occ}(t,d) = |\{p : (t,p,w) \in \text{tsvector}(d)\}|
  4. 累加求和 Σ把每个命中词条的贡献相加,得到文档的总相关度。
    R(d,q)=∑t∈qweight(t)×occ(t,d)\displaystyle R(d,q) = \sum_{t \in q} \text{weight}(t) \times \text{occ}(t,d)
sql5 行
SELECT title, ts_rank(body_tsv, query) AS rankFROM articles,     to_tsquery('english', 'database & performance') AS queryWHERE body_tsv @@ queryORDER BY rank DESC;

实践应用:何时换大设备

社区图书馆藏书一万本,配一个抽屉目录柜绰绰有余;可如果藏书千万册、高峰时段每分钟来一千个读者,手抄卡片柜就撑不住了——该上自动化检索系统了。PostgreSQL 全文搜索与 Elasticsearch 的分界线,本质上是数据量与并发量的分界线。

经验上,百万行以内的数据、峰值百 QPS 以内的查询,PG 全文搜索完全能胜任:它不需要引入额外服务,支持事务,数据一致性有保障。而 Elasticsearch 适合海量数据、高并发、需要复杂聚合分析、滚动搜索等场景。判断是否升级,可以套用一个粗略的决策式:

无论是否升级,PG 内部的优化都不可少:定期 VACUUM 控制 GIN 索引膨胀、把 tsvector 独立成列避免重复计算、用 EXPLAIN ANALYZE 确认索引真的被使用。理解了这些边界,你就能像那位老管理员一样——小图书馆用抽屉柜,大图书馆用自动化系统,心里始终有本明白账。

Need_ES  ⟺  Ndocs>106 ∨ QPSpeak>100\displaystyle \text{Need\_ES} \iff N_{\text{docs}} > 10^6 \ \lor\ \text{QPS}_{\text{peak}} > 100
这是一个“够用就好”的决策公式:数据量和并发量都没破阈值时,PostgreSQL 是架构更简单的选择。
  1. 文档量 N_docs可被搜索的文本行总数。GIN 索引能支撑百万级,再往上索引膨胀和维护成本会陡增。
    Ndocs=COUNT(searchable rows)\displaystyle N_{\text{docs}} = \text{COUNT}(\text{searchable rows})
  2. 峰值 QPS业务高峰期的每秒查询次数,超过百级后 PG 的排序开销和连接数可能成为瓶颈。
    QPSpeak=max⁡(queries per second)\displaystyle \text{QPS}_{\text{peak}} = \max(\text{queries per second})
  3. 布尔决策两个条件任意满足,就值得引入专用搜索引擎;否则继续用 PG,把架构保持轻量。
    Need_ES  ⟺  (N>106)∨(QPS>100)\displaystyle \text{Need\_ES} \iff (N > 10^6) \lor (\text{QPS} > 100)

AI 应用场景

内容管理系统的站内搜索

阶段:数据存储与检索层优化

解决的问题:传统 CMS 的站内搜索用 LIKE '%关键词%',数据量过万后响应延迟飙升至秒级,且无法按相关度排序。PostgreSQL 全文搜索将文章标题与正文分别转成 tsvector,通过 GIN 索引将查询时间压缩到毫秒级,还能用 setweight 让标题命中排在正文命中之前。

具体案例:某技术博客平台有 3 万篇文章,读者在搜索框输入“数据库索引优化”,系统用 to_tsquery('zhparser', '数据库 & 索引') 匹配,结合 ts_rank 排序后,把最相关的五篇文章呈现在第一页。整个过程无需引入 ES,架构保持单库。

中小型电商的商品搜索

阶段:特征匹配与排序服务

解决的问题:电商商品名和描述是典型的海量短文本,传统方案要么用数据库 LIKE 硬扛,要么过早引入 ES 增加运维负担。PG 全文搜索支持分词、逻辑组合、前缀匹配,足以支撑商品名检索和标签筛选组合查询。

具体案例:某跨境电商平台有 50 万商品,运营希望用户搜“无线 耳机”能同时命中“无线蓝牙耳机”和“头戴式无线耳机”。通过 pg_jieba 分词 + GIN 索引 + 权重排序,平台在守住 100 QPS 峰值的前提下,保持了 200ms 以内的搜索响应,且无需维护额外搜索集群。

Python 演示

python59 行
"""模拟 PostgreSQL 全文搜索:构建倒排索引并执行查询。"""import mathimport re STOPWORDS = {"de", "le", "et", "en", "un", "une"}  def tokenize(text):    """简易分词:英文按单词,中文按单字 + 相邻二元组。"""    text = text.lower()    latin = re.findall(r"[a-z0-9]+", text)    han = re.findall(r"[\u4e00-\u9fff]", text)    bigrams = [han[i] + han[i + 1] for i in range(len(han) - 1)]    words = [w for w in latin if w not in STOPWORDS] + han + bigrams    return words  def build_index(docs):    """返回倒排索引:term -> {doc_id: [positions]}。"""    index = {}    for doc_id, doc in enumerate(docs):        terms = tokenize(doc["content"])        for pos, term in enumerate(terms):            if term not in index:                index[term] = {}            if doc_id not in index[term]:                index[term][doc_id] = []            index[term][doc_id].append(pos)    return index  def search(query, docs, index, top_k=3):    """执行布尔查询,返回 (doc_id, score) 列表。"""    q_terms = set(tokenize(query))    scores = {}    for term in q_terms:        if term not in index:            continue        entries = index[term]        idf = math.log(len(docs) / (len(entries) + 1)) + 1        for doc_id, positions in entries.items():            scores[doc_id] = scores.get(doc_id, 0) + idf * len(positions)    ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)    return [(doc_id, round(score, 3)) for doc_id, score in ranked[:top_k]]  if __name__ == "__main__":    docs = [        {"title": "PostgreSQL 优化指南", "content": "数据库 索引 性能 优化 查询"},        {"title": "全文搜索原理", "content": "全文 检索 倒排 索引 数据库"},        {"title": "Redis 缓存实践", "content": "缓存 性能 数据库 并发"},        {"title": "搜索引擎设计", "content": "全文 搜索 索引 排序"},        {"title": "GIN 索引原理", "content": "索引 数据库 搜索 性能"},        {"title": "中文分词技术", "content": "分词 中文 全文 检索"},    ]    index = build_index(docs)    print("倒排索引片段:", {k: v for k, v in list(index.items())[:3]})    for doc_id, score in search("数据库 索引", docs, index):        print("命中:", docs[doc_id]["title"], "score =", score)
目的:用几十行代码还原 PG 全文搜索的四步流程——分词、建倒排索引、查索引、算相关度,帮你理解 GIN 索引为何比全表扫描快。
  1. latin = re.findall(r"[a-z0-9]+", text)先抽出英文单词和数字,模拟英文分词。
  2. bigrams = [han[i] + han[i + 1] for i in range(len(han) - 1)]中文按相邻两字组合成词,模拟简单二元分词。
  3. index[term][doc_id].append(pos)这就是在搭建倒排表:每个词条记录它出现在哪些文档、哪些位置。
  4. idf = math.log(len(docs) / (len(entries) + 1)) + 1计算逆文档频率,词条越罕见权重越高。
  5. scores[doc_id] = scores.get(doc_id, 0) + idf * len(positions)累加每个词条的贡献,相当于本文的 ts_rank 简化版。
  6. ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)按分数降序排列,返回最相关的 Top-K 篇文档。

常见误区

误区“我明明建了 GIN 索引,为什么查询还是慢?是不是索引没生效?” 于是开始怀疑 PostgreSQL 的优化器。

正解绝大多数情况是查询写法不匹配索引。比如对原始 body 列用 to_tsvector(body) @@ query 写法时,由于索引建在独立列 body_tsv 上,表达式没有直接指向索引列,优化器无法使用索引。应该把 tsvector 存成独立列再建索引,或者用表达式索引。这就像目录柜里的卡片按“书名”排序,你却拿着“作者”去查,柜子当然帮不上忙。

为何常见:很多人把“建索引”等同于“查询自动加速”,忽略了索引必须与查询表达式对齐这一前提。

误区“都是搜关键词,'%数据库%' 和全文搜索差不多嘛,LIKE 还简单。” 在数据量小的时候确实感觉不到差别。

正解LIKE '%关键字%' 无法利用索引(除非使用 pg_trgm 扩展),每查一次都要全表扫描;更致命的是它不支持分词、不支持相关度排序,也无法处理“数据库 性能”这种多词逻辑组合。全文搜索和 LIKE 是两种工具:一个是专业的目录柜,一个是手工翻书,性能与能力都不在同一量级。

为何常见:小数据量下 LIKE 的慢被掩盖了,等到数据量增长才发现问题,此时改动成本已经很高。

误区“我安装并配置好 zhparser 了,当时查询没问题,怎么重启数据库后中文分词又不工作了?” 以为一劳永逸。

正解zhparser 创建的解析器和配置存储在数据库中,但 search_path 是会话级设置。重启后如果搜索路径里没有包含扩展所在的 schema(通常是 public 或专有 schema),查询就找不到分词配置。需要在每次连接时显式设置 search_path,或者在数据库级别统一配置,否则就像编目规则册被锁进柜子,别人不知道去哪查。

为何常见:把“会话配置”误当成“持久化配置”,忽略了 PostgreSQL 中 search_path 按会话生效的机制。

误区“GIN 索引是只读的加速结构,建好之后不用管,反正它会自动更新。” 于是从不维护,直到查询越来越慢。

正解GIN 索引会随插入、更新产生索引膨胀,删除的词条并不会立刻从倒排表中消失。需要定期 VACUUM 回收垃圾空间,必要时 REINDEX 重建索引。此外,大量并发插入时 GIN 有 pending list 机制,可能导致查询短暂变慢。索引和仓库里的目录柜一样,用久了要清理、要除尘。

为何常见:B-tree 索引自动维护得比较“省心”,让人误以为所有索引都无需人工关怀,忽略了 GIN 倒排结构的维护特性。

面试题

Q1. PostgreSQL 全文搜索最推荐使用哪种索引?

单选题 · easy

  • A.B-tree 索引树
  • B.GIN 倒排索引← 正确答案
  • C.Hash 索引桶
  • D.BRIN 索引页

答案:B。GIN 是专门为全文搜索设计的倒排索引,适合存放 tsvector 中的海量词条。B-tree 适合范围查询和等值查询,Hash 只能做等值匹配,BRIN 适合按物理顺序排列的大表。就像目录柜必须能快速按关键词找到书——GIN 正是这个抽屉柜。

Q2. to_tsvector('english', 'The Quick Foxes') 的词项归一化结果是?

单选题 · easy

  • A.保留原词大小写复数
  • B.全部小写并还原词形← 正确答案
  • C.去掉所有字母只留空格
  • D.切分后保持原样不动

答案:B。全文搜索要求文档词和查询词用同一套规则归一,The 变 the、Foxes 还原为 fox,否则查询时“Fox“和”foxes“会被当成两个词。这和图书馆编目时把”数据库“与”DataBase”归为同一主题是同一个道理。

Q3. tsquery 中符号 & 表示什么逻辑关系?

单选题 · easy

  • A.表示逻辑与操作← 正确答案
  • B.表示逻辑或操作
  • C.表示逻辑非操作
  • D.表示距离短语

答案:A。& 对应 AND,要求文档同时包含两侧词条;| 对应 OR,! 对应 NOT。用查询单来类比:& 就是“我既要有数据库,又要有性能”的并列需求,缺一不可。

Q4. GIN 索引相对顺序扫描的核心优势是?

单选题 · mid

  • A.压缩索引占用空间
  • B.避免写入锁冲突
  • C.快速定位命中行← 正确答案
  • D.支持所有数据类型

答案:C。GIN 把“关键词 → 文档列表”预先存好,查询时直接取出列表,无需逐行扫描。它并不压缩空间、不解决写锁问题,也并非所有类型都支持。就像老管理员直接翻目录卡,而不是一本一本翻书。

Q5. 中文全文搜索为何需要扩展分词器?

单选题 · mid

  • A.默认分词器仅识别英文
  • B.中文文本缺少空格分界← 正确答案
  • C.PostgreSQL 不支持汉字
  • D.中文不使用数据库存储

答案:B。PG 默认分词器按空格和标点切分,而中文词语之间没有天然分界,“数据库性能”需要被切成“数据库/性能”而非“数/据/库”才能被正确索引。分词扩展提供了词典和统计模型来解决这个问题。

Q6. 下列哪些属于全文检索的词项归一化处理?

多选题 · mid

  • A.统一转为小写← 正确答案
  • B.去除停用词汇← 正确答案
  • C.还原词根形式← 正确答案
  • D.建立回表查询

答案:ABC。归一化是把不同写法归一到统一词素:转小写让“PostgreSQL“和”postgresql“同义,去停用词去掉”的”“了”,还原词根让“running“归为”run”。回表查询是索引命中后的取数据阶段,不属于归一化。归一化就是编目规则的统一化,确保目录卡的写法保持一致。

Q7. ts_rank 相关性排序时主要考虑哪些因素?

多选题 · mid

  • A.词频高低← 正确答案
  • B.权重等级← 正确答案
  • C.短语距离← 正确答案
  • D.硬件配置

答案:ABC。ts_rank 综合词条出现频次、权重等级(A/B/C/D)和短语匹配情况来计算相关度。词频越高、权重越大、短语距离越短,排名越靠前。硬件配置只影响查询速度,不影响结果排序——就像推荐度取决于书的内容相关性,而不是书架材质。

Q8. tsquery 中哪个写法能匹配“高性能数据库”这种连续短语?

单选题 · hard

  • A.高性能 & 数据库
  • B.高性能 <-> 数据库← 正确答案
  • C.高性能 | 数据库
  • D.高性能!数据库

答案:B。<-> 表示两个词相邻出现,是短语匹配的核心语法;& 只要求同时出现,两词可能相隔很远;| 是任一满足;! 是排除。查询单上写明“高性能紧挨着数据库”,管理员才知道要找的是这个连续短语。

Q9. GIN 索引的维护会带来哪些成本?

多选题 · hard

  • A.写入放大← 正确答案
  • B.索引膨胀← 正确答案
  • C.全表扫描
  • D.顺序排序

答案:AB。GIN 在插入一行时要更新多个词条的倒排表,产生写入放大;删除和更新后旧词条仍占据空间,引发索引膨胀,需靠 VACUUM 和 REINDEX 治理。全表扫描是没索引时的做法,顺序排序是查询结果处理,都不是 GIN 的维护成本。

Q10. 关于 PG 全文搜索与 Elasticsearch 的取舍,哪项正确?

单选题 · extreme

  • A.任何规模都该用 ES
  • B.PG 完全无法检索
  • C.百万行内 PG 可胜任← 正确答案
  • D.ES 不支持全文索引

答案:C。PostgreSQL 全文搜索在百万行数据、百 QPS 内性能足够且架构简单,不必引入 ES;ES 适合海量数据、高并发和复杂聚合场景。选项 A 过度设计,B 与事实不符,D 方向颠倒。判断标准就是那句老话:藏书少的社区图书馆,不必上自动化检索系统。