T-SQL의 Levenshtein 거리


답변:


100

내가 알고있는 다른 버전에 비해 속도를 향상시키는 몇 가지 최적화를 통해 TSQL에서 표준 Levenshtein 편집 거리 기능을 구현했습니다. 두 문자열의 시작 부분에 공통 문자 (공유 접두사), 끝 부분에 공통 문자 (공유 접미사), 문자열이 크고 최대 편집 거리가 제공되는 경우 속도 향상이 현저합니다. 예를 들어 입력이 두 개의 매우 유사한 4000 문자열이고 최대 편집 거리가 2로 지정된 경우 이는 거의 3 배 더 빠릅니다.edit_distance_within허용 된 답변에서 함수를 사용하여 0.073 초 (73 밀리 초) 대 55 초 내에 답변을 반환합니다. 또한 두 개의 입력 문자열 중 더 큰 공간에 일정한 공간을 더한 것과 동일한 공간을 사용하여 메모리 효율적입니다. 열을 나타내는 단일 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의 데이터 정렬은 대 / 소문자를 구분하지 않는 비교를 수행하는 데이터 정렬입니다. 이 함수를 항상 대소 문자를 구분하도록 수정하는 한 가지 방법은 문자열이 비교되는 두 위치에 특정 데이터 정렬을 추가하는 것입니다. 그러나, 특히 데이터베이스가 기본이 아닌 데이터 정렬을 사용하는 경우 부작용에 대해 철저하게 테스트하지 않았습니다. 다음은 대소 문자 구분 비교를 강제하기 위해 두 줄을 변경하는 방법입니다.

    -- 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

1
무차별 대입 (모든 주소 비교) 외에는 할 수 없습니다. Levenshtein은 인덱스를 쉽게 활용할 수있는 것이 아닙니다. 예를 들어 주소에 대한 우편 번호 또는 이름에 대한 음성 코드와 같이 색인화 할 수있는 항목을 통해 후보를 더 작은 하위 집합으로 좁힐 수있는 경우 여기에있는 답변과 같은 직선 Levenshtein을 하위 집합. 큰 전체 세트에 적용하려면 Levenshtein Automata와 같은 것으로 이동해야하지만 SQL로 구현하는 것은 여기에서 대답하는 SO 질문의 범위를 벗어납니다.
도끼 - SOverflow 함께 할

이론적으로 @MonsterMMORPG는 주어진 Levenshtein-distance에 대해 역순으로 모든 가능한 순열을 계산할 수 있습니다. 또는 주소의 단어가 유용 할만큼 짧은 목록을 구성하는지 확인할 수 있습니다 (아마도 거의 나타나지 않는 단어는 무시 함).
TheConstructor

@MonsterMMORPG-이것은 늦었지만 더 나은 답변을 추가 할 것이라고 생각했습니다. 허용 할 최소 편집 수를 알고 있다면 github의 symspell 프로젝트에서 수행 한 것과 같은 대칭 삭제 방법을 사용할 수 있습니다. 삭제 만 이루어진 순열의 작은 하위 집합을 저장 한 다음 검색 문자열의 작은 삭제 순열 집합에서 임의의 항목을 검색 할 수 있습니다. 반환 된 집합 (최대 편집 거리 1 또는 2 만 허용하는 경우 작음)에서 전체 levenshtein calc를 수행합니다. 그러나 그것은 모든 문자열에서하는 것보다 훨씬 적어야합니다.
도끼 - SOverflow으로 수행

1
@DaveCousineau-함수 주석에서 언급했듯이 문자열 비교는 유효한 SQL Server 데이터 정렬에 대 / 소문자 구분을 사용합니다. 기본적으로 이는 일반적으로 대소 문자를 구분하지 않음을 의미합니다. 방금 추가 한 내 게시물에 대한 편집 내용을 참조하십시오. 다른 답변의 Fribble 구현은 데이터 정렬과 유사하게 작동합니다.
도끼 - SOverflow 함께 할

58

Arnold Fribble은 sqlteam.com/forums에서 두 가지 제안을 했습니다.

이것은 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를 제거하고 입력에있는 두 문자열의 길이를 알고 있습니다.
Lieven Keersmaekers

2
@Lieven : 내 구현이 아니라 저자는 Arnold Fribble입니다. @d 매개 변수는 문자열이 너무 다양하고 함수가 -1을 리턴하는 도달 후 문자열간에 허용되는 최대 차이입니다. T-SQL의 알고리즘이 너무 느리게 작동하기 때문에 추가되었습니다.
Alexander Prokofyev

en.wikipedia.org/wiki/Levenshtein_distance 에서 알고리즘 의사 코드를 확인해야합니다 . 많이 개선되지는 않았습니다.
Norman H

13

IIRC, SQL Server 2005 이상에서는 모든 .NET 언어로 저장 프로 시저를 작성할 수 있습니다 . SQL Server 2005에서 CLR 통합 사용 . 그것으로 Levenstein 거리 를 계산하는 절차를 작성하는 것은 어렵지 않습니다 .

간단한 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!";
    }
}

그런 다음 SQL Server에서 다음을 실행하십시오.

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

Levenshtein Distance Algorithm을 사용하여 문자열을 비교할 수 있습니다.

여기 http://www.kodyaz.com/articles/fuzzy-string-matching-using-levenshtein-distance-sql-server.aspx 에서 T-SQL 예제를 찾을 수 있습니다 .

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

(Joseph Gama가 개발 한 기능)

사용법 :

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')

알고리즘은 한 단계에서 다른 문자를 대체하여 한 문자열을 다른 문자열로 변경하는 stpe 수를 반환합니다.


안타깝게도 문자열이 비어있는 경우는 다루지 않습니다.
Codeman

2

Levenshtein 알고리즘에 대한 코드 예제도 찾고 있었고 여기에서 찾을 수있어서 기뻤습니다. 물론 알고리즘이 어떻게 작동하는지 이해하고 싶었고 Veve 가 게시 한 위의 예제 중 하나를 약간 가지고 놀았습니다 . 코드를 더 잘 이해하기 위해 Matrix로 EXCEL을 만들었습니다.

FUZY 대비 FUZZY 거리

이미지는 1000 개 이상의 단어를 말합니다.

이 EXCEL을 통해 추가적인 성능 최적화 가능성이 있음을 발견했습니다. 오른쪽 상단 빨간색 영역의 모든 값은 계산할 필요가 없습니다. 각 빨간색 셀의 값은 왼쪽 셀에 1을 더한 값이됩니다. 이는 두 번째 문자열이 항상 해당 영역에서 첫 번째 문자열보다 길기 때문에 각 문자에 대해 거리가 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으로 수행

2

TSQL에서 두 항목을 비교하는 가장 좋고 빠른 방법은 인덱싱 된 열에서 테이블을 조인하는 SELECT 문입니다. 따라서 RDBMS 엔진의 이점을 활용하려면 편집 거리를 구현하는 것이 좋습니다. TSQL 루프도 작동하지만 Levenstein 거리 계산은 대량 비교를 위해 TSQL보다 다른 언어에서 더 빠릅니다.

해당 목적으로 만 설계된 임시 테이블에 대해 일련의 조인을 사용하여 여러 시스템에서 편집 거리를 구현했습니다. 임시 테이블 준비와 같은 무거운 전처리 단계가 필요하지만 많은 비교에서 매우 잘 작동합니다.

간단히 말해서, 전처리는 임시 테이블 생성, 채우기 및 인덱싱으로 구성됩니다. 첫 번째는 참조 ID, 한 글자 열 및 charindex 열을 포함합니다. 이 테이블은 모든 단어를 문자로 분할하는 일련의 삽입 쿼리를 실행하여 (SELECT SUBSTRING 사용) 소스 목록의 단어에 문자가있는 수만큼 행을 생성합니다 (나는 많은 행이지만 SQL 서버는 수십억 개의 행을 처리 할 수 ​​있음을 알고 있습니다). 행 수). 그런 다음 두 글자 열이있는 두 번째 테이블, 세 글자 열이있는 다른 테이블 등을 만듭니다. 최종 결과는 각 단어의 참조 ID와 하위 문자열, 위치 참조를 포함하는 일련의 테이블입니다. 단어에서.

이 작업이 완료되면 전체 게임은 이러한 테이블을 복제하고 일치 수를 계산하는 GROUP BY 선택 쿼리에서 중복 테이블과 결합하는 것입니다. 이렇게하면 가능한 모든 단어 쌍에 대한 일련의 측정 값이 생성되고 단어 쌍당 단일 Levenstein 거리로 다시 집계됩니다.

기술적으로 이것은 Levenstein 거리 (또는 그 변형)의 다른 대부분의 구현과 매우 다르므로 Levenstein 거리가 작동하는 방식과 그것이 그대로 설계된 이유를 깊이 이해해야합니다. 그 방법을 사용하면 동시에 편집 거리의 많은 변형을 계산하는 데 도움이되는 일련의 기본 메트릭이 생성되어 흥미로운 기계 학습 잠재력 개선을 제공 할 수 있으므로 대안도 조사하십시오.

이 페이지의 이전 답변에서 이미 언급 한 또 다른 요점은 거리 측정이 필요하지 않은 쌍을 제거하기 위해 가능한 한 많이 사전 처리하십시오. 예를 들어 공통 문자가없는 두 단어 쌍은 제외되어야합니다. 편집 거리는 문자열의 길이에서 얻을 수 있기 때문입니다. 또는 본질적으로 0이기 때문에 동일한 단어의 두 사본 사이의 거리를 측정하지 마십시오. 또는 측정을 수행하기 전에 중복을 제거하십시오. 단어 목록이 긴 텍스트에서 나온 경우 동일한 단어가 두 번 이상 나타날 가능성이 있으므로 거리를 한 번만 측정하면 처리 시간 등을 절약 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.