We recently launched Lakebase Search, a set of Postgres extensions for vector, BM25 keyword and hybrid search in Lakebase Postgres. So it was obvious to us that we needed to dogfood it for our own docs search. Until this week, the search box on neon.com ran on a hosted search vendor. Now it's built fully on Neon primitives and powered by Lakebase Search. In this post, I want to share how it went.
What is Lakebase Search #
Lakebase Search brings scalable vector and BM25 full-text search to Lakebase Postgres through two extensions:
lakebase_vectoris apgvector-compatible extension for vector search. Your existing types, operators and queries keep working, but the index needs a fraction of the memory of HNSW (under 10 GB for 100M vectors, where HNSW needs about 300 GB) and scales to over a billion vectors.lakebase_textbrings BM25 to Postgres. Native Postgres full-text search ranks matches withts_rank, which scores each document on its own. BM25, the industry-standard ranking for keyword search, also weighs how rare each term is across all documents and how long each document is, so the most relevant results rise to the top.lakebase_textworks on standardtsvectorcolumns and returns the top results without scoring every match.
Both extensions ship new index types: lakebase_ann for vectors and lakebase_bm25 for text. They're designed for the lakebase architecture, where compute and storage are separated.
Because the embeddings, the text and both indexes live in the same Postgres as the rest of your data, you can combine different search strategies into one hybrid query on top of your operational tables, in a single transaction. Read more about Lakebase Search in the launch post, which explains why and how we built it.
How it works #
Neon Docs search is a standalone service that runs as its own Neon project. It uses three Neon services:
- **[Lakebase Postgres](https://neon.com/docs/introduction/neon-and-lakebase)** stores the page chunks, their embeddings and the Lakebase Search indexes.
- **[Neon Functions](https://neon.com/docs/compute/functions/overview)** run a[Hono](https://hono.dev/) API that ingests changed pages and answers search requests.
- **[Neon AI Gateway](https://neon.com/docs/ai-gateway/overview)** generates the embeddings and reranks hybrid results.
To ingest the site, we needed a clean representation of its content, like markdown. Thanks to our agent experience work, neon.com already publishes every page as markdown (try neon.com/pricing.md). So ingestion mostly means getting notified when production content changes. Whenever a docs, guide or changelog page changes in the website repo, a GitHub Action sends the changed pages to the Function. The Function fetches each page as markdown, splits it into chunks by heading, gets an embedding per chunk through AI Gateway and writes the text, embedding and tsvector to one Lakebase Postgres table.
The same Function serves a /site-search endpoint, which the website's search route calls. The index holds seven kinds of content: docs, the API reference, the PostgreSQL tutorial, guides, FAQs, the changelog and blog posts. When we index a page, we tag it with its category based on the URL. This way, the caller can filter results by category when making an API call:
Building search as an application builder #
As an app builder, I was comfortable with most of this: setting up the Neon Function, calling AI Gateway for embeddings, wiring up the GitHub Action and the website route. Where I personally don't have expertise is search itself: which algorithms to use, how to structure semantic search, how to merge keyword and semantic matches and what trade-offs each option comes with. In the past, this is where I'd reach for Algolia or another search provider.
This time, I relied on my agent and the neon-postgres agent skill. Our incredible Lakebase engineering team had already put their search knowledge into the skill. So my agent walked me through the options: a lakebase_bm25 index for keyword matches, a lakebase_ann index for semantic matches and reciprocal rank fusion (RRF) to merge both rankings. My job was to tell the agent what to measure against: our existing search, on both result quality and speed. I was very excited to see that the result matched or beat our previous search experience.
Designing for search-as-you-type #
Docs search has to feel instant. The search box sends a request as soon as you typing, so a round trip has to land well under a second. Google's RAIL model treats responses under 100 ms as instantaneous and Nielsen Norman Group puts the limit for keeping users in their flow at one second. Since most of that budget goes to the network, we kept the server time under 32 ms at p50 and 150 ms at p95.
Semantic search matters too: questions like "How do Neon branches work?" don't always share keywords with the right page. The full hybrid pipeline (an embedding call, lakebase_ann and lakebase_bm25 results merged with RRF, then an LLM rerank) finds those pages, but its two model calls take seconds. So we split the work:
- Cached hybrid results: we run the full pipeline offline for common queries and store the ranked lists in a
site_search_cachetable. Lookups match on the normalized query and its content words, so "mcp" and "mcp server" read the same row. - BM25 for everything else: queries the cache hasn't seen go straight to the
lakebase_bm25index, with no model call.
Cache hits take 1.9 ms at p50 and 2.8 ms at p95 on the server; BM25 misses take 8.6 ms and 11.5 ms. Of course, long-tail queries get keyword ranking without the semantic half. For docs search, I want speed first.
That said, we wanted the new search to be at least as good as the old one in both result quality and speed.
Evaluating relevance and latency #
When you let an agent build something, you need to verify that the output matches your expectations. For search, that means two things: does the right page show up and how fast does it come back? So you need a verification loop.
For us, that loop is a few baseline and regression benchmarks. It was incredibly easy for my agent to set them up. They also told us when the new search matched or beat our previous one:
- Hit rate: 171 real queries, including the most common searches on neon.com, each paired with the page it should return. A change fails if an expected page falls out of the top five.
- Ranking quality: a public search benchmark (NFCorpus ) with labeled results, so changes to the search SQL can't make rankings worse.
- Speed: after every deploy, we time the search and fail if either path goes over its budget.
Here's how the new search compares to our previous one on the same queries:
The new search finds the right page in the top five for 94% of queries, where the previous search found it for 80%. Round trips take about the same time on both. Almost all of it is network: the Function itself spends single-digit milliseconds on each search.
Try it on your own data #
If I can build hybrid search for neon.com, your agent can build it for your app. Install the Neon agent skills: Then give your agent this prompt:
To see the result, head to the Neon docs, press Cmd+K and give the new search a try. Let us know in Discord what it should find better. Happy coding!