基本方案
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以内完成。