基于Mybatis实现PG文本搜索的更优实现

基本方案

Mybatis是目前使用最广泛的数据库框架,它实现了通用性。

但对于不同数据库一些差异化的能力,不能原生的支持,所以,需要使用SQL方式来实现。

通过mapper,将java函数和sql进行映射,这样,执行java搜索函数时,实际执行的是PG对应的SQL。

代码实现

Mapper

负责将java函数和SQL进行映射,其中,SQL由另一个SQL Builder的类负责动态生成SQL语句(根据java函数的入参,动态生成包含实际入参的SQL)。

@Mapper
public interface SparringWorkflowVectorCustomizedMapper {
    @UpdateProvider(value = WorkflowVectorSqlBuilder.class, method = "updateWorkflowVector")
    void updateWorkflow(String id, String workflowName, String workflowDesc);

    @TimeIt
    @SelectProvider(value = WorkflowVectorSqlBuilder.class, method = "searchWorkflowVector")
    List<SparringWorkflowEntity> searchWorkflow(String query, String workflowType);
}

SQL Builder(重点)

核心:学习使用PG的Text Search的SQL关键词实现文本搜索。

public class WorkflowVectorSqlBuilder {
/*
需要根据名称和描述进行搜索,为什么不能将名称和描述分别放到2列,而是把2部分的内容合入到一列中。
实际测试发现,在select时,将两部分的vector拼接,name_vector || ' ' || desc_vector,将无法使用倒排索引优势。
select时间随着行数的增加而线性增加。因此,目前只能将其在insert阶段,就直接合并,这样select时才能使用到倒排索引。
coalesce的作用是防止列值为NULL导致整个拼接出错,这个函数让列为NULL时,自动转为空字符串。
to_tsvector相比tsvector会做归一化,虽然对中文无效,但是对英文有效果。
setweight负责设置权重,不同列的内容的重要性通常是不一样的,良好的权重设置会让搜索后的排序效果更佳。
 */
    public static String updateWorkflowVector(final String id , final String workflowName, final String workflowDesc){
        return new SQL(){{
            UPDATE("sparring_workflow");
            SET("workflow_name_vector = to_tsvector(coalesce(#{workflowName} ,''))");
            SET("workflow_vector = setweight(to_tsvector(coalesce(#{workflowName} ,'')), 'A') || setweight(to_tsvector(coalesce(#{workflowDesc} ,'')), 'C')");
            WHERE("id = #{id}");
        }}.toString();
    }

/*
websearch_to_tsquery是web搜索的场景,可以支持 or - ""的常见搜索语法格式

@@和ES的MATCH含义一样,代表怎样的query去哪里的vector中去搜索

排序采用cover density ranking,在输入只有1-3个词的时候,效果比TF-IDF好
ts_rank_cd的cd就是cover density,包含的输入词越多,得分越高
参考论文:Relevance ranking for one to three term queries
rank函数的第三个参数代表考虑文档长度的方式,设置为1,相当于TF-IDF的公式
 */
    public static String searchWorkflowVector(final String query, final String workflowType){
        return new SQL(){{
            SELECT(" * ");
            FROM("sparring_workflow, websearch_to_tsquery(#{query}) search, ts_rank_cd(workflow_vector, search, 1) rank");
            WHERE("workflow_vector @@ search");
            WHERE("delete_flag = 0");
            if(!Strings.isBlank(workflowType)){
                WHERE("workflow_type = #{workflowType}");
            }
            ORDER_BY("rank DESC");
        }}.toString();
    }
}

中文搜索的文本处理(重点)
中文需要外置处理

本来,PG内部有文本Parser对输入文本进行分词,停用词处理等工作,只需要将用户输入文本直接传入SQL里即可,整个流程是最简单的。

但是,PG默认只集成了English的Parser,分词基于词与词之间的空格实现。

因此,中文搜索涉及的文本处理,需要外置实现。在软件中预处理好后(分词和去停用词),中文分词采用空格拼接后,再传入SQL。

中文预处理流程

分词,这里采用最简单的分词工具hanlp 1.x(同类产品还有jieba,ikanalyse),如果需要更好的分词效果,可以通过API调用外部的基于深度学习的分词模型(如hanlp 2.x)。

—— hanlp1.x 可以参考 https://github.com/hankcs/HanLP/tree/1.x

停用词,停用词表默认包含标点,英文和中文。这里只需要去除标点符号,由于涉及hanlp的停用词表自定义修改会导致整体引入比较多的文件。为了方便,没有使用hanlp的停用词处理,而是直接采用正则表达式进行过滤。

前面的处理属于插入和搜索的通用部署,但搜索由于使用了websearch_to_tsquery能支持搜索高级语法,因此,预处理会额外增加一些处理。

由于使用了PG的websearch_to_tsquery,会支持or - "" 这些特殊功能,因此,预处理这块需要修改为:

用户输入的词中,如果是or开头,-开头和英文双引号包括的词语,先正则提取出来。

剩下的词为普通输入,对这部分进行分词和停用词处理后,使用空格拼接。再和上面已经提取出来的特殊作用的词语拼接。

这样,就能做到即对用户的输入进行了分词处理,又保留了PG的websearch_to_tsquery的灵活能力。

搜索分页

采用Mybatis的PageHelper插件即可

插入数据

推荐在原有表基础上新增用于搜索的列(tsvector),而不是另起一张表,原因如下:

搜索也是需要进行按条件查询,例如,满足某种类型的,且数据软删除标志为0等。如果分为不同的表,这个操作将会比较麻烦且慢。

另外,放在同一张表,搜索出来即可返回对应的详细内容(而无需在另一张表搜索出id后,再在详细内容表根据id取回具体内容)

代码实现上,由于Mybatis默认只支持常见类型数据的操作。

这里,采用insert+update的2步操作实现

mybatis的insert除了tsvector列之外的全部内容,然后使用前面的基于SQL的update tsvector的函数,专门更新其tsvector列。

数据库表DDL

新增一列用于搜索,数据类型为tsvector

创建倒排索引(索引类型使用GIN):

CREATE INDEX idx_workflow_vector ON sparring_workflow USING GIN (workflow_vector);

性能测试

插入83万条新闻数据(标题最长50字,正文最长500字),进行搜索召回时,测试环境可在300ms以内完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值