
简介面向 Windows 与 VS2008 环境下 PostgreSQL 中文分词需求的 zhparser 编译安装包主要服务于不熟悉 Linux 构建工具、却希望在本地完成扩展部署的数据库开发与运维人员。它针对官方未提供 Windows 编译工程、scws 与 PostgreSQL 头文件及库依赖配置繁琐等痛点整理了可参考的解决方案与排错线索。压缩包共138个文件约11.19MB以 C 源码与头文件、Visual Studio 工程文件、编译中间产物、SQL 测试脚本、lib/dll 库文件以及词典和 ini 配置为主兼顾源码阅读、工程编译与扩展验证。已有1608人学习说明该 Windows 移植方案在同类资料中具有一定参考价值。包内保留 scws 与 zhparser 的源码结构、编译工程和测试用 SQL并附带 control、词典与配置文件便于读者对照排查函数导出、扩展安装及词典路径等问题。对于需要理解 zhparser 在 Windows 下如何落地、希望少走编译与配置弯路的读者这份资料提供了较完整的基础素材和实践线索。1. 装完扩展却分不出词一次位数不匹配的排查现场某公司的老业务系统跑在 32 位 PostgreSQL 上要做中文全文检索于是装了 zhparser。CREATE EXTENSION一条命令就成功了\dx里也能看到扩展名字但一执行to_tsvector就出事整句「数据库连接超时」被当成一个孤零零的 token 返回plainto_tsquery根本匹配不上。翻来覆去改 SQL 没用最后用dumpbin一看手里那个zhparser.dll是 64 位的postgres.exe是 32 位的——加载时被静默跳过扩展注册成功只是个假象。Windows VS2008 这套组合上的坑八成都不在 SQL 层而在编译和文件摆放这一层。下面把 SCWS 静态库、zhparser.dll、control 与 SQL 脚本的完整落地路径走一遍顺带把词典、索引表达式和排查手法都交代清楚。2. 依赖链拆解SCWS、zhparser 与 VS2008 工具链怎么对上2.1 SCWS 负责切词zhparser 负责接 PG 的解析器接口先把调用链讲清楚后面所有编译配置才有依据。PostgreSQL 的全文检索分两步parser 把原始文本切成 token 并打上词性标记dictionary 再决定哪些词性的 token 值得保留、要不要做词干化。zhparser 干的是前一件事——它实现 PG 的TSParser接口核心是四个回调初始化解析状态、取下一个 token、结束解析、返回词性列表。每个回调内部都把活转交给 SCWS把 UTF-8 字符串喂给scws_send_text再循环scws_get_result拿到词条、偏移和词性最后转成 PG 的ParsedWord结构返回。SCWS 自己是个纯 C 的分词库靠词典常用词表 规则表和多元切分策略工作支持scws_set_charset指定字符集。zhparser 在初始化时会把字符集固定成 utf8因为 PG 数据库编码基本都是 UTF8。这也解释了一个常见困惑为什么扩展装好了、to_tsvector却返回空字符串——parser 吐出了 token但你没给文本搜索配置建映射PG 不知道该保留哪些词性于是全丢了。映射不是可选项是必经步骤。理解这层分工再看编译就顺了SCWS 必须以静态库或 DLL 形式存在zhparser 才有东西可链zhparser 必须以动态库形式存在PG 后端才能LoadLibrary它。两者的位数、CRT 运行时、字符集处理必须和postgres.exe完全一致否则就是第 1 章那种「注册成功但不出活」的现场。2.2 Windows 上没有 PGXSVS2008 工程得自己搭Linux 下编 PG 扩展是make make install背后是 PGXS 那套 Makefile 框架。Windows 下 MSVC 不认识 Makefile官方源码树里的 contrib 模块靠src/tools/msvc下的脚本批量生成工程文件而 zhparser 不在 contrib 名单里脚本不会管它。摆在面前的路只有三条用 MinGW Makefile 编。产物是 MinGW ABI和 MSVC 编出来的postgres.exe混用跨模块传palloc指针、传Datum时容易出玄学崩溃老系统上不建议。用 VS2008 手工建工程。麻烦但产物 ABI 和官方 Windows 版 PG 完全一致最稳。找别人编好的 DLL。位数、PG 大版本、CRT 设置三样只要有一项不对就前功尽弃。PG 9.x 这一代的构建脚本里还留着 VS2008 的配置项这也是为什么很多老系统上手就是这套组合。VS2008 对应 MSVC 9.0它的 C 编译器对 C89 支持完整zhparser 和 SCWS 的源码都是这个年代的写法基本不用改代码改的都是工程配置。2.3 开工前的目录布局与位数核对动手之前先把四个目录固定下来后面所有路径都从这里派生能省掉大量「路径写错」的低级翻车。代号内容典型位置PGDIR已安装的 PostgreSQL 根目录C:\pg\9xPGINC编译用的头文件PGDIR\include、PGDIR\include\serverSCSWSRCSCWS 源码根目录C:\build\scwsZPSRCzhparser 源码根目录C:\build\zhparser位数核对放在第一步别等编完才发现白干。用 VS2008 自带的dumpbin看postgres.exe的机器类型:: 先加载 VS2008 环境变量dumpbin 才有得用 call C:\Program Files\Microsoft Visual Studio 9.0\VC\vcvarsall.bat x86 dumpbin /headers C:\pg\9x\bin\postgres.exe | findstr /i machine输出14C machine (x86)就是 32 位8664 machine (x64)是 64 位。后面vcvarsall.bat的参数、SCWS 静态库工程的目标平台、zhparser 工程的平台三处必须和这个结果一致。32 位 PG 配 64 位 DLL 的后果不是报错是 PG 启动时LoadLibrary失败但扩展条目已经写进pg_extension查询时才露馅排查成本极高。提示PGDIR\lib下如果有postgres.lib说明这是官方安装包链接时直接用它如果只有postgres.exe没有.lib那得去源码树的src\backend\Release\下找编译产物或者临时用lib /def从 DLL 导出一份导入库。3. 编译实操从 SCWS 静态库到 zhparser.dll 的完整命令链3.1 先把 SCWS 编成静态库SCWS 源码根目录下是一堆.c文件和若干子目录带main的示例程序在cli、examples这类子目录里绝对不能递归带进来。做法是在根目录就地编译只对当前目录的.c下手然后用lib打包。VS2008 的cl.exe命令行完全可以完成不必开 IDEecho off :: 路径和位数按 2.3 的核对结果改32 位 PG 就用 x86 call C:\Program Files\Microsoft Visual Studio 9.0\VC\vcvarsall.bat x86 cd /d C:\build\scws :: /c 只编译不链接/TC 强制按 C 语言编译/MT 静态链 CRT :: 只处理当前目录的 .c不要加 /S 递归否则会把带 main 的示例拖进来 cl /nologo /c /O2 /TC /MT /D_CRT_SECURE_NO_WARNINGS *.c :: 把生成的 obj 打成静态库zhparser 工程会链它 lib /nologo /OUT:libscws.lib *.obj/TC这一项很关键。VS2008 默认按扩展名判断语言但某些工程模板会把.c当 C 编一旦走 C 编译器scws.h里那些没有extern C保护的函数名会被 name mangling 处理链接阶段就是一片unresolved external symbol。/MT是静态链接 CRTSCWS 作为静态库最后会被塞进zhparser.dll用/MT可以避免和 PG 的/MD运行时之间产生 CRT 状态混用但如果你的 PG 是官方安装包最终 DLL 里两套 CRT 并存也不是不行稳妥起见libscws.lib用/MTzhparser 链接时统一到/MD让链接器自己合并。编完检查一下libscws.lib的大小是否在几百 KB 量级。如果只有几 KB说明*.c没匹配上八成是cd /d那行路径写错了cl编了个空集。3.2 zhparser 的 DLL 工程头文件路径、宏与运行时zhparser 源码核心就是一个zhparser.c加上zhparser.control和zhparser--x.y.sql两个安装文件。用 IDE 建工程的话选 Win32 Project → DLL → Empty Project平台选 Win32然后把zhparser.c加进去。更省事的还是命令行工程配置一目了然echo off call C:\Program Files\Microsoft Visual Studio 9.0\VC\vcvarsall.bat x86 cd /d C:\build\zhparser :: /LD 产出 DLL/MD 动态链接 CRT必须和 postgres.exe 一致 :: WIN32 宏让 PG 头文件走 Windows 分支_CRT_SECURE_NO_WARNINGS 压掉安全函数告警 cl /nologo /LD /MD /O2 /DWIN32 /D_CRT_SECURE_NO_WARNINGS ^ /IC:\pg\9x\include ^ /IC:\pg\9x\include\server ^ /IC:\pg\9x\include\server\port\win32 ^ /IC:\build\scws ^ zhparser.c ^ /link /LIBPATH:C:\pg\9x\lib postgres.lib libscws.lib /OUT:zhparser.dll三个-I一个都不能少。include目录负责postgres_ext.h这类公共头include\server放的是postgres.h、fmgr.h、tsearch/ts_public.h这些只有服务端才有的头include\server\port\win32里是 Windows 平台适配层缺了它postgres.h会在包含port.h时直接报错。很多人只加第一个目录编译第一行就死在postgres.h: No such file。/MD是血泪经验。官方 Windows 版 PG 用的是动态 CRTmsvcr90.dll那一套扩展 DLL 如果换成/MT同一个进程里会出现两套 CRT 堆malloc/free跨模块配对时崩得毫无规律日志里也留不下什么有用信息。判断方法是用dumpbin /dependents zhparser.dll看依赖列表里有没有msvcr90.dll有就对了。3.3 链接 postgres.lib 与导出符号的自检DLL 编出来别急着往PGDIR\lib里扔先看导出表:: 正常应该能看到 Pg_magic_func、zhparser_start、zhparser_gettoken 等符号 dumpbin /exports zhparser.dll | findstr /i Pg_magic_func zhparserPg_magic_func是PG_MODULE_MAGIC宏展开的产物PG 加载扩展时第一件事就是找它找不到直接报incompatible library。这个符号能导出说明postgres.h被正确包含、PGDLLEXPORT生效了。如果导出表是空的两种可能一是误建成了 Static Librarycl没加/LD二是源码里PG_MODULE_MAGIC被#ifdef条件编译掉了检查一下有没有定义多余的平台宏。链接阶段如果报一堆unresolved external symbol _pg_detoast_datum、_palloc、_errcode那不是源码问题是postgres.lib没链上或者位数不对。32 位工程链了 64 位导入库链接器会明确报 machine type 冲突如果报的是纯符号缺失先确认/LIBPATH指向的目录里真有这个文件。3.4 三个文件各归其位文件来源目标位置zhparser.dll3.2 的编译产物PGDIR\lib\zhparser.controlzhparser 源码根目录PGDIR\share\extension\zhparser--x.y.sqlzhparser 源码根目录PGDIR\share\extension\control文件里有一行default_version值必须和 SQL 脚本文件名里--后面那段严格一致比如default_version 1.0就得配zhparser--1.0.sql。差一个字符CREATE EXTENSION就报could not open extension control file或extension zhparser has no update path。SQL 脚本里的MODULE_PATHNAME会被替换成$libdir/zhparser对应PGDIR\lib\zhparser.dll所以 DLL 不要改名。文件放完不需要重启服务但当前已连接的会话看不到新扩展重连一次即可。4. 让分词生效从 CREATE EXTENSION 到 GIN 索引的四个配置点4.1 建扩展、建配置、建映射一步都不能少-- 注册扩展本质是把 control 和 sql 脚本执行一遍 CREATE EXTENSION zhparser; -- 基于 zhparser 这个 parser 新建一个文本搜索配置 CREATE TEXT SEARCH CONFIGURATION chinese_zh (PARSER zhparser); -- 给七类词性建映射simple 字典表示原样保留、不做词干化 ALTER TEXT SEARCH CONFIGURATION chinese_zh ADD MAPPING FOR n,v,a,i,e,l,j WITH simple;词性代号来自 SCWS 的标注体系n名词、v动词、a形容词、i成语、e叹词、l习用语、j简称。这七类是中文检索里最常用的组合基本覆盖业务字段。只建配置不建映射parser 照样切词但 dictionary 层会把所有 token 丢掉to_tsvector返回空——这是新手最容易卡住的地方因为命令全都执行成功了没有任何报错。如果业务里有大量人名、地名需要单独处理可以再加nr、ns的映射但要注意索引会变大。改完映射必须重建索引已有索引不会自动感知配置变化。4.2 GIN 索引与查询写法必须严格对称-- 索引表达式里的配置名、拼接方式必须和查询里一字不差 CREATE INDEX idx_doc_fts ON doc USING gin (to_tsvector(chinese_zh, coalesce(title,) || || coalesce(body,))); -- 查询侧照抄同一个表达式否则优化器认不出来 SELECT id, title FROM doc WHERE to_tsvector(chinese_zh, coalesce(title,) || || coalesce(body,)) plainto_tsquery(chinese_zh, 数据库 连接 超时);索引能不能命中全看这两个表达式是否完全一致。差异点常常藏在细节里索引里写了coalesce查询里没写索引用title || body查询用title || || body索引里配置名是chinese_zh查询里手滑写成zh。任何一种都会让EXPLAIN老老实实走 Seq Scan而数据量小的时候你还看不出来性能差别。plainto_tsquery会把输入按空白切开、各词之间用 AND 连接适合「用户输入一串关键词」的场景。手写to_tsquery用、|、!拼布尔表达式更灵活但中文词之间不能随便加空格否则会被 parser 切成单字。PG 11 之后有websearch_to_tsquery能直接吃「数据库连接 -超时」这种搜索框语法老版本没有别照抄新文档。4.3 分词结果对照与参数调整-- 看切分效果最直接的验证手段 SELECT to_tsvector(chinese_zh, 数据库连接超时问题排查方案); -- 和默认的 simple 配置对比差异一目了然 SELECT to_tsvector(simple, 数据库连接超时问题排查方案);正常输出会拆成数据库:1 连接:2 超时:3 问题:4 排查:5 方案:6这一类形态。如果输出是一长串单字或者整句只有一个 token先查词典加载再查 SCWS 的多元切分参数参数类型作用建议zhparser.punctuation_ignorebool丢弃标点 token开检索场景基本不需要标点zhparser.multi_shortbool短词复合切分开能切出「数据库」而不是「数据」「库」zhparser.multi_duplicatebool保留重复词开短语检索时有用zhparser.multi_zmainbool输出重要单字按需开了索引会膨胀zhparser.multi_zallbool输出全部单字一般关除非做单字召回zhparser.scws_extra_dictstring自定义词典绝对路径有业务专有名词时必须配这些参数是会话级的写SET zhparser.multi_short true;只影响当前连接。要全局生效就写进postgresql.conf或者用ALTER DATABASE dbname SET zhparser.multi_short true;按库固化。词典路径在 Windows 上要用正斜杠或者双反斜杠C:\dict\extra.txt里的\d、\e会被当转义序列吃掉路径静默失效词典根本没加载分词结果自然不对。5. 避坑清单六个把编译和查询卡住的现场5.1 编译与加载阶段现象cl编译zhparser.c时报Cannot open include file: pg_config.h。原因只把PGDIR\include加进了头文件路径pg_config.h实际在include\server下postgres.h第一行就会去包含它。解决三个-I目录补齐顺序上把include放最前、include\server其次、include\server\port\win32最后。顺序错了会出现头文件版本混用症状更隐蔽。现象链接报unresolved external symbol _pg_detoast_datum_packed之类但postgres.lib明明在目录里。原因工程位数和导入库位数不一致或者链接的是另一份 PG 版本的.lib。同机器上装过多个 PG 版本时特别容易拿错。解决dumpbin /headers postgres.lib核对 machine 类型和 DLL 工程平台对齐/LIBPATH写绝对路径别依赖环境变量里的LIB。现象DLL 编好了CREATE EXTENSION成功查询时报could not load library xxx/zhparser.dll: The specified module could not be found。原因报「找不到模块」不代表 DLL 不在多数是 DLL 自身依赖的库缺失比如 64 位 DLL 依赖的 CRT 版本系统里没有或者位数冲突。解决dumpbin /dependents zhparser.dll逐个核对依赖再用dumpbin /headers确认位数。32 位环境下 64 位 DLL 报的也是这个错别被措辞骗了。现象一切正常但to_tsvector返回空字符串。原因文本搜索配置建了ADD MAPPING没执行或者映射的词性里不包含实际切出来的词性。解决先SELECT * FROM ts_debug(chinese_zh, 测试文本);看每个 token 的词性和是否被字典吃掉再决定补哪类映射。5.2 查询与索引阶段现象数据量涨到几十万行查询还是走 Seq ScanEXPLAIN里看不到 Bitmap Index Scan。原因索引表达式和查询表达式不一致最常见的是coalesce有没有写、空格拼接方式不同、配置名大小写不同。解决把索引定义原样复制到WHERE里包括空格用EXPLAIN (ANALYZE, BUFFERS)对比改前改后的执行计划别靠猜。现象业务专有名词搜不到比如某个产品代号被切成了三个单字。原因scws_extra_dict没配或者路径里的反斜杠被转义或者词典文件编码不是 UTF-8。解决路径统一用C:/dict/extra.txt这种正斜杠写法词典文件另存为无 BOM 的 UTF-8改完重连会话让参数重新加载再用ts_debug确认新词被识别。6. 进阶自定义词典、热加载与升级回归脚本自定义词典的格式很简单一行一个词条三列用空格或制表符分隔词、词频、词性。词频影响切分优先级给业务专有名词一个较高的值比如 100它就不会被拆开词性写n或nz都行只要映射里有对应项。改完词典不需要重启数据库新连接建立时会重新加载已经连着的会话得断开重连。真正值得养成习惯的是回归脚本。每次换 PG 小版本、重编 DLL、调整词典都跑一遍同一组断言比事后翻日志高效得多-- 建一张两行的临时表覆盖长短句、中英混排、标点 CREATE TEMP TABLE fts_probe(body text); INSERT INTO fts_probe VALUES (数据库连接超时问题排查方案), (PostgreSQL 中文分词扩展安装步骤注意位数一致); -- 断言一长词没有被拆成单字 SELECT to_tsvector(chinese_zh, body) FROM fts_probe WHERE body LIKE 数据库%; -- 断言二查询能命中且执行计划走索引正式表上跑 EXPLAIN (ANALYZE, BUFFERS) SELECT id FROM doc WHERE to_tsvector(chinese_zh, coalesce(title,) || || coalesce(body,)) plainto_tsquery(chinese_zh, 分词 安装);第一段输出里如果出现数据:1 库:2这种切法说明词典没生效第二段的执行计划里如果还是 Seq Scan回头查 4.2 的表达式对称性。把这两段存成regress_zhparser.sql每次变更后psql -f跑一次两分钟的事。我自己的习惯是任何一次动PGDIR\lib下 DLL 的操作编译前先dumpbin核对一次位数装完先跑回归脚本再通知业务方从不例外。有一次图省事跳过了位数核对半夜被叫起来回滚从那以后这个流程就成了硬性动作。希望帮到你。本文还有配套的精品资源点击获取