← You Don't Need Elasticsearch

Chapter 6: Phonetic Search (fuzzystrmatch)

The Waiter of Gold Lapel · Published Apr 12, 2026 · 11 min

I promised, at the close of the last chapter, that this problem has been solved since 1918. Allow me to make good on that promise.

"Smith" and "Smyth." "Stefan" and "Stephen." "Schmidt" and "Shmidt." These are not typos. The user's fingers performed their task correctly — Chapter 5 would find no fault with them. The problem is that the English language, in its infinite generosity, offers more than one way to spell the same sound. The user chose one spelling. Your database chose another. Neither is wrong. They simply disagree, and the search system must be gracious enough to reconcile the disagreement without asking the user to guess which spelling you prefer.

Chapter 5's trigram similarity might catch some of these. "Smith" and "Smyth" share enough character sequences to produce a reasonable similarity score. But trigrams compare how strings look. What we need here are algorithms that compare how strings sound. The distinction is the difference between reading a name and hearing it spoken.

Elasticsearch handles this with the phonetic analysis plugin. PostgreSQL handles it with fuzzystrmatch — a contrib extension that provides four tools in one package: Soundex, Metaphone, Double Metaphone, and Levenshtein distance. The first of these, I am pleased to report, has been at its post for over a century.

Soundex

Soundex was invented in 1918 by Robert Russell and Margaret Odell for the United States Census Bureau. The problem was a practical one: census workers were collecting names by hand, and the same person might be recorded as "Schmidt" in one district and "Shmidt" in another. The Bureau needed an algorithm that could match these variations automatically. Russell and Odell provided one, and it has been in continuous service ever since. I confess I find something deeply reassuring about an algorithm that has outlasted every technology trend of the past century and shows no signs of retiring.

The algorithm works by keeping the first letter and converting the remaining consonants to digit groups:

  • B, F, P, V → 1
  • C, G, J, K, Q, S, X, Z → 2
  • D, T → 3
  • L → 4
  • M, N → 5
  • R → 6

Vowels, H, W, and Y are dropped. Adjacent duplicates collapse. The result is padded or truncated to exactly 4 characters.

Soundex examples
SELECT soundex('Robert'), soundex('Rupert');    -- Both: R163
SELECT soundex('Smith'), soundex('Smyth');      -- Both: S530
SELECT soundex('Stefan'), soundex('Stephen');   -- S315, S315

Same Soundex code means the names sound alike. Different codes mean they sound different — or at least different enough that Soundex, with its 1918-era understanding of English pronunciation, cannot reconcile them.

Limitations. I would not be serving you well if I presented Soundex without its boundaries. It was designed for English pronunciation, and non-English names may not code correctly. The 4-character fixed length means long names lose information after the first letter and three digits. And Soundex depends on the first letter — "Katherine" and "Catherine" produce different codes (K365 vs C365) despite sounding identical, because the algorithm preserves the initial character without interpreting its phonetics.

These are real limitations. Soundex is a century-old algorithm designed for a specific purpose — matching English names in handwritten census forms — and it handles that purpose remarkably well. For the edge cases it cannot reach, its successors were designed with exactly those edges in mind.

Metaphone and Double Metaphone

Metaphone (1990, Lawrence Philips) addressed many of Soundex's limitations. It handles consonant combinations more accurately — "gh," "wr," "kn" — accounts for silent letters, and applies English pronunciation rules with considerably more nuance. It produces variable-length codes rather than Soundex's fixed 4-character format, which means it retains more information about longer names.

Double Metaphone (2000, Lawrence Philips) went further still. It generates two codes — a primary and an alternate — to handle names with multiple valid pronunciations. A name with both Germanic and Slavic origins might be pronounced differently depending on the speaker's background. Double Metaphone captures both possibilities, which strikes me as a particularly considerate approach to the problem of human diversity.

Double Metaphone examples
SELECT dmetaphone('Stephen'), dmetaphone('Stefan');      -- Both: STFN
SELECT dmetaphone('Catherine'), dmetaphone('Katherine');  -- Both: K0RN

That second example deserves a moment of appreciation. Soundex produces different codes for Catherine and Katherine because it preserves the first letter without phonetic interpretation. Double Metaphone understands that C and K can produce the same sound in English and codes them identically. The Katherine/Catherine problem — a limitation that Soundex has carried for a century — is resolved.

When to use which. Soundex for simple, fast phonetic matching — especially common English names where speed matters and edge cases are rare. Double Metaphone when your data includes non-English names, when names may have multiple valid pronunciations, or when the first-letter problem matters for your users. Metaphone sits between them — an improvement over Soundex, less comprehensive than Double Metaphone. Each has earned its place, and the choice depends on your data rather than on any inherent superiority of one algorithm over another.

Levenshtein Distance

fuzzystrmatch also provides levenshtein() — the edit-distance function that Chapter 5 referenced in its comparison with trigrams. And since you mentioned an appreciation for Levenshtein, allow me to give it the attention it deserves.

Levenshtein examples
SELECT levenshtein('kitten', 'sitting');  -- 3 (k->s, e->i, insert g)
SELECT levenshtein('Smith', 'Smyth');     -- 1 (i->y)
SELECT levenshtein('Stefan', 'Stephen');  -- 3

Levenshtein distance answers a precise question: how many individual character operations — insertions, deletions, substitutions — are required to transform one string into another? The answer is an integer, not a ratio. "Smith" to "Smyth" requires one substitution. The distance is 1. There is an elegance to that directness — no normalization, no coefficients, no thresholds. Just a count of changes. One sometimes appreciates a tool that gives a straight answer.

This makes Levenshtein different from both trigram similarity (Chapter 5), which returns a normalized 0.0-1.0 score, and Soundex, which returns a binary match/no-match on phonetic codes. Levenshtein occupies a middle ground: more precise than Soundex (it tells you how different two strings are, not just whether they sound alike), more absolute than similarity (it counts changes rather than estimating proportional overlap).

Where Levenshtein truly shines: the combination pattern. There is a well-established technique for high-performance name matching that combines the speed of Soundex with the precision of Levenshtein. Use Soundex to quickly filter to phonetically similar candidates — a fast index scan. Then rank those candidates by Levenshtein distance — fine-grained precision among a small candidate set.

Soundex + Levenshtein combination
SELECT name, levenshtein(name, 'Smyth') AS distance
FROM customers
WHERE soundex(name) = soundex('Smyth')
ORDER BY distance
LIMIT 10;

The Soundex filter narrows the search to names that sound like "Smyth" — using the B-tree expression index for speed. Then Levenshtein ranks them by how many character edits separate each candidate from the query. The result is precise name matching with the performance of an index scan. The reported improvement over running Levenshtein against the full table is approximately 100x. I mention the number because it is the kind of improvement that changes architectural decisions.

This is, if I may say, a pattern worth remembering. Two algorithms, each contributing what it does best — one for speed, one for precision — producing a result that neither achieves alone. Good tools compose well. These compose exceptionally well.

Gold Lapel's search_phonetic()

Gold Lapel's approach takes a similar compositional philosophy, combining Soundex for filtering with pg_trgm's similarity() for ranking:

search_phonetic SQL
SELECT *, similarity(name, $1) AS _score
FROM customers
WHERE soundex(name) = soundex($1)
ORDER BY _score DESC, name
LIMIT 50;

The Soundex equality filters candidates. The similarity score ranks them — among all customers whose names sound like the query, which one looks most like what the user typed? One method call:

Gold Lapel wrapper
goldlapel.search_phonetic(conn, "customers", "name", "Smyth")

Phonetic filtering selects the candidates. Character-level similarity selects the best candidate. Each tool contributes its strength, and the result is more useful than either tool alone.

B-tree Expression Indexes

B-tree expression index
CREATE INDEX idx_soundex ON customers USING btree(soundex(name));

Without this index, soundex(name) = soundex('Smyth') requires computing Soundex for every row — the familiar sequential scan. With it, PostgreSQL looks up the precomputed Soundex code directly in the index. The phonetic comparison becomes an index lookup rather than a table scan, which is the kind of improvement that the user feels even if they cannot articulate why the search suddenly became faster.

Gold Lapel's proxy auto-creates this B-tree expression index when it detects soundex(col) = soundex(...) patterns. It also detects and indexes dmetaphone() patterns.

A note if you are keeping track: this is a different index type from Chapters 4 and 5. Full-text search uses GIN on tsvector. Fuzzy matching uses GIN with trigram ops. Phonetic matching uses B-tree on the expression result. Each pillar uses the index type best suited to its particular operation. If you find yourself wondering when to use which, Appendix D has the complete decision matrix — and I recommend it without reservation.

The Extension

Enable fuzzystrmatch
CREATE EXTENSION IF NOT EXISTS fuzzystrmatch;

Ships with PostgreSQL as a contrib extension. Available on every managed provider. Gold Lapel's wrapper creates it lazily on first search_phonetic() call, along with pg_trgm for the similarity ranking.

One extension. Four tools — Soundex, Metaphone, Double Metaphone, and Levenshtein. I appreciate an extension that arrives prepared for multiple occasions.

When to Use Which Algorithm

AlgorithmBest ForSpeedAccuracyKey Limitation
SoundexCommon English names, fast matchingFastest (fixed 4-char code)ModerateEnglish-centric, first-letter dependent
MetaphoneBetter English pronunciation rulesFastGoodStill primarily English-focused
Double MetaphoneMulti-lingual names, multiple pronunciationsModerateBestMore complex, two codes to manage
LevenshteinExact edit-distance thresholds, precision rankingSlowest (per-comparison)N/A (distance, not phonetic)Not phonetic — character-level

My recommendation: start with Soundex. It handles the majority of phonetic matching use cases with minimal complexity, and it has been handling them since before any of the alternatives existed. Move to Double Metaphone when your data includes non-English names or when the first-letter problem matters for your users. Use Levenshtein for precision ranking after phonetic pre-filtering — not as the primary filter, but as the judge that selects the best match from among the phonetic candidates.

For the combination that delivers both speed and precision: Soundex in WHERE, Levenshtein in ORDER BY. A partnership, if you will, between an algorithm that knows what sounds right and an algorithm that knows what looks closest.

Connecting the Fuzzy Tools: Chapters 5 and 6 Together

The reader now has three fuzzy matching tools in PostgreSQL, and it is worth pausing to see how they fit together:

  • pg_trgm similarity (Chapter 5) — character-level similarity. Best for typos and general fuzzy matching. "Postgre" → "PostgreSQL."
  • Soundex / Double Metaphone (Chapter 6) — phonetic similarity. Best for names that sound alike but are spelled differently. "Smyth" → "Smith."
  • Levenshtein (Chapter 6) — edit distance. Best for precise "within N edits" matching. "kitten" is 3 edits from "sitting."

These are complementary, not competing. A comprehensive name-matching system might use Soundex to find phonetic candidates, pg_trgm similarity to rank them by visual closeness, and Levenshtein to set a maximum edit-distance threshold — rejecting any match that requires more than, say, 3 edits. All three tools live in PostgreSQL. All three work together. None requires a separate service.

I mention this because no existing tutorial combines these tools as a unified strategy. They are typically covered in isolation — pg_trgm in one article, fuzzystrmatch in another — as if they existed in separate worlds. They do not. They exist in the same database, and they are more capable together than apart. Good tools deserve to know about each other.

Honest Boundary

Phonetic algorithms are strongest for proper names. They are less useful for general text search — "database" and "databases" sound similar but are better handled by stemming (Chapter 4). Phonetic search is a complement to full-text search, not a replacement for it. The tools serve different guests, if you will, and each serves well within its domain.

All phonetic algorithms carry cultural and linguistic assumptions. Soundex was designed for English names in the 1918 US Census. Double Metaphone is broader but still primarily English-focused. For non-Latin scripts — Chinese, Arabic, Hindi — phonetic matching requires entirely different approaches that are beyond the scope of this extension and this chapter. I note this boundary because acknowledging what a tool does not do is, in my view, as important as demonstrating what it does.


Phonetic search handles the gap between how a name is pronounced and how it happens to be spelled in your database. Fuzzy matching handles the gap between what the user meant to type and what actually arrived in the search box. Full-text search handles precise intent expressed in precise words. Three pillars, each addressing a different kind of imprecision — the fingers, the tongue, the vocabulary.

But there is a fourth kind of imprecision, and it is the most human of them all.

When a user searches for "comfortable office chair" and expects to find "ergonomic desk seating," no amount of stemming will help — the words share no roots. No amount of trigram matching will help — the strings share no character sequences. No amount of phonetic coding will help — the words do not sound alike. The user knows exactly what they mean. They simply chose different words than the ones your database happens to contain.

This is the gap between words and meaning. Bridging it requires a fundamentally different representation of text — not characters, not sounds, but meaning itself, encoded as numbers in a high-dimensional space.

Chapter 7 introduces the concept. It involves no code. It involves no SQL. It is, I think, the chapter where your understanding of what search can be will change most profoundly. If you will follow me, I should like to show you something rather remarkable.