一、别把全文检索外包给 Elasticsearch
很多系统一遇到"中文模糊搜索"的需求,第一反应就是引入一套 Elasticsearch------多部署一个集群、多一条数据同步链路、多一个运维负担。但对相当一部分场景来说,这是杀鸡用牛刀:如果数据本来就在数据库里,为什么不让数据库自己做搜索?
金仓(KingbaseES)的"多模"能力里,藏着一个容易被忽略的模块:内置中文全文检索。它自带两款中文分词器(zhparser 和 jieba)、GIN 倒排索引、相关性排序、关键词高亮------一套完整的搜索引擎基建,全在数据库内部。这篇我在鲲鹏服务器上把它跑通,对比两个分词器的真实差异,并趟过三个不写出来你一定会中招的坑。
二、两个分词器,切出两个世界
中文全文检索的第一道工序是分词------把"金仓数据库管理系统"切成一个个词。英文靠空格天然分词,中文没有空格,全靠分词器。金仓内置两款:

-
ZHPARSER:基于词性规则的分词器;
-
jieba:移植自著名的结巴分词,基于词典。
拿三句话做对比,大部分句子两者都切得对,比如经典歧义句"南京市长江大桥",两者都正确切成"南京市 / 长江大桥"而非"南京 / 市长 / 江大桥"。但有一句暴露了本质差异:
原句:金仓数据库管理系统
zhparser: '数据库':1 '管理系统':2 ← "金仓"不见了!
jieba : '数据库':2 '管理系统':3 '金仓':1 ← 正确切出"金仓"
三、第一个坑:|| 不是你以为的拼接
准备语料建索引时,我一头撞进了金仓 MySQL 兼容模式的经典陷阱:

我要把"标题 + 正文"拼起来送进分词器,顺手写了 title || ' ' || body------结果返回一个 f:
SELECT title || ' ' || body FROM articles WHERE id=1; -- 结果:f
SELECT concat(title, ' ', body) FROM articles WHERE id=1; -- 结果:金仓数据库迁移实战 把 Oracle...
在 MySQL 兼容模式下, || 是"逻辑或"而不是字符串拼接! 字符串 || 字符串 被当成布尔运算,结果是 f(假)。如果没发现,to_tsvector('jiebacfg', title||' '||body) 实际是在给一个 f 分词,生成的索引全是空的、检索永远返回零结果,而且不报任何错。
正解是用 concat()。凡是在金仓 MySQL 兼容库里做字符串拼接,一律 concat() ,永远别用 ||。改用 concat 后,倒排索引正常建立,12012 行文档就绪。
四、召回生死线:选错分词器,内容整段消失
现在验证第二节埋下的伏笔。同一批语料、同一个查询词"金仓",分别用 jieba 索引和 zhparser 索引检索:

结果触目惊心:
-
jieba 索引:召回 5 篇含"金仓"的文章;
-
zhparser 索引 :召回 0 篇。
原因正是第二节那个差异------zhparser 切不出"金仓"这个词,索引里根本没有"金仓"这个词条,查询时"金仓"被当成停用词忽略,召回自然是零。分词器的一个词典缺失,直接导致整类内容在搜索中彻底消失。 这就是为什么选分词器不是"配置细节"而是"生死决策":对以专名、新词、行业术语为主的语料(政务、金融、科技类内容尤其如此),词典型的 jieba 通常是更安全的选择。
五、GIN 倒排索引:把全表扫描降到毫秒
搜索引擎的另一半是索引 。全文检索用的是 GIN(倒排索引)------和搜索引擎同源的数据结构。看它的实际效果,在 12012 行文档里查一个低选择率组合"向量 & RAG":

走 GIN 倒排索引:Bitmap Index Scan on idx_jieba → Execution Time: 1.9 ms
强制全表扫描 :Seq Scan on articles → Execution Time: 4.7 ms
GIN 索引通过 Bitmap Index Scan 直接定位命中行,比全表扫描快 2.5 倍。这还只是 1.2 万行的小库------数据量越大、命中越稀疏(越像真实搜索场景),倒排索引的优势越是指数级放大。几百万文档里搜一个关键词,GIN 索引是毫秒级,全表扫描则可能是秒级甚至更久。
(一个真实观察:如果查询词命中率很高,比如查"数据库"命中 42% 的文档,优化器会主动放弃索引走全表扫描------因为命中太多时索引反而不划算。这说明金仓的优化器是懂全文检索代价的,不是无脑用索引。)
六、检索质量:排序与高亮,搜索引擎的"最后一公里"
能搜到还不够,好的搜索要把最相关的排前面、把命中的词标出来。金仓两样都有:

相关性排序 ts_rank:按关键词的词频、位置给文档打分。查"数据库 或 迁移",标题里同时命中两个词的"金仓数据库迁移实战"(0.0684)排在最前,相关度低的排后面。这就是搜索结果"最相关的在第一条"的实现。
关键词高亮 ts_headline:把命中的词用标记包起来,直接生成搜索结果里那段带高亮的摘要:
第1篇: 业务系统平滑【迁移】到金仓,零停机完成数据校验与回退。
第2篇: 提供 MySQL 兼容模式,存量应用只需更换驱动即可【迁移】。
第三个坑也在这里 :我本想用 round(ts_rank(...), 4) 保留 4 位小数,结果全部返回 0.0000------又是 MySQL 兼容模式的函数差异,round(小数, n) 在这个模式下对纯小数返回 0。改用 ::numeric(6,4) 强制类型精度才正常。这已经是本文第三个 MySQL 兼容模式的坑了(||、round、还有 tsvector 的 || 拼接操作符也被逻辑或覆盖)。
七、与 MySQL ngram 的正面对照
金仓的词典分词,和 MySQL 唯一的中文方案 ngram,到底差在哪?做一个决定性对照------查"据库"(它是"数据库"的一个片段,但本身不是一个有意义的词):

KES jieba :召回 0 篇 ← 词典知道"据库"不是词,精确地不匹配
MySQL ngram:召回 4 篇 ← "据库"是"数据库"里的合法二元组,全部误召回
这一刀切开了两种机制的本质:
-
ngram(MySQL) :把文本切成固定长度的字符片段(默认 2 字),不理解词义。优点是不需要词典、召回全;缺点是噪声大("据库"这种无意义片段也匹配)、索引体积大。
-
词典分词(金仓 jieba):基于词典理解词边界,精度高、索引小;代价是依赖词典质量。
没有绝对的优劣,只有场景适配。但关键在于:金仓两种都给你(zhparser 词性型 + jieba 词典型),你可以按语料特性选;而 MySQL 只有 ngram 一种。这种"给足选择"的能力,正是"多模融合"这个词落到全文检索场景里的真实含义。
八、结论
我用金仓内置的能力,给数据库装上了一个像模像样的中文搜索引擎:两款分词器可选、GIN 倒排索引毫秒级检索、相关性排序、关键词高亮------没有引入任何外部组件。
这趟实战最想传递两点:其一,分词器的选择是搜索的生死线 ------jieba 召回 5 篇而 zhparser 召回 0 篇,一个词典差异就能让整类内容在搜索中消失,选型必须基于真实语料实测;其二,MySQL 兼容不是"完全一样" ------||、round 这些运算符和函数的语义差异会静默地毁掉你的功能,涉及它们时永远要亲手验一遍。
对那些正在纠结"要不要为了中文搜索再上一套 ES"的团队,我的建议是:先看看金仓自己能不能扛下来。很多时候,答案是能------数据不用搬家,搜索就在原地发生。这,也是国产数据库"多模融合"给出的一个务实答案。