Tôi quan tâm đến thuật toán trong T-SQL tính toán khoảng cách Levenshtein.
Tôi quan tâm đến thuật toán trong T-SQL tính toán khoảng cách Levenshtein.
Câu trả lời:
Tôi đã triển khai chức năng chỉnh sửa khoảng cách Levenshtein tiêu chuẩn trong TSQL với một số tối ưu hóa giúp cải thiện tốc độ so với các phiên bản khác mà tôi biết. Trong trường hợp hai chuỗi có ký tự chung ở đầu (tiền tố dùng chung), ký tự chung ở cuối (hậu tố dùng chung) và khi chuỗi lớn và cung cấp khoảng cách chỉnh sửa tối đa, thì tốc độ cải thiện đáng kể. Ví dụ: khi đầu vào là hai chuỗi ký tự 4000 rất giống nhau và khoảng cách chỉnh sửa tối đa là 2 được chỉ định, thì điều này nhanh hơn gần ba bậc về độ lớn so vớiedit_distance_within
hoạt động trong câu trả lời được chấp nhận, trả về câu trả lời trong 0,073 giây (73 mili giây) so với 55 giây. Nó cũng hiệu quả về bộ nhớ, sử dụng không gian bằng lớn hơn của hai chuỗi đầu vào cộng với một số không gian không đổi. Nó sử dụng một "mảng" nvarchar đại diện cho một cột và thực hiện tất cả các tính toán tại chỗ trong đó, cộng với một số biến int trợ giúp.
Tối ưu hóa:
Đây là mã (được cập nhật 20/01/2014 để tăng tốc hơn một chút):
-- =============================================
-- 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
Như đã đề cập trong phần nhận xét của hàm này, độ phân biệt chữ hoa chữ thường của các phép so sánh ký tự sẽ tuân theo sự đối chiếu có hiệu lực. Theo mặc định, đối chiếu của SQL Server là đối chiếu sẽ dẫn đến so sánh không phân biệt chữ hoa chữ thường. Một cách để sửa đổi hàm này để luôn phân biệt chữ hoa chữ thường là thêm một đối chiếu cụ thể vào hai nơi mà các chuỗi được so sánh. Tuy nhiên, tôi đã không kiểm tra kỹ lưỡng điều này, đặc biệt là đối với các tác dụng phụ khi cơ sở dữ liệu đang sử dụng đối chiếu không mặc định. Đây là cách hai dòng sẽ được thay đổi để bắt buộc so sánh phân biệt chữ hoa chữ thường:
-- 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)
và
SELECT @distance =
CASE WHEN (@sChar = SUBSTRING(@t, @j, 1) COLLATE SQL_Latin1_General_Cp1_CS_AS) THEN @diag --match, no change
Arnold Fribble có hai đề xuất trên sqlteam.com/forums
Đây là đứa trẻ từ năm 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, với SQL Server 2005 trở lên, bạn có thể viết các thủ tục được lưu trữ bằng bất kỳ ngôn ngữ .NET nào: Sử dụng Tích hợp CLR trong SQL Server 2005 . Với điều đó, không khó để viết một thủ tục để tính khoảng cách Levenstein .
Xin chào, thế giới đơn giản! trích từ sự trợ giúp:
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!";
}
}
Sau đó, trong SQL Server của bạn chạy như sau:
CREATE ASSEMBLY helloworld from 'c:\helloworld.dll' WITH PERMISSION_SET = SAFE
CREATE PROCEDURE hello
@i nchar(25) OUTPUT
AS
EXTERNAL NAME helloworld.HelloWorldProc.HelloWorld
Và bây giờ bạn có thể chạy thử nó:
DECLARE @J nchar(25)
EXEC hello @J out
PRINT @J
Hi vọng điêu nay co ich.
Bạn có thể sử dụng Thuật toán khoảng cách Levenshtein để so sánh các chuỗi
Tại đây, bạn có thể tìm thấy một ví dụ T-SQL tại 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
(Hàm do Joseph Gama phát triển)
Sử dụng :
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')
Thuật toán chỉ đơn giản trả về số lượng stpe để thay đổi một chuỗi thành chuỗi khác bằng cách thay thế một ký tự khác ở một bước
Tôi cũng đang tìm kiếm một ví dụ mã cho thuật toán Levenshtein và rất vui khi tìm thấy nó ở đây. Tất nhiên, tôi muốn hiểu thuật toán hoạt động như thế nào và tôi đã tìm hiểu một chút về một trong những ví dụ ở trên, tôi đã thử một chút đã được đăng bởi Veve . Để hiểu rõ hơn về mã, tôi đã tạo EXCEL bằng Ma trận.
khoảng cách cho FUZZY so với FUZY
Hình ảnh nói hơn 1000 từ.
Với EXCEL này, tôi thấy rằng có tiềm năng tối ưu hóa hiệu suất bổ sung. Tất cả các giá trị trong vùng màu đỏ phía trên bên phải không cần phải tính toán. Giá trị của mỗi ô màu đỏ dẫn đến giá trị của ô bên trái cộng với 1. Điều này là do, chuỗi thứ hai sẽ luôn dài hơn trong vùng đó so với chuỗi đầu tiên, điều gì làm tăng khoảng cách bằng giá trị 1 cho mỗi ký tự.
Bạn có thể phản ánh điều đó bằng cách sử dụng câu lệnh IF @j <= @i và tăng giá trị của @i Trước câu lệnh này.
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')
sẽ trả về khoảng cách là 3, khi khoảng cách phải là 2. Để sửa lỗi bổ sung này, mã bổ sung nên được thêm vào các hoán đổi @s1
và @s2
nếu @s1
có độ dài ngắn hơn @s2
.
Trong TSQL, cách tốt nhất và nhanh nhất để so sánh hai mục là các câu lệnh SELECT nối các bảng trên các cột được lập chỉ mục. Do đó, đây là cách tôi đề xuất thực hiện khoảng cách chỉnh sửa nếu bạn muốn hưởng lợi từ những lợi thế của công cụ RDBMS. Vòng lặp TSQL cũng sẽ hoạt động, nhưng tính toán khoảng cách Levenstein sẽ nhanh hơn bằng các ngôn ngữ khác so với TSQL để so sánh khối lượng lớn.
Tôi đã triển khai khoảng cách chỉnh sửa trong một số hệ thống bằng cách sử dụng chuỗi Nối với các bảng tạm thời chỉ được thiết kế cho mục đích đó. Nó đòi hỏi một số bước xử lý trước nặng nề - chuẩn bị các bảng tạm thời - nhưng nó hoạt động rất tốt với số lượng lớn các phép so sánh.
Nói một cách ngắn gọn: quá trình tiền xử lý bao gồm việc tạo, điền và lập chỉ mục các bảng tạm thời. Cái đầu tiên chứa id tham chiếu, cột một chữ cái và cột charindex. Bảng này được điền bằng cách chạy một loạt các truy vấn chèn chia từng từ thành các chữ cái (sử dụng CHỌN SUBSTRING) để tạo nhiều hàng như từ trong danh sách nguồn có các chữ cái (Tôi biết, đó là rất nhiều hàng nhưng máy chủ SQL có thể xử lý hàng tỷ hàng). Sau đó, tạo bảng thứ hai với cột 2 chữ cái, một bảng khác có cột 3 chữ cái, v.v. Kết quả cuối cùng là một loạt các bảng chứa id tham chiếu và chuỗi con của mỗi từ, cũng như tham chiếu vị trí của chúng trong từ.
Khi điều này được thực hiện, toàn bộ trò chơi là sao chép các bảng này và nối chúng với bản sao của chúng trong một truy vấn chọn GROUP BY tính số trận đấu. Điều này tạo ra một loạt các thước đo cho mọi cặp từ có thể có, sau đó được tổng hợp lại thành một khoảng cách Levenstein cho mỗi cặp từ.
Về mặt kỹ thuật, điều này rất khác so với hầu hết các cách triển khai khác của khoảng cách Levenstein (hoặc các biến thể của nó), vì vậy bạn cần hiểu sâu sắc cách hoạt động của khoảng cách Levenstein và tại sao nó được thiết kế như vậy. Điều tra các lựa chọn thay thế cũng bởi vì với phương pháp đó, bạn sẽ có một loạt các chỉ số cơ bản có thể giúp tính toán nhiều biến thể của khoảng cách chỉnh sửa cùng một lúc, cung cấp cho bạn những cải tiến tiềm năng thú vị về học máy.
Một điểm khác đã được đề cập bởi các câu trả lời trước trong trang này: cố gắng xử lý trước càng nhiều càng tốt để loại bỏ các cặp không yêu cầu đo khoảng cách. Ví dụ, một cặp hai từ không có một chữ cái chung nào nên được loại trừ, vì khoảng cách chỉnh sửa có thể thu được từ độ dài của các chuỗi. Hoặc không đo khoảng cách giữa hai bản sao của cùng một từ, vì bản chất nó là 0. Hoặc loại bỏ các từ trùng lặp trước khi thực hiện phép đo, nếu danh sách các từ của bạn đến từ một văn bản dài thì có khả năng các từ giống nhau sẽ xuất hiện nhiều hơn một lần, vì vậy việc đo khoảng cách chỉ một lần sẽ tiết kiệm thời gian xử lý, v.v.