之前在做 gitlab resource center 搜索功能时,初版上线的搜索结果未能令人满意。因此便开始着手对分词和排序进行了改进。
在第一版搜索功能上线后,当输入关键字安全高效
和安全
时,系统能够正确返回相关文章。然而,在搜索高效
关键字时,却发现返回的文章为空。期望的功能是,只要文章标题、内容或描述中包含关键字高效
,就能够返回相关结果。
首先我们看看怎么在 PostgreSQL 中启用 zhparser
插件的:
CREATE EXTENSION IF NOT EXISTS zhparser;
DROP TEXT SEARCH CONFIGURATION IF EXISTS chinese_zh;
CREATE TEXT SEARCH CONFIGURATION chinese_zh (PARSER = zhparser);
ALTER TEXT SEARCH CONFIGURATION chinese_zh ADD MAPPING FOR a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z WITH simple;
这里的 a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z 均对应了一种词性。
下面是词性的关系映射表,先列举出来,用于我们后面分析。
(97, a, "adjective, 形容词")
(98, b, "differentiation, 区别词")
(99, c, "conjunction, 连词")
(100, d, "adverb, 副词")
(101, e, "exclamation, 感叹词")
(102, f, "position, 方位词")
(103, g, "root, 词根")
(104, h, "head, 前连接成分")
(105, i, "idiom, 成语")
(106, j, "abbreviation, 简称")
(107, k, "tail, 后连接成分")
(108, l, "tmp, 习用语")
(109, m, "numeral, 数词")
(110, n, "noun, 名词")
(111, o, "onomatopoeia, 拟声词")
(112, p, "prepositional, 介词")
(113, q, "quantity, 量词")
(114, r, "pronoun, 代词")
(115, s, "space, 处所词")
(116, t, "time, 时语素")
(117, u, "auxiliary, 助词")
(118, v, "verb, 动词")
(119, w, "punctuation, 标点符号")
(120, x, "unknown, 未知词")
(121, y, "modal, 语气词")
(122, z, "status, 状态词")
我们直接通过 zhparser 分词策略分词之后的看看他的返回结果
SELECT * FROM ts_parse('zhparser', '这种将安全高效整合进研发和运维流程中的安全构建方式');
tokid | token
-------+----------
114 | 这种
100 | 将
110 | 安全高效
118 | 整
110 | 合进
106 | 研发
99 | 和
118 | 运
113 | 维
110 | 流程
102 | 中
117 | 的
97 | 安全
118 | 构建
110 | 方式
(15 rows)
-- 对应的词性可以在上面的关系映射表里面看
此时 zhparser 会拆分出 安全高效
和 安全
这两个词,并没有拆分出我们所需要的 高效
关键词。
zhparser 有以下配置,且所有配置默认值均为 false。
zhparser.punctuation_ignore = f #忽略所有的标点等特殊符号:
zhparser.seg_with_duality = f #闲散文字自动以二字分词法聚合:
zhparser.dict_in_memory = f #将词典全部加载到内存里:
zhparser.multi_short = f #短词复合:
zhparser.multi_duality = f #散字二元复合:
zhparser.multi_zmain = f #重要单字复合:
zhparser.multi_zall = f #全部单字复合:
因为 zhparser 插件是一个基于 SCWS 能力开发的 PG 中文分词插件。所以我们可以去 SCWS 在线测试平台 根据需要做一些调整。最终我们决定将短词复合
配置打开。有以下两种方式去配置(具体怎么应用看对应的云服务商怎么方便处理。):
postgresql.conf
文件配置将 zhparser.multi_short = true
添加到 postgresql.conf
文件中,然后进入数据库执行 SELECT pg_reload_conf();
使 PostgreSQL 重新加载配置文件。
SELECT set_config('zhparser.multi_short', 'true', false);
ALTER SYSTEM SET zhparser.multi_short = true;
SELECT pg_reload_conf();
需要注意:执行 sql 必须是原生的 superuser 权限,并且执行 sql 的时候不能在事务中运行
将以上配置完毕之后再来看看 zhparser 分词结果是什么
SELECT * FROM ts_parse('zhparser', '这种将安全高效整合进研发和运维流程中的安全构建方式');
tokid | token
-------+----------
114 | 这种
100 | 将
110 | 安全高效
97 | 安全
100 | 高效
118 | 整
110 | 合进
106 | 研发
99 | 和
118 | 运
113 | 维
110 | 流程
102 | 中
117 | 的
97 | 安全
118 | 构建
110 | 方式
(17 rows)
-- 对应的词性可以在上面的关系映射表里面看
此时高效
关键词已经被拆分出来了,并且它是副词
词性,接下来调整下 chinese_zh
的策略,只保留我们需要的词性
-- 删除分词策略:
ALTER TEXT SEARCH CONFIGURATION chinese_zh DROP MAPPING IF exists FOR a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z;
-- 添加分词策略:
-- 添加名词(n)、动词(v)、形容词(a)、成语(i)、叹词(e)、习用语(l)和副词(d)七种分词策略:
ALTER TEXT SEARCH CONFIGURATION chinese_zh ADD MAPPING FOR n,v,a,i,e,l,d WITH simple;
然后观察 chinese_zh
的分词结果
SELECT to_tsvector('chinese_zh', '这种将安全高效整合进研发和运维流程中的安全构建方式');
to_tsvector
---------------------------------------------------------------------------------------------
'合进':6 '安全':3,9 '安全高效':2 '将':1 '整':5 '方式':11 '构建':10 '流程':8 '运':7 '高效':4
(1 row)
-- 每个单词后面跟随了对应的位置信息
至此我们分词调整策略基本告一段落,主要做两个事情
chinese_zh
分词策略所保留的词性除此之外 zhparser 还支持自定义词库,所有的自定义词都放在了 zhprs_custom_word
表里面,默认添加的自定义词的词性是 (120, x, "unknown, 未知词")
,可能用到的 sql 包含但不局限于如下:
-- 添加自定义词
insert into zhparser.zhprs_custom_word values('资金压力');
-- 自定义词库也支持停止词功能,例如我们不希望词语'这是'单独作为一个分词,同样可以在自定义词库中插入对应的词语和控制符停止特定分词:
insert into zhparser.zhprs_custom_word(word, attr) values('这是','!');
-- 添加/删除自定义分词之后需要执行以下命令才能使词库生效
select sync_zhprs_custom_word();
-- 查询已存在的自定义词库
select * from zhparser.zhprs_custom_word;
PostgreSQL 有权重赋值的方法 setweight
当然在 pg_search 中也集成了该接口,便于直接使用。
权重设置分别可以设定为 A
B
C
D
,默认值为 A
假设权重用数字来评判的话(满分为 1 分)他们之间的梯度关系如下所示:
权重 | 对应系数 |
---|---|
A | 1 |
B | 0.4 |
C | 0.2 |
D | 0.1 |
--- 设置权重为 A
select ts_rank(setweight(to_tsvector('chinese_zh', '这种将安全高效整合进研发和运维流程中的安全构建方式'), 'A'), (to_tsquery('chinese_zh', '高效')), 0);
ts_rank
-----------
0.6079271
--- 设置权重为 B
select ts_rank(setweight(to_tsvector('chinese_zh', '这种将安全高效整合进研发和运维流程中的安全构建方式'), 'B'), (to_tsquery('chinese_zh', '高效')), 0);
ts_rank
------------
0.24317084
(1 row)
--- 设置权重为 C
select ts_rank(setweight(to_tsvector('chinese_zh', '这种将安全高效整合进研发和运维流程中的安全构建方式'), 'C'), (to_tsquery('chinese_zh', '高效')), 0);
ts_rank
------------
0.12158542
--- 设置权重为 D
select ts_rank(setweight(to_tsvector('chinese_zh', '这种将安全高效整合进研发和运维流程中的安全构建方式'), 'D'), (to_tsquery('chinese_zh', '高效')), 0);
ts_rank
------------
0.06079271
这样的话我们便可以将文章的标题、描述、内容的权重分别设置为 A, B, C,以此算出一个综合的分数来做排序。但这种计算出来的分数只会和词频
相关。
-- A 代表权重,0 代表使用的关注度配置
select ts_rank(setweight(to_tsvector('chinese_zh', '高效'), 'A'), (to_tsquery('chinese_zh', '高效')), 0);
ts_rank
-----------
0.6079271
-- 即使文档长度很小,最终计算出来的分数依然是 0.6079271
由于较长的文档包含查询词的几率更高,因此可能还需要考虑文档的长度,例如具有五个搜索词实例的一百字文档可能比具有五个搜索词实例的千字文档更相关。
所以在此基础上我们需要引入 PostgreSQL 的 normalization 参数
具体怎么配置需要根据业务去调整,我们主要根据文档的长度做了些调整。
# normalization 查询结果关注度:
# 0(缺省)表示跟长度大小没有关系
# 1 表示关注度(rank)除以文档长度的对数+1
# 2 表示关注度除以文档的长度
# 4 表示关注度除以范围内的平均谐波距离,只能使用ts_rank_cd实现。
# 8 表示关注度除以文档中唯一分词的数量
# 16 表示关注度除以唯一分词数量的对数+1
# 32 表示关注度除以本身+1
测试一下长文本(由于文本太长这里就不贴出来了)和短文本之间的使用不同的 normalization
之后分数的差距。
文本长度 | normalization | 分数 |
---|---|---|
3887 | 0 | 0.6079271 |
1087 | 0 | 0.6079271 |
3887 | 1 | 0.057773568 |
1087 | 1 | 0.06928112 |
3887 | 2 | 0.00041355583 |
1087 | 2 | 0.0013911375 |
相对来说 normalization 为 1 时,分数变化随着文章长度越长,变化曲率越平滑。
至此我们排序策略基本告一段落,主要做两个事情
当然 pg_search 上面还有更多的开箱即用的搜索的配置,这里就不再一一阐述了
- SCWS
- zhparser
- Postgres Fulltext Search (一)
- Postgres Full Text Search with Docker Compose
- 【实操系列】如何通过云原生数据仓库实现“一站式全文检索”业务(链接触发到敏感词,原文可能需要自行搜索一下)