Phonetic and Edit-Distance Functions via fuzzystrmatch
pg_mentat integrates with the built-in PostgreSQL contrib extension
fuzzystrmatch to expose four scalar phonetic and edit-distance
functions as Datalog where-fns. Unlike the more powerful pg_tre
integration (which requires PG18+ and shared_preload_libraries),
fuzzystrmatch is contrib — already on disk in any standard
PostgreSQL build from PG13 onward, with no preload required and no
restart needed.
fuzzystrmatch is an optional dependency. If it is not installed
in the database, queries that use these where-fns fail at execution
with the standard PostgreSQL error
"function levenshtein(...) does not exist". Use
mentat.has_fuzzystrmatch() to detect availability.
When to use this vs (fuzzy-match ...)
fuzzystrmatch (this page) | pg_tre (fuzzy-search) | |
|---|---|---|
| PostgreSQL versions | 13+ | 18+ |
shared_preload_libraries | not required | required |
| Restart to enable | no | yes |
| Index-backed | no — scalar function per row | yes — three-tier filter funnel |
| Use case | Per-row name matching, predicates over distance | Bulk approximate-regex search |
| Result quality | Levenshtein distance is exact | Edit-distance regex with TRE semantics |
For a thousand-row table, fuzzystrmatch is fine. For a million-row table where you want "rows whose body matches this regex with up to k typos," use pg_tre.
Quick start
CREATE EXTENSION pg_mentat;
CREATE EXTENSION fuzzystrmatch; -- contrib, ships with PG
-- Define a string attribute as usual.
SELECT mentat.t('[
{:db/ident :person/name
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
]');
SELECT mentat.t('[
{:db/id "a" :person/name "Alice"}
{:db/id "b" :person/name "Alyce"}
{:db/id "c" :person/name "Robert"}
]');
-- Levenshtein: edit distance from each name to "Alice".
SELECT mentat.q('[:find ?n ?d
:where [?e :person/name ?n]
[(levenshtein ?n "Alice") ?d]]');
-- [["Alice", 0], ["Alyce", 1], ["Robert", 6]]
-- Soundex: groups homophones. "Alice" and "Alyce" both hash to "A420".
SELECT mentat.q('[:find ?n ?h
:where [?e :person/name ?n]
[(soundex ?n) ?h]]');
-- [["Alice", "A420"], ["Alyce", "A420"], ["Robert", "R163"]]
Where-fns
[(levenshtein ?a ?b) ?d]
Edit distance between two text values. Both args may be variables or text constants. Result is an integer.
[(levenshtein ?name "Alice") ?d] ;; ?d = distance from ?name to "Alice"
[(levenshtein ?a ?b) ?d] ;; ?d = distance between two bound vars
[(soundex ?s) ?code]
The classic 4-character American Soundex code for the input. Useful for grouping name homophones.
[(soundex ?surname) ?sx]
[(metaphone ?s ?max) ?code]
Metaphone phonetic encoding, truncated to ?max characters. Better
than Soundex for non-Anglo names. ?max must be an integer literal.
[(metaphone ?surname 5) ?mp]
[(daitch-mokotoff ?s) ?codes]
Daitch-Mokotoff Soundex — better for Slavic and Yiddish surnames.
Returns a text[] of codes; the binding receives a Postgres text
representation like "{367460}" or "{394000,367460}".
[(daitch-mokotoff ?surname) ?dm]
Helper
mentat.has_fuzzystrmatch() → boolean returns true when the contrib
extension is loaded. Use it as a guard in test suites and
application code that needs to fall back gracefully when the extension
isn't available.
Composing with predicates and other where-fns
Where-fn output variables can flow into other where-fns:
:where [?e :person/name ?n]
[(levenshtein ?n "Alice") ?d]
[(* ?d 2) ?da] ;; arithmetic chain works
There is a known limitation: where-fn output variables (?d, ?da,
etc.) are not yet usable inside (< ...)-style predicates. This
is a pre-existing gap in the predicate compiler that affects all
where-fn bindings (arithmetic too), not just fuzzystrmatch's. As a
workaround, use a wrapping query that filters at the SQL level, or
do the comparison entirely with arithmetic where-fns:
;; Instead of [(<= ?d 2)], do:
[(- 3 ?d) ?diff] ;; ?diff > 0 iff ?d < 3
Tracked as a future query-compiler improvement; pull requests welcome.
Errors
| Error | Cause | Fix |
|---|---|---|
function levenshtein(...) does not exist | fuzzystrmatch not installed in this database. | CREATE EXTENSION fuzzystrmatch; |
:db.error/fn-arity levenshtein requires exactly 2 text arguments | Wrong argument count. | Pass exactly the documented arity. |
:db.error/fn-arg fuzzystrmatch 'soundex' arguments must be text variables or string constants. | Passed a non-text constant (e.g. an int). | Use a string. |
:db.error/fn-binding fuzzystrmatch function 'levenshtein' requires a scalar binding. | Used [[?d]] or similar relation/tuple form. | Bind to a single variable: ?d. |
What this does NOT give you
- Soundex / Metaphone variants other than the four above. The contrib extension itself only ships those.
- Trigram similarity. That's
pg_trgm— see pg_trgm integration. - Edit-distance with regex semantics. That's
pg_tre— see fuzzy-search. - Index acceleration. These are scalar functions; there is no index for "names within 2 edits of X." For that workload use pg_tre or a precomputed Soundex column.