T-SQLのレーベンシュタイン距離


回答:


101

標準のレーベンシュタイン編集距離関数をTSQLに実装し、認識している他のバージョンよりも速度を向上させるいくつかの最適化を行いました。2つの文字列の先頭に共通の文字(共有プレフィックス)、末尾に共通の文字(共有サフィックス)があり、文字列が大きく、最大編集距離が指定されている場合、速度が大幅に向上します。たとえば、入力が2つの非常に類似した4000文字の文字列であり、最大編集距離2が指定されている場合、これは、よりもほぼ3桁高速です。edit_distance_within受け入れられた回答で機能し、55秒に対して0.073秒(73ミリ秒)で回答を返します。また、2つの入力文字列の大きい方に一定のスペースを加えたものに等しいスペースを使用するため、メモリ効率も高くなります。これは、列を表す単一のnvarchar「配列」を使用し、その中ですべての計算をインプレースで実行し、さらにいくつかのヘルパーint変数を実行します。

最適化:

  • 共有プレフィックスおよび/またはサフィックスの処理をスキップします
  • 大きい文字列が小さい文字列全体で開始または終了する場合の早期リターン
  • サイズの違いにより最大距離を超えることが保証されている場合は、早期返品
  • マトリックス内の列を表す単一の配列のみを使用します(nvarcharとして実装されます)
  • 最大距離が指定されると、時間計算量は(len1 * len2)から(min(len1、len2))、つまり線形になります。
  • 最大距離が与えられたとき、最大距離の限界が達成できないことがわかったらすぐに早期に戻る

コードは次のとおりです(もう少し高速化するために2014年1月20日更新):

-- =============================================
-- Computes and returns the Levenshtein edit distance between two strings, i.e. the
-- number of insertion, deletion, and sustitution edits required to transform one
-- string to the other, or NULL if @max is exceeded. Comparisons use the case-
-- sensitivity configured in SQL Server (case-insensitive by default).
-- 
-- Based on Sten Hjelmqvist's "Fast, memory efficient" algorithm, described
-- at http://www.codeproject.com/Articles/13525/Fast-memory-efficient-Levenshtein-algorithm,
-- with some additional optimizations.
-- =============================================
CREATE FUNCTION [dbo].[Levenshtein](
    @s nvarchar(4000)
  , @t nvarchar(4000)
  , @max int
)
RETURNS int
WITH SCHEMABINDING
AS
BEGIN
    DECLARE @distance int = 0 -- return variable
          , @v0 nvarchar(4000)-- running scratchpad for storing computed distances
          , @start int = 1      -- index (1 based) of first non-matching character between the two string
          , @i int, @j int      -- loop counters: i for s string and j for t string
          , @diag int          -- distance in cell diagonally above and left if we were using an m by n matrix
          , @left int          -- distance in cell to the left if we were using an m by n matrix
          , @sChar nchar      -- character at index i from s string
          , @thisJ int          -- temporary storage of @j to allow SELECT combining
          , @jOffset int      -- offset used to calculate starting value for j loop
          , @jEnd int          -- ending value for j loop (stopping point for processing a column)
          -- get input string lengths including any trailing spaces (which SQL Server would otherwise ignore)
          , @sLen int = datalength(@s) / datalength(left(left(@s, 1) + '.', 1))    -- length of smaller string
          , @tLen int = datalength(@t) / datalength(left(left(@t, 1) + '.', 1))    -- length of larger string
          , @lenDiff int      -- difference in length between the two strings
    -- if strings of different lengths, ensure shorter string is in s. This can result in a little
    -- faster speed by spending more time spinning just the inner loop during the main processing.
    IF (@sLen > @tLen) BEGIN
        SELECT @v0 = @s, @i = @sLen -- temporarily use v0 for swap
        SELECT @s = @t, @sLen = @tLen
        SELECT @t = @v0, @tLen = @i
    END
    SELECT @max = ISNULL(@max, @tLen)
         , @lenDiff = @tLen - @sLen
    IF @lenDiff > @max RETURN NULL

    -- suffix common to both strings can be ignored
    WHILE(@sLen > 0 AND SUBSTRING(@s, @sLen, 1) = SUBSTRING(@t, @tLen, 1))
        SELECT @sLen = @sLen - 1, @tLen = @tLen - 1

    IF (@sLen = 0) RETURN @tLen

    -- prefix common to both strings can be ignored
    WHILE (@start < @sLen AND SUBSTRING(@s, @start, 1) = SUBSTRING(@t, @start, 1)) 
        SELECT @start = @start + 1
    IF (@start > 1) BEGIN
        SELECT @sLen = @sLen - (@start - 1)
             , @tLen = @tLen - (@start - 1)

        -- if all of shorter string matches prefix and/or suffix of longer string, then
        -- edit distance is just the delete of additional characters present in longer string
        IF (@sLen <= 0) RETURN @tLen

        SELECT @s = SUBSTRING(@s, @start, @sLen)
             , @t = SUBSTRING(@t, @start, @tLen)
    END

    -- initialize v0 array of distances
    SELECT @v0 = '', @j = 1
    WHILE (@j <= @tLen) BEGIN
        SELECT @v0 = @v0 + NCHAR(CASE WHEN @j > @max THEN @max ELSE @j END)
        SELECT @j = @j + 1
    END

    SELECT @jOffset = @max - @lenDiff
         , @i = 1
    WHILE (@i <= @sLen) BEGIN
        SELECT @distance = @i
             , @diag = @i - 1
             , @sChar = SUBSTRING(@s, @i, 1)
             -- no need to look beyond window of upper left diagonal (@i) + @max cells
             -- and the lower right diagonal (@i - @lenDiff) - @max cells
             , @j = CASE WHEN @i <= @jOffset THEN 1 ELSE @i - @jOffset END
             , @jEnd = CASE WHEN @i + @max >= @tLen THEN @tLen ELSE @i + @max END
        WHILE (@j <= @jEnd) BEGIN
            -- at this point, @distance holds the previous value (the cell above if we were using an m by n matrix)
            SELECT @left = UNICODE(SUBSTRING(@v0, @j, 1))
                 , @thisJ = @j
            SELECT @distance = 
                CASE WHEN (@sChar = SUBSTRING(@t, @j, 1)) THEN @diag                    --match, no change
                     ELSE 1 + CASE WHEN @diag < @left AND @diag < @distance THEN @diag    --substitution
                                   WHEN @left < @distance THEN @left                    -- insertion
                                   ELSE @distance                                        -- deletion
                                END    END
            SELECT @v0 = STUFF(@v0, @thisJ, 1, NCHAR(@distance))
                 , @diag = @left
                 , @j = case when (@distance > @max) AND (@thisJ = @i + @lenDiff) then @jEnd + 2 else @thisJ + 1 end
        END
        SELECT @i = CASE WHEN @j > @jEnd + 1 THEN @sLen + 1 ELSE @i + 1 END
    END
    RETURN CASE WHEN @distance <= @max THEN @distance ELSE NULL END
END

この関数のコメントで述べたように、文字比較の大文字と小文字の区別は、有効な照合に従います。デフォルトでは、SQL Serverの照合順序は、大文字と小文字を区別しない比較になります。この関数を常に大文字と小文字を区別するように変更する1つの方法は、文字列が比較される2つの場所に特定の照合順序を追加することです。ただし、特にデータベースがデフォルト以外の照合を使用している場合の副作用については、これを徹底的にテストしていません。大文字と小文字を区別する比較を強制するために、2つの行を変更する方法は次のとおりです。

    -- prefix common to both strings can be ignored
    WHILE (@start < @sLen AND SUBSTRING(@s, @start, 1) = SUBSTRING(@t, @start, 1) COLLATE SQL_Latin1_General_Cp1_CS_AS) 

そして

            SELECT @distance = 
                CASE WHEN (@sChar = SUBSTRING(@t, @j, 1) COLLATE SQL_Latin1_General_Cp1_CS_AS) THEN @diag                    --match, no change

1
これを使用して、テーブル内の上位5つの最も近い文字列を検索するにはどうすればよいですか?つまり、10m行のストリート名テーブルがあるとしましょう。通りの名前を検索と入力しましたが、1文字が間違って書かれています。最大のパフォーマンスで上位5つの最も近い一致を検索するにはどうすればよいですか?
MonsterMMORPG 2017

1
ブルートフォース(すべてのアドレスを比較)以外では、できません。レーベンシュタインは、インデックスを簡単に利用できるものではありません。たとえば、住所の郵便番号や名前の音声コードなど、インデックスを作成できるものを使用して候補をより小さなサブセットに絞り込むことができる場合は、ここでの回答のようなストレートレーベンシュタインを適切に適用できます。サブセット。大規模なセット全体に適用するには、Levenshtein Automataのようなものに移動する必要がありますが、SQLでそれを実装することは、ここで回答するSOの質問の範囲をはるかに超えています。
手斧-SOverflowで2017

@MonsterMMORPG理論的には、逆を実行して、特定のレーベンシュタイン距離に対して可能なすべての順列を計算できます。または、アドレス内の単語が役立つほど短いリストを構成しているかどうかを確認することもできます(おそらく、めったに表示されない単語は無視します)。
TheConstructor 2017

@ MonsterMMORPG-これは遅いですが、もっと良い答えを追加したいと思いました。許可する編集の最小数がわかっている場合は、githubのsymspellプロジェクトで行われたようにSymmetricDeleteメソッドを使用できます。削除だけの順列の小さなサブセットを保存してから、検索文字列の削除順列の小さなセットから任意のものを検索できます。返されたセット(最大編集距離を1つまたは2つしか許可しない場合は小さい)で、完全なレーベンシュタイン計算を実行します。しかし、それはすべての文字列で行うよりもはるかに少ないはずです。
手斧-SOverflowで2017

1
@ DaveCousineau-関数のコメントで述べたように、文字列の比較では、有効なSQLServer照合の大文字と小文字の区別が使用されます。デフォルトでは、これは通常、大文字と小文字を区別しないことを意味します。追加したばかりの投稿の編集をご覧ください。別の回答のFribble実装は、照合に関して同様に動作します。
手斧-SOverflowで

58

アーノルドフリブルはsqlteam.com/forumsで2つの提案をしました

これは2006年からの若いものです:

SET QUOTED_IDENTIFIER ON 
GO
SET ANSI_NULLS ON 
GO

CREATE FUNCTION edit_distance_within(@s nvarchar(4000), @t nvarchar(4000), @d int)
RETURNS int
AS
BEGIN
  DECLARE @sl int, @tl int, @i int, @j int, @sc nchar, @c int, @c1 int,
    @cv0 nvarchar(4000), @cv1 nvarchar(4000), @cmin int
  SELECT @sl = LEN(@s), @tl = LEN(@t), @cv1 = '', @j = 1, @i = 1, @c = 0
  WHILE @j <= @tl
    SELECT @cv1 = @cv1 + NCHAR(@j), @j = @j + 1
  WHILE @i <= @sl
  BEGIN
    SELECT @sc = SUBSTRING(@s, @i, 1), @c1 = @i, @c = @i, @cv0 = '', @j = 1, @cmin = 4000
    WHILE @j <= @tl
    BEGIN
      SET @c = @c + 1
      SET @c1 = @c1 - CASE WHEN @sc = SUBSTRING(@t, @j, 1) THEN 1 ELSE 0 END
      IF @c > @c1 SET @c = @c1
      SET @c1 = UNICODE(SUBSTRING(@cv1, @j, 1)) + 1
      IF @c > @c1 SET @c = @c1
      IF @c < @cmin SET @cmin = @c
      SELECT @cv0 = @cv0 + NCHAR(@c), @j = @j + 1
    END
    IF @cmin > @d BREAK
    SELECT @cv1 = @cv0, @i = @i + 1
  END
  RETURN CASE WHEN @cmin <= @d AND @c <= @d THEN @c ELSE -1 END
END
GO

1
@Alexander、うまくいくようですが、変数名をもっと意味のあるものに変更します。また、@ dを削除します。入力内の、2つの文字列の長さを知っています。
Lieven Keersmaekers 2009

2
@Lieven:それは私の実装ではありません。作者はArnoldFribbleです。@dパラメーターは、到達後の文字列間の最大許容差であり、文字列は多様すぎると見なされ、関数は-1を返します。T-SQLのアルゴリズムの動作が遅すぎるために追加されました。
アレクサンダープロコフィエフ

en.wikipedia.org/wiki/Levenshtein_distanceでアルゴリズムの擬似コードを確認する必要がありますが、それほど改善されていません。
ノーマンH

13

IIRC、SQL Server 2005以降では、任意の.NET言語でストアドプロシージャを記述できます。SQLServer2005でのCLR統合の使用。それで、レーベンシュタイン距離を計算するための手順を書くのは難しいことではありません。

シンプルなHello、World!ヘルプから抽出:

using System;
using System.Data;
using Microsoft.SqlServer.Server;
using System.Data.SqlTypes;

public class HelloWorldProc
{
    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void HelloWorld(out string text)
    {
        SqlContext.Pipe.Send("Hello world!" + Environment.NewLine);
        text = "Hello world!";
    }
}

次に、SQLServerで次のコマンドを実行します。

CREATE ASSEMBLY helloworld from 'c:\helloworld.dll' WITH PERMISSION_SET = SAFE

CREATE PROCEDURE hello
@i nchar(25) OUTPUT
AS
EXTERNAL NAME helloworld.HelloWorldProc.HelloWorld

そして今、あなたはそれをテスト実行することができます:

DECLARE @J nchar(25)
EXEC hello @J out
PRINT @J

お役に立てれば。


7

文字列を比較するためにレーベンシュタイン距離アルゴリズムを使用できます

ここで、T-SQLの例をhttp://www.kodyaz.com/articles/fuzzy-string-matching-using-levenshtein-distance-sql-server.aspxで見つけることができます

CREATE FUNCTION edit_distance(@s1 nvarchar(3999), @s2 nvarchar(3999))
RETURNS int
AS
BEGIN
 DECLARE @s1_len int, @s2_len int
 DECLARE @i int, @j int, @s1_char nchar, @c int, @c_temp int
 DECLARE @cv0 varbinary(8000), @cv1 varbinary(8000)

 SELECT
  @s1_len = LEN(@s1),
  @s2_len = LEN(@s2),
  @cv1 = 0x0000,
  @j = 1, @i = 1, @c = 0

 WHILE @j <= @s2_len
  SELECT @cv1 = @cv1 + CAST(@j AS binary(2)), @j = @j + 1

 WHILE @i <= @s1_len
 BEGIN
  SELECT
   @s1_char = SUBSTRING(@s1, @i, 1),
   @c = @i,
   @cv0 = CAST(@i AS binary(2)),
   @j = 1

  WHILE @j <= @s2_len
  BEGIN
   SET @c = @c + 1
   SET @c_temp = CAST(SUBSTRING(@cv1, @j+@j-1, 2) AS int) +
    CASE WHEN @s1_char = SUBSTRING(@s2, @j, 1) THEN 0 ELSE 1 END
   IF @c > @c_temp SET @c = @c_temp
   SET @c_temp = CAST(SUBSTRING(@cv1, @j+@j+1, 2) AS int)+1
   IF @c > @c_temp SET @c = @c_temp
   SELECT @cv0 = @cv0 + CAST(@c AS binary(2)), @j = @j + 1
 END

 SELECT @cv1 = @cv0, @i = @i + 1
 END

 RETURN @c
END

(ジョセフガマによって開発された機能)

使用法 :

select
 dbo.edit_distance('Fuzzy String Match','fuzzy string match'),
 dbo.edit_distance('fuzzy','fuzy'),
 dbo.edit_distance('Fuzzy String Match','fuzy string match'),
 dbo.edit_distance('levenshtein distance sql','levenshtein sql server'),
 dbo.edit_distance('distance','server')

アルゴリズムは、1つのステップで別の文字を置き換えることにより、1つの文字列を別の文字列に変更するためにstpeカウントを返すだけです。


残念ながら、これは文字列が空白の場合をカバーしていません
Codeman 2015

2

レーベンシュタインアルゴリズムのコード例も探していたので、ここで見つけてうれしかったです。もちろん、アルゴリズムがどのように機能しているかを理解したかったので、Veveによって投稿された上記の例の1つを少し遊んでいました。コードをよりよく理解するために、マトリックスを使用してEXCELを作成しました。

FUZYと比較したFUZZYの距離

画像は1000語以上を言います。

このEXCELを使用すると、パフォーマンスをさらに最適化できる可能性があることがわかりました。右上の赤い領域のすべての値を計算する必要はありません。各赤いセルの値は、左側のセルの値に1を加えた値になります。これは、2番目の文字列が最初の文字列よりもその領域で常に長くなり、各文字の距離が1ずつ増えるためです。

これは、ステートメントIF @j <= @iを使用し、このステートメントの前に@iの値を増やすことで反映できます。

CREATE FUNCTION [dbo].[f_LevenshteinDistance](@s1 nvarchar(3999), @s2 nvarchar(3999))
    RETURNS int
    AS
    BEGIN
       DECLARE @s1_len  int;
       DECLARE @s2_len  int;
       DECLARE @i       int;
       DECLARE @j       int;
       DECLARE @s1_char nchar;
       DECLARE @c       int;
       DECLARE @c_temp  int;
       DECLARE @cv0     varbinary(8000);
       DECLARE @cv1     varbinary(8000);

       SELECT
          @s1_len = LEN(@s1),
          @s2_len = LEN(@s2),
          @cv1    = 0x0000  ,
          @j      = 1       , 
          @i      = 1       , 
          @c      = 0

       WHILE @j <= @s2_len
          SELECT @cv1 = @cv1 + CAST(@j AS binary(2)), @j = @j + 1;

          WHILE @i <= @s1_len
             BEGIN
                SELECT
                   @s1_char = SUBSTRING(@s1, @i, 1),
                   @c       = @i                   ,
                   @cv0     = CAST(@i AS binary(2)),
                   @j       = 1;

                SET @i = @i + 1;

                WHILE @j <= @s2_len
                   BEGIN
                      SET @c = @c + 1;

                      IF @j <= @i 
                         BEGIN
                            SET @c_temp = CAST(SUBSTRING(@cv1, @j + @j - 1, 2) AS int) + CASE WHEN @s1_char = SUBSTRING(@s2, @j, 1) THEN 0 ELSE 1 END;
                            IF @c > @c_temp SET @c = @c_temp
                            SET @c_temp = CAST(SUBSTRING(@cv1, @j + @j + 1, 2) AS int) + 1;
                            IF @c > @c_temp SET @c = @c_temp;
                         END;
                      SELECT @cv0 = @cv0 + CAST(@c AS binary(2)), @j = @j + 1;
                   END;
                SET @cv1 = @cv0;
          END;
       RETURN @c;
    END;

書かれているように、これは常に正しい結果をもたらすとは限りません。例えば、入力は、('jane', 'jeanne')距離は、この追加コードをスワップすることを添加すべき修正する2なければならない場合、3の距離が返される@s1@s2場合は、@s1より短い長さを有しています@s2
手斧-SOverflowで2017年

2

TSQLで、2つの項目を比較するための最良かつ最速の方法は、インデックス付き列のテーブルを結合するSELECTステートメントです。したがって、RDBMSエンジンの利点を活用したい場合は、これが編集距離の実装を提案する方法です。TSQLループも機能しますが、レーベンシュタイン距離の計算は、大量の比較のためにTSQLよりも他の言語で高速になります。

その目的のためだけに設計された一時テーブルに対して一連の結合を使用して、いくつかのシステムで編集距離を実装しました。いくつかの重い前処理ステップ(一時テーブルの準備)が必要ですが、多数の比較で非常にうまく機能します。

一言で言えば、前処理は、一時テーブルの作成、入力、およびインデックス付けで構成されます。最初のものには、参照ID、1文字の列、およびcharindex列が含まれています。このテーブルには、(SELECT SUBSTRINGを使用して)すべての単語を文字に分割する一連の挿入クエリを実行して、ソースリスト内の単語に文字が含まれる数の行を作成することでデータが入力されます(行数は多いですが、SQLサーバーは数十億を処理できます)行の)。次に、2文字の列を持つ2番目のテーブル、3文字の列を持つ別のテーブルなどを作成します。最終結果は、各単語の参照IDと部分文字列、およびそれらの位置の参照を含む一連のテーブルです。一言で言えば。

これが完了すると、ゲーム全体でこれらのテーブルを複製し、一致数をカウントするGROUPBY選択クエリでそれらのテーブルを結合します。これにより、考えられるすべての単語のペアに対して一連のメジャーが作成され、単語のペアごとに1つのレーベンシュタイン距離に再集計されます。

技術的には、これはレーベンシュタイン距離(またはその変形)の他のほとんどの実装とは大きく異なるため、レーベンシュタイン距離がどのように機能し、なぜそのまま設計されたのかを深く理解する必要があります。その方法を使用すると、編集距離の多くのバリエーションを同時に計算するのに役立つ一連の基礎となるメトリックが得られ、興味深い機械学習の潜在的な改善が得られるため、代替案も調査してください。

このページの以前の回答ですでに述べたもう1つのポイントは、距離測定を必要としないペアを排除するために、可能な限り前処理を行うようにしてください。たとえば、編集距離は文字列の長さから取得できるため、共通の文字が1つもない2つの単語のペアは除外する必要があります。または、同じ単語の2つのコピー間の距離を測定しないでください。これは、本質的に0であるためです。または、測定を行う前に重複を削除します。単語のリストが長いテキストからのものである場合、同じ単語が複数回表示される可能性が高いため、距離を1回だけ測定すると、処理時間などを節約できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.