Loading...

Replacing Algolia with an In-Memory Search in Next.js 14

June 2, 2026
Table of Contents

The most satisfying commit of our migration off Contentful was the one that only deleted things. Out went the Algolia client, the sync route that pushed every published post to a hosted index, and four environment variables I no longer had to keep secret. On-site search for cloudapp.dev now lives in a single file with no service behind it — and I want to walk through the reasoning, including the one part that actually needed thought.

Algolia wasn't the problem — our size was

I'll say it plainly: Algolia is good software, and I'd reach for it again on a large catalog. But our "catalog" is about 220 blog posts. Every publish still had to fan out to an external index, which means a sync job, a webhook, and a low-grade recurring worry — is the index actually in step with what's live, or is some post quietly missing from search? We were maintaining a second copy of our content, and paying for it, to search a dataset that fits comfortably in RAM.

When we cut over from Contentful to a self-hosted headless CMS, one detail made the decision for me: the adapter already holds everything in memory.

The store is already in memory

On the first request to a server instance, the adapter pulls the whole space — every record, asset and tag, about 8 MB of JSON — and memoizes it for that instance's lifetime. A small aside that turned out to matter: that payload is big enough that Next.js flatly refuses to put it in its data cache (you get a cheerful items over 2MB can not be cached in the logs). Which is fine, because I never wanted a second cache layer — the in-memory store is the cache. Search over that isn't an I/O problem. It's a string match over an array we're already holding:

The honest cost is the first request to a cold instance, which pays for that ~8 MB fetch; after that the store is warm and every search — and every tag-cloud render — is essentially free. For a site that ships a few times a week, that's a trade I'll take all day.

Scoring is deliberately boring

Each post is flattened once into a lowercased haystack — title, description, body and tags concatenated. Then the ranking does the obvious thing: weight a hit by which field it landed in, and require every term to appear somewhere, so a two-word query narrows the results instead of widening them.

The weights aren't tuned with any science — a title hit (8) should simply outrank a body hit (2), because if your query words are in the title, that post is almost certainly the one you meant. The same in-memory pass also feeds the homepage tag cloud: computeFacets() just counts tags across the same posts, so search results and facets can never disagree about what exists.

The only part I actually had to think about

The search itself was the easy twenty lines. The part I sat with was making sure the index can never lie. So I memoize the built index on a WeakMap keyed by the store object itself. When a content webhook fires and we swap the store via resetStore(), the old store object becomes unreachable — and its WeakMap entry goes with it, collected by the GC. No resetSearchIndex() to remember to call, no TTL to guess at, no path by which the index can drift from the content it describes. It simply cannot outlive its source. That property was the whole point; the token scan was almost incidental.

Where I'd still pay for Algolia

To be fair about the trade-off: this only works because the dataset is small and the matching is simple. There's no typo-tolerance, no synonyms, no stemming, no relevance tuning at scale. Index millions of documents or need fuzzy ranking, and a hosted engine earns every cent. But for a few hundred posts where people mostly type a framework name, exact substring matching with a handful of field weights is plenty — and "plenty" that I can read top-to-bottom in one file beats "sophisticated" that I have to keep operating.

The best part of the change was the diff: almost all red. The SDK, the sync route and the env vars are gone, and search results and the homepage tag cloud now come from the same store snapshot the rest of the site already renders from. Deleting a dependency you've outgrown is one of the quieter pleasures of the job.