Levenshtein 거리를 계산하는 T-SQL의 알고리즘에 관심이 있습니다.
Levenshtein 거리를 계산하는 T-SQL의 알고리즘에 관심이 있습니다.
답변:
내가 알고있는 다른 버전에 비해 속도를 향상시키는 몇 가지 최적화를 통해 TSQL에서 표준 Levenshtein 편집 거리 기능을 구현했습니다. 두 문자열의 시작 부분에 공통 문자 (공유 접두사), 끝 부분에 공통 문자 (공유 접미사), 문자열이 크고 최대 편집 거리가 제공되는 경우 속도 향상이 현저합니다. 예를 들어 입력이 두 개의 매우 유사한 4000 문자열이고 최대 편집 거리가 2로 지정된 경우 이는 거의 3 배 더 빠릅니다.edit_distance_within
허용 된 답변에서 함수를 사용하여 0.073 초 (73 밀리 초) 대 55 초 내에 답변을 반환합니다. 또한 두 개의 입력 문자열 중 더 큰 공간에 일정한 공간을 더한 것과 동일한 공간을 사용하여 메모리 효율적입니다. 열을 나타내는 단일 nvarchar "배열"을 사용하고 그 안에서 모든 계산과 일부 도우미 int 변수를 제자리에서 수행합니다.
최적화 :
다음은 코드입니다 (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
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
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
도움이 되었기를 바랍니다.
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 수를 반환합니다.
Levenshtein 알고리즘에 대한 코드 예제도 찾고 있었고 여기에서 찾을 수있어서 기뻤습니다. 물론 알고리즘이 어떻게 작동하는지 이해하고 싶었고 Veve 가 게시 한 위의 예제 중 하나를 약간 가지고 놀았습니다 . 코드를 더 잘 이해하기 위해 Matrix로 EXCEL을 만들었습니다.
이미지는 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
.
TSQL에서 두 항목을 비교하는 가장 좋고 빠른 방법은 인덱싱 된 열에서 테이블을 조인하는 SELECT 문입니다. 따라서 RDBMS 엔진의 이점을 활용하려면 편집 거리를 구현하는 것이 좋습니다. TSQL 루프도 작동하지만 Levenstein 거리 계산은 대량 비교를 위해 TSQL보다 다른 언어에서 더 빠릅니다.
해당 목적으로 만 설계된 임시 테이블에 대해 일련의 조인을 사용하여 여러 시스템에서 편집 거리를 구현했습니다. 임시 테이블 준비와 같은 무거운 전처리 단계가 필요하지만 많은 비교에서 매우 잘 작동합니다.
간단히 말해서, 전처리는 임시 테이블 생성, 채우기 및 인덱싱으로 구성됩니다. 첫 번째는 참조 ID, 한 글자 열 및 charindex 열을 포함합니다. 이 테이블은 모든 단어를 문자로 분할하는 일련의 삽입 쿼리를 실행하여 (SELECT SUBSTRING 사용) 소스 목록의 단어에 문자가있는 수만큼 행을 생성합니다 (나는 많은 행이지만 SQL 서버는 수십억 개의 행을 처리 할 수 있음을 알고 있습니다). 행 수). 그런 다음 두 글자 열이있는 두 번째 테이블, 세 글자 열이있는 다른 테이블 등을 만듭니다. 최종 결과는 각 단어의 참조 ID와 하위 문자열, 위치 참조를 포함하는 일련의 테이블입니다. 단어에서.
이 작업이 완료되면 전체 게임은 이러한 테이블을 복제하고 일치 수를 계산하는 GROUP BY 선택 쿼리에서 중복 테이블과 결합하는 것입니다. 이렇게하면 가능한 모든 단어 쌍에 대한 일련의 측정 값이 생성되고 단어 쌍당 단일 Levenstein 거리로 다시 집계됩니다.
기술적으로 이것은 Levenstein 거리 (또는 그 변형)의 다른 대부분의 구현과 매우 다르므로 Levenstein 거리가 작동하는 방식과 그것이 그대로 설계된 이유를 깊이 이해해야합니다. 그 방법을 사용하면 동시에 편집 거리의 많은 변형을 계산하는 데 도움이되는 일련의 기본 메트릭이 생성되어 흥미로운 기계 학습 잠재력 개선을 제공 할 수 있으므로 대안도 조사하십시오.
이 페이지의 이전 답변에서 이미 언급 한 또 다른 요점은 거리 측정이 필요하지 않은 쌍을 제거하기 위해 가능한 한 많이 사전 처리하십시오. 예를 들어 공통 문자가없는 두 단어 쌍은 제외되어야합니다. 편집 거리는 문자열의 길이에서 얻을 수 있기 때문입니다. 또는 본질적으로 0이기 때문에 동일한 단어의 두 사본 사이의 거리를 측정하지 마십시오. 또는 측정을 수행하기 전에 중복을 제거하십시오. 단어 목록이 긴 텍스트에서 나온 경우 동일한 단어가 두 번 이상 나타날 가능성이 있으므로 거리를 한 번만 측정하면 처리 시간 등을 절약 할 수 있습니다.