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.