IvorySQL 多模融合实战:pg_textsearch + pgvector 上生产前,必踩的 7 个坑

SpringBoot+Vue+MySQL 高校学科竞赛平台 完整可运行版(源码+文档) 本项目基于SpringBoot+Vue+MySQL构建高校学科竞赛管理平台,覆盖学生、教师、领队教师、管理员四大角色,实现竞赛发布、报名、刷题、评分、公示等全流程闭环管理。系统架构清晰,技术主流,配套完整源码、数据库脚本、万字论文、开题报告、答辩PPT及部署文档,支持一键运行,适用于毕业设计、课程设计及院系二次开发,真正实现“开箱即用”。 阅读详情

作者:魏波,北京晟数科技技术顾问、PG 分会副秘书长。

引言

本文为企业智能知识库多模融合系列(二):多模检索落地排障, IvorySQL 多模融合实战系列续篇(上篇:IvorySQL 多模融合实战:pgvector + AGE + pg_textsearch 在同一实例中的协同验证),基于 IvorySQL 5.4(PostgreSQL 18.4)版本,在 Windows 11+Docker Desktop 标准化容器环境中完成全量实测。所有验证依托 pg_textsearch 1.4.0、pgvector 0.8.5 最新稳定组合开展,7 类核心问题均成功复现,配套 8 组 SQL 脚本、9 张实测截图均可溯源重跑,动态浮动数据也已逐一标注,确保内容真实可信、可落地、可复现。

不同于常规技术评测,上篇文章验证了多类扩展能否共存协同,本文聚焦生产落地核心痛点,化身上生产前置排障清单,直面 pg_textsearch 与向量混合检索场景的各类隐藏坑点,拆解问题成因、给出可行修复方案,解答开发者与运维者真正关心的「敢不敢直接上生产」问题。

先说清楚四件事

这篇的版本底座

1.png

关于版本,简单交代两句

本系列上篇(系列一,已发表)的实验是在 pg_textsearch 0.6.1(预发布版)上做的——当时 Dockerfile 里的版本没有跟着上游更新,本文把全部实验在 1.4.0 上重新跑了一遍,两个结果:

  1. BM25 的检索行为两版逐位一致——5 个查询的 Top5、打分小数点后四位都相同。这对你的实际意义是:如果生产库要从 0.6.1 升到 1.4.0,检索结果不会变,已有的评测结论不用推翻重跑。
  2. 有一个坑在新版被修掉了:0.6.1 上"索引看着活着、查询却返回 0 行"的静默失效,在 1.4.0 上连续重启两次都复现不出来。这个坑没有从文中删掉,而是挪到了文末的「附录 D」——它本身就是"为什么要升级"的最好证据。

剩下 7 个坑全部在 1.4.0 上实测仍然成立。它们的根大多不在 pg_textsearch 自己身上,而在 PostgreSQL 的规划器、分词器和部署方式上——这也是它们不太可能被版本升级"顺手修掉"的原因。

七个坑速览

2.png

每个坑都按同一套结构写:症状 → 根因 → 解法 → 验证 → 记住一句。 救火的直接翻「解法」;想系统避坑的从头读,大约 20 分钟。

先说清楚:这些坑算在谁头上。 这 7 个坑里,坑 1 是部署顺序问题(先放扩展文件还是先改预加载);坑 2(分词器)、坑 3(规划器)、坑 5(成本估算)、坑 7(统计视图)源自 PostgreSQL 内核的通用机制;坑 4、坑 6 来自 pg_textsearch / pgvector 等生态扩展自身的实现——换成任何一款 PostgreSQL 系数据库,这些坑一个都躲不掉,不是 IvorySQL 独有的问题。反过来讲,正因为 IvorySQL 完整兼容 PostgreSQL 扩展生态(本文两个扩展在 5.4 上都是一次编译通过),PG 生态这些年积累的排障经验才能在这里直接复用——这是兼容性带来的红利,不是负担。

实验表、测试文档集和两个索引:后面反复出现的名字先认个脸

全文反复出现的 kb_pgops 表、12 篇测试文档和三个索引,都是附录 C 第 ③ 步由 code/kb_pgops_init.sql 一次性建好的(先建表、再建两个索引、最后灌 12 行数据),后面所有实验直接复用。先看建表 DDL:

CREATE TABLE kb_pgops (
    id SERIAL PRIMARY KEY,          -- 主键索引 kb_pgops_pkey(btree)是这一句自动建的
    title VARCHAR(300),
    content TEXT,
    category VARCHAR(50),
    embedding vector(384),          -- 384 维向量列,依赖 pgvector
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

灌进去的 12 篇文档,是下面这套 PostgreSQL 运维短文(每篇正文 180~220 字,全文在 code/kb_pgops_init.sql;分类刻意覆盖运维的不同侧面,好让"关键词精确命中"和"主题相近但用词不同"能拉开差异):

3.png

embedding 列的 384 维向量不是大模型生成的语义向量,而是数据集生成脚本对文档分词后做 token-hash(MD5、固定 seed=42)拼出、并已逐行固化在 kb_pgops_init.sql 里的可复现伪向量:这样实验零外部依赖,不需要任何模型服务,任何人重跑都得到同一组数字;代价是它没有真正的语义泛化能力,所以本文只演示机制、不做检索能力评测(这条边界在附录 A.1 有完整声明,严肃评测请换外部标准数据集)。

配套 5 个测试查询(坑 4、附录 A 会反复引用 Q1~Q5;gold = 人工标注"应当命中哪些 id"):

4.png

两个索引分别建在正文列和向量列上:

-- 全文检索索引(坑 2、坑 4、附录 D 的主角),访问方法 bm25 来自 pg_textsearch
CREATE INDEX kb_pgops_bm25_idx ON kb_pgops USING bm25(content) WITH (text_config='simple');

-- 向量近似检索索引(坑 3、坑 5、坑 6 的主角),访问方法 hnsw 来自 pgvector
CREATE INDEX kb_pgops_hnsw_idx ON kb_pgops USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);

5.png

三个索引建好后,执行下面这条 psql 元命令可一次性确认它们都在(含访问方法与体积的完整输出见图 8,坑 6 还会用同一条命令算索引体积);至于"计划里出现 pkey 算不算用上了 HNSW",需要结合执行计划才能讲清,留到坑 3。

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c "\di+ kb_pgops*"

预期列出 3 行(实测输出,体积口径见图 8):

Name        | Type  | Access method | Size
--------------------+-------+---------------+------
 kb_pgops_bm25_idx  | index | bm25          | 16 kB
 kb_pgops_hnsw_idx  | index | hnsw          | 32 kB
 kb_pgops_pkey      | index | btree         | 16 kB

坑 1:多模扩展要自己装,而且装的顺序反了会让数据库起不来

症状

IvorySQL 官方镜像 registry.highgo.com/ivorysql/ivorysql:5.4-ubi8 聚焦的是数据库内核与 Oracle 兼容能力,PostgreSQL 生态的多模扩展需要按自己的需求另行安装——这与 PostgreSQL 官方镜像的做法一致(系列第一篇也验证过这一点)。第一次装 pg_textsearch,通常要经历两步。

先装:

CREATE EXTENSION pg_textsearch;
ERROR:  could not access file "pg_textsearch": No such file or directory

按 PostgreSQL 的惯例去配 shared_preload_libraries、然后重启——

FATAL:  could not access file "pg_textsearch": No such file or directory

数据库直接起不来了,连 psql 都连不上。

根因

两件独立的事叠在了一起:

  1. 扩展文件需要先编译进文件系统。 这不是 IvorySQL 的短板,而是"内核保持精简、能力按需组合"的取舍——好处恰恰是扩展版本由我们自己说了算:本文直接用了上游最新的 pg_textsearch 1.4.0,不必被镜像里某个固定版本钉死。而且 PG 生态扩展在 IvorySQL 5.4 上编译安装没有任何障碍,本文的 pgvector 与 pg_textsearch 都是一次编译通过(完整 Dockerfile 见附录 C)。
  2. shared_preload_libraries 是启动契约,不是愿望清单。 这条是 PostgreSQL 的通用机制,与 IvorySQL 本身无关:数据库启动时会挨个加载名单里的库,缺一个就整体罢工——它绝不会"先跳过、以后再说"。

所以这个坑的实质不是"要自己装",而是顺序:文件还没到位就先写了预加载,数据库立刻起不来。

顺带说一句版本差异(这次要夸的是 pg_textsearch 上游):0.6.1 时代这两条报错一模一样、且都不带任何提示;1.4.0 把第一条改得相当友好了:

ERROR:  pg_textsearch library not loaded.
        Add pg_textsearch to shared_preload_libraries and restart.

还附赠版本一致性校验(库版本和 SQL 脚本版本对不上会明确报错)。但"必须预加载"这个要求本身没变——所以坑还在,只是现在报错会直接告诉你缺什么、该怎么做。

解法

顺序不能乱,三步:

# ① 先把扩展文件放进文件系统(编译安装)
#    最小集只装本文用到的两个:pgvector + pg_textsearch
#    完整 Dockerfile 见附录 C / 06_生产级部署_Dockerfile/

# ② 再改预加载配置(保留原有项,追加新的)
#    等号右侧前 3 项 gb18030_2022、liboracle_parser、ivorysql_ora 是镜像出厂自带的原有项
#    (依次为 GB18030-2022 国标字符集、Oracle 兼容解析器、Oracle 兼容层),原样保留不要删;
#    只在末尾追加第 4 项 pg_textsearch。本文容器改完后的实测值:
#    shared_preload_libraries = 'gb18030_2022, liboracle_parser, ivorysql_ora, pg_textsearch'

# ③ 最后重启
docker restart ivorysql-ts140

为什么强调“保留原有项”:shared_preload_libraries 是整体覆盖、不是增量追加——如果只写 pg_textsearch,前 3 项出厂库会被一起挤掉,IvorySQL 的国标字符集与 Oracle 兼容能力随之失效。改动前可以先跑 SHOW shared_preload_libraries; 把这 3 个原有项原样抄下来,再在末尾追加新库。

如果已经 FATAL 起不来了:把配置改回原值让库先起来 → 补装 .so → 再按 ①②③ 来一遍。

不想每次都编译:把附录 C 的 Dockerfile 固化成自己的基础镜像,构建一次、团队长期复用。也期待社区后续推出预置多模扩展的镜像变体,把第 ① 步彻底省掉。

验证

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c \
  "SELECT name, default_version, installed_version FROM pg_available_extensions WHERE name IN ('vector','pg_textsearch') ORDER BY name;" -c \
  "SHOW shared_preload_libraries;"

第一条应返回 2 行扩展记录(installed_version 有值才算真装上),第二条应在预加载名单里看到 pg_textsearch。第一条返回 0 行 = 第 ① 步没做成;两行扩展都在但 CREATE EXTENSION 仍失败 = 第 ②③ 步漏了。

本文容器实测见图:installed_version 两列都有值,说明真的装上了;pg_textsearch 也已在 shared_preload_libraries 预加载名单中。

6.png

图:扩展版本与预加载配置实测(sql/00_env_check.sql 前两项)

记住一句

shared_preload_libraries 是"启动契约"——写上去的库必须存在,否则数据库宁可不起。这条是 PostgreSQL 的通用机制,与 IvorySQL 本身无关;要自己装扩展也不是 IvorySQL 的短板,别把账算错地方。

坑 2:simple 配置对中文"零词位"

症状

跑一条纯中文查询,BM25 路恒返回 0 行,什么都没匹配到。你可能以为索引坏了——其实索引好好的,只是它从头到尾就没看见中文。

根因

先亲眼看看索引到底"看见"了什么:

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c \
  "SELECT to_tsvector('simple','pg_dump 逻辑备份与恢复') AS probe1;" -c \
  "SELECT to_tsvector('simple','逻辑备份与恢复') AS probe2;"

实测输出见图:第一条里,中文连续串一个词位都没剩下(只剩 pg/dump 两个英文 token);第二条纯中文,直接是空的。

7.png

图:to_tsvector 实测——混排英文还剩两个词位,纯中文返回空

备注:这不是"中文分得差",是这个配置下根本不产出中文词位。PostgreSQL 全文检索是「parser 切 token → dictionary 处理 → configuration 组合」的机制,simple 意为"只按空白/标点切、不查词典"——英文天然按空格分词所以够用,中文连续书写就全灭。结论只针对 simple 配置,不等于"PostgreSQL 不能检索中文"。

一个连带效应值得知道:pg_dump 被切成了 pg、dump 两个词。于是只含 pg 的文档(pg_partman、pg_locks)也能拿到 BM25 分数——这解释了后文很多"为什么这篇也匹配上了"。

再挖一层:中文为什么连一个 token 都不是

换 ts_debug 看 parser 的分类结果,比只看 to_tsvector 输出清楚得多。跑这一条(输出见下方图 3 上半部分):

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c \

  "SELECT alias, token, lexemes FROM ts_debug('simple','pg_dump 逻辑备份与恢复');"

结果里中文整段被归为 blank(非词字符)——不是"切出了一个词但词典不认",而是压根没被当成词(图 3 上半部分:blank | 逻辑备份与恢复)。

判定"哪些字符算一个词"的是 parser 的字符分类,它取决于数据库编码与 lc_ctype。本文容器的实测值(图 3 下半部分):

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c \

  "SHOW server_encoding;" -c "SELECT datcollate, datctype FROM pg_database WHERE datname = current_database();"

SQL_ASCII + C 的组合下,中文字符不被识别为字母,直接落进 blank,于是零词位。

这套 SQL_ASCII + C 不是本文手工配置出来的,而是 IvorySQL 官方镜像初始化数据簇(initdb)时的集群级默认值:连不允许修改的模板库 template0 都是 SQL_ASCII + C;业务库 ivorysql 建库时没有显式指定编码与 locale,便沿模板原样继承。后面「验证」里的 zh_probe 之所以是 UTF8,正是因为建库时显式写了 ENCODING 'UTF8' LC_CTYPE 'en_US.utf8'。想一次看清自己实例里每个库的编码来历,跑这一条:

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c \

  "SELECT datname, pg_encoding_to_char(encoding) AS enc, datcollate, datctype FROM pg_database ORDER BY 1;"

8.2.png

本文容器实测:template0、template1、postgres、ivorysql 四行全部是 SQL_ASCII | C | C,只有显式指定编码/locale 创建的 zh_probe 一行是 UTF8 | en_US.utf8 | en_US.utf8——默认库无一例外,正是“集群默认、非手工配置”的直接证据;如果你的实例这四行是 UTF8,说明镜像或建库环节已显式指定过编码,坑 2 的现象要以你自己的查询结果为准。

8.png

图:坑 2 实证——parser 给中文整段打的类型是 blank;SQL_ASCII + C 就是零词位的环境前提

所以在你自己的环境复跑,现象可能不同:

9.png

三种环境现象不同,结论相同:simple 处理中文不可用。想知道自己属于哪一种,别猜,跑上面那条 ts_debug 看 alias 列。

解法

中文语料必须换分词方案,三选一(都是 PG 生态的成熟方案):

10.png

换完配置后重建 BM25 索引,新词位才会生效。

注(范围说明):zhparser / pg_jieba 的实际编译安装与分词效果本文未逐一实测(需编译 SCWS 等分词引擎),选型方向如上、按你的语料落地即可;三者中 pg_bigm 已在系列一验证可用。本文「验证」节用同实例换编码的附库,证的是根因(编码/locale 决定中文是否出词位),不是 zhparser 的分词效果。

验证

换配置后重跑上面的 to_tsvector 与 ts_debug:中文串应当产出非空词位,ts_debug 的 alias 列应从 blank 变成 word。

装 zhparser / pg_jieba 需要额外编译,不装任何扩展也能当场验证:在同一个实例里建一个 UTF8 + en_US.utf8 的测试库(TEMPLATE template0,本文容器实测可用)——同一台机器、同一套扩展,只有编码与 lc_ctype 变了,正好单独验证上文"环境前提"的结论:

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c "CREATE DATABASE zh_probe TEMPLATE template0 ENCODING 'UTF8' LC_COLLATE 'en_US.utf8' LC_CTYPE 'en_US.utf8';"

# 重跑两条探针(注意 -d 换成 zh_probe)
docker exec ivorysql-ts140 psql -U ivorysql -d zh_probe -p 5432 -c \

  "SELECT to_tsvector('simple','pg_dump 逻辑备份与恢复') AS probe1;" -c \

  "SELECT to_tsvector('simple','逻辑备份与恢复') AS probe2;" -c \

  "SELECT alias, token, lexemes FROM ts_debug('simple','pg_dump 逻辑备份与恢复');"

本文容器实测:probe2 从空变成 ‘逻辑备份与恢复’:1,ts_debug 里中文的 alias 从 blank 变成 word、lexemes 非空——中文产出词位了,这就是上文三环境表第三行的实测依据。同时注意:产出的词位是整个连续串,simple 词典不会把"逻辑备份与恢复"切开,只有整串完全相等才命中——真正的词典切词仍需 zhparser 这类扩展。验证完 DROP DATABASE zh_probe; 即可。

11.png

图:验证——同一实例、同一套扩展,只换库的编码与 lc_ctype,中文即从零词位变为产出词位

记住一句

simple 对中文不是"效果差",是"零产出"。上线前先 SELECT to_tsvector() 看一眼索引到底看见了什么,再谈检索质量。

坑 3:一个标量子查询,让 HNSW 索引白建

症状

先说"#2"是什么:kb_pgops 数据集一共 12 篇文档,主键 id 从 1 到 12,#2 就是 id=2 的那篇;"以 #2 为锚点找相似文档"是很常见的需求——先把这篇文档的向量从库里取出来,再拿它当查询向量,全表找最像它的 5 篇(“看了这篇的人还看了……”)。SQL 很自然会这么写(注意 -d 必须是装了 kb_pgops 的业务库 ivorysql;坑 2 里建的编码探针库 zh_probe 没有这张表,连错库只会得到 relation "kb_pgops" does not exist):

# 先直接执行锚点查询
docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c "SELECT id FROM kb_pgops ORDER BY embedding <=> (SELECT embedding FROM kb_pgops WHERE id=2) LIMIT 5;"

# 再看执行计划(图 5 就是这条的输出)
docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c "EXPLAIN (ANALYZE, BUFFERS, COSTS OFF) SELECT id FROM kb_pgops ORDER BY embedding <=> (SELECT embedding FROM kb_pgops WHERE id=2) LIMIT 5;"

EXPLAIN (ANALYZE, BUFFERS) 里冒出一个 InitPlan,计划退化成了 Seq Scan + Sort——HNSW 索引在旁边看着,没被用上。

这棵计划树要分层读,别被里面的 Index Scan 迷惑(手工执行时最容易误判的地方):

Limit                                   ← 最外层:截断 5 行
  InitPlan 1                            ← 先独立执行的标量子查询:取 #2 的向量
    -> Index Scan using kb_pgops_pkey       走的是【主键 btree 索引】,Index Cond: (id = 2),只扫 1 行
  -> Sort                               ← 主检索路径:排序
       Sort Key: embedding <=> (InitPlan 1).col1
       -> Seq Scan on kb_pgops              12 行【全表扫描】后逐行算距离再排序
  • InitPlan 下面那个 Index Scan using kb_pgops_pkey 只是子查询"取锚点向量"时走了主键索引,跟向量检索无关;
  • 真正干"找相似"活的主路径是 Sort → Seq Scan,全表 12 行一行不落;
  • 判断 HNSW 用没用上,只看计划里有没有出现 kb_pgops_hnsw_idx 这个名字——没有,就是没用上。

根因

向量不是字面量、而是"运行期才算得出来的值"时,ORDER BY embedding <=> <标量> 无法被 HNSW 访问方法识别为可索引的排序表达式,规划器只能全表扫 + 排序。

同样的查询,把向量写成字面量(或绑定参数),计划立刻变成 Index Scan using kb_pgops_hnsw_idx。两者在 12 行小表上看不出快慢,百万行量级下是数量级的差距。

解法(按优先级)

首选(根治):应用层算好向量,用绑定参数 $1::vector 传入。 向量在规划阶段就确定,HNSW 索引接通,大表走 Index Scan。这是唯一能根治本坑的写法。两种写法差别只在查询向量怎么传:

-- ✗ 锚点写法:向量来自标量子查询,HNSW 用不上(本文复现的退化现场)
SELECT id FROM kb_pgops
ORDER BY embedding <=> (SELECT embedding FROM kb_pgops WHERE id = 2)
LIMIT 5;

-- ✓ 参数写法:向量在规划期已确定,HNSW 接通(应用层用 $1::vector 绑定)
SELECT id FROM kb_pgops
ORDER BY embedding <=> $1::vector
LIMIT 5;

如果"从库里取一篇当锚点"的需求去不掉,就在应用层分两步:先查出锚点向量,再以绑定参数回传做第二次查询,不要在一个 SQL 里套两层。

验证

对照计划:sql/02_vec_plan_check.sql 在同一会话里先出默认计划(Seq Scan + Sort,hit=6),再 SET enable_seqscan=off 出对照计划(Index Scan using kb_pgops_hnsw_idx,约 34 个缓冲区页),最后自动 RESET。一条命令跑完三段(PowerShell 5.1 不支持 < 重定向,外面套 cmd /c):

cmd /c "docker exec -i ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 < sql\02_vec_plan_check.sql"

本文实采的锚点写法计划(Sort Key 里明明白白写着 (InitPlan 1).col1):

12.png

图:坑 3 实证——InitPlan 1 下的主键 Index Scan 只负责取 #2 的向量;主路径 Sort → Seq Scan 全表扫,全程没有出现 kb_pgops_hnsw_idx(Buffers 读数随缓存冷热小幅浮动,计划形态不会变)

记住一句

查询向量怎么传进 SQL,决定了 HNSW 到底能不能用。想"从库里取一条当查询"的写法,往往正好把索引废掉。

坑 4:w=1 不等于"走 BM25 索引扫描"

先说清楚 w 是什么:混合检索里,最终得分是两路分数相加——

总分 = w × BM25 文本分 + (1 − w) × 向量相似度。

w 就是 BM25 和向量的配比系数:w=1 表示"结果分里 BM25 占 100%、向量分被乘 0 丢弃";w=0 表示"完全只看向量";w=0.3 表示 BM25 占 30%、向量占 70%。

它只决定两路分数怎么相加,并不决定 BM25 那一路走不走索引、也不决定没命中的行要不要参与排序。 下面这个坑,正是把这两件事搞混了。

症状

融合 SQL 里把权重设成 w=1(直觉上 = “完全听 BM25 的”),结果榜单出现 [2,3,4,5,1]——
Q3 的 BM25 明明只命中了 #2,后面那几位是怎么混进来的?注意:并列行的次序由物理存储决定,表一旦做过 VACUUM / UPDATE / INSERT,这个"并列内排名"就可能换人——它从根上就不稳定。

根因

很多人以为 w=1 会自动让数据库像独立 BM25 查询那样,“只返回命中关键词的文档”。这是两个完全不同层级的事:

13.png

问题出在最后一步排序:ORDER BY 总分 DESC LIMIT 5 时——

  • #2 总分 1.0,稳拿第一;
  • 剩下 11 行全是 0.0,并列。PostgreSQL 遇到并列,会按物理存储顺序(磁盘上行的实际位置)取前几个,而物理顺序不是 id 顺序,还会被 VACUUM / UPDATE / INSERT 打乱。

所以:榜单变成 [2,3,4,5,1] 是因为 #2 是冠军,后面 4 个是从 11 个 0.0 里按物理顺序抓出来的;静态表上连跑多次看着一样,但只要 VACUUM / UPDATE / INSERT 动过物理顺序,并列里被抓到的就是另一批。

1.4.0 实测复现(权重扫描 Q3):

w=0.0 : ids=[2, 3, 8, 6, 10] hits=3/3
w=0.3 : ids=[2, 3, 8, 6, 10] hits=3/3
w=0.5 : ids=[2, 3, 8, 6, 10] hits=3/3
w=0.7 : ids=[2, 3, 8, 6, 10] hits=3/3
w=1.0 : ids=[2, 3, 4, 5, 1] hits=2/3     ← 退化在这里

为什么 w=0 和 w=0.3 反而稳定?因为向量分那一路没有零分问题——12 行每行的相似度都不同,w 越小,向量分越能把行与行之间的差距拉开,就不会出现大量并列 0.0。只有 w=1 时向量系数被乘成 0,这个坑才暴露出来。

w 在代码里落在哪、怎么改成 1:手写融合 SQL 时,它就是最后打分那行的两个系数:

-- 通用形态:w 是 BM25 路系数,(1-w) 是向量路系数
round((w*b.ns + (1-w)*v.ns)::numeric, 4) AS hybrid_s
-- sql/04a_w1_zero_tie.sql 里的 w=1:BM25 系数 1.0、向量系数 0.0
round((1.0*b.ns + 0.0*v.ns)::numeric, 4) AS hybrid_s
-- 把两个系数换成 0.5/0.5 即 w=0.5

用配套脚手架时,w 是 build_sql(..., weight=w) 的入参,扫描档位 [0.0, 0.3, 0.5, 0.7, 1.0] 配在 code/experiment_kit/configs/kb_pgops_ts140.json 的 hybrid.weights 里,w=1.0 是扫描的边界一档。

什么场景真会把 w 设成 1:

① 调试时临时退化成"纯关键词检索",拿 BM25 单路结果当对照基线;

② 权重调参扫描扫到边界值 1.0(本文的退化现场正是这么扫出来的,见上面的权重扫描表);

③ 业务上某些查询只信精确词(错误码、型号、SQL 关键字),主动关掉语义路;w=0 是对称的另一头(纯向量)。

三种场景都合理,真正的坑是误以为"设成 w=1 就等价于独立 BM25 查询"——w 只改分数权重、不改候选集与执行路径,没命中的行照样以 0 分留在榜单里占坑(即上面对照表的三层差异)。

解法

零分候选保留还是丢弃,是融合 SQL 设计时必须显式写出来的策略,不能交给默认排序。 想让 w=1 严格等价于独立 BM25,就在 CTE 里显式过滤,只让真正命中的行参与融合。这里有一个必须实测才知道的细节:逐行函数路径下,没命中的行拿到的是 0.0 而不是 NULL(只有走 BM25 索引扫描时,未命中行才不出现、表现为 NULL),所以只写 IS NOT NULL 根本过滤不掉它们,必须把 0 分也剔掉:

-- 在 bm25_n 的来源处过滤,后续内连接时零分候选自然不参与融合
bm25_n AS (
  SELECT id, /* …min-max 归一化… */ AS ns
  FROM (SELECT * FROM bm25_r WHERE s IS NOT NULL AND s <> 0) b0
)

为什么可以用 s <> 0:pg_textsearch 的 BM25 分对命中行为负值、对零命中行为 0.0,命中行不可能恰好是 0;若你的语料里可能出现合法 0 分,改用 content @@ plainto_tsquery('simple', '查询词') 这类"是否命中"谓词更严谨。

验证

配套脚本连跑三段:① w=1.0 退化现场;② 加 s IS NOT NULL AND s <> 0 过滤后的修复结果;③ 独立 BM25 路对照。

想一次看完三段,跑合并版(PowerShell 用 cmd /c 重定向,避免中文编码问题):

cmd /c "docker exec -i ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 < sql\04_zero_score_tie.sql"

想逐段执行、逐步核对,下面三条命令分别对应三段(预期输出已标注,与图 6 的三个面板一一对应):

# ① w=1.0 退化现场:预期 5 行,榜单 [2,3,4,5,1],后四行 hybrid_s 全是 0.0000
cmd /c "docker exec -i ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 < sql\04a_w1_zero_tie.sql"

# ② 修复:预期只剩 1 行(id=2,bm25_n/vec_n/hybrid_s 都是 1.0000)
cmd /c "docker exec -i ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 < sql\04b_filter_fix.sql"

# ③ 独立 BM25 对照:预期只有 id=2、原始 BM25 分 -2.0694
#    (ORDER BY <@> 走 BM25 索引扫描,零命中文档不返回——与①的逐行函数路径正好形成对照)
cmd /c "docker exec -i ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 < sql\04c_bm25_only.sql"

本文容器实测图:① 的榜单是 [2,3,4,5,1],后四位 hybrid_s 全是 0.0000;② 过滤后只剩 #2 一篇;③ 独立 BM25 也只返回 #2——修复后两路严格一致、重复执行稳定。

14.png

图:坑 4 实证——零分并列制造"假榜单",显式剔除后与独立 BM25 榜单一致

记住一句

融合不是把两路 SQL 拼起来就完事。“没命中的行给 0 分"和"没命中的行不参与”,是两个完全不同的设计。

坑 5:建了 HNSW 却走 Seq Scan,idx_scan = 0

症状

建了 HNSW 索引(DDL 见 0.4,即 kb_pgops_hnsw_idx),EXPLAIN 里却是 Seq Scan;查 pg_stat_user_indexes,idx_scan = 0。

第一反应:“索引白建了 / 参数配错了”。

根因:这是规划器在帮你省钱

12 行小表上的实测对照(1.4.0 实采):

15.png

全表算 12 次余弦距离只需访问 6 个共享缓冲区页;走 HNSW 要访问约 34 个页(含回表),反而更贵。

这不是"索引没用",是"这次全表更便宜"。

想亲眼对照,跑坑 3 那份脚本即可(前两段就是本节内容):

cmd /c "docker exec -i ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 < sql\02_vec_plan_check.sql"

16.png

图:坑 5 实证——规划器选 Seq Scan 是因为访问的缓冲区页更少(6 vs 约 34)

一个有意思的实测旁证——本文容器的 idx_scan 计数:

kb_pgops_bm25_idx |  42    ← BM25 查询基本都走索引
 kb_pgops_hnsw_idx |   8    ← 仅有的几次都来自对照实验里手动 SET enable_seqscan=off
 kb_pgops_pkey     |  44

idx_scan 是累计计数器,数值随容器执行历史只增不减(你复现时几乎不会正好是 42/8/44);要看的是对比关系:自然查询下 HNSW 扫描次数长期接近 0。

不是索引坏了,是规划器算过账。

Buffers 别当固定常数:它随索引物理大小、缓存冷热浮动(同一索引 REINDEX 前后、容器刚启动与运行一段时间,读数都会变)。看计划形态(走没走索引),别背具体数字。

解法

不用解。 要做的只有两件事:

  1. 小表上坦然接受 Seq Scan;
  2. 数据量上来后用 EXPLAIN 复核是否切换,不要凭感觉强制走索引。

若一定要验证 HNSW 可用(比如坑 3 的对照实验),SET enable_seqscan=off 临时验证,验证完记得 RESET——别把这开关留在生产会话里。

记住一句

idx_scan = 0 在小表上是正常的。规划器是个会算账的管家,它选 Seq Scan 是因为这样更便宜。

坑 6:HNSW 维护与容量预算

维护:三条实测结论

17.png

另外三条实用经验:

  • 死节点靠 VACUUM 回收:更新/删除在图里留死节点,不影响正确性(扫描时跳过)但占空间;大批量写入后可手动 VACUUM (ANALYZE) 表名;
  • 大批量建索引提速:会话内 SET maintenance_work_mem='4GB'; SET max_parallel_maintenance_workers=4;,建完恢复;
  • 检索侧 GUC(0.8.5 实测):hnsw.ef_search(默认 40)、hnsw.iterative_scan(改善带过滤条件的召回)、hnsw.max_scan_tuples(默认 20000)。

容量:公式会低估,维度换算就是"翻倍即翻倍"

实测锚点(同一容器、同一建索引参数 m=16, ef_construction=64,每种维度各造 1 万行):

18.png

本文 12 行表上三个索引的实测体积,一条 psql 元命令即可复现(图 8):

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c "\di+ kb_pgops*"

19.png

图:bm25 / hnsw / btree 三种访问方法的体积实测

为什么不能直接套公式:理论口径是向量本体 4 × 维度 + 8 = 1544 B/行(384 维),加图链接经验值 150~300 B/行 ≈ 1694~1844 B/行,比实测 2048.8 B 偏低约 10~20%。差额来自页内开销、多层图结构与索引 tuple header。

维度换算:实测的结论是"维度翻倍、每行字节数也翻倍"。

这一点非常容易想错。直觉上会觉得"只有向量本体(4 B/维)随维度增长,图链接那些是固定开销",所以维度翻倍时容量应该只涨一部分——实测不是这样。上面三档的字节数是 2048.8 → 4096.8 → 8192.8,系数稳定在约 5.33 B/维;把向量本体扣掉后,剩余开销是 504.8 / 1016.8 / 2040.8 B,它自己也在随维度涨(约 1.3 B/维)。原因是行变宽之后,页内对齐、tuple header 与多层图的存储开销同样被放大了。

所以估容量就按"维度翻倍、容量翻倍"估,这既保守也最接近实测。千万别用只算向量本体的口径,那会低估 20% 以上。

顺带一个现场提醒——1536 维才 1 万行,建索引时就撞上了这个:

NOTICE:  hnsw graph no longer fits into maintenance_work_mem after 9665 tuples
DETAIL:  Building will take significantly more time.
HINT:  Increase maintenance_work_mem to speed up builds.

维度越高、行数越多越容易触发。建索引前先按 §6.1 把 maintenance_work_mem 调大,别等它慢下来了才发现。

预算建议:

  • 按维度取每行字节数:384 维 2048.8 B / 768 维 4096.8 B / 1536 维 8192.8 B(m=16 口径)。384 维下 100 万行 ≈ 1.91 GiB、1000 万行 ≈ 19 GiB
  • 其他维度用 5.33 B/维 × 维度 粗估,或按下面的办法实测
  • 建索引期间需要额外临时空间(受 maintenance_work_mem 影响)
  • 生产预留估算值的 1.5~2 倍
  • BM25 倒排索引体积取决于语料 token 总量,不能按行数套,必须实测

最准的办法:造一张 N 行同维度的表,CREATE INDEX 后 SELECT pg_relation_size('索引名')/N 即得每行真实字节数。脚本见 code/probe_capacity.sql(384 维);上面三档维度的对照实测见 code/probe_capacity_dims.sql。

记住一句

HNSW 的维护负担比想象中轻(增量可见),但磁盘负担比公式算出来的重(实测高 10~20%),而且维度翻倍、容量就翻倍——别只算向量本体那部分。

坑 7:上线后没得看——监控基线从零开始搭

pg_stat_statements:镜像里有,但默认没开

先查它在不在(可直接复制执行):

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c "SELECT name, default_version, installed_version FROM pg_available_extensions WHERE name='pg_stat_statements';"

本文容器实测输出——注意:“有一行返回"不等于"已经启用”:

name        | default_version | installed_version
--------------------+-----------------+-------------------
 pg_stat_statements | 1.12            |
(1 row)

default_version = 1.12 只说明镜像里带着这个 contrib 模块;installed_version 是空的才是关键——它表示"可用但尚未 CREATE EXTENSION"。再看一眼预加载名单,里面同样没有它:

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c "SHOW shared_preload_libraries;"
# 实测:gb18030_2022, liboracle_parser, ivorysql_ora, pg_textsearch(没有 pg_stat_statements)

它属于 contrib,必须先出现在 shared_preload_libraries 里才能工作。启用三步:

  1. 把 pg_stat_statements 追加进容器内 ivorysql.conf 的 shared_preload_libraries(保留原有项)
  2. docker restart ivorysql-ts140
  3. CREATE EXTENSION pg_stat_statements;

启用后它能回答"哪条 SQL 累计最耗时、哪个查询吃 buffer 最多"。多模检索这种每毫秒都要算钱的场景,没它等于闭眼开车。生产必开。

死元组与索引使用率

-- 死元组比例(写入型表重点看)
SELECT relname, n_live_tup, n_dead_tup, n_tup_upd, n_tup_del,
       round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct
FROM pg_stat_user_tables WHERE relname = 'kb_pgops';

-- 索引被用了多少次
SELECT indexrelname, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes WHERE relid = 'kb_pgops'::regclass;

经验阈值:

20.png

一条巡检清单

串成定时任务(完整脚本见 sql/03_monitor.sql):

  1. canary 探针(见附录 D)——BM25 索引返回 0 行即告警,这是发现"查询异常"最快的手段
  2. 索引状态 indisvalid/indisready/indislive —— 注意它正常不代表算得对
  3. 死元组比例 —— 阈值 10% / 20%
  4. 索引使用率 —— 小表 idx_scan=0 属正常,大表持续为 0 才需要查
  5. TOP 10 慢查询 —— 需先启用 pg_stat_statements

记住一句

多模检索的故障大多是"静默"的——不报错、不崩、只是慢慢不准。没有巡检,你会在很久以后才发现。

如果只记三件事

7 个坑末尾各有一句"记住一句";下面三条是最容易直接造成线上事故或评测失真的点(覆盖坑 3、坑 5、坑 6 与附录 D),其余坑回看 0.3 速览表即可。

  1. 查询向量怎么传,决定 HNSW 生不生效。 绑定参数进得去,标量子查询进不去(坑 3)。
  2. 规划器选 Seq Scan 是在帮你省钱,别急着改参数。 但容量要按实测每行字节数算,公式会低估 10~20%(坑 5、坑 6)。
  3. 巡检要覆盖"查询结果"而不只是"索引状态"。 系统目录说索引活着,不代表它算得对(见附录 D)。

附录 A:12 行最小对照实验(可选)

七个坑都不依赖这个实验。如果你想知道"BM25 和向量到底谁召回得好",可以用这套最小集跑一遍——但请先看完这段声明。

A.1 能证明什么、不能证明什么

21.png

所以 Recall@5(即"前 5 个结果里的命中率";本文 5 个查询共 9 个 gold 标注点、全部落在 Top-5 = 9/9)这一列只能读成"机制演示的一致性验证",不能读成能力得分。 想做真正的能力评测,请换外部标准数据集(如 BEIR / MTEB 公开子集 + 官方 qrels)。

A.2 怎么跑

# ① 建表 + 灌入 12 篇短文 + 建双索引(先把脚本拷进容器,文件位置见附录 B)
docker cp code/kb_pgops_init.sql ivorysql-ts140:/tmp/
docker exec -i ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -f /tmp/kb_pgops_init.sql

# ② 一键跑完五查询三路对比 + 权重扫描
cd code/experiment_kit
python run_all.py --config configs/kb_pgops_ts140.json

约 1 分钟后在 results/<时间戳>/article_tables.md 得到全部表格。零三方依赖,Python 3 标准库即可。

本文 1.4.0 实测关键自检点(读者复跑时应得到相同结果):

Q5: bm25 top3=[11,6,7] / vec #11 第3位 / hybrid #11 第1位
权重扫描 Q3 w=1.0 : ids=[2, 3, 4, 5, 1] hits=2/3   ← 坑 4 的现场

A.3 换成你的数据

抽 8 条以上真实业务查询重跑:

  • BM25 也接近全中 → 查询以精确术语为主,向量侧可以省
  • 只有 50~70% → 混合的收益值得投入

这一步只有你能做,因为真实查询语料在你手里。

A.4 评测设计的三条纪律:让结论经得起别人复跑

这三条是本文设计实验时实际用来"卡住自己"的检查项,也是读者给自己的多模检索实验做体检、或评审他人评测报告时可以直接逐条对照的清单。

22.png

一句话

评测的可信度不来自漂亮的数字,而来自别人按你的步骤复跑时,清楚知道哪些结果必须一样、哪些本来就会浮动。

附录 B:文件清单

配套文件已开源于 GitHub,读者可克隆后按上表相对路径取用:

仓库:

https://github.com/markboluo26330/ivorysql-multimodal-series

克隆:git clone

https://github.com/markboluo26330/ivorysql-multimodal-series.git

重跑 make_shots.py 需 Python 3 + Pillow / matplotlib。

23.png

附录 C:环境复现

C.1 推荐路径:从官方镜像全量构建(任何人都能跑通)

本路径的基础镜像是 IvorySQL 官方公开镜像 registry.highgo.com/ivorysql/ivorysql:5.4-ubi8,不依赖任何本地私有镜像,换台机器一样能复现。构建过程中会依次编译 pgvector 0.8.5、Apache AGE、pg_bigm 与 pg_textsearch 1.4.0,耗时约 2~15 分钟(视网络与机器性能)。

# ① 构建(Dockerfile 位于本仓库的 06_生产级部署_Dockerfile/ 目录)
cd 06_生产级部署_Dockerfile
docker build -f Dockerfile -t ivorysql-kb:pg18-ts140 .

# ② 启动
docker run -d --name ivorysql-ts140 -p 5436:5432 \
  -e IVORYSQL_PASSWORD=Test@2026 ivorysql-kb:pg18-ts140

# ③ 建扩展 + 灌数据
docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 \
  -c "CREATE EXTENSION pg_textsearch;" -c "CREATE EXTENSION IF NOT EXISTS vector;"
docker cp code/kb_pgops_init.sql ivorysql-ts140:/tmp/
docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -f /tmp/kb_pgops_init.sql

该镜像默认预加载四扩展(age 也在内)。本文容器为最小集(多模扩展只预加载 pg_textsearch,不含 age)——两者对本文全部结论没有影响,坑 1 的实测值以你自己的 SHOW shared_preload_libraries 为准。

C.2 可选路径:只重编 pg_textsearch(版本对照用)

如果你已按本系列上篇(系列一)构建过 ivorysql-kb:pg18 镜像,可以用增量 Dockerfile 只重编 pg_textsearch,几十秒出结果,便于做 0.6.1 / 1.4.0 的版本对照:

docker build -f Dockerfile.ts_version -t ivorysql-kb:pg18-ts140 .                      # 1.4.0(默认)
docker build -f Dockerfile.ts_version --build-arg TS_VERSION=v0.6.1 \
  -t ivorysql-kb:pg18-ts061 .                                                            # 复现旧版故障

注意:该 Dockerfile 的 FROM 是本地镜像 ivorysql-kb:pg18,没有这个基础镜像会构建失败。首次复现请走 C.1。

关于命令执行方式:本文命令均在 Windows PowerShell 5.1 下实测。PowerShell 5.1 不支持 < 输入重定向,喂 .sql 文件统一套一层 cmd /c “docker exec -i … < 文件”(正文与截图均为此写法,中文不乱码);也可以先 docker cp 进容器再用 -f 执行。Linux / macOS / Git Bash 直接 < 重定向即可。

附录 D:一个已经被新版本修掉的坑

这一节记录 0.6.1 上的一个真实故障。它在 1.4.0 上已复现不出,但保留价值有三:

给还在用 0.x 的读者一个完整的排障样本;说明 canary 探针为什么值得配;以及一个朴素道理——prerelease 的警告不是吓唬人的。

症状:索引状态全 t,查询却返回 0 行

0.6.1 容器持续运行、跨重启之后:kb_pgops_bm25_idx 在系统目录里一切正常——

SELECT indisvalid, indisready, indislive FROM pg_index
WHERE indexrelid='kb_pgops_bm25_idx'::regclass;
-- t | t | t

但索引扫描对所有查询返回 0 行。

更隐蔽的是混合路不报错,只是分数悄悄漂移:融合 CTE 里 BM25 走逐行函数路径还在出分,但语料统计已经错了——实测出现过「不含 pg_dump 的文档被打出 -2.1401 的分」。这种漂移没有任何告警。

根因:0.6.1 的内存架构

0.6.1 是纯内存倒排索引,重启后从堆表重建,重建路径在部分场景下不可靠——这也是官方当时给出的警告所指的那类风险:

WARNING: pg_textsearch v0.6.1 is a prerelease. Do not use in production.

pg_textsearch 1.0 把架构整个重写成了「内存 memtable + 磁盘 segment」(LSM 式),索引随 WAL 持久化。这正是本次升级对照里唯一"消失"的坑——1.4.0 容器连续重启两次,canary 探针稳定返回 id=1,五个查询的打分与重启前逐位一致。

排障手段今天依然有用

发现——为每个 BM25 索引配 2~3 条"已知必有结果"的探针,纳入定时巡检:

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c \
  "SELECT id FROM kb_pgops ORDER BY content <@> 'pg_dump 备份' LIMIT 1;"

返回 0 行(而不是 id=1)= 检索出了问题,立即告警。 完整巡检脚本见 sql/01_canary_probe.sql。

巡检基线(1.4.0 实测:探针 id=1、索引状态三个 t、死元组 0%、hnsw_idx 的 idx_scan 长期接近 0——最后这个正是坑 5 的正常现象;计数器只增不减,具体数值以你自己的环境为准,见图):

24.png

图:巡检基线(对应 sql/01_canary_probe.sql + sql/03_monitor.sql)

恢复(0.x 上有效)——REINDEX(本数据规模秒级完成):

docker exec ivorysql-ts140 psql -U ivorysql -d ivorysql -p 5432 -c \
  "REINDEX INDEX kb_pgops_bm25_idx;"
-- 预期 NOTICE:BM25 index build completed: 12 documents, avg_length=11.75

生产上用 REINDEX INDEX CONCURRENTLY 避免长时间持锁。

顺带一个实测结论:不存在 VACUUM INDEX 这个语法(会报错)。BM25 是自定义访问方法,维护靠表级 VACUUM/autovacuum,重建靠 REINDEX [CONCURRENTLY]。

两条教训(不随版本失效)

  1. 索引健康不能只看 indisvalid。 系统目录说它活着,不代表它算得对。必须配探针。
  2. 两条路径的故障模式不同,监控要分别覆盖:独立 BM25 路坏了 → “查不到”(容易发现);混合路坏了 → “分数悄悄变”(更危险,且没人报警)。

本文所有数字均来自 ivorysql-ts140 容器(IvorySQL 5.4 + pg_textsearch 1.4.0 + pgvector 0.8.5)实测,最近一次采集日期 2026-09-14;0.6.1 专属现象已集中在附录 D 并标注。
8 个配套 SQL 脚本与 9 张配图均在该环境完整执行/采集通过,复现步骤见附录 C。

术语表

25.png

WhatsApp API接口的隐秘角落:从协议解析到自定义消息引擎 本文深入解析WhatsApp API接口的隐秘角落,从协议逆向工程到自定义消息引擎开发实战。通过分析WhatsApp的二进制XML协议和加密机制,构建超越官方API限制的自定义消息引擎,实现高效、灵活的即时通讯解决方案。文章还提供了企业级架构设计和风控规避策略,助力开发者突破官方限制,提升消息到达率和成本效益。 阅读详情

相关推荐

Unity AR涂涂乐实战:用户上传图片秒变3D模型新皮肤(附完整代码)

本文详细介绍了如何在Unity AR应用中实现用户上传图片动态生成3D模型皮肤的功能,重点讲解了动态创建材质球的技术方案和性能优化策略。通过完整的代码示例,开发者可以学习到多源图片处理、移动端内存管理以及AR场景适配等核心技巧,快速打造高性能的儿童教育类AR应用。

weixin_31509753的博客 370

IvorySQL 多模融合实战:pgvector + AGE + pg_textsearch 在同一实例中的协同验证

维度结论向量数据✅完全支持。通过 pgvector 扩展,支持 16000 维向量、HNSW/IVFFLAT 索引、多种距离算法,Oracle 兼容模式下可用。知识图谱✅完全支持。通过 Apache AGE 扩展,支持 openCypher、属性图、SQL 混合查询。适合中等规模知识图谱,超大规模建议专用图数据库。全文检索✅完全支持。多扩展方案(pg_textsearch/pg_bigm/PGroonga),覆盖 BM25、中日韩分词、模糊检索、混合搜索等场景。⚠️本文聚焦功能可行性验证。

IvorySQL的博客 405

Python处理高光谱数据#3 降维

基于Python的高光谱降维操作 高光谱数据:点击此处可下载 用到的库:matplotlib、scipy、spectral 主要内容:主成分分析(PCA)与线性判别(LDA) 欢迎有兴趣的朋友交流指点。最后,废话不多说直接上代码???? import matplotlib.pyplot as plt from scipy.io import loadmat import spectral as spy

qq_273058791的博客 6939

fast-lio2复现跑avia数据集

本文详细介绍了在Windows WSL2中安装Ubuntu 22.04并配置ROS2 Humble环境的全过程,涵盖系统初始化、ROS2安装、Livox雷达驱动编译及FAST-LIO2 ROS2版本的构建与验证。通过完整数据集回放与建图测试,成功实现点云地图生成与轨迹记录,并利用evo工具完成轨迹分析与可视化。最终输出包括累积点云地图、位姿轨迹及实验配置文件,为后续精度评估与复现提供基准。

chase_my_dream的博客 369

rk3568 android 11.0 userdata 改为ext4格式

功能:解决设备在恢复出厂设置进行擦除数据的时候卡住问题。 1.修改userdata分区为ext4格式 diff --git a/device/rockchip/common/scripts/fstab_tools/fstab.in b/device/rockchip/common/scripts/fstab_tools/fstab.in index 583e75579d..036336835d 100755 --- a/device/rockchip/common/scripts/fstab_tools/

chgh1306的专栏 2578

Django 前后端分离实战:网络设备运维系统 - 从登录认证到业务模块

这篇文章分享了Django+Vue前后端分离开发网络设备运维系统的实战经验,重点介绍了登录认证、全局布局和设备管理三大模块的实现。文章包含以下核心内容: 登录模块实现:包括表单验证、Token认证、记住密码功能,以及美观的UI设计(渐变背景、毛玻璃效果) 全局布局架构:采用侧边栏+顶部导航的响应式设计,支持菜单权限控制和多设备适配 业务功能开发:详细展示了设备分类和部门管理的CRUD功能实现,包括列表展示、搜索过滤、分页等功能 常见问题解决:提供了5个开发中常见问题的解决方案,如Vue Router警告、C

qq_16766705的博客 526

linux学习

sudo make install:组装好的家具(编译好的库文件),原本放在你的临时工作区。坏处是,你的程序体积会变大。于是,他会根据你的计划书,结合你电脑的当前情况(比如装了什么编译器,库在哪),生成一本极度详细的《施工进度表》。很多大牌的第三方库(比如你刚装的 libevent)为了照顾不同的需求,会同时提供动态库(.so)和静态库(.a),但并不是所有库都这样。只有动态库:常见于系统的核心库(比如你系统的底层 C 库),或者一些商业闭源软件(厂商只给你一个黑盒子,不让你直接把代码打包进你的程序)。

d_mail的博客 194

PX4自定义uORB消息与MAVLink消息映射实战:从飞控到QGC地面站完整流程解析

本文详细解析PX4飞控中自定义uORB消息与MAVLink消息映射的完整流程,涵盖从飞控内部消息定义、MAVLink消息创建到QGC地面站接收显示的全链路实践。重点介绍如何使用mavlink inspector工具进行调试,解决版本兼容性和多实例消息处理等常见问题,为无人机开发者提供可靠的实时数据监控方案。

ll5678的博客 1069

ubuntu 偶发启动失败问题分析

ubuntu 系统启动偶发异常问题分析

平面到立体 193

LLaVA-1.5:多模态视觉问答新突破

LLaVA-1.5在多模态视觉问答(VQA)任务中取得了显著进展,主要得益于视觉-语言连接器的优化和特定任务数据集的增加。通过将连接器升级为两层MLP,模型在处理视觉与语言关系时表现更佳。同时,引入如OKVQA、OCRVQA等数据集,增强了模型在复杂视觉推理任务中的能力。在11个基准测试中,LLaVA-1.5超越了现有方法,展示了其强大的多模态能力。此外,模型通过提升输入图像分辨率、扩展语言模型和改进多语言能力,进一步提升了性能。尽管在短格式回答任务中初期表现不佳,但通过调整提示格式和微调,其性能显著提升

weixin_57060548的博客 946

利用Dify平台打造大学生专属AI模拟面试助手:从零到精通的完整指南

在这个充满竞争的就业市场中,拥有一个24小时在线的AI面试教练无疑是大学生求职路上的秘密武器。通过本文的指导,你已经掌握了使用Dify平台构建专业级模拟面试助手的全套技能。从环境部署、模型选择,到Prompt设计、知识库集成,再到高级功能开发和效果优化,我们一步步打造了一个能够适应不同岗位、提供专业反馈的智能面试系统。无论是技术岗的算法问答,还是咨询业的案例分析,或是外企的英语面试,你的定制助手都能提供针对性训练。更重要的是,这个方案具有极强的可扩展性。

专注于编程、AI、人工智能等多个领域内容研究和分析 2795

Docker Compose:搭建本地多服务开发环境

从一个 Java API、PostgreSQL 和 Redis 开始,配置服务名、端口、环境变量、健康检查、数据卷和依赖关系。理解 Compose 文件如何被解析、项目如何隔离,以及哪些数据不能依赖“重建容器”保留,能够把个人开发环境整理成团队可复用的启动入口。

m0_62221377的博客 477

2025 华为OD机试真题e卷 C语言 实现【竖直四子棋】

竖直四子棋的棋盘是竖立起来的,双方轮流选择棋盘的一列下子,棋子因重力落到棋盘底部或者其他棋子之上,当一列的棋子放满时,无法再在这列上下子。

MISAYAONE的博客 1418

AI全栈实战 | 3.9-01 爬虫入门:requests 加 BeautifulSoup 三分钟跑通,但 90% 的人栽在这 4 个“礼貌“问题上

⚠️:本文内容仅用于。请遵守目标站点的服务条款与相关法律法规,不得对目标服务造成负担,不得采集、传播个人隐私数据。技术是中性的,边界在你手里。爬虫的入门技术(requests 发请求 + BeautifulSoup 解析)三分钟就能学会。真正区分"会用爬虫"和"会做爬虫的人"的,是这篇我们跑通"请求→解析→存储"的最小闭环,然后重点把"合规与礼貌"这根弦给你绷上。

源码获取 | 运行部署 | 定制二开修改,如果回复不及时请耐心等待,请见谅。 432

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

【基于 WebSocket 的多用户网页五子棋】—— 数据管理模块

但行好事,莫问前程

qq_42996461的博客 250

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

C++算法(七)

C++ stack算法题和一些客观题

2503_94479566的博客 378

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

OpenClaw 深度调查:把 AI 助手做成你自己的控制平面

对 openclaw/openclaw 的完整技术尽调:Gateway 架构、Agent 运行时、工具与沙箱、30 个消息通道、181 个插件、安全信任边界、工程代价与采用建议。17 章 / 10 张图,全部数字标注口径,数据截至 2026-10-31。

厚积薄发 294

IDEA 简介:从零认识 IntelliJ IDEA

IDEA 全称,是由 JetBrains 公司开发的一款主流 Java 集成开发环境(IDE)。它集代码编辑、编译、调试、测试、版本控制、构建管理等功能于一体,被大量 Java 开发者视为日常开发的首选工具。所谓 IDE(Integrated Development Environment,集成开发环境),就是把写代码所需的各类工具整合到同一个界面中。相比只用文本编辑器加命令行编译,IDE 能显著减少重复操作,让开发者把精力集中在业务逻辑上。

ztI10的博客 177

TransactionInactiveError:await 之后事务已经不活跃了

TransactionInactiveError 出现于 IndexedDB 事务已结束(非 active 状态)时调用 put、get 等操作。常见于 await 后或 setTimeout 中执行请求,因任务已完成,事务已提交或进入 finished 状态。请求必须在创建事务的同一任务中挂起,否则无效。解决方法:在 onsuccess 回调内同步发起后续操作,或提前将所有请求挂入事务。避免在异步上下文中使用事务操作,确保请求在事务活跃期内完成。

qq_40822000的博客 159

云计算课程小组PPT汇报任务书

36人分为6组,每组6人;形式:生活案例调研 + 理论解读;汇报时长7‑9min;PPT建议10‑14页。

Michelle8023的博客 208

大表 ALTER TABLE 怎么做?NineData OnlineDDL 配置与执行实践

介绍NineData OnlineDDL大表变更实践,涵盖执行策略、切表控制、拷贝批次与可视化进度追踪。

dbasenewbie的博客 295

软件项目交付时,源代码之外还应该交付哪些技术资料?

软件项目交付时,除可运行程序外,还应核对源代码、数据库结构、接口文档、部署运维、测试验收和账号资产。本文从实际交付环节拆解完整资料清单,并说明合同如何约定交付范围、知识产权与维护边界。

chengduyoushu的博客 306

Axum 初步入门学习笔记

)自定义错误实现后,可以直接作为Result的ErrAxum 是一个构建在 Tokio、Tower、Hyper 之上的 Rust Web 框架,核心能力是:Router 路由、Extractor 提取器、Tower 中间件、IntoResponse 响应机制、State 共享状态。那张分层架构图是“地图”不是“说明书”:它帮你建立全局观,但 Service、Repository、数据库、Redis、MQ 都是你自己在 Handler 内部调用的业务/基础设施层,不是 Axum 自带的。

weixin_43219667的博客 187

MySQL报Too many open files?句柄耗尽的排查与参数配置

文章讲清 max_connections 与 open_files_limit 两个额度、句柄被谁占走、分区表为什么最容易踩,给出从 error log 到 /proc/pid/limits 的定位顺序与句柄预算算法。

「一个在数据库赛道上边学边分享的学妹」——聚焦国产数据库/信创方向、分享学习笔记、踩坑经验和行业观察。 336

SQL Server 数据库双备份:简化配置,兼顾本地留存与异机归档

本文针对中小企业SQLServer数据库备份难题,提出简化配置与本地+异机双副本方案。传统原生备份依赖手动脚本,存在配置复杂、传输易中断、日志分散、静默失败等问题。推荐使用图形化工具实现备份、本地留存与异机同步一体化管理,支持断点续传、加密传输、自动清理与版本控制,降低运维负担。双副本策略提升数据安全性,本地快速恢复,异地防硬件故障。建议定期验证恢复流程,确保备份可用性。

2501_93776716的博客 291

Redis 入门详解:核心特性、重要文件与全局命令实战

本文系统介绍了Redis的核心特性、重要文件及全局命令。Redis凭借内存存储、C语言实现和单线程设计,具备极高的读写性能;支持持久化(RDB/AOF)、主从复制与集群架构,适用于缓存、排行榜、计数器等场景,但不适合存储海量冷数据。文中还解析了常见面试题,如为何快速、单线程原理、持久化机制、KEYS *禁用原因及过期键处理策略,强调其在高并发场景下的优势与局限。

2303_77004089的博客 206
上一篇: PostgreSQL 日报|8 字节 TOAST 值支持(9 月 16 日)
下一篇: AI 时代,PostgreSQL 正在发生什么变化?
IvorySQL
博客等级 码龄5年 745粉丝 565原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值