← You Don't Need Elasticsearch

Chapter 19: Gold Lapel and Search

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

I should like to address a reasonable question. You have just read eighteen chapters demonstrating that PostgreSQL can do everything Elasticsearch does for application search. The SQL is shown. The extensions are named. The materialized views are designed. The hybrid pipeline is assembled. The migration playbook is written. All of it works. All of it is PostgreSQL, and all of it is available to you right now, tonight, with the database you are already running.

So why do you need Gold Lapel?

The honest answer: you do not. You can write every query by hand. You can create every index yourself. You can manage every extension, tune every HNSW parameter, choose between GIN and GiST for each use case, and refresh every materialized view on your own schedule. Everything this book has taught you works without Gold Lapel. I would not have spent eighteen chapters teaching it if it did not.

What Gold Lapel offers is not the capability. It is the automation. The wrapper gives you an Elasticsearch-like API — one method call instead of the SQL you now know how to write. The proxy gives you automatic indexes — the right type created for the right pattern without you writing CREATE INDEX. Together, they eliminate the gap between “PostgreSQL can do this” and “PostgreSQL does this without me managing it.”

Two layers. Allow me to show you how they work.

The Wrapper API: 13 Methods

All 13 methods exist in all 7 languages (Chapter 18). Each generates parameterized SQL. Each is SQL injection safe by construction. They are organized into five categories, and I have added a column that I believe you will find illuminating:

Search (5)

MethodES EquivalentSQL PatternExtension
search()match querytsvector @@ tsquery + ts_rankNone
search_fuzzy()fuzzy querypg_trgm similaritypg_trgm
search_phonetic()phonetic pluginsoundex/dmetaphone equalityfuzzystrmatch
similar()kNN searchpgvector <=> cosine + HNSWvector
suggest()completion suggesterILIKE prefix + similaritypg_trgm

Aggregations (2)

MethodES EquivalentSQL PatternExtension
facets()terms aggregationGROUP BY + COUNT with tsvector filterNone
aggregate()metric aggregationsCOUNT/SUM/AVG/MIN/MAX with GROUP BYNone

Percolator (3)

MethodES EquivalentSQL PatternExtension
percolate_add()register percolatorINSERT tsquery with metadataNone
percolate()percolate APItsvector @@ stored tsquery + ts_rankNone
percolate_delete()delete percolatorDELETE RETURNINGNone

Configuration (1)

MethodES EquivalentSQL PatternExtension
create_search_config()custom analyzerCREATE TEXT SEARCH CONFIGURATION COPYNone

Relevance Tuning (2)

MethodES EquivalentSQL PatternExtension
analyze()_analyze APIts_debug()None
explain_score()_explain APItsvector + tsquery + rank + headlineNone

The pattern: one method call → parameterized SQL → proxy detects the search pattern → appropriate index created automatically. The developer writes one line. The optimization is automatic.

I mentioned an illuminating column. Look at “Extension.” Nine of the thirteen methods require no extensions at all. Full-text search, aggregations, percolator, custom analyzers, relevance tuning — all built-in PostgreSQL. The configuration trade-off from Chapter 17 applies to four methods, not thirteen. I thought you might find that reassuring, and I wanted you to see it clearly rather than discover it by counting.

The Proxy: Automatic Search Indexes

The proxy sits between your application and PostgreSQL. It observes every query. When it detects a search pattern, it creates the optimal index automatically. The developer does not decide which index to create. The proxy observes what you search for and ensures it is fast.

SQL Pattern DetectedIndex CreatedSchema/Prefix
to_tsvector(...) @@ ...GIN tsvector index_goldlapel.tsv_
column LIKE/ILIKE $1GIN trigram index_goldlapel.trgm_
soundex(col) = soundex(...)B-tree expression index_goldlapel.fzm_
dmetaphone(col) = dmetaphone(...)B-tree expression index_goldlapel.fzm_
col <=> vector ORDER BYHNSW cosine index_goldlapel.vec_
col <-> vector ORDER BYHNSW L2 index_goldlapel.vec_

Detection threshold: Index creation happens after a pattern repeats — configurable, default 3 occurrences. A one-off query does not trigger index creation. A repeated pattern does. I consider this an important distinction: the proxy optimizes habits, not accidents.

Namespacing: All auto-created indexes live in the _goldlapel schema with descriptive prefixes. They do not intrude on your application’s schema. They are visible in the dashboard’s index inventory with type, table, column, creation date, and size — because what is automatic should also be transparent.

Toggleability: Every index type is individually toggleable via CLI flags, environment variables, or goldlapel.toml configuration. Hot-reloadable — changes take effect without restarting the proxy. If you prefer to manage HNSW indexes yourself while letting the proxy handle GIN indexes, that arrangement is available. The proxy is opinionated. It is not inflexible.

Extension Management

At startup, the proxy checks each extension: installed? installable? If installable, creates it. If not available, logs specific instructions and disables that index type.

The wrappers also create extensions lazily on first use — CREATE EXTENSION IF NOT EXISTS on the first call to a method that requires it. Belt and suspenders. I prefer systems with redundant safety mechanisms, and I suspect you do too.

The dashboard’s environment status grid shows the state of every extension — green (installed), yellow (installable but not yet created), red (not available on this server). A developer opening the dashboard sees immediately which capabilities are available and which require action. No guessing. No trial and error. The grid tells you.

Graceful degradation: if pg_trgm is unavailable, fuzzy search and autocomplete are disabled. If pgvector is unavailable, semantic search is disabled. Full-text search, aggregations, percolator, and relevance tuning continue to work. The system runs with what it has, and it tells you what it does not have. I find this preferable to failing silently.

Write-Driven Cache Invalidation

Gold Lapel’s three-tier cache applies to search queries:

  • L1 (in-process): ~2µs. Native to each language wrapper. Frequently repeated search queries served from memory.
  • L2 (proxy cache): ~0.5ms. Shared across all connections through the proxy.
  • L3 (materialized views): ~0.7ms. PostgreSQL-native. The search infrastructure described throughout this book.

All three tiers are invalidated automatically on writes. The proxy detects writes via PostgreSQL’s NOTIFY mechanism. When a write occurs, the proxy invalidates the affected cache entries across all connected instances. For writes that bypass the proxy — direct database connections, batch jobs, external tools — logical decoding detects them. This requires wal_level = logical, as Chapter 17 discussed.

No cache management code in your application. No get/set. No cache keys. No TTLs. The invalidation is write-driven and automatic. Returning readers from Book 1 will recognize this architecture. It is the same principle applied to a different workload — and I find that consistency satisfying.

Citus Awareness

The proxy auto-detects Citus at startup. When Citus is present:

  • Dashboard shows “PostgreSQL + Citus” with worker node count.
  • Auto-created indexes propagate to all shards automatically — Citus handles this natively.
  • No configuration changes. No code changes.

The proxy talks to the Citus coordinator the same way it talks to a single-node PostgreSQL. The distribution is transparent. You will recall from Chapter 15 that the scaling story’s key property is “Application Change: None.” The proxy honors that property.

The One-Line Install

Python
goldlapel.start("postgres://user:pass@localhost:5432/mydb")

One line. The wrapper connects through the proxy. The proxy observes queries. Indexes appear. Search gets fast.

From your perspective as a developer: you call search methods and get results. You never write CREATE INDEX. You never tune HNSW parameters. You never decide between GIN and GiST. You never think about which extension to install. You write goldlapel.start() and call the methods this book has demonstrated across eighteen chapters.

The same one-line install works in all seven languages. The connection string is the only parameter. I worked rather hard to make it this simple, and I hope you will not hold the simplicity against me.

Honest Boundary

Gold Lapel makes PostgreSQL search easy. It does not make it possible — PostgreSQL already has the capabilities. Every SQL query that Gold Lapel generates can be written by hand. Every index it creates can be created manually. A developer who prefers to write raw SQL, create their own indexes, and manage their own extensions can do everything Gold Lapel does. The eighteen chapters preceding this one prove it. Gold Lapel automates the management, not the capability.

The wrapper methods cover the 13 most common search operations. For queries that don’t fit — complex hybrid RRF pipelines (Chapter 13), custom aggregation patterns, specialized percolator triggers — raw SQL is always available. Gold Lapel is the fast path, not the only path. And the path it does not cover is the path PostgreSQL covers directly.

The proxy adds a network hop. For most applications, the latency is negligible — sub-millisecond. For latency-critical applications measuring individual microseconds, the proxy overhead is measurable. The three-tier cache typically more than compensates, but the overhead exists and I would not hide it from you.

The search is automated. The indexes are automatic. The extensions are managed. The caching invalidates itself. The developer writes goldlapel.start() and calls search methods. The rest happens beneath — observed, optimized, and maintained without requiring your attention.

The household is in order. The kitchen is running. The guests are served.

What remains is for me to take my leave — which, as returning readers will know, I do the same way I arrived. Chapter 20 is brief. It is also, if I may say so, the chapter I have been looking forward to most. Not because it contains the most technical content. Because it contains the most honest.

Good evening is, after all, both a greeting and a farewell.