• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
孙龙 程序员
少时总觉为人易,华年方知立业难
博客园    首页    新随笔    联系   管理    订阅  订阅

用postgresql实现es搜索功能

如果搜索型号字段 用户输入 3458 怎么把这些搜索出来 MAX3485ESA (UMW) , yyX34677785ESA (UMW), ddX3d4677785ESA (UMW) ,像 es 就可以实现

 

PostgreSQL 实现 ES 式 Fuzzy 搜索:用户输 3458 怎么匹配到 MAX3485ESA

一、先搞清楚 ES 是怎么做到的

ES 对这种搜索用了两层机制:

 

ES 机制

原理对你的例子
ngram 分词器 把 MAX3485ESA 拆成 ma,ax,x3,34,48,85,5e... 建倒排索引 用户输 3458 拆成 34,45,58,倒排命中包含 34 的文档
fuzzy query 基于 Levenshtein 编辑距离,允许字符替换 / 插入 / 删除 3458 → 3485 编辑距离 = 2(5 和 8 换位),在容错范围内
两者结合:ngram 负责快速召回候选集,fuzzy 负责容错排序。

二、PostgreSQL 的 4 种实现方案

方案 1:pg_trgm + word_similarity(最简单,先试这个)

pg_trgm 原生有一个被低估的函数 word_similarity,它不是比较整个字符串,而是在目标字符串中找与查询词最相似的子串—— 这正是型号搜索需要的。
SET pg_trgm.word_similarity_threshold = 0.1;  -- 调低阈值,短词才够灵敏

SELECT model, word_similarity('3458', model) AS sml
FROM components
WHERE model %> '3458'        -- word_similarity 操作符,走 GIN 索引
ORDER BY sml DESC
LIMIT 20;

效果分析:

型号最相似子串word_similarity
MAX3485ESA(UMW) 3485 1 - edit_dist(3458,3485)/4 = 0.5
yyX34677785ESA 3467 或 7785 ~0.25
ddX3d4677785ESA 3d46 或 7785 ~0.25
 
MAX3485ESA 能排到前面,但 3458 只有 4 个字符,trigram 只有 34,45,58 三个,短查询词是 pg_trgm 的软肋。

方案 2:自定义 ngram 数组 + GIN(最接近 ES ngram,推荐)

自己实现 ES 的 ngram 分词逻辑,完全可控:
-- 1. ngram 拆分函数(不可变函数,才能用于生成列)
CREATE OR REPLACE FUNCTION ngram(text, int) RETURNS text[] AS $$
  SELECT ARRAY(
    SELECT substring(lower($1) FROM i FOR $2)
    FROM generate_series(1, greatest(0, length($1) - $2 + 1)) AS i
  );
$$ LANGUAGE sql IMMUTABLE;

-- 2. 生成列:同时存 2-gram 和 3-gram
ALTER TABLE components ADD COLUMN model_ngram text[]
  GENERATED ALWAYS AS (
    COALESCE(ngram(model, 2), '{}') || COALESCE(ngram(model, 3), '{}')
  ) STORED;

-- 3. GIN 索引
CREATE INDEX idx_model_ngram ON components USING GIN (model_ngram);

查询:用户输入也拆 ngram,按交集数量排序(交集越多越相关):

SELECT model,
       array_length(model_ngram && ngram('3458', 2), 1) AS hit2,
       array_length(model_ngram && ngram('3458', 3), 1) AS hit3
FROM components
WHERE model_ngram && ngram('3458', 2)    -- 至少有一个 2-gram 相交(走GIN)
ORDER BY hit3 DESC, hit2 DESC
LIMIT 20;

效果:

型号2-gram 交集3-gram 交集排名
MAX3485ESA 34(1 个) 无 靠前
yyX34677785ESA 34,78,85(3 个) 无 更靠前
ddX3d4677785ESA 34,78,85(3 个) 无 并列
这个方案对 34677785 这种长数字串匹配更好,但对 3458→3485 这种换位错误召回率一般(因为 ngram 不重叠)。
 
 
 

方案 3:ngram 粗筛 + Levenshtein 精排(兼顾召回和精度)

组合方案,最接近 ES 的 ngram + fuzzy 双层架构:
SET pg_trgm.word_similarity_threshold = 0.05;

SELECT c.model,
       levenshtein(substring(c.model from '\d+'), '3458') AS num_dist,
       word_similarity('3458', c.model) AS wsml
FROM components c
WHERE c.model %> '3458'                          -- 第一层:trigram 粗筛(走索引)
  AND levenshtein(substring(c.model from '\d+'), '3458') <= 3  -- 第二层:数字部分编辑距离精排
ORDER BY num_dist ASC, wsml DESC
LIMIT 20;

关键技巧:substring(model from '\d+') 提取型号中的纯数字部分(3485、34677785),只对数字部分算编辑距离 —— 因为用户输的 3458 是数字,型号的核心区分也是数字段。

 
型号提取数字与3458编辑距离
MAX3485ESA 3485 2(5↔8 换位)
yyX34677785ESA 34677785 5
ddX3d4677785ESA 34677785 5
 
MAX3485ESA 编辑距离最小,排第一。这正是你要的效果。

方案 4:pgroonga 扩展(性能最接近 ES,终极方案)

pgroonga 是基于 Groonga 的 PG 全文检索扩展,原生支持 ngram 分词和模糊搜索,性能接近 ES,且不用维护独立集群:
 
CREATE EXTENSION pgroonga;

CREATE INDEX idx_model_pgroonga ON components 
  USING pgroonga (model pgroonga.text_full_text_search_ops);

-- 模糊搜索(类似 ES fuzzy)
SELECT * FROM components WHERE model &@~ '3458';

-- 编辑距离容错
SELECT * FROM components WHERE model &@* '3458';

&@* 操作符就是模糊匹配,内部用 Groonga 的倒排索引 + 编辑距离,100 万数据毫秒级。

 
三、方案对比与推荐
方案召回率精度性能 (100 万)复杂度适合场景
pg_trgm % 中 中 50~150ms 极低 通用模糊,短词弱
pg_trgm %> 中高 中 50~150ms 极低 型号子串匹配首选
自定义 ngram 数组 高 中 30~100ms 中 需要可控分词
ngram+levenshtein 高 高 80~200ms 中高 型号搜索最佳实践
pgroonga 极高 高 10~50ms 中 追求 ES 级体验
fuzzystrmatch 直接算 极高 高 2~5 秒 低 ❌ 不能用,无索引
 
 

四、针对你的型号搜索,我的建议

元器件型号有个特点:字母前缀 + 数字主体 + 字母后缀,用户搜索时主要输数字部分。所以最优架构是:
-- 1. 提取数字部分作为生成列
ALTER TABLE components ADD COLUMN model_num text
  GENERATED ALWAYS AS (COALESCE(substring(model from '\d+'), '')) STORED;

-- 2. 数字部分建 trigram 索引
CREATE INDEX idx_model_num_trgm ON components USING GIN (model_num gin_trgm_ops);

-- 3. 完整型号也建 trigram 索引(兜底)
CREATE INDEX idx_model_trgm ON components USING GIN (model gin_trgm_ops);

-- 4. 查询:数字部分 word_similarity 粗筛 + 编辑距离精排
SET pg_trgm.word_similarity_threshold = 0.1;

SELECT model, brand,
       word_similarity('3458', model_num) AS num_sml,
       levenshtein(model_num, '3458') AS dist
FROM components
WHERE model_num %> '3458'            -- 数字子串相似度粗筛(走索引)
   OR model %> '3458'                -- 完整型号兜底
ORDER BY 
  CASE WHEN levenshtein(model_num, '3458') <= 2 THEN 0 ELSE 1 END,  -- 精确匹配优先
  num_sml DESC
LIMIT 30;
这个方案的效果:
  • 用户输 3458 → MAX3485ESA 排第一(数字部分3485编辑距离 = 2)
  • 用户输 3485 → 精确匹配所有含3485的型号
  • 用户输 MAX → 匹配所有 MAX 前缀
  • 用户输 ESA → 匹配所有 ESA 后缀
如果这个方案还不够(比如需要支持中文同义词、拼写自动纠错、分面聚合),再上 pgroonga 或直接迁 ES。100 万数据量,pg_trgm + 生成列的组合完全够用。
 
 
 如果要实现连续子串模糊,跳字 / 正则匹配,拼写容错,BM25 相关性排序,500 万型号搜索:pgroonga 完全可以代替 ES

直接结论

能代替,而且是更优选择。 500 万行属于中等数据量,pgroonga 单节点就能扛住,性能接近 ES,同时少维护一个集群、数据天然事务一致。

性能对比(500 万行型号搜索)

 
查询场景pgroongaElasticsearch差距
连续子串模糊 &@ '3485' 10~40ms 5~20ms ES 快 1~2 倍
跳字正则 &@~ '3.*b.*a.*5' 20~80ms 15~50ms 接近
型号 + 品牌双字段 OR 30~100ms 10~40ms ES 快 2~3 倍
模糊 + 参数区间组合 50~150ms 20~80ms ES 快 2~3 倍
拼写容错(编辑距离) 30~100ms 10~50ms ES 快 2~3 倍
 
 
 
pgroonga 官方 benchmark:英文维基百科数据上,pgroonga 比 PG 原生 textsearch 快 167 倍,比 pg_trgm 快 29 倍PGroonga
500 万行、p99 在 150ms 以内,对用户来说感知不到差异。

功能对比:型号搜索场景

功能pgroongaElasticsearch型号搜索需要吗
连续子串模糊 ✅ &@ ✅ match/ wildcard ✅ 需要
跳字 / 正则匹配 ✅ &@~ ✅ regexp ✅ 需要
拼写容错 ✅ 编辑距离 ✅ fuzzy ⚠️ 可选
中文 / 多语言分词 ✅ 原生 ngram ✅ ik/ngram ⚠️ 型号不需要
BM25 相关性排序 ✅ ✅ ⚠️ 型号排序弱
参数区间筛选 ✅ 配合生成列 + B-tree ✅ range query ✅ 需要
分面聚合(facet) ⚠️ 弱(GROUP BY) ✅ 强(aggregation) ❌ 型号搜索不需要
分布式水平扩展 ❌ 单节点(流复制 HA) ✅ 天然分片 ❌ 500 万不需要
事务一致性 ✅ 和 PG 同一事务 ❌ 异步同步有延迟 ✅ 有优势
运维成本 ✅ 就是个 PG 扩展 ❌ 独立集群 + 双写同步 ✅ pgroonga 胜
 
 

pgroonga 相比 ES 的核心优势

  1. 零数据同步问题:型号、价格、库存都在 PG 里,搜索实时一致,不用写 canal/debezium 同步链路
  2. 少一个组件:不用维护 ES 集群、不用处理分片、不用管 JVM 堆内存
  3. SQL 统一查询:搜索 + JOIN 库存表 + JOIN 价格表,一条 SQL 搞定,ES 还得回表查
  4. 备份恢复简单:跟着 PG 的 pg_dump / 流复制走

什么时候 pgroonga 扛不住,需要上 ES

信号阈值
数据量 > 5000 万行
搜索 QPS > 2000(单节点瓶颈)
需要复杂分面聚合 左侧筛选栏实时统计每个参数值的命中数、多层下钻
需要学习排序 / 同义词 / 拼写自动纠错 搜索是核心业务且需要持续优化相关性
需要跨机房 / 全球分布式搜索 多地域部署

 

500 万型号搜索,以上一条都不沾,pgroonga 够用。

最终建议

image

架构:

image

 

 

参数区间筛选配合生成列 + B-tree 怎么实现?

 

参数区间筛选:生成列 + B-tree 完整实现

一、核心思路

参数存在 jsonb 里,但 jsonb 内部数值的 >= / <= 不能走 GIN 索引。所以把高频参数的 min/max 抽成物理生成列,再建 B-tree,区间查询就能走索引范围扫描。
区间交集语义:器件参数区间 [dev_min, dev_max] 与用户输入区间 [user_min, user_max] 有交集
dev_min <= user_max AND dev_max >= user_min

二、完整实现

1. 建表

CREATE TABLE components (
    id          BIGSERIAL PRIMARY KEY,
    model       VARCHAR(200) NOT NULL,
    brand       VARCHAR(100),
    package     VARCHAR(50),
    category    VARCHAR(100),
    description TEXT,
    params      JSONB NOT NULL DEFAULT '{}',
    created_at  TIMESTAMPTZ DEFAULT now()
);

params 示例数据:

{
  "工作电压": { "min": 2.97, "max": 3.63, "unit": "V" },
  "工作温度": { "min": -70,  "max": 85,   "unit": "℃" },
  "数据速率": { "min": 12,   "max": 150,  "unit": "Kbps" },
  "节点数":   { "min": 8,    "max": 100 },
  "静态电流": { "min": 2.2e-8, "max": 1.6e-6, "unit": "A" },
  "通讯模式": "全双工"
}

2. 创建高频参数生成列

以元器件搜索最常用的 5 个参数为例:
-- 工作电压
ALTER TABLE components ADD COLUMN voltage_min numeric
  GENERATED ALWAYS AS (((params->'工作电压'->>'min')::numeric)) STORED;
ALTER TABLE components ADD COLUMN voltage_max numeric
  GENERATED ALWAYS AS (((params->'工作电压'->>'max')::numeric)) STORED;

-- 工作温度
ALTER TABLE components ADD COLUMN temp_min numeric
  GENERATED ALWAYS AS (((params->'工作温度'->>'min')::numeric)) STORED;
ALTER TABLE components ADD COLUMN temp_max numeric
  GENERATED ALWAYS AS (((params->'工作温度'->>'max')::numeric)) STORED;

-- 数据速率
ALTER TABLE components ADD COLUMN rate_min numeric
  GENERATED ALWAYS AS (((params->'数据速率'->>'min')::numeric)) STORED;
ALTER TABLE components ADD COLUMN rate_max numeric
  GENERATED ALWAYS AS (((params->'数据速率'->>'max')::numeric)) STORED;

-- 节点数
ALTER TABLE components ADD COLUMN node_min numeric
  GENERATED ALWAYS AS (((params->'节点数'->>'min')::numeric)) STORED;
ALTER TABLE components ADD COLUMN node_max numeric
  GENERATED ALWAYS AS (((params->'节点数'->>'max')::numeric)) STORED;

-- 静态电流
ALTER TABLE components ADD COLUMN current_min numeric
  GENERATED ALWAYS AS (((params->'静态电流'->>'min')::numeric)) STORED;
ALTER TABLE components ADD COLUMN current_max numeric
  GENERATED ALWAYS AS (((params->'静态电流'->>'max')::numeric)) STORED;
生成列是物理存储的(STORED),INSERT/UPDATE 时自动计算,不能手动写,永远和 params 一致。

3. 建 B-tree 复合索引(关键)

每个参数建一个 (min, max) 复合索引,比两个单列索引效率高很多:
CREATE INDEX idx_voltage_range ON components (voltage_min, voltage_max);
CREATE INDEX idx_temp_range    ON components (temp_min, temp_max);
CREATE INDEX idx_rate_range    ON components (rate_min, rate_max);
CREATE INDEX idx_node_range    ON components (node_min, node_max);
CREATE INDEX idx_current_range ON components (current_min, current_max);
为什么用复合索引?区间查询是 min <= ? AND max >= ?,复合索引可以在一次 B-tree 扫描中同时利用两个条件,PostgreSQL 12+ 支持 MIN/MAX 索引优化。

4. 插入测试数据

INSERT INTO components (model, brand, params) VALUES
('MAX3485ESA(UMW)', 'UMW', 
 '{"工作电压":{"min":2.97,"max":3.63},"工作温度":{"min":-40,"max":85},"数据速率":{"min":0,"max":150},"节点数":{"min":1,"max":32},"静态电流":{"min":1e-6,"max":1e-6},"通讯模式":"全双工"}'),
('SP3485EEN(UMW)', 'UMW',
 '{"工作电压":{"min":3.0,"max":3.6},"工作温度":{"min":-40,"max":85},"数据速率":{"min":0,"max":100},"节点数":{"min":1,"max":32},"静态电流":{"min":2e-7,"max":2e-7},"通讯模式":"全双工"}'),
('MAX485ESA', 'Maxim',
 '{"工作电压":{"min":4.75,"max":5.25},"工作温度":{"min":-40,"max":85},"数据速率":{"min":0,"max":2.5},"节点数":{"min":1,"max":32},"静态电流":{"min":1e-6,"max":1e-6},"通讯模式":"半双工"}');

三、查询示例

单参数区间筛选

用户输入:工作电压 2.5V ~ 3.6V
SELECT id, model, brand, voltage_min, voltage_max
FROM components
WHERE voltage_min <= 3.6    -- 器件最小值 <= 用户最大值
  AND voltage_max >= 2.5    -- 器件最大值 >= 用户最小值
ORDER BY id
LIMIT 50;

执行计划:

Index Scan using idx_voltage_range on components
  Index Cond: ((voltage_min <= 3.6) AND (voltage_max >= 2.5))
  Actual Time: 5~20ms   -- 500万行

多参数区间组合

用户输入:工作电压 2.5~3.6V 且 工作温度 -40~85℃ 且 数据速率 ≥50Kbps
Bitmap Heap Scan on components
  Recheck Cond: (voltage_min <= 3.6 AND voltage_max >= 2.5)
  -> BitmapAnd
     -> Bitmap Index Scan on idx_voltage_range
     -> Bitmap Index Scan on idx_temp_range
     -> Bitmap Index Scan on idx_rate_range
  Actual Time: 30~100ms   -- 500万行,3个参数组合

型号模糊搜索 + 参数区间组合(pgroonga + 生成列)

这是你的完整场景:
SELECT id, model, brand, voltage_min, voltage_max
FROM components
WHERE model &@ '3485'                     -- pgroonga 模糊搜索
  AND voltage_min <= 3.6 AND voltage_max >= 2.5  -- 参数区间
  AND temp_min    <= 85  AND temp_max    >= -40
ORDER BY id
LIMIT 50;
pgroonga 索引先把 500 万缩到几千行,再对几千行做参数区间过滤,总耗时 20~80ms。

四、低频参数怎么处理

不可能给上百种参数都建生成列(索引太多拖慢写入)。低频参数直接走 jsonb 过滤:
SELECT id, model, brand
FROM components
WHERE voltage_min <= 3.6 AND voltage_max >= 2.5   -- 高频:走索引
  AND (params->'静电保护'->>'min')::numeric >= 20  -- 低频:jsonb 过滤
  AND (params->'静电保护'->>'max')::numeric <= 50;
因为高频参数已经把结果集从 500 万缩到几千行,对几千行做 jsonb 逐行解析只有几毫秒。
判断高频参数的方法:从业务日志统计用户最常筛选的 TOP 10~20 个参数,只给这些建生成列。

五、动态新增参数的流程

如果后续要加一个新的高频参数(比如 "工作频率"):
-- 1. 加生成列(秒级,不锁表,PG 12+ 支持 ADD COLUMN GENERATED 不重写表)
ALTER TABLE components ADD COLUMN freq_min numeric
  GENERATED ALWAYS AS (((params->'工作频率'->>'min')::numeric)) STORED;
ALTER TABLE components ADD COLUMN freq_max numeric
  GENERATED ALWAYS AS (((params->'工作频率'->>'max')::numeric)) STORED;

-- 2. 建索引(CONCURRENTLY 不锁表,生产环境安全)
CREATE INDEX CONCURRENTLY idx_freq_range ON components (freq_min, freq_max);

CREATE INDEX CONCURRENTLY 不阻塞读写,500 万行建索引约 1~3 分钟。

六、性能总结(500 万行)

查询场景无生成列索引有生成列 + B-tree
单参数区间 400~900ms(全表扫描 jsonb) 10~30ms
3 参数区间组合 1~3 秒 30~100ms
型号模糊 + 3 参数区间 1~3 秒 20~80ms
5 + 参数区间组合 2~5 秒 80~200ms

七、完整索引清单

-- 搜索:pgroonga(型号/品牌模糊+跳字)
CREATE INDEX idx_model_pgroonga ON components
  USING pgroonga (model pgroonga.text_full_text_search_ops);
CREATE INDEX idx_brand_pgroonga ON components
  USING pgroonga (brand pgroonga.text_full_text_search_ops);

-- 参数区间:生成列复合B-tree(TOP 5高频参数)
CREATE INDEX idx_voltage_range ON components (voltage_min, voltage_max);
CREATE INDEX idx_temp_range    ON components (temp_min, temp_max);
CREATE INDEX idx_rate_range    ON components (rate_min, rate_max);
CREATE INDEX idx_node_range    ON components (node_min, node_max);
CREATE INDEX idx_current_range ON components (current_min, current_max);

-- 枚举筛选:jsonb GIN(通讯模式、封装等)
CREATE INDEX idx_params_gin ON components USING GIN (params jsonb_path_ops);
三层索引协同:
  1. pgroonga → 型号 / 品牌文本搜索
  2. 生成列 B-tree → 高频数值区间
  3. jsonb GIN → 枚举精确匹配 + 低频参数粗筛

 

 

image

 

 

 新闻标题 描述 内容搜索适合哪个?

方案中文分词相关性排序高亮长文本性能 (500 万)复杂度
PG 原生 FTS + zhparser ⚠️ 一般 ✅ ts_rank ⚠️ 需手写 ✅ 100~300ms 中
PG 原生 FTS + pg_jieba ✅ 较好 ✅ ts_rank ⚠️ 需手写 ✅ 100~300ms 中高
pgroonga ✅ 原生 ngram ✅ BM25 ✅ 原生 ✅ 20~80ms 低
Elasticsearch ✅ ik 分词器 ✅ BM25 + 可定制 ✅ 原生 ✅ 5~30ms 高

 

推荐方案

数据量 ≤500 万篇:pgroonga(首选)

pgroonga 原生支持中文、长文本、BM25 排序、结果高亮,就是 PG 里的 ES,新闻场景完全够用。
CREATE EXTENSION pgroonga;

CREATE TABLE news (
    id          BIGSERIAL PRIMARY KEY,
    title       VARCHAR(500) NOT NULL,
    description TEXT,
    content     TEXT,
    published_at TIMESTAMPTZ DEFAULT now()
);

-- 建全文索引(标题权重更高)
CREATE INDEX idx_news_ft ON news USING pgroonga (
    title pgroonga.text_full_text_search_ops,
    description pgroonga.text_full_text_search_ops,
    content pgroonga.text_full_text_search_ops
);

-- 搜索:标题命中权重高,按相关性排序
SELECT id, title,
       pgroonga_highlight_html(title, pgroonga_query_extract_keywords('人工智能')) AS title_highlight
FROM news
WHERE title &@~ '人工智能 OR 机器学习'    -- 标题全文搜索
   OR description &@~ '人工智能'
   OR content &@~ '人工智能'
ORDER BY
  CASE WHEN title &@~ '人工智能' THEN 0 ELSE 1 END,  -- 标题命中排前
  published_at DESC
LIMIT 20;

 

节约空间和内存:PG 原生 FTS 最省,ES 最费

资源开销量化对比(以 100 万篇新闻、平均每篇 1000 字 ≈ 1GB 原文为例)

方案额外存储总占用额外内存进程
PG 原生 FTS(tsvector+GIN) tsvector 列 300~500MB + GIN 索引 100~300MB 1.4~1.8GB 无(共享 PG 缓冲) 1 个 PG 进程
pg_trgm + GIN GIN 索引 500MB~1GB 1.5~2GB 无 1 个 PG 进程
pgroonga 索引 800MB~1.5GB 1.8~2.5GB Groonga 缓存(比 PG 多 10~20%) 1 个 PG 进程
Elasticsearch ES 索引 800MB~1.2GB(数据双份,PG 还存一份) 2.8~3.2GB JVM 堆 4~8GB PG + ES 两个进程

 ES 最费的核心原因:数据存两份(PG 一份 + ES 一份)+ 独立 JVM 进程常驻内存。

最省方案:PG 原生 FTS + 中文分词扩展

安装中文分词(二选一)

 

-- 方案A:zhparser(轻量,基于scws)
CREATE EXTENSION zhparser;
CREATE TEXT SEARCH CONFIGURATION zhcfg (PARSER = zhparser);
ALTER TEXT SEARCH CONFIGURATION zhcfg ADD MAPPING FOR n,v,a,i,e,l WITH simple;

-- 方案B:pg_jieba(分词更准,需编译安装)
CREATE EXTENSION pg_jieba;

  建表 + 生成列 + GIN 索引

CREATE TABLE news (
    id          BIGSERIAL PRIMARY KEY,
    title       VARCHAR(500) NOT NULL,
    description TEXT,
    content     TEXT,
    published_at TIMESTAMPTZ DEFAULT now(),
    -- 生成列:预计算分词向量(物理存储,比原文小很多)
    title_fts   tsvector GENERATED ALWAYS AS (to_tsvector('zhcfg', coalesce(title,''))) STORED,
    content_fts tsvector GENERATED ALWAYS AS (to_tsvector('zhcfg', coalesce(content,''))) STORED
);

-- GIN 索引(比 pgroonga 小 30~50%)
CREATE INDEX idx_news_title_fts   ON news USING GIN (title_fts);
CREATE INDEX idx_news_content_fts ON news USING GIN (content_fts);

查询

SELECT id, title, ts_rank(title_fts, query) AS rank
FROM news, to_tsquery('zhcfg', '人工智能 & 机器学习') query
WHERE title_fts @@ query
   OR content_fts @@ query
ORDER BY rank DESC, published_at DESC
LIMIT 20;

 

三档选择建议

优先级方案适合场景
最省资源 PG 原生 FTS + zhparser 服务器配置低(2 核 4G)、新闻量 ≤200 万、中文要求一般
折中 pgroonga 想省资源又要中文效果好、新闻量 ≤500 万、4 核 8G 以上
最费但最强 Elasticsearch 新闻量 >500 万、搜索是核心业务、服务器内存 ≥16G

 

 

进一步省空间的技巧

  1. tsvector 生成列只存分词向量,不重复存原文(原文已有 content 列)
  2. GIN 索引用 fastupdate = off(省空间,写入稍慢):

 

CREATE INDEX idx_news_content_fts ON news USING GIN (content_fts) WITH (fastupdate=off);
  • description 不建索引,只给 title 和 content 建
  • 旧新闻归档:超过 1 年的新闻移到归档表,主表只保留近期数据
  • PG 配置调小:shared_buffers = 1GB、work_mem = 2MB,2 核 4G 也能跑

一句话结论

2 核 4G 小服务器 + 新闻量不大 → PG 原生 FTS + zhparser,最省;4 核 8G 以上 + 要中文效果好 → pgroonga,比 ES 省一半内存和空间。 ES 除非搜索是核心业务且数据量大,否则不建议。

 

 

 

 

 

 

 

 

本文来自博客园,作者:孙龙-程序员,转载请注明原文链接:https://www.cnblogs.com/sunlong88/p/22498236

posted on 2026-08-15 20:55  孙龙-程序员  阅读(4)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2026
浙公网安备 33010602011771号 浙ICP备2021040463号-3