Main Page » XQuery » Functions » String Functions

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
SummaryComputes 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
boundsThe specified string exceeds the maximum supported length (10000 characters).
Examples
string:levenshtein('flower', 'flowers')
Result: 0.8571428571428571e0. One insertion in a string of 7 characters: 1 - 1 div 7.
string:levenshtein('HOUSES', 'house', { 'stemming': true(), 'case': 'insensitive' })
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?
SummaryReturns 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:
optiondefaultdescription
max The maximum distance. Only the distances up to this value are computed, and an empty sequence is returned if it is exceeded (or if the value is negative).

The bounded computation is much cheaper: only a diagonal band of the distance matrix is evaluated, and the width of the band grows with max, not with the length of the strings. The option should therefore always be specified if larger distances are of no interest; it is also the only variant that has no length limit.

Use string:levenshtein if you prefer a normalized similarity, and string:closest if you want to compare a string with many candidates.

Errors
boundsThe specified string exceeds the maximum supported length (10000 characters).
Examples
string:levenshtein-distance('flower', 'flowers')
Result: 1
string:levenshtein-distance('kitten', 'sitting')
Result: 3. Two substitutions and one insertion.
string:levenshtein-distance('kitten', 'sitting', { 'max': 2 })
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
SummaryComputes 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
string:jaro-winkler('flower', 'flowers')
Result: 0.97e0
string:jaro-winkler(
  'Müller',
  'MULLER',
  { 'case': 'insensitive', 'diacritics': 'insensitive' }
)
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
SummaryTokenizes $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
boundsThe specified string exceeds the maximum supported length (10000 characters).
Examples
string:token-sort-ratio('Ishida Yūtei', 'Yūtei Ishida')
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
SummaryTokenizes $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
boundsThe specified string exceeds the maximum supported length (10000 characters).
Examples
string:token-set-ratio('Fortuny y Marsal', 'Fortuny Marsal')
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
SummaryComputes 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
ngramThe specified n-gram length is not positive.
Examples
string:ngram-similarity('Ishida Yūtei', 'Yūtei Ishida')
Result: 0.8181818181818182e0. Most bigrams survive the reordering of the two words.
string:ngram-similarity('Massys', 'Metsys')
Result: 0.4e0. The two name variants share 2 of 5 bigrams each.
string:ngram-similarity('night', 'nacht', { 'n': 3 })
Result: 0e0. Short strings yield few n-grams; with a larger n, the sets become disjoint.
string:ngram-similarity('night', 'nacht', { 'padding': true() })
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
SummaryCompares 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
boundsThe specified string exceeds the maximum supported length (10000 characters).
Examples
string:partial-ratio('Rembrandt', 'Rembrandt van Rijn (1606-1669)')
Result: 1e0. The first string occurs in the second one. string:levenshtein returns 0.3 for the same input.
string:partial-ratio('Rembrant', 'Rembrandt van Rijn')
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(*)*
SummaryCompares $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:
optiondefaultdescription
measurestring:levenshtein#2 The similarity measure: any function that takes two strings and returns a double value.
threshold0 The minimum similarity (0 – 1) a candidate must reach. Candidates below this value are discarded.
limit1 The maximum number of results. If the value is 0 or negative, all candidates are returned.

In addition, the options of the similarity measures can be specified. They are applied to $value and all $candidates. The n and padding options are passed on to string:ngram-similarity if it is chosen as measure.

Any function can be supplied as measure, including your own. If one of the measures of this module is referenced by name, it is computed internally: the input is normalized once, and no function is invoked for the single candidates. With string:levenshtein, the comparison of a candidate is additionally aborted as soon as it can no longer reach the threshold, which makes the lookup much faster. A threshold should therefore always be specified if one is known.

ft:tokens supplies the indexed vocabulary of a database as candidates.

Errors
boundsThe specified string exceeds the maximum supported length (10000 characters).
Examples
string:closest('Rembrant', ('Rembrandt', 'Rubens', 'Vermeer'))
Result: { 'value': 'Rembrandt', 'similarity': 0.8888888888888888e0 }. The best of the three candidates is returned.
string:closest(
  'Jan Steen',
  ('Steen, Jan', 'Jan Vermeer'),
  { 'measure': string:token-set-ratio#2, 'threshold': 0.9, 'limit': 0 }
)?value
Result: 'Steen, Jan'. All candidates that reach the threshold are returned. Jan Vermeer is too different.
string:closest('night', ('nacht'), { 'measure': string:ngram-similarity#2, 'n': 3 })
The candidates are compared via their trigrams.
let $candidates := ft:tokens('db', 'rembrandt', { 'fuzzy': true(), 'errors': 2 })
return string:closest('rembrandt', $candidates, { 'limit': 0 })
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*
SummaryReturns 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:
optiondefaultdescription
n2 The n-gram length.
paddingfalse() Surround the input with n - 1 spaces before the n-grams are generated.

Without padding, a non-empty string that is shorter than n yields a single n-gram with the whole string. Such an n-gram is shorter than all others, so it will never match: string:ngram-similarity('ab', 'abc', { 'n': 3 }) returns 0. Padding solves this, and it additionally rewards a common start and end of two strings, which is often desirable for names.

The distinct n-grams are the building block of string:ngram-similarity, which equals the Sørensen-Dice coefficient over distinct-values(string:ngrams(...)) of both arguments. This function gives you the n-grams themselves, so you can compute other measures, or build an n-gram index that reduces the number of candidates you need to compare.

Errors
ngramThe specified n-gram length is not positive.
Examples
string:ngrams('flower')
Result: 'fl', 'lo', 'ow', 'we', 'er'
string:ngrams('flower', { 'n': 3 })
Result: 'flo', 'low', 'owe', 'wer'
string:ngrams('ab', { 'n': 3, 'padding': true() })
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
SummaryComputes 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
string:soundex('Michael')
Result: 'M240'
string:soundex('OBrien') = string:soundex("O'Brien")
Result: true()

string:cologne-phonetic

Signature
string:cologne-phonetic(
  $value  as xs:string
) as xs:string
SummaryComputes 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
string:cologne-phonetic('Michael')
Result: '645'
every $s in ('Mayr', 'Maier', 'Meier')
satisfies string:cologne-phonetic($s) = '67'
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

CodeDescription
boundsThe specified string exceeds the maximum supported length (10000 characters).
ngramThe specified n-gram length is not positive.

Changelog

Version 13.0Version 11.0Version 10.0
  • Updated: Renamed from Strings Module to String Module. The namespace URI has been updated as well.
  • Updated: string:format, string:cr, string:nl and string:tab adopted from the obsolete Output Module.
Version 8.3
  • Added: New module added. Functions were adopted from the obsolete Utility and Output Modules.

⚡Generated with XQuery