Welcome to the new DOCS.vastdata.com. We've added new menus and a version selector, plus new persona and resource pages to help customers get started. Looking for a previous article? Use the docs search.

jaro_similarity

Prev Next

Applies to VAST Cluster 5.5.

Functions › Text basics › Similarity. Returns the Jaro similarity of two strings: a DOUBLE that is higher the more alike they are, with an optional cutoff below which the score is 0.0. It has no aliases.

Syntax

jaro_similarity(a, b)
jaro_similarity(a, b, cutoff)
Argument Type Description
a, b VARCHAR The two strings to compare.
cutoff DOUBLE Optional. A score below cutoff is returned as 0.0. An integer or a numeric string such as '0.5' is converted to DOUBLE.

Returns: DOUBLE.

Aliases: none.

Set up the examples

The examples connect with the ADBC driver, as in VAST Query Engine (ADBC), and work in a schema called docs_sql_fn_jaro_similarity. Set QE_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and VASTDB_BUCKET (SQL reference), and run the blocks in order in one Python session. Below each block is the output you should see.

This block connects, defines the run() and plan() helpers, and creates a table of four contacts: the name on record and the name as someone typed it. The last row has no values:

import os
import re

import adbc_driver_manager
import adbc_driver_manager.dbapi as dbapi
import adbc_driver_vastdb

BUCKET = os.environ["VASTDB_BUCKET"]
SCHEMA = f'"{BUCKET}/docs_sql_fn_jaro_similarity"'

conn = dbapi.connect(
    driver=adbc_driver_vastdb.get_driver_path(),
    db_kwargs={
        "vast.db.endpoint": os.environ["QE_ENDPOINT"],
        "vast.db.access_key": os.environ["AWS_ACCESS_KEY_ID"],
        "vast.db.secret_key": os.environ["AWS_SECRET_ACCESS_KEY"],
    },
    autocommit=True,
)

def server_message(e):
    """The server's message in a driver error, without the request ID or the echoed statement."""
    text = re.sub(r"\[req=0x[0-9a-f]+\]: ", "", str(e))
    found = (re.search(r'message: "(.*?)", request_id', text, re.S)
             or re.search(r"description: (.*)\)", text, re.S))
    message = found.group(1) if found else text.strip().splitlines()[-1]
    message = re.sub(r"^Plan query: '.*?' failed with: ", "", message, flags=re.S)
    message = re.split(r"\\n|\n", message)[0].replace('\\"', '"')
    return message.replace(BUCKET, "<bucket>")

def run(sql):
    """Run one statement. Print the rows it returns, or the error."""
    try:
        with conn.cursor() as cur:
            cur.execute(sql)
            rows = cur.fetchall() if cur.description else []
    except (adbc_driver_manager.Error, OSError) as e:
        print(f"{type(e).__name__}: {server_message(e)}")
    else:
        print(rows)

def plan(sql):
    """Print the plan without its ~N Rows lines: the table's row count, which can still be 0 just after an insert."""
    with conn.cursor() as cur:
        cur.execute("EXPLAIN " + sql)
        steps = cur.fetchall()[0][1]
    print("\n".join(line for line in steps.splitlines() if "Rows" not in line))

TABLE = f"{SCHEMA}.contacts"
run(f"CREATE SCHEMA IF NOT EXISTS {SCHEMA}")
run(f"CREATE TABLE IF NOT EXISTS {TABLE} (id BIGINT, name VARCHAR, typed VARCHAR, city VARCHAR)")
run(f"INSERT INTO {TABLE} VALUES "
    "(1, 'Ada Lovelace', 'Ada Lovelase', 'London'), "
    "(2, 'Grace Hopper', 'Grace Hoper', 'New York'), "
    "(3, 'José Martí', 'Jose Marti', 'Havana'), "
    "(4, NULL, NULL, NULL)")
[]
[]
[(4,)]

What it does

The Jaro similarity measures how many units two strings share, and whether the shared units appear in the same order. It's built for short strings such as names and codes, where a typo or two swapped letters should still score high:

  1. Matches. A unit of a matches an equal unit of b if their positions are no further apart than half the length of the longer string, minus one. Each unit matches at most once. Call the number of matches m.
  2. Transpositions. Take the matched units of each string in order. Count the positions where the two sequences differ, and halve that count: that's t.
  3. Score. If m is 0, the score is 0.0. Otherwise it's the average of three ratios: m / length of a, m / length of b, and (m − t) / m.

The units are bytes of the UTF-8 encoding, not characters: an accented letter such as é is two bytes, so it counts as two units. Two different accented letters can share their first byte: jaro_similarity('é', 'è') returns 0.6666666666666666, one match out of two units on each side, not 0.0. The comparison is case-sensitive: jaro_similarity('abc', 'abc') returns 1.0 and jaro_similarity('abc', 'ABC') returns 0.0. Apply lower() to both arguments for a case-insensitive score.

Call m and t Result
jaro_similarity('Hello', 'Hallo') 4 matches (H, l, l, o) of 5, no transpositions 0.8666666666666667
jaro_similarity('martha', 'marhta') 6 matches; t and h are swapped, so t = 1 0.9444444444444445
jaro_similarity('kitten', 'sitting') 4 matches (i, t, t, n), no transpositions 0.746031746031746
jaro_similarity('café', 'Hallo') both strings are 5 bytes; 1 match (a) 0.4666666666666666
jaro_similarity('abc', 'xyz') no matches 0.0

The cutoff. With a third argument, a score below the cutoff comes back as 0.0, so a query can treat "not similar enough" as zero. jaro_similarity('abc', 'abd') is 0.7777777777777777, so with a cutoff of 0.5 it's returned unchanged and with 0.9 it's 0.0. A negative cutoff such as -1.0 leaves the score unchanged. A cutoff above 1.0 makes every result 0.0: jaro_similarity('abc', 'abc', 2.0) returns 0.0 even though the strings are equal.

Empty strings score 0.0. Two empty strings aren't treated as equal: jaro_similarity('', '') returns 0.0, as does an empty string against any other string.

NULLs and edge cases

Input Result
NULL as a or b NULL
NULL as cutoff NULL
'' and '' 0.0
'abc' and '' 0.0
'abc' and 'xyz' (nothing in common) 0.0
'abc', 'abd', cutoff 0.9 0.0
'abc' and 'abd', no cutoff 0.7777777777777777
'abc', 'abc', cutoff 2.0 0.0
'abc' and 'abc', no cutoff 1.0
A number, decimal, date, boolean, BLOB or list Error: No function matches the given name and argument types

The same cases, as the cluster returns them, with the scores without a cutoff for comparison. They use the table and helpers from Set up the examples:

run("SELECT jaro_similarity(NULL, 'Hallo'), jaro_similarity('a', NULL), jaro_similarity('a', 'a', NULL)")
run("SELECT jaro_similarity('', ''), jaro_similarity('abc', ''), jaro_similarity('abc', 'xyz')")
run("SELECT jaro_similarity('abc', 'abd', 0.9), jaro_similarity('abc', 'abd'), "
    "jaro_similarity('abc', 'abc', 2.0), jaro_similarity('abc', 'abc')")
run("SELECT jaro_similarity(123, 'Hallo')")
run("SELECT jaro_similarity('\\xAA'::BLOB, 'Hallo')")
[(None, None, None)]
[(0.0, 0.0, 0.0)]
[(0.0, 0.7777777777777777, 0.0, 1.0)]
InternalError: Binder Error: No function matches the given name and argument types 'jaro_similarity(INTEGER_LITERAL, STRING_LITERAL)'. You might need to add explicit type casts.
InternalError: Binder Error: No function matches the given name and argument types 'jaro_similarity(BLOB, STRING_LITERAL)'. You might need to add explicit type casts.

Examples

Score each name as typed against the name on record. The row with no values scores NULL:

run(f"SELECT id, name, typed, round(jaro_similarity(name, typed), 3) FROM {TABLE} ORDER BY id LIMIT 10")
[(1, 'Ada Lovelace', 'Ada Lovelase', 0.944), (2, 'Grace Hopper', 'Grace Hoper', 0.972), (3, 'José Martí', 'Jose Marti', 0.822), (4, None, None, None)]

Find the closest names to a misspelt search term, best first:

run(f"SELECT name, round(jaro_similarity(name, 'Grace Hoper'), 3) AS score FROM {TABLE} "
    "WHERE name IS NOT NULL ORDER BY score DESC LIMIT 3")
[('Grace Hopper', 0.972), ('Ada Lovelace', 0.557), ('José Martí', 0.396)]

Keep only the rows where the typed name is close to the name on record:

run(f"SELECT id, name, typed FROM {TABLE} WHERE jaro_similarity(name, typed) > 0.9 ORDER BY id LIMIT 10")
[(1, 'Ada Lovelace', 'Ada Lovelase'), (2, 'Grace Hopper', 'Grace Hoper')]

How it runs in VAST

The Query Engine runs on the cluster's CNodes, and a query on a table starts with a scan in VAST DataBase (VAST_SCAN). VAST DataBase can evaluate a comparison of a column with a constant, but not a function of a column, so jaro_similarity() is always computed by the Query Engine:

  • In the SELECT list, the scan reads the columns and the Query Engine computes jaro_similarity() for each row it returns.
  • In WHERE, the scan sends the columns for every row, and a FILTER step in the Query Engine computes jaro_similarity() and keeps the rows that match. No rows are skipped in storage.

The plan of the WHERE example shows the FILTER step above the scan:

plan(f"SELECT id FROM {TABLE} WHERE jaro_similarity(name, typed) > 0.9")
┌───────────────────────────┐
│         PROJECTION        │
│    ────────────────────   │
│             #2            │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│           FILTER          │
│    ────────────────────   │
│   (jaro_similarity(name,  │
│        typed) > 0.9)      │
│                           │
└─────────────┬─────────────┘
┌─────────────┴─────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│                           │
│        Projections:       │
│            name           │
│           typed           │
│             id            │
│                           │
└───────────────────────────┘

Make it faster

  • Narrow the rows with a pushed-down condition first. A comparison of a raw column with a constant is evaluated in VAST DataBase, so only the rows that pass it reach the FILTER step. In city = 'London' AND jaro_similarity(name, 'Ada Lovlace') > 0.9, city = 'London' is pushed down and jaro_similarity() runs only on the London rows. The plan of the raw comparison on its own has no FILTER step:
plan(f"SELECT id FROM {TABLE} WHERE city = 'London'")
┌───────────────────────────┐
│         VAST_SCAN         │
│    ────────────────────   │
│    Function: VAST_SCAN    │
│      Projections: id      │
│                           │
└───────────────────────────┘
  • Store the score if you filter on it often. If you compare against the same reference value every time, compute the score when you write the rows and keep it in its own column: name_score > 0.9 is pushed down, jaro_similarity(name, 'Grace Hopper') > 0.9 isn't.

Limitations and errors

  • Strings only. A number, a decimal, a date, a boolean, a BLOB or a list fails with No function matches the given name and argument types. Cast it first: jaro_similarity(CAST(n AS VARCHAR), '123').
  • Bytes, not characters. Accented and other non-ASCII letters take two or more bytes each, and each byte is a separate unit in the score. See What it does.
  • Case-sensitive. 'abc' and 'ABC' score 0.0. Compare lower(a) with lower(b) if case shouldn't count.
  • Two empty strings score 0.0, not 1.0. Test for empty strings separately if your logic treats them as equal.
run("SELECT jaro_similarity(DATE '2026-10-16', 'Hallo')")
run("SELECT jaro_similarity(['a', 'b'], 'Hallo')")
InternalError: Binder Error: No function matches the given name and argument types 'jaro_similarity(DATE, STRING_LITERAL)'. You might need to add explicit type casts.
InternalError: Binder Error: No function matches the given name and argument types 'jaro_similarity(VARCHAR[], STRING_LITERAL)'. You might need to add explicit type casts.

Similar functions

Function Measures Higher means Result for 'kitten' and 'sitting'
jaro_similarity matches within a window and their order more alike 0.746031746031746
jaro_winkler_similarity Jaro, plus extra weight for a common prefix more alike 0.746031746031746 (no common prefix)
levenshtein insertions, deletions and substitutions needed less alike 3
damerau_levenshtein as levenshtein, and a swap of two adjacent units counts as one edit less alike 3
jaccard the overlap of the two strings' sets of bytes more alike 0.42857142857142855
hamming positions that differ; strings of equal length only less alike 2 for 'kitten' and 'sittin'
run("SELECT levenshtein('kitten', 'sitting'), damerau_levenshtein('kitten', 'sitting'), hamming('kitten', 'sittin'), "
    "jaccard('kitten', 'sitting'), jaro_similarity('kitten', 'sitting'), jaro_winkler_similarity('kitten', 'sitting')")
run("SELECT jaro_similarity('martha', 'marhta'), jaro_winkler_similarity('martha', 'marhta')")
[(3, 3, 2, 0.42857142857142855, 0.746031746031746, 0.746031746031746)]
[(0.9444444444444445, 0.9611111111111111)]

Clean up:

run(f"DROP TABLE IF EXISTS {TABLE}")
run(f"DROP SCHEMA IF EXISTS {SCHEMA}")
conn.close()
[]
[]

See also