Semantic search feels like magic until a user searches for ERR_CONN_4021 and gets back three passages about "connection reliability" and nothing containing the actual error code. Then it feels broken. The fix isn't to abandon semantic search — it's to stop relying on it alone. Hybrid search runs vector and keyword retrieval together and fuses the results, catching both the meaning and the exact terms. This is how to use it well.

Why neither method alone is enough
The two retrieval methods fail on opposite inputs:
Vector search matches meaning. It shines when the query and the answer use different words for the same idea — "get on the network remotely" finding a doc about "VPN." It struggles with exact tokens that carry no semantic signal: error codes, SKUs, function names, rare proper nouns. To an embedding model, ERR_CONN_4021 is near-meaningless, so it can't reliably find the passage that contains it.
Keyword search (BM25) matches exact terms. It nails ERR_CONN_4021 because it's literally looking for that string. It's useless when the query and document share no vocabulary — it has no notion that "VPN" and "remote network access" are the same thing.
Real queries are a mix. Users write "how do I fix ERR_CONN_4021 on the VPN" — part exact code, part fuzzy intent. You need both retrievers, and you need their results combined intelligently.
Calling hybrid search
Kognita's hybrid endpoint does the fusion for you. One call, both methods, reranked:
curl -X POST "$BASE/knowledge-bases/$KB_ID/search/hybrid" \
-H "X-API-Key: $KOGNITA_API_KEY" \
-H "X-Organization-Id: $KOGNITA_ORG_ID" \
-H "Content-Type: application/json" \
-d '{
"query": "how do I fix ERR_CONN_4021 on the VPN",
"limit": 5
}'
Under the hood this runs a vector search and a keyword search, merges the two ranked lists, and applies reranking so the final top-k reflects true relevance to the full query — not just proximity in one space. You get passages that match the error code and passages that match the intent, ordered sensibly.
Tuning for answer quality
A few knobs meaningfully change how good the downstream answer is:
Retrieve wider than you think, then trim. When search feeds an LLM, the reranking stage does its best work with a healthy candidate pool. Pulling limit: 5 after an internally wider retrieve-and-rerank gives the reranker room to promote the genuinely relevant passage over a merely similar one. Asking for too few candidates upstream starves that step.
Match limit to your context budget, not your optimism. More passages isn't strictly better. Every passage you pass to the model spends context tokens and adds noise the model has to see past. Five strong, relevant passages beat fifteen where ten are marginal. Start at 5 and adjust based on whether answers are missing information (raise it) or getting distracted (lower it).
Scope with metadata filtering. If your knowledge base spans teams or products, filter retrieval to the relevant subset so a query about Product A can't surface Product B's docs. Narrowing the candidate space before ranking is one of the cleanest quality wins available — the reranker can't promote an irrelevant passage that was never retrieved.
Feeding results to the model
Hybrid search gets you the right passages. Turning them into a good answer is about how you hand them over:
results = hybrid_search(query, limit=5)
context = "\n\n".join(
f"[Source: {r['source']}]\n{r['content']}" for r in results
)
answer = llm(
system="Answer only from the provided sources. Cite the source of "
"each claim. If the sources don't contain the answer, say so.",
user=f"Sources:\n{context}\n\nQuestion: {query}",
)
Two details carry most of the quality:
- Label each passage with its source. This lets the model cite, which lets users verify, which is what makes the answer trustworthy rather than just plausible.
- Instruct grounding explicitly. "Answer only from the provided sources" and "say so if they don't contain the answer" are what stop the model from smoothing over a retrieval gap with invention.
When to reach past hybrid
Hybrid search is the right default for the overwhelming majority of applications, and it's what you should start with. Reach further only for specific needs: multi-hop questions that require reasoning across several retrieval rounds (an agentic loop that searches, reads, and searches again), or cases where you want the model itself to decide when and what to retrieve (expose search as an MCP tool and let it drive).
But those are refinements on top of good retrieval, not replacements for it. Get hybrid search returning the right passages first. A clever agent loop over a bad retriever is still a bad system; a plain answer over a great retriever is already a good one.