Chapter 07

JSONB 与全文搜索

PostgreSQL 内置文档数据库能力:高效的 JSONB 操作与强大的全文搜索引擎

7.1 JSONB 操作符全览

PostgreSQL 的 JSONB 操作符是一套完整的 JSON 查询语言,配合 GIN 索引可以实现接近 MongoDB 的文档查询性能。

操作符返回类型说明示例
-> 'key'jsonb获取对象字段(返回 jsonb)'{"a":1}'::jsonb -> 'a'1
->> 'key'text获取对象字段(返回 text)'{"a":"b"}'::jsonb ->> 'a'b
-> 2jsonb获取数组元素(0-based)'[1,2,3]'::jsonb -> 23
#> '{a,b}'jsonb路径访问(返回 jsonb)'{"a":{"b":1}}'::jsonb #> '{a,b}'1
#>> '{a,b}'text路径访问(返回 text)'{"a":{"b":"c"}}'::jsonb #>> '{a,b}'c
@> '{...}'bool左边包含右边'{"a":1,"b":2}'::jsonb @> '{"a":1}'true
<@ '{...}'bool左边被右边包含'{"a":1}'::jsonb <@ '{"a":1,"b":2}'true
? 'key'bool顶层键是否存在'{"a":1}'::jsonb ? 'a'true
?| arraybool任意一个键存在'{"a":1}'::jsonb ?| ARRAY['a','b']true
?& arraybool所有键都存在'{"a":1,"b":2}'::jsonb ?& ARRAY['a','b']true
|| '{...}'jsonb合并两个 jsonb 对象'{"a":1}'::jsonb || '{"b":2}'{"a":1,"b":2}
- 'key'jsonb删除键'{"a":1,"b":2}'::jsonb - 'a'{"b":2}
#- '{a,b}'jsonb按路径删除'{"a":{"b":1,"c":2}}'::jsonb #- '{a,b}'{"a":{"c":2}}

7.2 JSONB 函数

SQL-- jsonb_set:修改指定路径的值
SELECT jsonb_set(
  '{"name":"Alice","score":90}'::JSONB,
  '{score}',         -- 路径
  '95'::JSONB,       -- 新值(必须是 jsonb)
  true               -- 如果路径不存在是否创建(默认 true)
);
-- 结果: {"name": "Alice", "score": 95}

-- jsonb_insert:在数组指定位置插入元素
SELECT jsonb_insert(
  '{"skills":["Go","Python"]}'::JSONB,
  '{skills,1}',       -- 在 index=1 之前插入
  '"Rust"'::JSONB
);
-- 结果: {"skills": ["Go", "Rust", "Python"]}

-- jsonb_strip_nulls:删除所有 null 值的键
SELECT jsonb_strip_nulls('{"a":1,"b":null,"c":{"d":null,"e":2}}'::JSONB);
-- 结果: {"a": 1, "c": {"e": 2}}

-- jsonb_each / jsonb_each_text:展开对象为键值对行
SELECT key, value
FROM jsonb_each('{"name":"Alice","age":30,"city":"Beijing"}'::JSONB);
-- 结果:
--  key  |  value
-- ------+---------
--  name | "Alice"
--  age  | 30
--  city | "Beijing"

-- jsonb_array_elements:展开数组为多行
SELECT value FROM jsonb_array_elements('["Go","Python","Rust"]'::JSONB);

-- jsonb_object_keys:获取所有顶层键
SELECT jsonb_object_keys('{"a":1,"b":2,"c":3}'::JSONB);

-- jsonb_agg:将多行聚合为 JSON 数组
SELECT
  category_id,
  jsonb_agg(jsonb_build_object('id', id, 'name', name, 'price', price)
    ORDER BY price
  ) AS products_json
FROM products
GROUP BY category_id;

7.3 GIN 索引加速 JSONB 查询

SQL-- 创建 GIN 索引(jsonb_ops:支持所有操作符,但体积较大)
CREATE INDEX idx_profiles_gin ON user_profiles USING GIN (profile);

-- jsonb_path_ops:只支持 @> 但索引更小,查询更快(推荐用于只需要 @> 的场景)
CREATE INDEX idx_profiles_path_ops ON user_profiles
  USING GIN (profile jsonb_path_ops);

-- 使用 GIN 索引的查询(必须用 @> 操作符)
EXPLAIN ANALYZE
SELECT * FROM user_profiles
WHERE profile @> '{"level": 5}';
-- 输出应看到 Bitmap Index Scan on idx_profiles_path_ops(命中 GIN 索引)

-- 对特定 JSONB 字段建立 B-Tree 索引(当只查询某个字段时更高效)
CREATE INDEX idx_profiles_level ON user_profiles
  ((profile->>'level'));  -- 对 level 字段的文本值建 B-Tree 索引

-- 查询时使用相同表达式
SELECT * FROM user_profiles
WHERE (profile->>'level')::integer > 5;  -- 可能不走上面的索引,需转换类型

-- 更好:对转换后的整型建索引
CREATE INDEX idx_profiles_level_int ON user_profiles
  (((profile->>'level')::integer));

7.4 全文搜索基础

PostgreSQL 内置了完整的全文搜索引擎,核心是两个类型:tsvector(已分词的文档向量)和 tsquery(搜索查询)。

SQL-- 基础全文搜索
SELECT to_tsvector('english', 'PostgreSQL is the world's most advanced open source database');
-- 结果: 'advanc':7 'databas':10 'open':8 'postgresql':1 'sourc':9 'world':4
-- 注意:is/the/most 是英语停用词,被去除;advanced → advanc(词干化)

SELECT to_tsquery('english', 'PostgreSQL & database');
-- 结果: 'postgresql' & 'databas'

-- 执行全文搜索
SELECT
  title,
  to_tsvector('english', title || ' ' || body) @@ to_tsquery('english', 'PostgreSQL') AS matches
FROM articles;

-- 使用 websearch_to_tsquery(推荐,用户友好)
SELECT title, body FROM articles
WHERE to_tsvector('english', title || ' ' || body)
   @@ websearch_to_tsquery('english', 'PostgreSQL database -MySQL');
-- websearch 语法:空格=AND, "短语"=短语匹配, -词=排除

7.5 生产环境全文搜索方案

SQL-- 建文章表,用生成列存储 tsvector(自动维护)
CREATE TABLE articles (
  id           BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  title        TEXT NOT NULL,
  body         TEXT NOT NULL,
  author_id    BIGINT,
  published_at TIMESTAMPTZ,
  -- 生成列:自动更新 tsvector,title 权重 A(最高),body 权重 B
  search_vec   TSVECTOR GENERATED ALWAYS AS (
    setweight(to_tsvector('english', COALESCE(title, '')), 'A') ||
    setweight(to_tsvector('english', COALESCE(body, '')), 'B')
  ) STORED
);

-- 对生成列创建 GIN 索引
CREATE INDEX idx_articles_fts ON articles USING GIN (search_vec);

-- 查询(带相关性评分和高亮)
SELECT
  id,
  title,
  ts_rank(search_vec, query) AS rank,
  ts_headline(
    'english', body, query,
    'MaxWords=30, MinWords=10, ShortWord=3, HighlightAll=false, MaxFragments=2, FragmentDelimiter=" … "'
  ) AS snippet
FROM articles,
     websearch_to_tsquery('english', 'PostgreSQL performance') query
WHERE search_vec @@ query
ORDER BY rank DESC
LIMIT 10;

7.6 中文全文搜索

PostgreSQL 内置的文本搜索不支持中文分词(中文没有空格分隔词语)。需要安装扩展:

🔤

zhparser基于 SCWS(Simple Chinese Word Segmentation)的 PostgreSQL 扩展,是使用最广泛的中文分词方案。支持自定义词典,配置简单。

🔤

pg_jieba基于 Jieba 分词器的扩展,分词质量更高,支持 HMM 新词发现,适合内容质量要求高的场景。

SQL-- 安装 zhparser(需先在系统级安装 scws)
CREATE EXTENSION zhparser;

-- 创建中文文本搜索配置
CREATE TEXT SEARCH CONFIGURATION chinese (PARSER = zhparser);
ALTER TEXT SEARCH CONFIGURATION chinese
  ADD MAPPING FOR n,v,a,i,e,l WITH simple;
-- n=名词, v=动词, a=形容词, i=习语, e=叹词, l=习用语

-- 测试中文分词
SELECT to_tsvector('chinese', 'PostgreSQL是世界上最先进的开源关系型数据库');
-- 结果: 'postgresql':1 '世界':3 '先进':5 '关系':8 '数据库':9 '开源':7

-- 建表使用中文全文搜索
CREATE TABLE cn_articles (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  title      TEXT NOT NULL,
  content    TEXT NOT NULL,
  search_vec TSVECTOR GENERATED ALWAYS AS (
    setweight(to_tsvector('chinese', title),   'A') ||
    setweight(to_tsvector('chinese', content),  'B')
  ) STORED
);

CREATE INDEX idx_cn_articles_fts ON cn_articles USING GIN (search_vec);

-- 中文搜索查询
SELECT
  title,
  ts_rank(search_vec, query) AS rank
FROM cn_articles,
     to_tsquery('chinese', '数据库 & 性能') AS query
WHERE search_vec @@ query
ORDER BY rank DESC;
📌

本章小结JSONB 的核心操作符:->/->& gt;(取值)、#>/#>>(路径)、@>(包含)、?(键存在)、||(合并)、-(删键)。GIN 索引是 JSONB 查询的关键,jsonb_path_ops 比 jsonb_ops 更小更快但只支持 @>。全文搜索核心:to_tsvector 分词、to_tsquery/websearch_to_tsquery 构建查询、@@ 匹配、ts_rank 相关性排名、ts_headline 结果高亮。中文全文搜索需要 zhparser 或 pg_jieba 扩展。