Retrieval & Hybrid Search7 min readยท

How to Implement Hybrid Search (BM25 + Vectors) for Better Relevance

Your users search for "cancel subscription" and get back a slick product page about subscription plans, because a pure vector search matched on the theme and missed the intent.

Your users search for "cancel subscription" and get back a slick product page about subscription plans, because a pure vector search matched on the theme and missed the intent. Flip to keyword search and the opposite happens: a query for "reset password link expired" returns nothing, because the exact tokens never co-occur in your docs even though three articles answer the question. Both failure modes ship the same result: an agent that retrieves confidently wrong context, then generates a confidently wrong answer on top of it.

Hybrid search exists to close that gap. By blending lexical scoring (BM25) with semantic similarity (vectors), you catch both the exact-match query and the paraphrased one, and you rank by a combined signal instead of betting the retrieval quality on a single method. This guide covers how BM25 and vector search each fail alone, how to fuse their scores, how to tune the blend, and how to keep the whole thing fresh in production.

Sanity Context is the AI Content Operating System's retrieval layer, an intelligent backend where hybrid search runs natively inside the content store rather than being stitched across a vector database, a search engine, and glue code. That native placement is the difference between a demo and a system you can operate.

Illustration for How to Implement Hybrid Search (BM25 + Vectors) for Better Relevance
Illustration for How to Implement Hybrid Search (BM25 + Vectors) for Better Relevance

Why keyword search and vector search each fail alone

Start with the mechanisms, because the fix only makes sense once you see the two distinct failure modes. BM25 is a lexical ranking function: it scores a document by how often the query terms appear in it, dampened by how common those terms are across the corpus and normalized for document length. It is precise and interpretable. When a user types an exact SKU, an error code, a product name, or a legal clause, BM25 nails it. Its weakness is that it has no concept of meaning. "Cancel my plan" and "end my subscription" share almost no tokens, so a lexical index treats them as unrelated, and the right article never surfaces.

Vector search inverts both the strength and the weakness. Each document and each query is embedded into a high-dimensional space where semantic neighbors sit close together, so "end my subscription" and "cancel my plan" land near each other regardless of shared words. That is a genuine leap for paraphrase and intent. But embeddings smear precision. A query for a specific error code, a version number, or a rare proper noun gets pulled toward the general topic cluster, and the exact document that resolves the ticket ranks below three thematically similar but useless neighbors.

The operational cost of picking one is asymmetric and quiet. Neither method throws an error when it retrieves the wrong passage. It simply hands your agent plausible context, and the model, having no way to know the retrieval was off, generates an authoritative answer grounded in the wrong source. Hybrid search is not an optimization here. It is the correction for two complementary blind spots, so that exact-match queries and intent-match queries both resolve from a single retrieval pass rather than forcing you to guess which kind of query the user will type.

How score fusion actually works

Combining two retrievers means combining two scores that live on incompatible scales. A BM25 score is unbounded and corpus-dependent; a cosine similarity sits between roughly zero and one. Add them raw and BM25 dominates purely because its numbers are bigger. So the first job of any hybrid pipeline is reconciliation, and there are two common strategies.

Reciprocal Rank Fusion (RRF) sidesteps the scale problem by throwing away the scores and keeping only the ranks. Each document gets a contribution of one divided by (k plus its rank) from each retriever, and those contributions sum. It is robust, tuning-light, and a sensible default when your two score distributions are wildly different. The tradeoff is that it discards magnitude: a document that a retriever ranks first by a landslide gets the same credit as one that barely edged into first.

Weighted score fusion keeps the magnitudes but requires normalization first. You rescale each retriever's scores to a common range, then compute a weighted sum, for example 0.6 times the semantic score plus 0.4 times the lexical score. This preserves confidence information and lets you dial the blend toward precision or recall, but it is more sensitive and needs an evaluation set to tune honestly.

The deeper architectural question is where fusion happens. In a bolt-on stack you run the vector database, run the keyword index, pull two result sets over the network, and fuse them in application code, which means two systems to keep consistent and a fusion step no one owns. Sanity Context collapses that: a single GROQ query blends `text::semanticSimilarity()` for the vector side with a BM25 `match()` for the lexical side, combined through `score()` and `boost()`, so the fusion is expressed declaratively in one query against one store rather than assembled from parts.

Tuning the blend for your corpus

There is no universal weight. The right balance between lexical and semantic depends on what your users type and what your content looks like, and the only honest way to find it is to measure. Build a small labeled evaluation set: fifty to a few hundred real queries paired with the documents that should be retrieved for each. Pull these from actual logs, not from your imagination, because the queries you invent are cleaner and more keyword-shaped than the ones users actually submit.

Then sweep the blend and watch two metrics. Recall at k tells you whether the right document made it into the top k results at all, which is what matters most for grounding an agent, since a passage that never gets retrieved can never inform the answer. Mean Reciprocal Rank tells you how high the right document landed, which matters when you feed the model only the top few passages. A blend that improves recall but buries the answer at rank nine is not actually helping a model that only reads the top three.

Expect the weight to drift by corpus. Technical documentation full of exact identifiers, error codes, and API names rewards a heavier lexical weight, because users search for the literal string. Conversational support content and marketing copy reward the semantic side, because users describe problems in their own words. Multilingual corpora lean semantic harder still, since embeddings bridge languages that share no tokens.

The practical trap is treating tuning as a one-time launch task. Corpora grow, query patterns shift, and a blend tuned on last quarter's traffic slowly decays. Wiring the evaluation into a repeatable job, rather than a spreadsheet someone ran once, is what keeps relevance from quietly eroding after launch.

Keeping embeddings fresh when content changes

The relevance problem nobody demos is staleness. Hybrid search is only as good as the index behind it, and in most architectures the vector index is a separate copy of your content that has to be rebuilt whenever the source changes. A support article gets corrected, a product spec is updated, a policy is rewritten, and until the re-embedding pipeline catches up, your agent retrieves and cites the old version with full confidence.

This is where the bolt-on pattern quietly accrues cost. You stand up a content backend, then a separate vector database, then an embedding pipeline that watches for changes, re-chunks, re-embeds, and upserts, plus the plumbing to keep document identity aligned across two systems. Every one of those hops is a place where the vector store and the source of truth drift apart, and drift in retrieval does not announce itself. It just serves stale context.

Sanity Context ties embeddings to content in the Content Lake, so when a document changes the embeddings update within minutes and there is no separate vector pipeline to own, monitor, or fall behind. The retrieval path reads from the same store editors write to, which removes the class of bugs where "the search index is behind the CMS." Knowledge Bases extend the same path to websites, PDFs, and support databases, turning unstructured sources into agent-readable documents that share the retrieval path rather than living in yet another silo. This is the concrete meaning of content-operations end to end: retrieval is not a downstream copy of your content, it is a view of your content.

Governing what the agent retrieves and how it behaves

Better relevance is not only a ranking problem; it is a governance problem. The passages an agent is allowed to retrieve, the instructions that shape how it uses them, and the freedom to change all of that safely are what separate a controlled system from a liability. When retrieval logic and agent instructions live in application code, a change means a deploy, a review the content team cannot read, and no safe way to preview the effect on real queries before it ships to users.

Sanity treats agent behavior the way it treats the website. Studio Workspaces give editors a place to author and review the instructions that ground an agent, and Content Releases let you stage a change to that behavior and preview it before it goes live, the same mechanism used to stage a site launch. That means a content lead can adjust which sources an agent draws on, or refine its instructions, without waiting on an engineering deploy, and can roll it back if the preview looks wrong. Roles and Permissions, Audit logs, and Content Source Maps make it clear who changed what and which source a given answer was grounded in.

The governance story also carries the compliance one. Sanity is SOC 2 Type II compliant and GDPR compliant, offers regional hosting and data residency, and publishes its sub-processor list, which matters when the content feeding your agent includes regulated or customer data. The point is not a checklist. It is that retrieval, the instructions around it, and the audit trail behind it live in one governed system that editors and engineers share, instead of being scattered across a vector database, a prompt file, and a config repo that no reviewer outside engineering can see.

Wiring hybrid retrieval into your agent in production

The last mile is connecting all of this to the agent actually answering users. A retrieval layer that works in a notebook still has to expose an interface the agent can call at request time, return passages fast enough to sit inside a conversation, and do it without a bespoke integration for every model or framework you adopt.

Production agents connect to Sanity Context through its MCP endpoint, so the agent queries the same hybrid retrieval path, the same fresh embeddings, and the same governed instructions through a standard interface rather than a hand-rolled API per project. Because the MCP endpoint is shaped to the product, the agent gets structured, queryable content back rather than a flat blob it has to parse, which is what lets it ground an answer in a specific document instead of a vague topical match. Agent Actions provide schema-aware APIs when the workflow needs more than retrieval, for generating, transforming, or translating content in a way that respects the content model rather than treating everything as free text.

Put the pieces together and the architecture is deliberately boring in the best way. One store holds the content, the embeddings, and the retrieval logic. GROQ expresses the hybrid blend of `match()` and `text::semanticSimilarity()` declaratively. Embeddings stay current because they are tied to that content. The Studio governs what the agent may retrieve and how it behaves, with releases and audit trails around every change. The MCP endpoint hands all of it to the agent through one interface. What you are not doing is running and reconciling three separate systems, which is where relevance projects usually go to die: not in the ranking math, but in the operational drift between the parts.

Hybrid retrieval: native in the content backend vs assembled across a stack

FeatureSanityPineconeContentfulpgvector / Neon
Lexical + vector fusionNative: text::semanticSimilarity() blended with a BM25 match() via score() and boost() in one GROQ query.Sparse-dense hybrid supported natively, but lexical relevance is a sparse vector approximation, not a full BM25 text index.No native hybrid ranking; you pair the content API with an external search service and fuse results in application code.Vector distance is native via pgvector; BM25-grade lexical ranking means adding tsvector or ParadeDB and fusing in SQL yourself.
Embedding freshness on content changeEmbeddings are tied to content in the Content Lake and update within minutes; no separate vector pipeline to maintain.Freshness depends on the external embedding and upsert pipeline you build and operate against the source of truth.Requires a change-driven pipeline to re-embed edited entries into a separate vector store; drift is on you to prevent.You own the trigger, re-embed, and upsert flow; embeddings are only as fresh as the job that refreshes them.
Content store and vector indexOne store holds content, embeddings, and retrieval logic, so the retrieval path reads what editors write.Vector index only; content lives elsewhere, so document identity must be kept aligned across two systems.Content backend with vectors bolted on via App Framework and external search, so the index is a second copy to reconcile.Vectors sit alongside relational data in Postgres, though production content typically lives in a separate CMS to sync.
Governing agent instructionsStudio Workspaces and Content Releases let editors stage and preview agent instructions the same way they stage a site.Out of scope; instruction and prompt governance live in your application code and deploy process.Content workflow is strong for entries, but agent instructions and retrieval config live outside it in code.Database layer only; no editorial governance for agent behavior or instructions.
Agent connection interfaceAgents connect through the Sanity Context MCP endpoint and get structured, queryable content back, not a flat blob.Query via SDK or REST; the retrieval interface is generic, and agent wiring is a per-project integration.Delivery via GraphQL or REST content APIs; connecting an agent is a custom integration you build and maintain.SQL access via client libraries; exposing a retrieval interface to an agent is entirely hand-rolled.
Compliance posture for regulated contentSOC 2 Type II and GDPR compliant, with regional hosting, data residency, and a published sub-processor list.SOC 2 Type II and GDPR compliant as a managed vector database; content governance sits in whatever store holds the source.SOC 2 and GDPR compliant as a content platform; retrieval and vector handling depend on the external services you add.Compliance depends on your Neon or Postgres deployment and hosting choices; you own the posture end to end.