模糊的字符串记录搜索算法(支持单词转置和字符转置)

时间:2019-01-12 19:16:10

标签: algorithm search language-agnostic string-matching

我正在尝试为我的特定应用找到最佳算法。我在SO,Google上搜索过,阅读了有关Levenshtein距离的各种文章,但是说实话,这有点超出我的专业领域。大多数人似乎发现两个输入字符串有多相似,例如字符串之间的汉明距离。

我要寻找的是不同的,更多是模糊记录搜索(而且我敢肯定有一个名字,我对Google不了解)。我确信有人之前已经解决了这个问题,我正在寻找建议,为我的进一步研究指明正确的方向。

就我而言,我需要模糊搜索音乐艺术家及其专辑条目的数据库。可以想象,数据库将具有数百万个条目,因此,良好扩展的算法至关重要。对于我的问题,艺术家和专辑不在不同的列中,这并不重要,数据库可以将所有单词存储在一个列中,只要这有助于搜索。

要搜索的数据库:

|-------------------|---------------------|
| Artist            | Album               |
|-------------------|---------------------|
| Alanis Morissette | Jagged Little Pill  |
| Moby              | Everything is Wrong |
| Air               | Moon Safari         |
| Pearl Jam         | Ten                 |
| Nirvana           | Nevermind           |
| Radiohead         | OK Computer         |
| Beck              | Odelay              |
|-------------------|---------------------|

查询文本将仅包含整个Artist_Album串联中的一个单词,直至整个单词。查询文本来自OCR,并且可能具有单个字符换位符,但是最有可能发生的事情是单词不能保证具有正确的顺序。此外,搜索中可能还会有一些不属于专辑的单词(例如封面文字)。例如,“确定计算机”可能在专辑的顶部,而在其下方的“ Radiohead”,或者某些专辑的文字排列在列中,混合了单词顺序。

可能的搜索字符串:

C0mputer Rad1ohead
Pearl Ten Jan
Alanis Jagged Morisse11e Litt1e Pi11
Air Moon Virgin Records
Moby Everything

请注意,使用OCR时,某些字母看起来像数字,或者完全是错误的字母(用Jan代替Jam)。对于Radiohead的 OK计算机和Moby的万事大吉,查询文本甚至都没有全部单词。对于Air的 Moon Safari ,将搜索Virgin Records多余的单词,但缺少Safari。

是否有一种通用算法可以从数据库中返回最可能的单个结果,并且如果没有一个算法满足某些“似然性”分数阈值,则什么也不返回?我实际上是用Python开发的,但这只是一个好处,我正在寻找开始研究的更多地方。

1 个答案:

答案 0 :(得分:0)

让我们将问题分为两部分。

  • 首先,您要定义一些相似度的度量(称为度量)。如果查询文字与专辑/艺术家封面非常匹配,则该指标应返回一个较小的数字,否则返回较大的数字。
  • 第二,您需要一个加快此过程的数据结构。显然,您不想每次运行查询都计算此指标。

第1部分:指标

您已经提到Levenshtein距离,这是一个很好的起点。 跳出框框思考。

LD做出某些假设(每个字符替换的可能性均相同,删除与插入的可能性相同,等等)。考虑到OCR可能带来的故障,显然可以提高此度量标准的性能。

例如将“ 1”转换为“ i”不应像将“ 0”转换为“ _”那样严厉地处罚。

我将分两个阶段实施该指标。对于任何给定的两个字符串:

  • 将两个字符串都拆分为令牌(假设使用空格作为分隔符)
  • 寻找最相似的词(使用LD的修改版)
  • 根据“匹配词”,“缺失词”和“添加词”(最好是加权的)分配最终分数

这是一个示例实现(弄乱常量):

static double m(String a, String b){
    String[] aParts = a.split(" ");
    String[] bParts = b.split(" ");
    boolean[] bUsed = new boolean[bParts.length];
    int matchedTokens = 0;
    int tokensInANotInB = 0;
    int tokensInBNotInA = 0;
    for(int i=0;i<aParts.length;i++){
        String a0 = aParts[i];
        boolean wasMatched = true;
        for(int j=0;j<bParts.length;j++){
            String b0 = bParts[j];
            double d = levenshtein(a0, b0);
            /* If we match the token a0 with a token from b0
             * update the number of matchedTokens
             * escape the loop
             */
            if(d < 2){
                bUsed[j]=true;
                wasMatched = true;
                matchedTokens++;
                break;
            }
        }
        if(!wasMatched){
            tokensInANotInB++;
        }
    }
    for(boolean partUsed : bUsed){
        if(!partUsed){
            tokensInBNotInA++;
        }
    }
    return (matchedTokens 
    + tokensInANotInB * -0.3  // the query is allowed to contain extra words at minimal cost
    + tokensInBNotInA * -0.5  // the album title should not contain too many extra words
    ) / java.lang.Math.max(aParts.length, bParts.length); 
}

此函数使用了经过修改的levenshtein函数:

static double levenshtein(String x, String y) {
double[][] dp = new double[x.length() + 1][y.length() + 1];

for (int i = 0; i <= x.length(); i++) {
    for (int j = 0; j <= y.length(); j++) {
        if (i == 0) {
            dp[i][j] = j;
        }
        else if (j == 0) {
            dp[i][j] = i;
        }
        else {
            dp[i][j] = min(dp[i - 1][j - 1] 
             + costOfSubstitution(x.charAt(i - 1), y.charAt(j - 1)), 
              dp[i - 1][j] + 1, 
              dp[i][j - 1] + 1);
        }
    }
}
return dp[x.length()][y.length()];
}

哪个使用了“替代成本”功能(按说明工作)

static double costOfSubstitution(char a, char b){
    if(a == b)
        return 0.0;
    else{
        // 1 and i
        if(a == '1' && b == 'i')
            return 0.5;
        if(a == 'i' && b == '1')
            return 0.5;

        // 0 and O
        if(a == '0' && b == 'o')
            return 0.5;
        if(a == 'o' && b == '0')
            return 0.5;
        if(a == '0' && b == 'O')
            return 0.5;
        if(a == 'O' && b == '0')
            return 0.5;

        // default
        return 1.0; 
    }
}

我仅列举了几个示例(将“ 1”变成“ i”或将“ 0”变成“ o”)。 但是我敢肯定,你会明白的。

第2部分:数据结构

查看BK-trees。它们是用于保存度量标准信息的特定数据结构。您的指标必须是真实的指标(从字面上的数学意义上来说)。但这很容易安排。