【问题标题】:Detecting string similarity using Levenshtein distance (is SLOW)使用 Levenshtein 距离检测字符串相似度(慢)
【发布时间】:2011-10-01 13:13:17
【问题描述】:

查询返回 2100 万条记录

我遍历这张表的方式需要很长时间。还有哪些其他解决方案?

SqlDataReader rd = DbInfo.DataRdr(Conn,
 "SELECT a.NAME AS ANAME, b.NAME AS BNAME, a.ID as AID, b.ID AS BUD " +
 "FROM myTable a JOIN myTable b ON a.NUM = b.NUM AND a.ID <> b.ID");

while (rd.Read())
{
   if (rd["ANAME"].ToString().LevenshteinDistance(rd["BNAME"].ToString()) <= 10)
   {

        Logging.Write(...);

   }
}



    public static int LevenshteinDistance(this string s, string t)
    {
        if (s == null)
            throw new ArgumentNullException("s");
        if (t == null)
            throw new ArgumentNullException("t");
        int n = s.Length;
        int m = t.Length;
        int[,] d = new int[n+1,m+1];

        if (n == 0 || m == 0)
            return Math.Max(m, n);

        for (int i = 0; i <= n; i++)
        {
            d[i, 0] = i;
        }
        for (int i = 0; i < m; i++)
        {
            d[0, i] = i;
        }

        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < m; j++)
            {
                int cost = (t[j] == s[i]) ? 0 : 1;

                d[i + 1, j + 1] = Math.Min(Math.Min(d[i, j + 1] + 1, d[i + 1, j] + 1), d[i, j] + cost);
            }
        }

        return d[n, m];
    }

【问题讨论】:

  • SELECT * 不好开始。您有机会改进查询吗?
  • 我们能看到 LevenshteinDistance 函数的代码吗?
  • @John haha​​... 他大概的意思是其中一个应该是“j”。
  • 我是哪个? :P 好的,我修好了
  • 不会更快,但值得注意的是,这在纯 SQL 中是可能的:sqlteam.com/forums/topic.asp?TOPIC_ID=51540&whichpage=1

标签: c# .net sql string-comparison


【解决方案1】:

在完成本线程中提到的其他优化之后,您可以将 Levenshtein 计算移至服务器,并且只选择与您的编辑距离标准匹配的行。我在一个项目中需要这个功能,所以我用它做了一个库,here。 lib中使用的编辑距离方法只需要n * 2内存而不是n * m。

例如,即使在服务器上,您也只想在字符串长度的差异

SELECT a.NAME as NameA, b.NAME as NameB
FROM table a
JOIN table b ON a.NUM = b.NUM
WHERE a.Id < b.Id
AND length(a.NAME) - length(b.NAME) BETWEEN -10 AND 10 OR
    EditDistance(a.Name, b.Name) < 10

【讨论】:

    【解决方案2】:

    您可以做出的最大改进是减少正在考虑的解决方案空间。由于您希望最大距离为 10,因此长度差异超过 10 的任何字符串都不可能符合条件:

    SELECT a.NAME AS ANAME, b.NAME AS BNAME, a.ID as AID, b.ID AS BUD 
     FROM myTable a JOIN myTable b ON a.NUM = b.NUM AND a.ID < b.ID
     WHERE length(a.NAME) - length(b.NAME) BETWEEN -10 AND 10;
    

    接下来,分析您的代码并查看热点在哪里。一篇不错的入门文章:Find Application Bottlenecks with Visual Studio Profiler.

    看看Optimizing the Levenshtein Algorithm in C#

    编辑

    Chris 还注意到,由于levenshtein(a,b) == levenshtein(b,a),您只需要在连接 a.ID

    【讨论】:

      【解决方案3】:

      优化:

      1) 查看您的数据。也许您可以进行一些检查以更快地整理出无效对。如果 Name 的长度变化超过 10,您可以检查 s.Lenghtt.Length 之间的差异是否大于 10 并立即返回一个高距离(可能是 int.MaxValue 或仅 100)。如果距离明显超出范围,计算距离是没有意义的。

      2) 寻找小的优化。在 150k 行上循环两次意味着 225 亿次迭代。微小的变化可能会产生很大的影响。您可以尝试缓存到行对象并通过使用Equals() 方法删除ToString()。我认为这比访问数据表的第 i 个元素 150000 次要快。

      for (int i = 0; i < dt1.Rows.Count; i++)
      {
         var outerRow = dt1.Rows[i];
         for (int j = 0; i + 1 < dt1.Rows.Count; j++)
         {
           var innerRow = dt1.Rows[j];
           if (Equals(outerRow["NUM"] == innerRow["NUM"]))
           {
              if (outerRow["Name"].ToString().LevenshteinDistance(innerRow.ToString()) <= 10)
              {
                 Logging.Write(...);
              }
           }
        }
      

      3) 尝试减少/拆分数据集。执行查询以获取 NUM select distinct NUM from myTable 的所有可能值。然后为结果中的每个NUM 执行原始查询,但使用 where 条件并仅选择名称:SELECT name from myTable where NUM = currentNum

      这样你就不用t have to compare the NUM row and you dont 选择奇数数据。您的代码可以优化为仅执行 levenshtein 距离,但使用 1+2 中所述的优化。

      4) 尝试不同的方法,例如全文搜索。

      我只是想解决一个类似的问题,在 120 万行的表中查找匹配项。我使用了lucene.net,它在搜索我的行的一个或多个属性时为我提供了实时结果。

      他们也有 levenshtein,但也许它比你的实现要快 ;) MSSQL Server 也支持全文搜索。

      【讨论】:

      • 我当前的问题是运行 Chris Cunningham 查询时引发的 System.OutOfMemoryexception 错误
      • 我对代码做了一些改动,请看上面的问题代码。这些更改解决了内存异常错误,但速度仍然很慢。
      • 看来这是最快的了,我去掉了LevenShtein方法,对速度影响不大。
      【解决方案4】:

      您可以先使用以下内容作为查询,具体取决于NUM 列实际相等的频率:

      SELECT a.NAME AS ANAME, b.NAME AS BNAME, other things
      FROM myTable a
      JOIN myTable b
      ON a.NUM = b.NUM
      AND a.id < b.id
      

      然后,您的 SQL 查询将为您提供与 NUMs 匹配的行对,您可以在其上调用 LevenshteinDistance。比如:

      DataTable dt1 = new DataTable();
      dt1.Load(DbInfo.DataRdr(Conn, "[the query I wrote above]"));
      
      for (int i = 0; i < dt1.Rows.Count; i++)
      {
         if (dt1.Rows[i]["ANAME"].ToString().LevenshteinDistance(dt1.Rows[i]["BNAME"].ToString()) <= 10)
         {
           Logging.Write(...[modify the query so it returns the things you want to log]...);
         }
      }
      

      【讨论】:

      • 你的查询返回超过700万条记录,我的表有130000条记录
      • 您的原始代码需要很长时间才能执行的原因是您正在执行一个返回 130000 条记录的查询,然后执行您的 Levenshtein 函数 130000*130000 = 16.9 十亿次. 700 万是一个显着的改进(我的查询返回所有要比较的记录对)!
      • 正如另一个答案所说,写 LevenshteinDistance 更好,这样它就不会那么麻烦。如果您只关心距离是否大于 10,您可以在大量情况下很早就停止距离计算。
      • count 返回 2100 万行
      • 如何优化 Lev 方法
      【解决方案5】:

      即使i == j,您也在将dt1.Rows[i]["Name"].ToString()dt1.Rows[j]["Name"].ToString() 进行比较。

      尝试循环 0 &lt;= i &lt; dt1.Rows.Count - 1i + 1 &lt;= j &lt; dt1.Rows.Count

      此外,您仅在 dt1.Rows[i]["NUM"].ToString() == dt1.Rows[i]["NUM"].ToString() 时才记录,这可能是一种更快的检查。如果那是假的,那么做 Levenshtein 就没有意义了。

      编辑:@John 关于dt1.Rows[i]["NUM"].ToString() == dt1.Rows[i]["NUM"].ToString() 是正确的(两者都是i?)。

      【讨论】:

        猜你喜欢
        • 2012-07-25
        • 2018-09-21
        • 2017-05-01
        • 2020-03-05
        • 2012-10-24
        • 1970-01-01
        • 2021-10-02
        • 1970-01-01
        相关资源
        最近更新 更多