用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 万行型号搜索)
| 查询场景 | pgroonga | Elasticsearch | 差距 |
|---|---|---|---|
连续子串模糊 &@ '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 以内,对用户来说感知不到差异。
功能对比:型号搜索场景
| 功能 | pgroonga | Elasticsearch | 型号搜索需要吗 |
|---|---|---|---|
| 连续子串模糊 | ✅ &@ |
✅ match/ wildcard | ✅ 需要 |
| 跳字 / 正则匹配 | ✅ &@~ |
✅ regexp | ✅ 需要 |
| 拼写容错 | ✅ 编辑距离 | ✅ fuzzy | ⚠️ 可选 |
| 中文 / 多语言分词 | ✅ 原生 ngram | ✅ ik/ngram | ⚠️ 型号不需要 |
| BM25 相关性排序 | ✅ | ✅ | ⚠️ 型号排序弱 |
| 参数区间筛选 | ✅ 配合生成列 + B-tree | ✅ range query | ✅ 需要 |
| 分面聚合(facet) | ⚠️ 弱(GROUP BY) | ✅ 强(aggregation) | ❌ 型号搜索不需要 |
| 分布式水平扩展 | ❌ 单节点(流复制 HA) | ✅ 天然分片 | ❌ 500 万不需要 |
| 事务一致性 | ✅ 和 PG 同一事务 | ❌ 异步同步有延迟 | ✅ 有优势 |
| 运维成本 | ✅ 就是个 PG 扩展 | ❌ 独立集群 + 双写同步 | ✅ pgroonga 胜 |
pgroonga 相比 ES 的核心优势
- 零数据同步问题:型号、价格、库存都在 PG 里,搜索实时一致,不用写 canal/debezium 同步链路
- 少一个组件:不用维护 ES 集群、不用处理分片、不用管 JVM 堆内存
- SQL 统一查询:搜索 + JOIN 库存表 + JOIN 价格表,一条 SQL 搞定,ES 还得回表查
- 备份恢复简单:跟着 PG 的 pg_dump / 流复制走
什么时候 pgroonga 扛不住,需要上 ES
| 信号 | 阈值 |
|---|---|
| 数据量 | > 5000 万行 |
| 搜索 QPS | > 2000(单节点瓶颈) |
| 需要复杂分面聚合 | 左侧筛选栏实时统计每个参数值的命中数、多层下钻 |
| 需要学习排序 / 同义词 / 拼写自动纠错 | 搜索是核心业务且需要持续优化相关性 |
| 需要跨机房 / 全球分布式搜索 | 多地域部署 |
500 万型号搜索,以上一条都不沾,pgroonga 够用。
最终建议

架构:

参数区间筛选配合生成列 + 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);
三层索引协同:
- pgroonga → 型号 / 品牌文本搜索
- 生成列 B-tree → 高频数值区间
- jsonb GIN → 枚举精确匹配 + 低频参数粗筛

新闻标题 描述 内容搜索适合哪个?
| 方案 | 中文分词 | 相关性排序 | 高亮 | 长文本 | 性能 (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 |
进一步省空间的技巧
- tsvector 生成列只存分词向量,不重复存原文(原文已有 content 列)
- 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
浙公网安备 33010602011771号