I've reworked patch with your suggestion. In this version I found a little slowdown in comparison with previous version:
SELECT * FROM words WHERE levenshtein_less_equal(a, 'extensize', 2) <= 2; 48,069 ms => 57,875 ms SELECT * FROM words2 WHERE levenshtein_less_equal(a, 'клубничный', 3) <= 2; 100,073 ms => 113,975 ms select * from phrases where levenshtein_less_equal('kkkknucklehead courtliest sapphires be coniferous emolument antarctic Laocoon''s deadens unseemly', a, 10) <= 10; 22,876 ms => 24,721 ms test=# select * from phrases2 where levenshtein_less_equal('таяй раскупорившийся передислоцируется юлианович праздничный лачужка присыхать опппливший ффехтовальный уууудобряющий', a, 10) <= 10; 55,405 ms => 57,760 ms I think it is caused by multiplication operation for each bound movement. Probably, this slowdown is ignorable or there is some way to achieve the same performance. ---- With best regards, Alexander Korotkov.
diff --git a/contrib/fuzzystrmatch/fuzzystrmatch.c b/contrib/fuzzystrmatch/fuzzystrmatch.c index fe5fa88..7739431 100644 --- a/contrib/fuzzystrmatch/fuzzystrmatch.c +++ b/contrib/fuzzystrmatch/fuzzystrmatch.c @@ -5,7 +5,7 @@ * * Joe Conway <m...@joeconway.com> * - * $PostgreSQL$ + * contrib/fuzzystrmatch/fuzzystrmatch.c * Copyright (c) 2001-2010, PostgreSQL Global Development Group * ALL RIGHTS RESERVED; * @@ -61,6 +61,8 @@ PG_MODULE_MAGIC; */ extern Datum levenshtein_with_costs(PG_FUNCTION_ARGS); extern Datum levenshtein(PG_FUNCTION_ARGS); +extern Datum levenshtein_less_equal_with_costs(PG_FUNCTION_ARGS); +extern Datum levenshtein_less_equal(PG_FUNCTION_ARGS); extern Datum metaphone(PG_FUNCTION_ARGS); extern Datum soundex(PG_FUNCTION_ARGS); extern Datum difference(PG_FUNCTION_ARGS); @@ -92,7 +94,7 @@ soundex_code(char letter) #define MAX_LEVENSHTEIN_STRLEN 255 static int levenshtein_internal(text *s, text *t, - int ins_c, int del_c, int sub_c); + int ins_c, int del_c, int sub_c, int max_d); /* @@ -202,10 +204,14 @@ rest_of_char_same(const char *s1, const char *s2, int len) * between supplied strings. Generally * (1, 1, 1) penalty costs suffices common * cases, but your mileage may vary. + * Returns accurate value if max_d < 0 or + * actual distance is less or equal than + * max_d, otherwise returns value greater + * than max_d. */ static int levenshtein_internal(text *s, text *t, - int ins_c, int del_c, int sub_c) + int ins_c, int del_c, int sub_c, int max_d) { int m, n, @@ -219,6 +225,9 @@ levenshtein_internal(text *s, text *t, const char *s_data; const char *t_data; const char *y; + const char *prev_x = NULL; + int min_i = 0, max_i = 0, d; + int delta = 0, min_d = 0, theor_max_d; /* Extract a pointer to the actual character data. */ s_data = VARDATA_ANY(s); @@ -240,6 +249,20 @@ levenshtein_internal(text *s, text *t, return m * del_c; /* + * There is theoretical maximum distance based of string lengths. It + * represents the case, when no characters are matching. If max_d + * reaches this value than we can use accurate calculation of distance + * which is faster in this case. + */ + if (max_d >= 0) + { + theor_max_d = Min(m*del_c + n*ins_c, (m > n) ? + (n * sub_c + (m - n) * del_c):(m * sub_c + (n - m) * ins_c)) - 1; + if (max_d >= theor_max_d) + max_d = -1; + } + + /* * For security concerns, restrict excessive CPU+RAM usage. (This * implementation uses O(m) memory and has O(mn) complexity.) */ @@ -251,6 +274,23 @@ levenshtein_internal(text *s, text *t, MAX_LEVENSHTEIN_STRLEN))); /* + * We can find the minimal distance by the difference of string lengths. + */ + if (max_d >= 0) + { + delta = m - n; + if (delta > 0) + min_d = delta * del_c; + else if (delta < 0) + min_d = - delta * ins_c; + else + min_d = 0; + + if (min_d > max_d) + return max_d + 1; + } + + /* * In order to avoid calling pg_mblen() repeatedly on each character in s, * we cache all the lengths before starting the main loop -- but if all the * characters in both strings are single byte, then we skip this and use @@ -277,92 +317,209 @@ levenshtein_internal(text *s, text *t, ++n; /* - * Instead of building an (m+1)x(n+1) array, we'll use two different - * arrays of size m+1 for storing accumulated values. At each step one - * represents the "previous" row and one is the "current" row of the - * notional large array. + * One way to compute Levenshtein distance is to incrementally construct + * an (m+1)x(n+1) matrix where cell (i, j) represents the minimum number + * of operations required to transform the first i characters of s into + * the first j characters of t. The last column of the final row is the + * answer. + * + * We use that algorithm here with some modification. In lieu of holding + * the entire array in memory at once, we'll just use two arrays of size + * m+1 for storing accumulated values. At each step one array represents + * the "previous" row and one is the "current" row of the notional large + * array. */ prev = (int *) palloc(2 * m * sizeof(int)); curr = prev + m; - /* Initialize the "previous" row to 0..cols */ + /* + * To transform the first i characters of s into the first 0 characters + * of t, we must perform i deletions. + */ for (i = 0; i < m; i++) prev[i] = i * del_c; + /* + * If we have limitation of max_d, than we'll maintain [min_i; max_i] + * interval, which bound cells, where sum of cell value and smallest + * possible residual cost is less or equal to max_d (we don't include + * 0 index into this interval). Residual cost represent cost of insertions + * or deletions for moving to diagonal, which containing bottom right cell. + * The sum value saves important property of original matrix, that this + * sum for cell always greater or equal than such sums for cells, which are + * used for it's calculation. That's why this sum can be used for bound + * interval. + */ + if (max_d >= 0) + { + min_i = 1; + max_i = 1; + while (max_i < m && prev[max_i] + + ((delta - max_i > 0) ? (delta - max_i) * del_c : + (-delta + max_i) * ins_c) <= max_d) + { + max_i++; + } + max_i--; + prev_x = s_data; + } /* Loop through rows of the notional array */ for (y = t_data, j = 1; j < n; j++) { int *temp; - const char *x = s_data; int y_char_len = n != t_bytes + 1 ? pg_mblen(y) : 1; /* - * First cell must increment sequentially, as we're on the j'th row of - * the (m+1)x(n+1) array. + * To transform the first 0 characters of s into the first j + * characters of t, we must perform j insertions. */ curr[0] = j * ins_c; /* - * This inner loop is critical to performance, so we include a + * This inner loop is critical to performance, that's why we + * handle case without max_d limitation separatetly. Also we include a * fast-path to handle the (fairly common) case where no multibyte * characters are in the mix. The fast-path is entitled to assume * that if s_char_len is not initialized then BOTH strings contain * only single-byte characters. */ - if (s_char_len != NULL) + if (max_d < 0) { - for (i = 1; i < m; i++) - { - int ins; - int del; - int sub; - int x_char_len = s_char_len[i - 1]; + const char *x = s_data; + /* + * First cell must increment sequentially, as we're on the j'th row of + * the (m+1)x(n+1) array. + */ + curr[0] = j * ins_c; - /* - * Calculate costs for insertion, deletion, and substitution. - * - * When calculating cost for substitution, we compare the last - * character of each possibly-multibyte character first, - * because that's enough to rule out most mis-matches. If we - * get past that test, then we compare the lengths and the - * remaining bytes. - */ - ins = prev[i] + ins_c; - del = curr[i - 1] + del_c; - if (x[x_char_len-1] == y[y_char_len-1] - && x_char_len == y_char_len && - (x_char_len == 1 || rest_of_char_same(x, y, x_char_len))) - sub = prev[i - 1]; - else - sub = prev[i - 1] + sub_c; + if (s_char_len != NULL) + { + for (i = 1; i < m; i++) + { + int ins; + int del; + int sub; + int x_char_len = s_char_len[i - 1]; + + /* + * Calculate costs for insertion, deletion, and substitution. + * + * When calculating cost for substitution, we compare the last + * character of each possibly-multibyte character first, + * because that's enough to rule out most mis-matches. If we + * get past that test, then we compare the lengths and the + * remaining bytes. + */ + ins = prev[i] + ins_c; + del = curr[i - 1] + del_c; + if (x[x_char_len-1] == y[y_char_len-1] + && x_char_len == y_char_len && + (x_char_len == 1 || rest_of_char_same(x, y, x_char_len))) + sub = prev[i - 1]; + else + sub = prev[i - 1] + sub_c; - /* Take the one with minimum cost. */ - curr[i] = Min(ins, del); - curr[i] = Min(curr[i], sub); + /* Take the one with minimum cost. */ + curr[i] = Min(ins, del); + curr[i] = Min(curr[i], sub); - /* Point to next character. */ - x += x_char_len; + /* Point to next character. */ + x += x_char_len; + } } - } - else - { - for (i = 1; i < m; i++) + else { - int ins; - int del; - int sub; + for (i = 1; i < m; i++) + { + int ins; + int del; + int sub; - /* Calculate costs for insertion, deletion, and substitution. */ - ins = prev[i] + ins_c; - del = curr[i - 1] + del_c; - sub = prev[i - 1] + ((*x == *y) ? 0 : sub_c); + /* Calculate costs for insertion, deletion, and substitution. */ + ins = prev[i] + ins_c; + del = curr[i - 1] + del_c; + sub = prev[i - 1] + ((*x == *y) ? 0 : sub_c); - /* Take the one with minimum cost. */ - curr[i] = Min(ins, del); - curr[i] = Min(curr[i], sub); + /* Take the one with minimum cost. */ + curr[i] = Min(ins, del); + curr[i] = Min(curr[i], sub); - /* Point to next character. */ - x++; + /* Point to next character. */ + x++; + } + } + }else{ + const char *x = prev_x; + + /* + * Fill the first cell of the row taking care about it's position + * relatively last cell's diagnal. + */ + curr[0] = j * ins_c; + + if (s_char_len != NULL) + { + for (i = min_i; i < m && i < max_i + 2; i++) + { + int x_char_len = s_char_len[i - 1]; + /* + * Take minimum costs for insertion, deletion, and + * substitution when corresponding previous cell doesn't + * exceed bounds where value was less or equal than max_d. + */ + d = max_d + 1; + if (i <= max_i) + d = Min(prev[i] + ins_c, d); + if (i == 1 || i > min_i) + { + d = Min(curr[i - 1] + del_c, d); + d = Min(prev[i - 1] + ((x[x_char_len-1] == y[y_char_len-1] + && x_char_len == y_char_len + && (x_char_len == 1 || rest_of_char_same(x, y, x_char_len))) + ? 0 : sub_c), d); + } + curr[i] = d; + x += x_char_len; + } + } + else + { + for (i = min_i; i < m && i < max_i + 2; i++) + { + /* + * Take minimum costs for insertion, deletion, and + * substitution when corresponding previous cell doesn't + * exceed bounds where value was less or equal than max_d. + */ + d = max_d + 1; + if (i <= max_i) + d = Min(prev[i] + ins_c, d); + if (i == 1 || i > min_i) + { + d = Min(curr[i - 1] + del_c, d); + d = Min(prev[i - 1] + (*x == *y ? 0 : sub_c), d); + } + curr[i] = d; + x++; + } + } + if (max_i < m) + max_i++; + while (min_i <= max_i && curr[min_i] + ((delta - min_i + j > 0) ? + (delta - min_i + j) * del_c : (-delta + min_i - j) * ins_c) > max_d) + { + if (s_char_len != NULL) + prev_x += s_char_len[min_i - 1]; + else + prev_x++; + min_i++; + } + if (min_i > max_i) + break; + while (curr[max_i] + ((delta - max_i + j > 0) ? (delta - max_i + j) * del_c : + (-delta + max_i - j) * ins_c) > max_d) + { + max_i--; } } @@ -376,6 +533,13 @@ levenshtein_internal(text *s, text *t, } /* + * If we have limitation of max_d and haven't less or equal, than max_d + * value in the last row than we should return max_d + 1. + */ + if (max_d >= 0 && min_i > max_i) + return max_d + 1; + + /* * Because the final value was swapped from the previous row to the * current row, that's where we'll find it. */ @@ -393,7 +557,7 @@ levenshtein_with_costs(PG_FUNCTION_ARGS) int del_c = PG_GETARG_INT32(3); int sub_c = PG_GETARG_INT32(4); - PG_RETURN_INT32(levenshtein_internal(src, dst, ins_c, del_c, sub_c)); + PG_RETURN_INT32(levenshtein_internal(src, dst, ins_c, del_c, sub_c, -1)); } @@ -404,7 +568,34 @@ levenshtein(PG_FUNCTION_ARGS) text *src = PG_GETARG_TEXT_PP(0); text *dst = PG_GETARG_TEXT_PP(1); - PG_RETURN_INT32(levenshtein_internal(src, dst, 1, 1, 1)); + PG_RETURN_INT32(levenshtein_internal(src, dst, 1, 1, 1, -1)); +} + + +PG_FUNCTION_INFO_V1(levenshtein_less_equal_with_costs); +Datum +levenshtein_less_equal_with_costs(PG_FUNCTION_ARGS) +{ + text *src = PG_GETARG_TEXT_PP(0); + text *dst = PG_GETARG_TEXT_PP(1); + int ins_c = PG_GETARG_INT32(2); + int del_c = PG_GETARG_INT32(3); + int sub_c = PG_GETARG_INT32(4); + int max_d = PG_GETARG_INT32(5); + + PG_RETURN_INT32(levenshtein_internal(src, dst, ins_c, del_c, sub_c, max_d)); +} + + +PG_FUNCTION_INFO_V1(levenshtein_less_equal); +Datum +levenshtein_less_equal(PG_FUNCTION_ARGS) +{ + text *src = PG_GETARG_TEXT_PP(0); + text *dst = PG_GETARG_TEXT_PP(1); + int max_d = PG_GETARG_INT32(2); + + PG_RETURN_INT32(levenshtein_internal(src, dst, 1, 1, 1, max_d)); } diff --git a/contrib/fuzzystrmatch/fuzzystrmatch.sql.in b/contrib/fuzzystrmatch/fuzzystrmatch.sql.in index 05a347d..0e75491 100644 --- a/contrib/fuzzystrmatch/fuzzystrmatch.sql.in +++ b/contrib/fuzzystrmatch/fuzzystrmatch.sql.in @@ -11,6 +11,14 @@ CREATE OR REPLACE FUNCTION levenshtein (text,text,int,int,int) RETURNS int AS 'MODULE_PATHNAME','levenshtein_with_costs' LANGUAGE C IMMUTABLE STRICT; +CREATE OR REPLACE FUNCTION levenshtein_less_equal (text,text,int) RETURNS int +AS 'MODULE_PATHNAME','levenshtein_less_equal' +LANGUAGE C IMMUTABLE STRICT; + +CREATE OR REPLACE FUNCTION levenshtein_less_equal (text,text,int,int,int,int) RETURNS int +AS 'MODULE_PATHNAME','levenshtein_less_equal_with_costs' +LANGUAGE C IMMUTABLE STRICT; + CREATE OR REPLACE FUNCTION metaphone (text,int) RETURNS text AS 'MODULE_PATHNAME','metaphone' LANGUAGE C IMMUTABLE STRICT; diff --git a/doc/src/sgml/fuzzystrmatch.sgml b/doc/src/sgml/fuzzystrmatch.sgml index 69777e4..83a39d4 100644 --- a/doc/src/sgml/fuzzystrmatch.sgml +++ b/doc/src/sgml/fuzzystrmatch.sgml @@ -84,6 +84,8 @@ SELECT * FROM s WHERE difference(s.nm, 'john') > 2; <synopsis> levenshtein(text source, text target, int ins_cost, int del_cost, int sub_cost) returns int levenshtein(text source, text target) returns int +levenshtein_less_equal(text source, text target, int ins_cost, int del_cost, int sub_cost, int max_d) returns int +levenshtein_less_equal(text source, text target, int max_d) returns int </synopsis> <para> @@ -92,6 +94,11 @@ levenshtein(text source, text target) returns int specify how much to charge for a character insertion, deletion, or substitution, respectively. You can omit the cost parameters, as in the second version of the function; in that case they all default to 1. + <literal>levenshtein_less_equal</literal> is accelerated version of + levenshtein functon for low values of distance. If actual distance + is less or equal then max_d, then <literal>levenshtein_less_equal</literal> + returns accurate value of it. Otherwise this function returns value + which is greater than max_d. </para> <para> @@ -110,6 +117,18 @@ test=# SELECT levenshtein('GUMBO', 'GAMBOL', 2,1,1); ------------- 3 (1 row) + +test=# SELECT levenshtein_less_equal('extensive', 'exhaustive',2); + levenshtein_less_equal +------------------------ + 3 +(1 row) + +test=# SELECT levenshtein_less_equal('extensive', 'exhaustive',4); + levenshtein_less_equal +------------------------ + 4 +(1 row) </screen> </sect2>
-- Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org) To make changes to your subscription: http://www.postgresql.org/mailpref/pgsql-hackers