全文检索往往会把系统拆成两套基础设施:业务数据放在 PostgreSQL,搜索索引交给另一套专用服务。TIN 提供了另一条路线:在 PostgreSQL 内使用一个快速、功能完整的全文检索索引,把搜索能力尽量留在现有的数据边界里。
这并不意味着所有项目都应该立刻替换现有搜索系统。更实际的做法是:先明确搜索需求,再用可重复的 SQL 和压测验证 TIN 是否能满足延迟、相关性、更新速度与运维约束。
为什么 PostgreSQL 内的全文检索值得关注
当文档、权限、租户和业务状态都保存在 PostgreSQL 中时,外部搜索服务通常会带来额外工作:
- 维护数据同步链路,以及处理最终一致性;
- 在数据库查询和搜索结果之间重新实现权限过滤;
- 监控另一套集群、索引构建流程和容量规划;
- 处理删除、更新和批量导入时的重试与补偿。
TIN 的定位是全文检索索引,而不是一个独立的搜索平台。它的价值在于让开发者可以继续使用 PostgreSQL 的数据模型、事务边界和 SQL 查询方式,同时获得更适合全文搜索的索引能力。
这里需要区分两个概念:
- 索引能力:如何建立倒排结构、处理文本更新并加速匹配。
- 搜索产品能力:如何做联想词、拼写纠错、分布式扩展、分析报表和多语言词典管理。
TIN 主要解决的是第一类问题。是否还需要独立搜索服务,取决于项目是否需要第二类能力,以及数据规模和查询形态。
先把搜索契约写清楚
在接入任何新的索引之前,建议先固定搜索接口的行为。至少应明确以下问题:
- 搜索哪些字段,标题和正文是否需要不同权重;
- 是否支持短语、布尔条件、前缀或模糊匹配;
- 结果如何排序,是相关性优先还是时间优先;
- 租户、可见性、状态等过滤条件是否必须和全文匹配一起执行;
- 文档更新后,多久必须能被搜索到;
- 删除或权限变更后,旧内容最多允许暴露多久。
这一步很重要。一个索引即使查询速度很快,如果无法稳定满足权限过滤或更新可见性,也不能算作可用的搜索方案。
用 PostgreSQL 基线验证你的需求
下面的例子使用 PostgreSQL 原生全文检索建立一个可运行的基线。它不是 TIN 的安装或 DDL 示例,而是一个可以在接入 TIN 前后复用的测试契约:相同的数据、查询和过滤条件,可以用来比较延迟、结果质量与更新行为。
运行前只需要准备一个 PostgreSQL 数据库。示例使用 docker compose 启动数据库:
services:
postgres:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: search_demo
ports:
- "5432:5432"
启动后执行以下 SQL:
CREATE TABLE documents (
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL,
title text NOT NULL,
body text NOT NULL,
status text NOT NULL DEFAULT 'published',
updated_at timestamptz NOT NULL DEFAULT now(),
search_text tsvector GENERATED ALWAYS AS (
setweight(to_tsvector('simple', coalesce(title, '')), 'A') ||
setweight(to_tsvector('simple', coalesce(body, '')), 'B')
) STORED
);
CREATE INDEX documents_search_gin
ON documents USING gin (search_text);
INSERT INTO documents (tenant_id, title, body) VALUES
(10, 'PostgreSQL 索引设计', '讨论全文搜索、过滤条件和查询计划。'),
(10, '数据库迁移手册', '介绍在线变更、回滚策略和数据校验。'),
(20, '搜索服务运维', '记录索引重建、监控指标和故障排查。');
SELECT
id,
title,
ts_rank(search_text, websearch_to_tsquery('simple', '全文搜索')) AS score
FROM documents
WHERE tenant_id = 10
AND status = 'published'
AND search_text @@ websearch_to_tsquery('simple', '全文搜索')
ORDER BY score DESC, updated_at DESC
LIMIT 20;
实际接入 TIN 时,可以保留这组输入与验收条件,再按照 TIN 的安装方式和索引定义替换索引部分。不要只比较一条简单查询的平均耗时,还应覆盖以下场景:
- 单词搜索、多个词搜索和短语搜索;
- 带租户和状态过滤的查询;
- 批量导入后查询;
- 高频更新和删除;
- 冷缓存与热缓存;
- 小结果集和深分页。
评估 TIN 时要看哪些指标
查询延迟
至少记录 p50、p95 和 p99,而不是只看平均值。全文检索通常服务于用户请求,长尾延迟会直接影响页面响应和 API 超时率。
可以把查询样本保存成固定集合,然后重复执行。例如:
#!/usr/bin/env bash
set -euo pipefail
queries=(
"全文搜索"
"数据库 索引"
"在线 迁移"
"监控 故障排查"
)
for q in "${queries[@]}"; do
echo "=== $q ==="
psql "$DATABASE_URL" -v q="$q" -c \
"EXPLAIN (ANALYZE, BUFFERS, TIMING)
SELECT id, title
FROM documents
WHERE tenant_id = 10
AND status = 'published'
AND search_text @@ websearch_to_tsquery('simple', :'q')
ORDER BY ts_rank(search_text, websearch_to_tsquery('simple', :'q')) DESC
LIMIT 20;"
done
将 DATABASE_URL 指向测试数据库即可运行。使用 TIN 时,保持查询集合和数据快照不变,记录索引构建时间、查询计划、缓冲区访问和结果数量,比较才有意义。
写入与维护成本
全文索引不是免费的。文档更新会触发索引维护,批量导入会增加构建时间和磁盘占用。因此需要观察:
- 单条插入和更新的吞吐量;
- 批量导入期间的数据库负载;
- 索引大小与表大小的比例;
- 删除大量文档后空间是否需要回收或重建;
- 备份、恢复和升级流程是否包含该索引。
TIN 的“快速”应放在具体工作负载中验证:不同语言、文档长度、更新比例和过滤条件,可能产生完全不同的结果。
相关性与边界条件
速度只是搜索体验的一部分。还要用一组人工标注的查询检查结果质量,例如:
- 标题命中是否应该高于正文命中;
- 多词查询是否需要同时命中所有词;
- 结果排序是否会被更新时间压过相关性;
- 不同租户之间是否绝对不会串数据;
- 被撤回或无权访问的文档是否仍可能出现在结果中。
特别是权限过滤,不要只在应用层过滤搜索结果。正确的做法是让租户、状态和可见性条件进入数据库查询,并为这些条件设计相应的测试。
什么时候适合采用,什么时候保持分离
TIN 更适合以下类型的项目:
- 已经以 PostgreSQL 作为核心数据存储;
- 搜索结果必须和事务数据保持紧密一致;
- 希望减少同步链路和运维组件;
- 搜索主要是站内文档、知识库、工单或业务记录;
- 团队愿意用 SQL 和数据库工具完成搜索能力建设。
仍然应该谨慎评估以下情况:
- 需要跨多个数据源建立统一搜索索引;
- 依赖复杂的搜索分析、推荐、拼写纠错或联想能力;
- 数据规模、写入峰值或隔离需求已经超过单个 PostgreSQL 集群的舒适范围;
- 团队已经有成熟的外部搜索平台和完善的数据同步体系。
这不是“数据库搜索一定优于专用搜索引擎”的结论。TIN 的吸引力在于减少系统边界,同时提供一个面向全文检索的索引选项;是否值得采用,最终要由真实数据和真实查询决定。
一份可执行的落地清单
- 用固定数据集定义查询、过滤、排序和权限契约。
- 先建立当前实现的基线,记录 p50、p95、p99、索引大小和写入吞吐。
- 按 TIN 的官方安装与索引配置方式建立隔离测试环境,不要直接在生产库试验。
- 使用同一批查询比较延迟、结果质量和更新可见性。
- 验证备份恢复、升级、故障切换和索引重建流程。
- 上线时保留回退方案,并监控查询长尾、数据库 CPU、内存、磁盘和锁等待。
如果你的系统已经被“数据库加同步搜索集群”的复杂度拖慢,TIN 值得作为一个严肃的候选方案进行基准测试。最稳妥的路径不是凭产品描述做决定,而是把全文搜索变成一组可重复、可观测、可回退的数据库实验。