小言_互联网的博客

算法高级(31)-搜索引擎中的拼写纠错功能该如何实现?

657人阅读  评论(0)

一、Word中的拼写检查功能

拼写检查程序是指将输入的每个字词与存储器里的字典比较,检查其正确性并在屏幕上显示差异的计算机程序。如果在字典中没有这个单词,用户就会被警告可能拼写错误,同时经常提供几个纠正错误建议。拼写检查程序不能识别不常用的人或专用术语,但是它容许你生成自己的个人字典,把自己常用的词语加进去。

二、拼写纠错功能的实现

在使用搜索引擎时,当我们输入错误的关键词时,当然这里的错误是拼写错误,搜索引擎的下拉框中仍会显示以正确关键词为前前辍的提示,当你直接回车搜索错误的关键词时,搜索引擎的结果中仍包括正确关键词的结果。你有没有想过它是如何实现的呢?

这个的实现可以有多种方案,常见的方法就是我们前面学习过的最长公共子序列和今天要讲的莱文斯坦距离方式。

最长公共子序列可以查看我前面的博文程序员的算法课(6)-最长公共子序列(LCS)

这两种方案都需要跟正确的词典进行对照,计算编辑距离或者最长公共子序列,将编辑距离最小或子序列最长的单词,作为纠正之后的单词,返回给用户。

三、莱文斯坦距离

【百度百科】莱文斯坦距离,又称Levenshtein距离,是编辑距离的一种。指两个字串之间,由一个转成另一个所需的最少编辑操作次数。允许的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

如单词three,我不小心手误打成了ethre,可以看到ethre只需要把首字母e移动到末尾即可变成正确的单词three。此时ethre相对于three的编辑距离就是1。

莱文斯坦距离允许增加、删除、替换字符这三个编辑操作,最长公共子串长度只允许增加、删除字符这两个编辑操作。

莱文斯坦距离和最长公共子串长度,从两个截然相反的角度,分析字符串的相似程度。莱文斯坦距离的大小,表示两个字符串差异的大小;而最长公共子串的大小,表示两个字符串相似程度的大小。

四、Lucene、Solr、ES中的拼写纠错

原理其实比较简单,无非就是通过算法进行计算得出一个最贴切的单词返回给用户,网上也有很多这样的案例讲解。既然是实战,那我们就分别来看看Java领域中鼎鼎大名的几个开源搜索框架对于拼写纠错是如何实现的吧。

1.Lucene中通过Suggest模块下的SpellChecker功能来实现拼写纠错。

源码中,定义了两个public的静态成员变量, DEFAULT_ACCURACY表示默认的最小分数,SpellCheck会对字典里的每个词与用户输入的搜索关键字进行一个相似度打分,默认该值是0.5,相似度分值范围是0到1之间,数字越大表示越相似,小于0.5会认为不是同一个结果。F_WORD是对于字典文件里每一行创建索引时使用的默认域名称,默认值为:word。

几个重要的API:

  • getAccuracy accuracy是精确度的意思,这里表示最小评分,评分越大表示与用户输入的关键字越相似
  • suggestSimilar:这个方法就是用来决定哪些word会被判定为比较相似的然后以数组的形式返回,这是SpellChecker的核心;
  • setComparator:设置比较器,既然涉及到相似度问题,那肯定有相似度大小问题,有大小必然存在比较,有比较必然需要比较器,通过比较器决定返回的建议词的顺序,因为一般需要把最相关的显示在最前面,然后依次排序显示;
  • setSpellIndex:设置拼写检查索引目录的
  • setStringDistance:设置编辑距离

看到了setStringDistance这个方法,想都不用想,Lucene肯定也是使用编辑距离的方式进行匹配的。

源码中还有两个私有属性,分别代表前缀和后缀的权重,前缀要比后缀大。

private float bStart = 2.0f;
private float bEnd = 1.0f;

几个重要的方法:

  • formGrams:对输入的字符串按指定长度分割,返回分割后的数组。
  • indexDictionary:将字典文件里的词进行ngram操作后得到多个词然后分别写入索引。 
  • suggestSimilar:用来计算最后返回的建议词。有三种建议模式:
    • SUGGEST_WHEN_NOT_IN_INDEX:意思就是只有当用户提供的搜索关键字在索引Term中不存在我才提供建议,否则我会认为用户输入的搜索关键字是正确的不需要提供建议。
    • SUGGEST_MORE_POPULAR:表示只返回频率较高的词组,用户输入的搜索关键字首先需要经过ngram分割,创建索引的时候也需要进行分词,如果用户输入的词分割后得到的word在索引中出现的频率比索引中实际存在的Term还要高,那说明不需要进行拼写建议了。
    • SUGGEST_ALWAYS:永远进行建议,只是返回的建议结果受numSug数量限制即最多返回几条拼写建议。

最终返回用户输入关键字和索引中当前Term的相似度,这个取决于你Distance实现,默认实现是LevenshteinDistance

(莱文斯坦距离)即计算编辑距离。采用三个一维数组代替了一个二维数组,一个数组为上一轮计算的值,一个数组保存本轮计算的值,最后一个数组用于交换两个数组的值。核心源码如下:

package org.apache.lucene.search.spell;

public class LevenshteinDistance implements StringDistance {

    @Override
    public float getDistance (String target, String other) {
        char[] sa;
        int n;
        int p[]; //上一行计算的值
        int d[]; //当前行计算的值
        int _d[]; //用于交换p和d
      
        sa = target.toCharArray();
        n = sa.length;
        p = new int[n+1]; 
        d = new int[n+1]; 
      
        final int m = other.length();
        if (n == 0 || m == 0) {
          if (n == m) {
            return 1;
          }
          else {
            return 0;
          }
        } 
 
 
        int i; // target的索引
        int j; // other的索引
 
        char t_j; //other的第j个字符
 
        int cost; 
		//初始化,将空字符串转换为长度为i的target字符串的操作次数
        for (i = 0; i<=n; i++) {
            p[i] = i;
        }
		
        for (j = 1; j<=m; j++) {
            t_j = other.charAt(j-1);
            //左方的初始值为将长度为j的other字符串转换为空字符串的操作次数
			d[0] = j;
			//计算将长度为i的target字符串转换为长度为j的other字符串的操作次数
            for (i=1; i<=n; i++) {
                cost = sa[i-1]==t_j ? 0 : 1;
				//d[i-1]左方、p[i]上方、p[i-1]左上
                d[i] = Math.min(Math.min(d[i-1]+1, p[i]+1),  p[i-1]+cost);
            }
 
            //交换p和d,用于下一轮计算
            _d = p;
            p = d;
            d = _d;
        }
 
		//计算相似度,p中最后一个元素为LevensteinDistance
        return 1.0f - ((float) p[n] / Math.max(other.length(), sa.length));
    }
}

Lucene还内置了另外几种相似度实现,都是基于距离计算的:JaroWinklerDistance、LuceneLevenshteinDistance和NGramDistance。

源码中还有大量同步锁的使用,跟本次内容关系不大,暂时先不考虑。

2.solr有SpellCheckComponent拼写纠错功能,此外还有依靠文件来纠错和主索引纠错的方式。

solr的纠错依赖于lucene,主要通过插件的方式进行使用。solr通过配置文件的方式指定纠错的规则,里面一个非常重要的属性:

accuracy,这个值每下降0.1就可以纠错一个字母,下降0.2可以纠错一个汉字,例如:将其调整到0.8时可以搜索到数据但是仅仅只能出错一个汉字或者两个字母。

3.es纠错

官方公式:https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters-phrase.html

es中使用phrase Suggester来进行拼写纠错。phrase suggester在term suggester之上添加了额外的逻辑以选择整体更正的phrase,而不是基于单个分词加权的ngram语言模型。在实际中phrase suggester能够根据单词的词频等信息作出更好的选择。

ES中常用的4种Suggester类型:Term、Phrase、Completion、Context。

  • Term suggester正如其名,只基于analyze过的单个term去提供建议,并不会考虑多个term之间的关系。API调用方只需为每个token挑选options里的词,组合在一起返回给用户前端即可。 那么有无更直接办法,API直接给出和用户输入文本相似的内容? 答案是有,这就要求助Phrase Suggester了。
  • Phrase suggester在Term suggester的基础上,会考量多个term之间的关系,比如是否同时出现在索引的原文里,相邻程度,以及词频等等。
  • Completion Suggester,它主要针对的应用场景就是"Auto Completion"。 此场景下用户每输入一个字符的时候,就需要即时发送一次查询请求到后端查找匹配项,在用户输入速度较高的情况下对后端响应速度要求比较苛刻。因此实现上它和前面两个Suggester采用了不同的数据结构,索引并非通过倒排来完成,而是将analyze过的数据编码成FST和索引一起存放。对于一个open状态的索引,FST会被ES整个装载到内存里的,进行前缀查找速度极快。但是FST只能用于前缀查找,这也是Completion Suggester的局限所在。
  • Context Suggester,会根据上下文进行补全,这种方式补全效果较好,但性能较差,用的人不多。这也是es中拼写纠错的高级用法。

五、搜索引擎中对拼写纠错的算法猜想

Google搜索框的补全/纠错功能,如果用ES怎么实现呢?我能想到的一个的实现方式:

  1. 在用户刚开始输入的过程中,使用Completion Suggester进行关键词前缀匹配,刚开始匹配项会比较多,随着用户输入字符增多,匹配项越来越少。如果用户输入比较精准,可能Completion Suggester的结果已经够好,用户已经可以看到理想的备选项了。 
  2. 如果Completion Suggester已经到了零匹配,那么可以猜测是否用户有输入错误,这时候可以尝试一下Phrase Suggester。
  3. 如果Phrase Suggester没有找到任何option,开始尝试term Suggester。

精准程度上(Precision)看: Completion >  Phrase > term, 而召回率上(Recall)则反之。从性能上看,Completion Suggester是最快的,如果能满足业务需求,只用Completion Suggester做前缀匹配是最理想的。 Phrase和Term由于是做倒排索引的搜索,相比较而言性能应该要低不少,应尽量控制suggester用到的索引的数据量,最理想的状况是经过一定时间预热后,索引可以全量map到内存。

真正的搜索引擎的拼写纠错优化,肯定不止我讲的这么简单,但是万变不离其宗。掌握了核心原理,就是掌握了解决问题的方法,剩下就靠你自己的灵活运用和实战操练了。


我的微信公众号:架构真经(关注领取免费资源)

参考文章

  1. https://baike.baidu.com/item/%E8%8E%B1%E6%96%87%E6%96%AF%E5%9D%A6%E8%B7%9D%E7%A6%BB/14448097?fr=aladdin
  2. https://www.jianshu.com/p/f2cacf1d5d1b
  3. http://www.chepoo.com/spelling-correction-function-realization.html
  4. https://blog.csdn.net/qq_25800311/article/details/90665244
  5. https://blog.csdn.net/yzl_8877/article/details/53375132
  6. https://blog.csdn.net/sqh201030412/article/details/51038870
  7. https://blog.csdn.net/github_26672553/article/details/72639396
  8. https://elasticsearch.cn/article/142

转载:https://blog.csdn.net/m0_37609579/article/details/101001985
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场