String Functions
This module contains functions for string operations and computations: measuring the similarity of two strings, finding the most similar candidates in a sequence of strings, splitting a string into character n-grams, encoding words phonetically, and formatting values.
Conventions
All functions and errors are in the http://basex.org/modules/string namespace, to which the string prefix is statically bound.
Similarity Measures
The similarity measures of this section all answer the same question: How similar are two strings? They all return a normalized xs:double in the range 0–1: 1 means identical, 0 means maximally different. They differ in the kind of difference they tolerate.
The following table lists what each measure is suited for…
| Function | Suited for | Typo | Order | Extra | Translit. | Other |
|---|---|---|---|---|---|---|
string:levenshtein |
Typos, single-character errors | 0.857 | 0 | 0.875 | 0.6 | 0.222 |
string:jaro-winkler |
Short strings with common prefix | 0.97 | 0.5 | 0.98 | 0.76 | 0.48 |
string:token-sort-ratio |
Reordered words | 0.857 | 1 | 0.875 | 0.6 | 0.222 |
string:token-set-ratio |
Additional or omitted words | 0.857 | 1 | 1 | 0.6 | 0.222 |
string:ngram-similarity |
Reordering and insertions | 0.909 | 0.818 | 0.963 | 0.25 | 0 |
string:partial-ratio |
Short string inside longer one | 1 | 0 | 0.857 | 0.6 | 0.429 |
…and how it rates some typical pairs of strings:
- Typo:
'flower','flowers' - Order:
'Ishida Yutei','Yutei Ishida' - Extra:
'Fortuny y Marsal','Fortuny Marsal' - Transliteration:
'night','nacht' - Other:
'Rembrandt','Vermeer'
Most measures are based on the edit distance of two strings: the number of insertions, deletions, substitutions, and transpositions of adjacent characters that are required to turn one string into the other.
The algorithm is the optimal string alignment: no substring is edited more than once. The distance of CA and ABC is thus 3, and not 2, as with the Damerau-Levenshtein distance. The same algorithm is used for fuzzy querying.
Options
All functions of this section and string:ngrams share the case, diacritics, stemming and language options of ft:tokenize, and process their input accordingly. Only the defaults differ: nothing is folded and nothing is stemmed, so strings are compared literally unless you request otherwise:
string:levenshtein('Rubens', 'rubens') (: 0.8333333333333334 :),
string:levenshtein('Rubens', 'rubens', { 'case': 'insensitive' }) (: 1 :)
If any option is specified, the input is tokenized with the full-text lexer. For string:token-sort-ratio and string:token-set-ratio this has a second effect: the input is no longer split on whitespace, but on word boundaries, so punctuation no longer influences the result.
string:token-set-ratio('Tanguy, Yves', 'Yves Tanguy') (: 0.916 :),
string:token-set-ratio('Tanguy, Yves', 'Yves Tanguy', { 'case': 'insensitive' }) (: 1 :)
string:levenshtein
Updated: New $options parameter.
| Signature | string:levenshtein(
$value1 as xs:string,
$value2 as xs:string,
$options as map(*)? := {}
) as xs:double | ||
|---|---|---|---|
| Summary | Computes the edit distance of $value1 and $value2 and returns a normalized similarity, computed as 1 - distance / max(lengths of strings). Two empty strings yield 1. See string:levenshtein-distance if you need the number of edits. | ||
| Errors |
| ||
| Examples | Result: 0.8571428571428571e0. One insertion in a string of 7 characters: 1 - 1 div 7. Result: 1e0. The words are stemmed and converted to lower case before they are compared. |
string:levenshtein-distance
Added: New function.
| Signature | string:levenshtein-distance(
$value1 as xs:string,
$value2 as xs:string,
$options as map(*)? := {}
) as xs:integer? | ||||||
|---|---|---|---|---|---|---|---|
| Summary | Returns the edit distance of $value1 and $value2: the number of edits that are required to turn one string into the other. 0 is returned if the strings are equal. In addition to the options of this module, the following option is available:
The bounded computation is much cheaper: only a diagonal band of the distance matrix is evaluated, and the width of the band grows with Use | ||||||
| Errors |
| ||||||
| Examples | Result: 1 Result: 3. Two substitutions and one insertion. Result: (). The distance exceeds the specified maximum. |
string:jaro-winkler
Updated: New $options parameter.
Updated: The common prefix is limited to 4 characters.
| Signature | string:jaro-winkler(
$value1 as xs:string,
$value2 as xs:string,
$options as map(*)? := {}
) as xs:double |
|---|---|
| Summary | Computes the Jaro-Winkler similarity of $value1 and $value2, rounded to two decimal places. Matching characters may occur at different positions, and a common prefix of up to 4 characters is rewarded; the measure is therefore well suited for short strings such as personal names. 0 is returned if the strings share no characters. Unlike the other similarity functions, it imposes no limit on the input length. |
| Examples | Result: 0.97e0 Result: 1e0. Case and diacritics are folded before the comparison. |
string:token-sort-ratio
Added: New function.
| Signature | string:token-sort-ratio(
$value1 as xs:string,
$value2 as xs:string,
$options as map(*)? := {}
) as xs:double | ||
|---|---|---|---|
| Summary | Tokenizes $value1 and $value2, sorts the tokens, and returns the string:levenshtein similarity of the rejoined strings. As the tokens are sorted before the comparison, the measure is insensitive to their order. For single-token input, the result is the same as that of string:levenshtein. | ||
| Errors |
| ||
| Examples | Result: 1e0. 1 is returned, as both strings consist of the same tokens in a different order. |
string:token-set-ratio
Added: New function.
| Signature | string:token-set-ratio(
$value1 as xs:string,
$value2 as xs:string,
$options as map(*)? := {}
) as xs:double | ||
|---|---|---|---|
| Summary | Tokenizes $value1 and $value2 and compares them as sets: the shared tokens and the tokens unique to either string are reassembled, compared with the string:levenshtein similarity, and the best of the resulting values is returned. A high value is returned if the tokens of one string are a subset of the other; this makes the measure robust against extra or omitted words. Duplicate tokens are ignored, and the order of the tokens is irrelevant. | ||
| Errors |
| ||
| Examples | Result: 1e0. 1 is returned, as the tokens of the second string are a subset of the first. |
string:ngram-similarity
Added: New function.
| Signature | string:ngram-similarity(
$value1 as xs:string,
$value2 as xs:string,
$options as map(*)? := {}
) as xs:double | ||
|---|---|---|---|
| Summary | Computes the similarity of $value1 and $value2 via the Sørensen-Dice coefficient on the sets of their character n-grams, and returns a double value (0 – 1). 1 is returned if the n-gram sets are equal; 0 is returned if they are disjoint. In addition to the options of this module, the n and padding options of string:ngrams are supported. As the n-grams are compared as sets, the measure is robust against reordered and inserted words, and it is considerably cheaper than the other measures. On short strings it is strict, however: a single differing character invalidates up to n n-grams, so the values are lower than those of string:levenshtein. | ||
| Errors |
| ||
| Examples | Result: 0.8181818181818182e0. Most bigrams survive the reordering of the two words. Result: 0.4e0. The two name variants share 2 of 5 bigrams each. Result: 0e0. Short strings yield few n-grams; with a larger n, the sets become disjoint. Result: 0.5e0. With padding, the shared first and last characters are rewarded: 0.25 is returned without it. |
string:partial-ratio
Added: New function.
| Signature | string:partial-ratio(
$value1 as xs:string,
$value2 as xs:string,
$options as map(*)? := {}
) as xs:double | ||
|---|---|---|---|
| Summary | Compares the shorter of $value1 and $value2 with the best matching substring of the longer one, and returns the string:levenshtein similarity of the two. 1 is returned if the shorter string occurs in the longer one; strings of equal length yield the string:levenshtein similarity. Use this measure to find a short string inside a longer one, e.g. a name in a title: the other measures punish the additional characters, even if the match is perfect. | ||
| Errors |
| ||
| Examples | Result: 1e0. The first string occurs in the second one. string:levenshtein returns 0.3 for the same input. Result: 0.875e0. The best matching substring differs in a single character: 1 - 1 div 8. |
string:closest
Added: New function.
| Signature | string:closest(
$value as xs:string,
$candidates as xs:string*,
$options as map(*)? := {}
) as map(*)* | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Summary | Compares $value with all $candidates and returns the most similar ones, the best match first. Each result is a map with the keys value (the candidate) and similarity (its similarity, 0 – 1). Candidates with an equal similarity are returned in input order. The following $options are available:
In addition, the options of the similarity measures can be specified. They are applied to Any function can be supplied as
| ||||||||||||
| Errors |
| ||||||||||||
| Examples | Result: { 'value': 'Rembrandt', 'similarity': 0.8888888888888888e0 }. The best of the three candidates is returned. Result: 'Steen, Jan'. All candidates that reach the threshold are returned. Jan Vermeer is too different.The candidates are compared via their trigrams.ft:tokens retrieves the spelling variants of a name from the full-text index, and this function ranks them. The candidates are untyped index entries; they are atomized before they are compared. |
Tokenization
string:ngrams
Added: New function.
| Signature | string:ngrams(
$value as xs:string,
$options as map(*)? := {}
) as xs:string* | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Summary | Returns the character n-grams of $value: all substrings of n consecutive characters, in string order and including duplicates. An empty string yields no n-grams. Whitespace is treated like any other character, and the input is only normalized if the options of this module are specified. The following options are available in addition:
Without padding, a non-empty string that is shorter than The distinct n-grams are the building block of | |||||||||
| Errors |
| |||||||||
| Examples | Result: 'fl', 'lo', 'ow', 'we', 'er' Result: 'flo', 'low', 'owe', 'wer' Result: ' a', ' ab', 'ab ', 'b '. Without padding, the string is shorter than n, and is returned as a single n-gram: 'ab'. |
Phonetic Encodings
The functions of this section encode a word as a code that reflects its pronunciation: two words are considered similar if their codes are equal. Case and diacritics are folded, and each function is bound to a single language, so the codes of different algorithms cannot be compared with each other. Words with a similar meaning are found with ft:thesaurus.
A single word is encoded. Strings with multiple words are encoded as if the whitespace was absent, and the resulting code is usually meaningless. Encode the words one by one instead, and sort the codes if the word order is irrelevant:
declare function soundex-key($name as xs:string) as xs:string* {
sort(ft:tokenize($name) ! string:soundex(.))
};
deep-equal(soundex-key('Jacob van Ruisdael'), soundex-key('van Ruisdael, Jacob'))
Result: true
string:soundex
Updated: A string without letters yields an empty string.
| Signature | string:soundex( $value as xs:string ) as xs:string |
|---|---|
| Summary | Computes the Soundex value for the specified string $value. The algorithm can be used to find and index English words with similar pronunciation. The returned code consists of the initial letter and three digits. A string without letters has no pronunciation, and yields an empty string. |
| Examples | Result: 'M240' Result: true() |
string:cologne-phonetic
| Signature | string:cologne-phonetic( $value as xs:string ) as xs:string |
|---|---|
| Summary | Computes the Kölner Phonetik value for the specified string $value. Similar to Soundex, the algorithm is used to find similarly pronounced words, but for the German language. The returned code is a digit string of variable length; as its first digit can be 0, the result is returned as string. |
| Examples | Result: '645' Result: true() |
Examples
Finding Duplicates
The following query groups the spelling variants of a list of names. It combines the building blocks of this module: options fold case, diacritics and punctuation, string:token-set-ratio tolerates reordered and omitted words, the threshold keeps unrelated names apart, and string:closest supplies all matches of a name in one call. The first name of each group serves as its representative:
let $names := (
'Ruisdael, Jacob van',
'Jacob van Ruisdael',
'Rembrandt van Rijn',
'Rembrandt van Rhijn',
'Johannes Vermeer'
)
let $options := {
'measure': string:token-set-ratio#2,
'case': 'insensitive',
'diacritics': 'insensitive',
'threshold': 0.9,
'limit': 0
}
for $name in $names
group by $group := sort(string:closest($name, $names, $options)?value) => head()
return $group || ': ' || string-join($name, ' | ')
Result:
Jacob van Ruisdael: Ruisdael, Jacob van | Jacob van Ruisdael
Rembrandt van Rhijn: Rembrandt van Rijn | Rembrandt van Rhijn
Johannes Vermeer: Johannes Vermeer
With larger inputs, the vocabulary of a database is a better source of candidates than the input itself: see ft:tokens.
Ranking Similar Names
Comparing a string with all candidates is expensive, as the similarity measures inspect every character. A phonetic code is much cheaper, and it can be used to discard the candidates that do not even sound alike. The remaining ones are ranked with string:closest, and the result is tabulated with string:format:
let $names := (
'Meier', 'Maier', 'Mayr', 'Meyer', 'Major',
'Schmidt', 'Schmitt', 'Müller'
)
let $query := 'Meyr'
let $code := string:cologne-phonetic($query)
let $candidates := $names[string:cologne-phonetic(.) = $code]
for $match in string:closest($query, $candidates,
{ 'measure': string:jaro-winkler#2, 'limit': 0 })
return string:format('%-6s %.2f', $match?value, $match?similarity)
Result:
Meyer 0.95
Mayr 0.85
Meier 0.83
Maier 0.63
Major 0.63
Schmidt, Schmitt and Müller are not compared at all: their phonetic codes differ from the one of the query.
N-gram Index
A phonetic code only discards candidates that sound different. string:ngrams allows you to be more selective: if you index the n-grams of your candidates once, you can restrict the comparison to the candidates that share at least one n-gram with the query. This is how you can search a long list of strings without a full-text index:
let $names := ('Rembrandt', 'Rubens', 'Vermeer')
let $index := map:merge(
for $name in $names, $gram in distinct-values(string:ngrams($name))
group by $gram
return { $gram: $name }
)
let $query := 'Rembrant'
let $candidates := distinct-values(string:ngrams($query) ! $index(.))
return string:closest($query, $candidates)
Result:
{ 'value': 'Rembrandt', 'similarity': 0.8888888888888888e0 }
The index maps each bigram to the names that contain it. Rubens and Vermeer share no bigram with the query, so they are never compared. The larger the list, the more you save; the price is the memory for the index and the effort of keeping it up to date.
ft:tokens narrows the candidates in a comparable way, but the two are not interchangeable: it is bound to the vocabulary of a full-text index, it returns single tokens (never a name such as Rembrandt van Rijn), and its fuzzy option selects the candidates by edit distance, not by shared n-grams.
Errors
| Code | Description |
|---|---|
bounds | The specified string exceeds the maximum supported length (10000 characters). |
ngram | The specified n-gram length is not positive. |
Changelog
Version 13.0- Added:
string:closest,string:levenshtein-distance,string:partial-ratio,string:token-sort-ratio,string:token-set-ratio,string:ngram-similarity,string:ngrams - Updated: The similarity functions and
string:ngramsadopt the$optionsofft:tokenize(case,diacritics,stemming,language). - Updated:
string:soundex: a string without letters yields an empty string (before:0000). - Removed:
string:formatin favor offn:pad-string.
- Added:
string:jaro-winklerfor computing the Jaro-Winkler similarity. - Removed:
string:tab,string:nlandstring:crin favor offn:char.
- Updated: Renamed from Strings Module to String Module. The namespace URI has been updated as well.
- Updated:
string:format,string:cr,string:nlandstring:tabadopted from the obsolete Output Module.
- Added: New module added. Functions were adopted from the obsolete Utility and Output Modules.