Springboot非侵入性实现全文检索(兼容PG与Oracle)

前言:我把需求分析、实现分析、实现过程都记录下来了,需要改进的地方,敬请不吝赐教。

需求:

迎新系统需要实现一个聊天机器人的功能,机器人根据用户发送的信息(搜索内容)在数据库内搜索对应的应答内容进行回复(响应)。

需求与实现分析:

1、聊天机器人本质上就是根据关键词在数据表内进行搜索

2、在数据表内要怎样进行搜索呢?用Like关键字进行搜索?行不通!如:

用户的搜索内容为:学校的电话号码是多少?

搜索的Where语句为 ... WHERE question LIKE "%学校的电话号码是多少?%"

在问题数据表内有以下数据

questionanswer
学校固定电话0123-88888888
学校官网www.123.com

显然是搜索不到任何数据的。

3、用户是不会根据问题库里面的问题来提问的。那么,要怎样才能搜索到这条数据呢?

最简单粗暴的方法就是将用户搜索的内容拆分为一个一个字符,然后每个字符都用LIKE子句来匹配。

这样确实能达到预期,但是太粗暴了,不够科学,性能消耗大

这时候就可以使用全文检索技术了。

4、什么是全文检索?

笼统来说全文检索就是将搜索内容进行分词,然后根据分词结果进行搜索匹配

5、要怎样才能进行全文检索?

一般需要数据库支持全文检索功能的话,需要为数据库安装插件,然后在SQL语句中执行配套的全文检索函数。

这样做侵入性太大了,而且每次部署项目都要为数据库安装插件,还要为不同的数据库写兼容的SQL,太麻烦了。

6、结合需求进行分析,问题数据表的数据量一般来说不会很大,可以考虑使用程序的方法非侵入性实现全文检索功能。

7、程序要实现全文检索功能,第一步就是对用户的搜索内容进行分词。

分词需要根据自然语义进行分词,而不是把内容拆分为一个一个字符这样的暴力分词,这时候就需要分词器的支持了。

根据开源协议依赖安全性分词效果等方面从11大Java开源中文分词器中筛选出了最适合当前需求的分词器“Jcseg

8、分词分好了,那要怎样根据分词结果在数据表中进行搜索呢?

使用LIKE关键字搜索效率太低了,建议使用position(PG)、instr(Oracle)

9、内容也搜索出来了,但结果没有达到预期

因为分词结果为["学校","电话","号码","是","多少","?"],而两条记录都包含“学校”,所以都被查出来了。

但是我们只需要“学校固定电话”这一条数据,那么要怎么让程序知道“学校固定电话”是相对最标准的问题数据呢?

这个就涉及到了文本的匹配度,需要将用户搜索的内容与搜索结果集的问题进行匹配度计算,并让结果集根据匹配度进行排序,最后取结果集第一项回复即可。

questionanswer
学校固定电话0123-88888888
学校官网www.123.com

10、文本匹配度要怎么计算呢?

这里基于编辑距离算法结合需求来计算文本的匹配度。

代码实现:

1、引入Jcseg中文分词

        <!--中文分词器-->
        <dependency>
            <groupId>org.lionsoul</groupId>
            <artifactId>jcseg-core</artifactId>
            <version>2.6.2</version>
        </dependency>

2、编写Jcseg的业务方法

/**
 * Description: jcseg分词器业务类
 *
 * @Author: NonNullPointer
 */
@Service
public class JcsegService {
    //创建SegmenterConfig分词配置实例,自动查找加载jcseg.properties配置项来初始化
    private SegmenterConfig config = new SegmenterConfig(true);
    //创建默认单例词库实现,并且按照config配置加载词库
    private ADictionary dic = DictionaryFactory.createSingletonDictionary(config);

    //依据给定的ADictionary和SegmenterConfig来创建ISegment
    //NLP模式
    private ISegment iSegment = ISegment.NLP.factory.create(config, dic);

    public String[] seg(String str) {
        HashSet<String> segHashSet = new HashSet<>();
        try {
            iSegment.reset(new StringReader(str));
            //获取分词结果
            IWord word = null;
            //去重
            while ((word = iSegment.next()) != null) {
                String wordStr = word.getValue();
                //去除空白符
                wordStr = NNPStringUtil.clearWhite(wordStr);
                //去除符号
                wordStr = NNPStringUtil.removeSymbol(wordStr);
                if(wordStr.length()>0){
                    segHashSet.add(wordStr);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
        if (segHashSet.size() == 0) {
            return null;
        }
        return NNPCollectionUtil.hashSet2Arr(segHashSet);
    }


}

3、编写适配并生成全文检索SQL的工具方法

    /**
     * Description: 创建伪全文搜索的where条件
     *
     * @param: [words, field]
     * @Author: NonNullPointer
     * @return: java.lang.String
     */
    public static String createWhereByFullText(String[] words, String field) {
        //对大小写不敏感
        final String pgTemp = "position('%s' in lower(%s))>0";
        final String oracleTemp = "instr(lower(%s),'%s')>0";
        StringBuilder whereBuilder = new StringBuilder();
        if (DbUtil.dbIsOracle()) {
            for (int i = 0; i < words.length; i++) {
                String word = words[i];
                if (i > 0) {
                    whereBuilder.append(" OR ");
                }
                whereBuilder.append(String.format(oracleTemp, field, word));
            }
        } else {
            for (int i = 0; i < words.length; i++) {
                String word = words[i];
                if (i > 0) {
                    whereBuilder.append(" OR ");
                }
                whereBuilder.append(String.format(pgTemp, word, field));
            }
        }
        return "(" + whereBuilder.toString() + ")";
    }

4、编写文本匹配度计算算法工具类

/**
 * Description: 计算文本相似度工具类(基于字符串编辑距离)
 *
 * @Cover: NonNullPointer
 */
public class NNPCalcLDSimUtil {
    //计算文本相似度
    public static double calcSim(String str1, String str2) {
        //去除符号与空白符,忽略大小写
        str1 = NNPStringUtil.clearWhite(NNPStringUtil.removeSymbol(str1)).toLowerCase();
        str2 = NNPStringUtil.clearWhite(NNPStringUtil.removeSymbol(str2)).toLowerCase();
        //全等返回1.1最高值
        if(str1.equalsIgnoreCase(str2)){
            return 1.1;
        }
        //相互包含直接返回1(100%)
        if (str1.indexOf(str2) > -1 || str2.indexOf(str1) > -1) {
            return 1;
        }
        int ld = getLD(str1, str2);
        return 1 - (double) ld / Math.max(str1.length(), str2.length());
    }

    private static int min(int one, int two, int three) {
        int min = one;
        if (two < min) {
            min = two;
        }
        if (three < min) {
            min = three;
        }
        return min;
    }

    //获取字符串编辑距离
    public static int getLD(String str1, String str2) {
        int d[][]; // 矩阵
        int n = str1.length();
        int m = str2.length();
        int i; // 遍历str1的
        int j; // 遍历str2的
        char ch1; // str1的
        char ch2; // str2的
        int temp; // 记录相同字符,在某个矩阵位置值的增量,不是0就是1
        if (n == 0) {
            return m;
        }
        if (m == 0) {
            return n;
        }
        d = new int[n + 1][m + 1];
        for (i = 0; i <= n; i++) { // 初始化第一列
            d[i][0] = i;
        }
        for (j = 0; j <= m; j++) { // 初始化第一行
            d[0][j] = j;
        }
        for (i = 1; i <= n; i++) { // 遍历str1
            ch1 = str1.charAt(i - 1);
            // 去匹配str2
            for (j = 1; j <= m; j++) {
                ch2 = str2.charAt(j - 1);
                if (ch1 == ch2) {
                    temp = 0;
                } else {
                    temp = 1;
                }
                // 左边+1,上边+1, 左上角+temp取最小
                d[i][j] = min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + temp);
            }
        }
        return d[n][m];
    }

}

文章不足之处还请斧正!

本文By:NonNullPointer --2024/01/08

最后修改:2024 年 01 月 08 日
如果觉得我的文章对你有用,请随意赞赏