karloscodes

Before you add an Elasticsearch cluster

· 4 min read

AI-aided. I wrote the first version. AI helped me polish the writing.

I’ve reached for Elasticsearch more than once without thinking about it. You need search, ES is what you reach for. It’s the reflex.

The reflex skips the thing that matters most when you’re small. Elasticsearch isn’t a feature you add. It’s a system you run. Another database sitting next to the one you already have, kept in sync, one more thing to monitor, back up, and get paged for. At 3am, alone, because you are the whole ops team.

That cost has nothing to do with speed. For a solo founder or a small team the question is whether you want to operate a search cluster to find out, and the honest answer is usually no.

Because Postgres already searches, and for the size most of us run, it isn’t a compromise.

ALTER TABLE docs ADD COLUMN search tsvector
  GENERATED ALWAYS AS (
    to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,''))
  ) STORED;

CREATE INDEX docs_search_idx ON docs USING GIN (search);
SELECT id, ts_rank(search, q) AS rank
FROM docs, websearch_to_tsquery('english', $1) q
WHERE search @@ q
ORDER BY rank DESC LIMIT 20;

No new service. No sync. The index updates in the same transaction as the write, so stale search results aren’t a bug you can have. It’s a column and an index on the database you already run.

Does skipping the cluster cost you speed? I measured it, and put the benchmark online. A million real product reviews, Postgres and Elasticsearch side by side. This isn’t a lab-grade benchmark and doesn’t try to be. It’s numbers from a Mac Mini M4 running Docker, meant to show roughly how search slows down as the data grows, not to give you exact figures:

rowstypicalworst case
10,0000.1 ms1 ms
100,0000.8 ms12 ms
1,000,00015 ms143 ms

Read the top two rows, because that’s the size most apps are. At a hundred thousand documents Postgres answers a typical query in under a millisecond. No human can tell that apart from a dedicated cluster. You would run a second system to save time nobody can notice.

The bottom row is real, though. At a million rows the worst queries cross 100 ms. But a query gets slow from the rows it matches, not from the size of the table. Your everyday searches stay instant even at a million rows. Only broad common-term queries over a large dataset cost you.

In practice that ties back to dataset size, and other benchmarks put the working range clearly. Xata recommends starting with Postgres-only search for tables under 100,000 rows. Neon, testing at ten million, found Postgres’s built-in search had gotten too slow by then, while a stronger search index kept up. So the range is roughly this. The built-in search is comfortable into the hundreds of thousands, and once you’re into the millions it’s worth adding something stronger.

You have two options inside Postgres, and you reach for them in order. First, the built-in full-text search, the code above. Nothing to install, it ships with every Postgres, and for a small-to-medium dataset it’s all you need. The costs are mild. It adds some storage, and its ranking is basic, so it won’t give you finely tuned relevance. Second, pg_search, an extension that adds the same kind of ranking Elasticsearch uses, directly to Postgres. In Neon’s test it matched or beat Elasticsearch and handled a single database up to hundreds of millions of rows. The catch is it’s a third-party extension, so you need a Postgres you can install it on.

My opinion, plainly. We over-engineer search, and it’s almost never about load. It’s fear, or habit, or wanting the resume line. We reach for the cluster before we have the problem the cluster solves.

Match the tool to your scale. Start with the search that’s built into every Postgres. Into the millions, pg_search gives you stronger ranking without leaving the database. Elasticsearch comes last, for when you outgrow a single machine. Billions of rows, search load you need off your main database, or scale you have to spread across several servers. Those are real reasons, and you’ll know when they’re yours.

Until then, add the column and the index above, ship it, and check the row count once a year. Under a million rows, you’re done.

New posts in your inbox. Unsubscribe anytime.

Or grab the RSS feed.